rvlib-shipkit 0.38.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +371 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +216 -18
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
package/src/collections/pings.ts
CHANGED
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
// gitignored territory); a repo without node_modules gets NO pings rather
|
|
5
5
|
// than re-fired backlogs (no persistence = no dedup = stay silent). For
|
|
6
6
|
// ask-backed items the marker is the ask file's own `pinged:` field — the
|
|
7
|
-
// SAME once-ever guarantee every other ping path uses.
|
|
7
|
+
// SAME once-ever guarantee every other ping path uses. A hub in a linked
|
|
8
|
+
// worktree is a side checkout (a test hub, a branch under review): it never
|
|
9
|
+
// notifies and never marks, so the phone and the markers belong to the main
|
|
10
|
+
// checkout's hub alone.
|
|
8
11
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
9
12
|
import { join } from 'path'
|
|
10
13
|
import { markPinged } from '../asks/asks-queue.ts'
|
|
14
|
+
import { isLinkedWorktree } from '../core/git-root.ts'
|
|
11
15
|
import type { NeedItem } from './collection-types.ts'
|
|
12
16
|
|
|
13
17
|
export type PingIo = {
|
|
@@ -24,13 +28,33 @@ export type PingIo = {
|
|
|
24
28
|
|
|
25
29
|
const stateDir = (root: string): string | null => (existsSync(join(root, 'node_modules')) ? join(root, 'node_modules', '.cache', 'shipkit') : null)
|
|
26
30
|
|
|
27
|
-
|
|
31
|
+
/** the state file's shape. It is bumped when the IDS a surface emits move,
|
|
32
|
+
* because dedup marks are worthless under a name nothing emits any more:
|
|
33
|
+
* v1 was a bare array, written while a repo health row was one wire line of
|
|
34
|
+
* STATUS.md; v2 arrived with one resource per gate check. */
|
|
35
|
+
const STATE_VERSION = 2
|
|
36
|
+
|
|
37
|
+
/** `adopt` = the marks in hand cannot be compared to the ids in front of us,
|
|
38
|
+
* so this tick MARKS the machine-owned reds and notifies about none of them.
|
|
39
|
+
* The alternative is the whole backlog firing again, at once, on his phone. */
|
|
40
|
+
type PingState = { ids: Set<string>; adopt: boolean }
|
|
41
|
+
|
|
42
|
+
const loadPinged = (root: string): PingState | null => {
|
|
28
43
|
const dir = stateDir(root)
|
|
29
44
|
if (dir == null) return null
|
|
45
|
+
const file = join(dir, 'pinged.json')
|
|
46
|
+
// no file at all = a repo that never pinged: there is no backlog to
|
|
47
|
+
// protect, and its first tick is the normal first tick
|
|
48
|
+
if (!existsSync(file)) return { ids: new Set(), adopt: false }
|
|
30
49
|
try {
|
|
31
|
-
|
|
50
|
+
const raw = JSON.parse(readFileSync(file, 'utf8')) as unknown
|
|
51
|
+
if (Array.isArray(raw)) return { ids: new Set(raw as string[]), adopt: true }
|
|
52
|
+
const s = raw as { v?: number; ids?: string[] }
|
|
53
|
+
return { ids: new Set(s.ids ?? []), adopt: s.v !== STATE_VERSION }
|
|
32
54
|
} catch {
|
|
33
|
-
|
|
55
|
+
// unreadable marks say nothing about what he has already seen, and
|
|
56
|
+
// the safe direction is silence, never a burst
|
|
57
|
+
return { ids: new Set(), adopt: true }
|
|
34
58
|
}
|
|
35
59
|
}
|
|
36
60
|
|
|
@@ -38,28 +62,30 @@ const savePinged = (root: string, ids: Set<string>): void => {
|
|
|
38
62
|
const dir = stateDir(root)
|
|
39
63
|
if (dir == null) return
|
|
40
64
|
mkdirSync(dir, { recursive: true })
|
|
41
|
-
writeFileSync(join(dir, 'pinged.json'), JSON.stringify([...ids].sort()))
|
|
65
|
+
writeFileSync(join(dir, 'pinged.json'), JSON.stringify({ v: STATE_VERSION, ids: [...ids].sort() }))
|
|
42
66
|
}
|
|
43
67
|
|
|
44
68
|
/** one tick: ping ONCE for the batch of never-pinged red items, mark them.
|
|
45
69
|
* Returns the pinged ids (empty = silence). */
|
|
46
70
|
export function pingTick(root: string, asksDir: string, needs: NeedItem[], io: PingIo): string[] {
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
if (isLinkedWorktree(root)) return []
|
|
72
|
+
const state = loadPinged(root)
|
|
73
|
+
if (state == null) return [] // no persistence, no pings — never re-fire a backlog
|
|
49
74
|
const fresh: NeedItem[] = []
|
|
50
75
|
for (const n of needs) {
|
|
51
76
|
if (n.severity !== 'red') continue
|
|
52
77
|
if (n.askOwned === true) continue // the covering ask's pinged: field already fired the once-ever ping
|
|
53
78
|
if (n.collection === 'asks') {
|
|
54
|
-
// ask
|
|
79
|
+
// an ask's marker lives in the ask FILE and survives any id move,
|
|
80
|
+
// so it is never adopted: a new ask still fires on a migrating tick
|
|
55
81
|
const file = n.id.replace(/^ask:/, '')
|
|
56
82
|
if (markPinged(join(root, asksDir), file)) fresh.push(n)
|
|
57
|
-
} else if (!
|
|
58
|
-
|
|
59
|
-
fresh.push(n)
|
|
83
|
+
} else if (!state.ids.has(n.id)) {
|
|
84
|
+
state.ids.add(n.id)
|
|
85
|
+
if (!state.adopt) fresh.push(n)
|
|
60
86
|
}
|
|
61
87
|
}
|
|
62
|
-
savePinged(root,
|
|
88
|
+
savePinged(root, state.ids)
|
|
63
89
|
if (fresh.length === 0) return []
|
|
64
90
|
const head = fresh.slice(0, 3).map((n) => n.text.slice(0, 80)).join(' · ')
|
|
65
91
|
const message = `${fresh.length} new item(s) need you: ${head}${fresh.length > 3 ? ' · …' : ''}${io.link != null ? ` → ${io.link}` : ''}`
|
package/src/collections/posts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// social-posts collection — projects `.
|
|
1
|
+
// social-posts collection — projects `.shipkit/social/<channel>/` post files
|
|
2
2
|
// (contract v2) into the needs feed. Status-aware: rejected/posted history
|
|
3
3
|
// stays on disk as training data and never floods the feed. Drafts-as-rolls:
|
|
4
4
|
// variants are sibling files `<slug>.a.md`, `<slug>.b.md`; picking one flips
|
|
@@ -43,7 +43,7 @@ export function parsePostFile(dir: string, channel: string, file: string): PostF
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export function listPosts(root: string): PostFile[] {
|
|
46
|
-
const social = join(root, '.
|
|
46
|
+
const social = join(root, '.shipkit/social')
|
|
47
47
|
if (!existsSync(social)) return []
|
|
48
48
|
const out: PostFile[] = []
|
|
49
49
|
for (const ch of readdirSync(social)) {
|
|
@@ -71,12 +71,17 @@ export function socialPostsCollection(root: string): Collection {
|
|
|
71
71
|
return lines.length > 0 ? lines.slice(-3) : undefined
|
|
72
72
|
}
|
|
73
73
|
for (const [key, variants] of bySlug) {
|
|
74
|
-
const fileOf = (v: PostFile): string => `.
|
|
74
|
+
const fileOf = (v: PostFile): string => `.shipkit/social/${v.channel}/${v.file}`
|
|
75
|
+
// the row is the post, the venue is the slot beside it
|
|
76
|
+
const place = { label: key.split('/').slice(1).join('/'), slot: variants[0]!.channel }
|
|
75
77
|
const missing = variants.filter((v) => v.status == null)
|
|
76
78
|
if (missing.length > 0) {
|
|
77
79
|
out.push({
|
|
78
80
|
id: `post:${key}:invalid`,
|
|
81
|
+
...place,
|
|
79
82
|
text: `${key} — missing/invalid status: frontmatter (${missing.map((v) => v.file).join(', ')})`,
|
|
83
|
+
type: 'post',
|
|
84
|
+
path: fileOf(missing[0]!),
|
|
80
85
|
severity: 'red',
|
|
81
86
|
file: fileOf(missing[0]!),
|
|
82
87
|
})
|
|
@@ -86,7 +91,10 @@ export function socialPostsCollection(root: string): Collection {
|
|
|
86
91
|
if (picked != null) {
|
|
87
92
|
out.push({
|
|
88
93
|
id: `post:${key}:topost`,
|
|
94
|
+
...place,
|
|
89
95
|
text: `${key} — picked, waiting to be POSTED (copy → post → paste the URL)`,
|
|
96
|
+
type: 'post',
|
|
97
|
+
path: fileOf(picked),
|
|
90
98
|
severity: 'red',
|
|
91
99
|
artifact: { copy: postSection(picked.body) },
|
|
92
100
|
verbs: ['paste-proof', 'rate'],
|
|
@@ -100,7 +108,10 @@ export function socialPostsCollection(root: string): Collection {
|
|
|
100
108
|
if (drafts.length > 0) {
|
|
101
109
|
out.push({
|
|
102
110
|
id: `post:${key}:pick`,
|
|
111
|
+
...place,
|
|
103
112
|
text: `${key} — ${drafts.length} draft(s), pick one`,
|
|
113
|
+
type: 'post',
|
|
114
|
+
path: fileOf(drafts[0]!),
|
|
104
115
|
severity: 'yellow',
|
|
105
116
|
verbs: ['pick', 'rate'],
|
|
106
117
|
candidates: drafts.map((v) => ({ id: v.file, label: v.variant ?? 'draft', hint: postSection(v.body).slice(0, 200) })),
|
|
@@ -114,7 +125,12 @@ export function socialPostsCollection(root: string): Collection {
|
|
|
114
125
|
const shown = posted ?? variants[0]!
|
|
115
126
|
out.push({
|
|
116
127
|
id: `post:${key}:${posted != null ? 'posted' : 'rejected'}`,
|
|
128
|
+
...place,
|
|
117
129
|
text: `${key} — ${posted != null ? `posted${posted.posted != null ? ` (${posted.posted})` : ''}` : 'rejected'}`,
|
|
130
|
+
type: 'post',
|
|
131
|
+
path: fileOf(shown),
|
|
132
|
+
// posted and rejected are history: visible, never loud
|
|
133
|
+
state: 'quiet',
|
|
118
134
|
file: fileOf(shown),
|
|
119
135
|
remarks: feedback(shown),
|
|
120
136
|
})
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
// asks READ path never minting asks/ on a repo without one. Dedup doctrine
|
|
6
6
|
// : dedup = acted-on once, NEVER hidden — an
|
|
7
7
|
// ask-covered missing cred still shows RED in secrets with a pointer to the
|
|
8
|
-
// ask; verbs and pings stay with the ask.
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// ask; verbs and pings stay with the ask. No provider reads STATUS.md: a
|
|
9
|
+
// verdict comes from the run record, which gate runs and health passes both
|
|
10
|
+
// write, through the checks collection (collections/checks.ts).
|
|
11
11
|
import { existsSync, readFileSync } from 'fs'
|
|
12
12
|
import { join } from 'path'
|
|
13
13
|
import { isStale } from '../assets/gen/derived.ts'
|
|
@@ -19,13 +19,19 @@ import { assetDir, listRolls, readPick } from '../assets/roll-workspace.ts'
|
|
|
19
19
|
import type { healIdentity } from '../assets/run-assets.ts'
|
|
20
20
|
import { doneAsks, openAsks } from '../asks/asks-queue.ts'
|
|
21
21
|
import { evaluateContracts } from '../contracts/contracts-engine.ts'
|
|
22
|
-
import {
|
|
22
|
+
import { CheckCtx } from '../core/check-ctx.ts'
|
|
23
23
|
import type { RepoConfig } from '../repo-config.ts'
|
|
24
24
|
import { activeContracts, contractsConfigFrom } from '../contracts/apply-contracts.ts'
|
|
25
|
-
import type { Collection, Instance } from './collection-types.ts'
|
|
25
|
+
import type { CheckFacts, Collection, Instance } from './collection-types.ts'
|
|
26
|
+
import { mdBody } from './md-body.ts'
|
|
26
27
|
import { socialPostsCollection } from './posts.ts'
|
|
28
|
+
import { assetState, secretState, type SecretProbe } from './state-rules.ts'
|
|
29
|
+
import { reflectionsCollection } from './reflections.ts'
|
|
30
|
+
import { testsCollection } from './tests.ts'
|
|
31
|
+
import { checksCollection } from './checks.ts'
|
|
32
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
27
33
|
|
|
28
|
-
const askDirOf = (root: string, c: RepoConfig): string => join(root, c.asks?.dir ??
|
|
34
|
+
const askDirOf = (root: string, c: RepoConfig): string => join(root, c.asks?.dir ?? LAYOUT.asks)
|
|
29
35
|
// read path must not mint an asks/ dir (listAsks mkdirs on read)
|
|
30
36
|
const openAsksSafe = (root: string, c: RepoConfig): ReturnType<typeof openAsks> => {
|
|
31
37
|
const dir = askDirOf(root, c)
|
|
@@ -34,21 +40,81 @@ const openAsksSafe = (root: string, c: RepoConfig): ReturnType<typeof openAsks>
|
|
|
34
40
|
|
|
35
41
|
const CRED_LINE = /cred ([a-z0-9-]+\/[a-z0-9-]+\/[a-z0-9-]+) missing/
|
|
36
42
|
|
|
37
|
-
/**
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
/** the secret name as a WHOLE token in a wire line: `rv/npm/token` must not
|
|
44
|
+
* read itself in a line about `rv/npm/token-2` */
|
|
45
|
+
const namesSecret = (text: string, name: string): boolean =>
|
|
46
|
+
new RegExp(`(^|[^\\w/-])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^\\w/-])`).test(text)
|
|
47
|
+
|
|
48
|
+
/** what a secret row reads off one check: the last recorded verdict and the
|
|
49
|
+
* lines that run printed, the fields of the checks collection's own facts */
|
|
50
|
+
export type CheckVerdict = Pick<CheckFacts, 'checkId' | 'status' | 'lastLines' | 'lastAt' | 'measuredAt' | 'keepsUntil' | 'ttl'>
|
|
51
|
+
|
|
52
|
+
/** a check's printed lines, one item per WARN/FAIL/skip line with its
|
|
53
|
+
* indented fix: pointers riding along */
|
|
54
|
+
type PrintedLine = { kind: 'fail' | 'warn' | 'skip' | 'other'; text: string; fixes: string[] }
|
|
55
|
+
const printed = (lines: string[]): PrintedLine[] => {
|
|
56
|
+
const out: PrintedLine[] = []
|
|
57
|
+
for (const raw of lines) {
|
|
58
|
+
const fix = raw.match(/^\s+fix: (.*)$/)
|
|
59
|
+
if (fix != null) { out.at(-1)?.fixes.push(fix[1]!); continue }
|
|
60
|
+
const m = raw.match(/^(FAIL: |WARN |skip: )(.*)$/)
|
|
61
|
+
const kind = m == null ? 'other' : m[1] === 'FAIL: ' ? 'fail' : m[1] === 'WARN ' ? 'warn' : 'skip'
|
|
62
|
+
out.push({ kind, text: m == null ? raw.replace(/^ok: /, '') : m[2]!, fixes: [] })
|
|
63
|
+
}
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** every secret some check proved MISSING, with the line that said so. A
|
|
68
|
+
* check that cannot block has its FAIL demoted to a WARN on the wire, so a
|
|
69
|
+
* WARN counts too when the run record says the check itself failed */
|
|
70
|
+
const missingFrom = (verdicts: CheckVerdict[]): Map<string, PrintedLine> => {
|
|
71
|
+
const out = new Map<string, PrintedLine>()
|
|
72
|
+
for (const v of verdicts)
|
|
73
|
+
for (const l of printed(v.lastLines)) {
|
|
74
|
+
if (!(l.kind === 'fail' || (l.kind === 'warn' && v.status === 'fail'))) continue
|
|
75
|
+
const name = l.text.match(CRED_LINE)?.[1]
|
|
76
|
+
if (name != null && !out.has(name)) out.set(name, l)
|
|
77
|
+
}
|
|
78
|
+
return out
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `just now`, `4m ago`, `3h ago`, `2d ago` */
|
|
82
|
+
const ago = (ms: number): string => {
|
|
83
|
+
const m = Math.max(0, Math.round(ms / 60_000))
|
|
84
|
+
if (m === 0) return 'just now'
|
|
85
|
+
return `${m < 60 ? `${m}m` : m < 60 * 48 ? `${Math.round(m / 60)}h` : `${Math.round(m / 1440)}d`} ago`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** what the probing check's last recorded run says about one present secret,
|
|
89
|
+
* and the plain words the row carries for it */
|
|
90
|
+
export function probeOf(name: string, probedBy: string | undefined, verdicts: Map<string, CheckVerdict>, now: number): { probe: SecretProbe; words: string; md?: string } {
|
|
91
|
+
if (probedBy == null) return { probe: 'none', words: 'no check probes this secret' }
|
|
92
|
+
const v = verdicts.get(probedBy)
|
|
93
|
+
if (v == null) return { probe: 'warn', words: `probed by ${probedBy}, which is not a check of this repo` }
|
|
94
|
+
if (v.status == null) return { probe: 'unmeasured', words: `its probe ${probedBy} never ran here` }
|
|
95
|
+
const lines = printed(v.lastLines)
|
|
96
|
+
const md = v.lastLines.length > 0 ? wireMd(v.lastLines.map((l) => l.trim())) : undefined
|
|
97
|
+
if (v.status === 'skip') return { probe: 'unmeasured', words: `its probe ${probedBy} was skipped`, md }
|
|
98
|
+
const unmeasured = lines.find((l) => l.kind === 'skip' && namesSecret(l.text, name))
|
|
99
|
+
if (unmeasured != null) return { probe: 'unmeasured', words: `${probedBy} did not check it: ${unmeasured.text}`, md }
|
|
100
|
+
if (v.status === 'fail') {
|
|
101
|
+
const why = lines.find((l) => l.kind === 'fail') ?? lines.find((l) => l.kind === 'warn')
|
|
102
|
+
return { probe: 'fail', words: `its probe ${probedBy} failed${why != null ? `: ${why.text}` : ''}`, md }
|
|
48
103
|
}
|
|
49
|
-
|
|
104
|
+
if (v.status === 'warn') {
|
|
105
|
+
const why = lines.find((l) => l.kind === 'warn')
|
|
106
|
+
return { probe: 'warn', words: `its probe ${probedBy} warned${why != null ? `: ${why.text}` : ''}`, md }
|
|
107
|
+
}
|
|
108
|
+
const at = v.measuredAt ?? v.lastAt ?? now
|
|
109
|
+
if (v.keepsUntil != null && now > v.keepsUntil)
|
|
110
|
+
return { probe: 'stale', words: `last checked ${ago(now - at)} by ${probedBy}, older than its ${v.ttl} ttl`, md }
|
|
111
|
+
return { probe: 'pass', words: `checked ${ago(now - at)} by ${probedBy}`, md }
|
|
50
112
|
}
|
|
51
113
|
|
|
114
|
+
/** a wire line as ONE path segment. The line text is the instance id too,
|
|
115
|
+
* so equal slugs mean equal lines and nothing distinct collides */
|
|
116
|
+
const slug = (t: string): string => t.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 72)
|
|
117
|
+
|
|
52
118
|
/** wire lines re-rendered as a fenced block for an instance's md */
|
|
53
119
|
const wireMd = (lines: string[]): string => `\`\`\`\n${lines.join('\n')}\n\`\`\``
|
|
54
120
|
|
|
@@ -58,14 +124,20 @@ export function asksCollection(root: string, c: RepoConfig): Collection {
|
|
|
58
124
|
title: 'asks',
|
|
59
125
|
media: 'text',
|
|
60
126
|
list: async () => {
|
|
61
|
-
const rel = c.asks?.dir ??
|
|
127
|
+
const rel = c.asks?.dir ?? LAYOUT.asks
|
|
62
128
|
const open = openAsksSafe(root, c).map((a): Instance => ({
|
|
63
129
|
id: `ask:${a.file}`,
|
|
64
130
|
text: `${a.title}${a.kind !== '' ? ` (${a.kind})` : ''}`,
|
|
131
|
+
// the tree carries the title and puts the kind in its own
|
|
132
|
+
// slot, so a column of asks reads as a column of kinds
|
|
133
|
+
label: a.title,
|
|
134
|
+
slot: a.kind !== '' ? a.kind : undefined,
|
|
135
|
+
type: 'ask' as const,
|
|
136
|
+
path: `${rel}/${a.file}`,
|
|
65
137
|
severity: 'red' as const,
|
|
66
138
|
verbs: ['done' as const, 'paste-proof' as const],
|
|
67
139
|
file: `${rel}/${a.file}`,
|
|
68
|
-
md:
|
|
140
|
+
md: mdBody(join(root, rel, a.file)),
|
|
69
141
|
remarks: lastRemarks(join(root, rel, a.file)),
|
|
70
142
|
}))
|
|
71
143
|
// the done/ archive rides along QUIET (no severity) — past answers
|
|
@@ -73,8 +145,13 @@ export function asksCollection(root: string, c: RepoConfig): Collection {
|
|
|
73
145
|
const done = doneAsks(askDirOf(root, c)).map((a): Instance => ({
|
|
74
146
|
id: `ask:done/${a.file}`,
|
|
75
147
|
text: `${a.title}${a.kind !== '' ? ` (${a.kind})` : ''}`,
|
|
148
|
+
label: a.title,
|
|
149
|
+
slot: 'done',
|
|
150
|
+
type: 'ask' as const,
|
|
151
|
+
path: `${rel}/done/${a.file}`,
|
|
152
|
+
state: 'quiet' as const,
|
|
76
153
|
file: `${rel}/done/${a.file}`,
|
|
77
|
-
md:
|
|
154
|
+
md: mdBody(join(root, rel, 'done', a.file)),
|
|
78
155
|
remarks: lastRemarks(join(root, rel, 'done', a.file)),
|
|
79
156
|
}))
|
|
80
157
|
return [...open, ...done]
|
|
@@ -82,19 +159,6 @@ export function asksCollection(root: string, c: RepoConfig): Collection {
|
|
|
82
159
|
}
|
|
83
160
|
}
|
|
84
161
|
|
|
85
|
-
// a card, not a document dump — the cut is loud (…) so a clamped card never
|
|
86
|
-
// passes for the whole truth file
|
|
87
|
-
const MD_CAP = 4000
|
|
88
|
-
|
|
89
|
-
/** the ask file's markdown body: frontmatter stripped, trimmed, capped */
|
|
90
|
-
const askMd = (path: string): string | undefined => {
|
|
91
|
-
if (!existsSync(path)) return undefined
|
|
92
|
-
const body = readFileSync(path, 'utf8').replace(/^---\n[\s\S]*?\n---\n?/, '').trim()
|
|
93
|
-
if (body === '') return undefined
|
|
94
|
-
// code-point slice — a UTF-16 cut can strand half a surrogate pair
|
|
95
|
-
return body.length > MD_CAP ? `${[...body].slice(0, MD_CAP).join('')}…` : body
|
|
96
|
-
}
|
|
97
|
-
|
|
98
162
|
/** the latest dated remark lines from a file's tail, the boot briefing */
|
|
99
163
|
const lastRemarks = (path: string, max = 3): string[] | undefined => {
|
|
100
164
|
if (!existsSync(path)) return undefined
|
|
@@ -102,55 +166,73 @@ const lastRemarks = (path: string, max = 3): string[] | undefined => {
|
|
|
102
166
|
return lines.length > 0 ? lines.slice(-max) : undefined
|
|
103
167
|
}
|
|
104
168
|
|
|
105
|
-
|
|
106
|
-
|
|
169
|
+
export type CredsOpts = {
|
|
170
|
+
/** injected by tests; production reads the checks collection, which is
|
|
171
|
+
* the run record every gate run and health pass writes */
|
|
172
|
+
verdicts?: () => Promise<CheckVerdict[]>
|
|
173
|
+
now?: () => number
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** the verdicts a secret row reads, off the checks collection's own rows, so
|
|
177
|
+
* a secret and its probing check can never disagree */
|
|
178
|
+
export const verdictsOf = async (checks: Collection): Promise<CheckVerdict[]> =>
|
|
179
|
+
(await checks.list()).flatMap((i) => (i.check == null ? [] : [i.check]))
|
|
180
|
+
|
|
181
|
+
/** a declared service, in parentheses after the name */
|
|
182
|
+
const named = (name: string, service: string | undefined): string => (service != null ? `${name} (${service})` : name)
|
|
183
|
+
|
|
184
|
+
/** repo-declared secrets (repo.config `creds:`) plus every secret a check
|
|
185
|
+
* proved missing. The tile carries the mint artifact as data. EVERY declared
|
|
107
186
|
* cred is listed. Missing + unclaimed = red with the set verb + artifact;
|
|
108
|
-
* missing + ask-covered = STILL red (never hidden), no verbs
|
|
109
|
-
* points at the ask that owns the action. The
|
|
110
|
-
* pointers) rides the instance's md, never a health line.
|
|
111
|
-
*
|
|
112
|
-
|
|
187
|
+
* missing + ask-covered = STILL red (never hidden), no verbs: the text
|
|
188
|
+
* points at the ask that owns the action. The line that proved it missing
|
|
189
|
+
* (+ its fix: pointers) rides the instance's md, never a health line.
|
|
190
|
+
* A present secret wears the verdict of the check that probes it
|
|
191
|
+
* (`probedBy`), read from the run record (state-rules.ts secretState) */
|
|
192
|
+
export function credsCollection(root: string, c: RepoConfig, o: CredsOpts = {}): Collection {
|
|
113
193
|
return {
|
|
114
194
|
id: 'creds',
|
|
115
195
|
title: 'secrets',
|
|
116
196
|
media: 'pointer',
|
|
117
197
|
list: async () => {
|
|
118
|
-
const rel = c.asks?.dir ??
|
|
198
|
+
const rel = c.asks?.dir ?? LAYOUT.asks
|
|
119
199
|
const asks = openAsksSafe(root, c)
|
|
120
200
|
const coveringAsk = (name: string): { file: string } | undefined =>
|
|
121
201
|
asks.find((a) => a.title.toLowerCase().includes(name.split('/')[1] ?? name))
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const name = i.text.match(CRED_LINE)?.[1]
|
|
127
|
-
if (name != null && !probes.has(name)) probes.set(name, i)
|
|
128
|
-
}
|
|
202
|
+
const verdicts = await (o.verdicts ?? (() => verdictsOf(checksCollection(root, c))))()
|
|
203
|
+
const now = (o.now ?? Date.now)()
|
|
204
|
+
const missing = missingFrom(verdicts)
|
|
205
|
+
const byId = new Map(verdicts.map((v) => [v.checkId, v]))
|
|
129
206
|
const out: Instance[] = []
|
|
130
|
-
for (const [name,
|
|
207
|
+
for (const [name, line] of missing) {
|
|
131
208
|
const decl = (c.creds ?? []).find((d) => d.name === name)
|
|
132
|
-
const md = wireMd([`FAIL: ${
|
|
209
|
+
const md = wireMd([`FAIL: ${line.text}`, ...line.fixes.map((f) => `fix: ${f}`)])
|
|
133
210
|
const ask = coveringAsk(name)
|
|
134
211
|
if (ask != null)
|
|
135
212
|
// dedup = acted-on once, never hidden: the ask keeps the
|
|
136
213
|
// verbs and the ping; the cred still shows RED here
|
|
137
214
|
out.push({
|
|
138
215
|
id: `cred:${name}`,
|
|
139
|
-
text: `${name
|
|
216
|
+
text: `${named(name, decl?.service)} missing, ask ${rel}/${ask.file} owns this`,
|
|
217
|
+
...credPlace(name, true),
|
|
140
218
|
severity: 'red',
|
|
141
219
|
md,
|
|
142
220
|
askOwned: true,
|
|
143
221
|
})
|
|
144
222
|
else out.push({ ...credInstance(name, c.credsCommand, decl), md })
|
|
145
223
|
}
|
|
146
|
-
// every declared cred not proven missing is
|
|
147
|
-
//
|
|
148
|
-
//
|
|
224
|
+
// every declared cred not proven missing is present: no copy
|
|
225
|
+
// command (nothing to run), the mint pointer when one is declared,
|
|
226
|
+
// and the colour of what its probe last proved
|
|
149
227
|
for (const d of c.creds ?? []) {
|
|
150
|
-
if (
|
|
228
|
+
if (missing.has(d.name)) continue
|
|
229
|
+
const p = probeOf(d.name, d.probedBy, byId, now)
|
|
151
230
|
out.push({
|
|
152
231
|
id: `cred:${d.name}`,
|
|
153
|
-
text: `${d.name
|
|
232
|
+
text: `${named(d.name, d.service)} present, ${p.words}`,
|
|
233
|
+
...credPlace(d.name, false),
|
|
234
|
+
...secretState({ missing: false, probe: p.probe }),
|
|
235
|
+
md: p.md,
|
|
154
236
|
artifact: d.mintUrl != null ? { url: d.mintUrl } : undefined,
|
|
155
237
|
})
|
|
156
238
|
}
|
|
@@ -161,9 +243,16 @@ export function credsCollection(root: string, c: RepoConfig): Collection {
|
|
|
161
243
|
|
|
162
244
|
// the store command is repo config DATA (`credsCommand` template) — the lib
|
|
163
245
|
// never names any particular secret manager
|
|
246
|
+
// a secret has no file: the name is already slash shaped, so a virtual root
|
|
247
|
+
// groups it by vendor for free
|
|
248
|
+
const credPlace = (name: string, missing: boolean): Pick<Instance, 'type' | 'path' | 'virtual' | 'label' | 'slot'> =>
|
|
249
|
+
// the tree row is the NAME, and present or missing is the slot beside it
|
|
250
|
+
({ type: 'secret', path: `secrets/${name}`, virtual: true, label: name, slot: missing ? 'missing' : 'present' })
|
|
251
|
+
|
|
164
252
|
const credInstance = (name: string, cmdTemplate: string | undefined, decl?: { service?: string; mintUrl?: string; steps?: string[] }): Instance => ({
|
|
165
253
|
id: `cred:${name}`,
|
|
166
|
-
text: `${name
|
|
254
|
+
text: `${named(name, decl?.service)} missing`,
|
|
255
|
+
...credPlace(name, true),
|
|
167
256
|
severity: 'red',
|
|
168
257
|
artifact: { copy: cmdTemplate?.replaceAll('{name}', name), url: decl?.mintUrl, steps: decl?.steps },
|
|
169
258
|
verbs: ['set'],
|
|
@@ -185,7 +274,7 @@ export function mediaKitCollection(root: string, kit: ResolvedKit | null, heal:
|
|
|
185
274
|
const idn = await heal(root)
|
|
186
275
|
const out: Instance[] = []
|
|
187
276
|
if (idn.state === 'blocked')
|
|
188
|
-
out.push({ id: 'media:identity', text: `identity — ${idn.detail ?? 'blocked'}`, severity: 'red', href: '/#identity-logo', verbs: ['pick'] })
|
|
277
|
+
out.push({ id: 'media:identity', text: `identity — ${idn.detail ?? 'blocked'}`, type: 'asset', path: 'assets/identity', virtual: true, severity: 'red', href: '/#identity-logo', verbs: ['pick'] })
|
|
189
278
|
if (kit == null) return out
|
|
190
279
|
const notes = assetNotes(root)
|
|
191
280
|
for (const f of allAssets(kit)) {
|
|
@@ -195,32 +284,45 @@ export function mediaKitCollection(root: string, kit: ResolvedKit | null, heal:
|
|
|
195
284
|
const base: Instance = {
|
|
196
285
|
id: `media:${f.asset.id}`,
|
|
197
286
|
text: f.asset.id,
|
|
287
|
+
// the roll count is the one fact a row of assets is read for
|
|
288
|
+
slot: rolls.length > 0 ? `${rolls.length} roll${rolls.length === 1 ? '' : 's'}` : manual ? 'manual' : undefined,
|
|
289
|
+
type: 'asset',
|
|
290
|
+
// the roll workspace dir, which is where this asset's
|
|
291
|
+
// bytes actually live: on the kit file they all collide
|
|
292
|
+
// on one node
|
|
293
|
+
path: `${kit.workspace}/${f.asset.id}`,
|
|
198
294
|
href: `/#${f.asset.id}`,
|
|
199
295
|
// image kinds only — /api/thumb serves raw roll bytes, and
|
|
200
296
|
// an mp3 inside an <img> is a broken glyph on every tile
|
|
201
297
|
img: rolls.length > 0 && mediaOf(f.kind) === 'image' ? `/media/api/thumb?id=${encodeURIComponent(f.asset.id)}` : undefined,
|
|
202
|
-
file: '.
|
|
298
|
+
file: '.shipkit/assets/kit.ts',
|
|
203
299
|
remarks: notes.get(f.asset.id),
|
|
204
300
|
}
|
|
205
|
-
|
|
301
|
+
const picked = rolls.length > 0 && (await readPick(dir)).pick != null
|
|
302
|
+
const stale = picked && (await isStale(kit, join(root, kit.workspace), f))
|
|
303
|
+
// the colour comes from the ONE asset rule, the words and the
|
|
304
|
+
// verbs from the case that produced it
|
|
305
|
+
const colour = assetState({ manual, rolls: rolls.length, picked, stale })
|
|
306
|
+
const said: Partial<Instance> = manual && rolls.length === 0
|
|
206
307
|
// the brief IS the paste-ready artifact ("I don't even
|
|
207
308
|
// know what to paste" — the tile hands him the prompt)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
309
|
+
? { text: `${f.asset.id} — manual, nothing uploaded`, label: f.asset.id, verbs: ['provide'], artifact: f.asset.prompt != null ? { copy: f.asset.prompt } : undefined }
|
|
310
|
+
: rolls.length > 0 && !picked
|
|
311
|
+
? { text: `${f.asset.id} — ${rolls.length} roll(s), default in use, none picked`, label: f.asset.id, verbs: ['pick'] }
|
|
312
|
+
: stale
|
|
313
|
+
? { text: `${f.asset.id} — stale (a parent was re-picked)`, label: f.asset.id, verbs: ['pick'] }
|
|
314
|
+
: {}
|
|
315
|
+
out.push({ ...base, ...said, ...colour })
|
|
214
316
|
}
|
|
215
317
|
return out
|
|
216
318
|
},
|
|
217
319
|
}
|
|
218
320
|
}
|
|
219
321
|
|
|
220
|
-
/** `.
|
|
322
|
+
/** `.shipkit/assets/notes.md`, dated asset remarks in one git-tracked file:
|
|
221
323
|
* `- YYYY-MM-DD [asset-id]: text` */
|
|
222
324
|
const assetNotes = (root: string): Map<string, string[]> => {
|
|
223
|
-
const p = join(root, '.
|
|
325
|
+
const p = join(root, '.shipkit/assets', 'notes.md')
|
|
224
326
|
const map = new Map<string, string[]>()
|
|
225
327
|
if (!existsSync(p)) return map
|
|
226
328
|
for (const m of readFileSync(p, 'utf8').matchAll(/^- (\d{4}-\d{2}-\d{2}) \[([^\]]+)\]: (.+)$/gm)) {
|
|
@@ -230,12 +332,13 @@ const assetNotes = (root: string): Map<string, string[]> => {
|
|
|
230
332
|
return map
|
|
231
333
|
}
|
|
232
334
|
|
|
233
|
-
/** repo health —
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
335
|
+
/** repo health — what belongs to no check and no other collection: contract
|
|
336
|
+
* FAIL/WARN lines and hub process staleness. Every check, rule and task of
|
|
337
|
+
* the gate is its own resource now (collections/checks.ts), so re-parsing
|
|
338
|
+
* STATUS.md into one row per wire line would state the same verdicts a
|
|
339
|
+
* second time, under ids that are whole sentences, and the two would
|
|
340
|
+
* disagree the moment one run narrowed. Cred probe FAIL lines (+ their
|
|
341
|
+
* fixes) stay routed to the secrets collection. */
|
|
239
342
|
export function repoHealthCollection(root: string, c: RepoConfig, o?: { bootVersion?: string; libVersion?: () => string }): Collection {
|
|
240
343
|
return {
|
|
241
344
|
id: 'health',
|
|
@@ -246,35 +349,39 @@ export function repoHealthCollection(root: string, c: RepoConfig, o?: { bootVers
|
|
|
246
349
|
if (o?.bootVersion != null && o.libVersion != null) {
|
|
247
350
|
const now = o.libVersion()
|
|
248
351
|
if (now !== o.bootVersion)
|
|
249
|
-
out.push({ id: 'health:stale-hub', severity: 'red', text: `hub is running shipkit ${o.bootVersion} but ${now} is on disk (stale process, stale pages, old studio code). Restart the 🚀 shipkit task` })
|
|
352
|
+
out.push({ id: 'health:stale-hub', type: 'health', path: 'health/stale-hub', virtual: true, severity: 'red', text: `hub is running shipkit ${o.bootVersion} but ${now} is on disk (stale process, stale pages, old studio code). Restart the 🚀 shipkit task` })
|
|
250
353
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// the feed line carries severity already — a raw FAIL:/WARN marker would stack the signal
|
|
254
|
-
// id = full text (never sliced: a shared 60-char prefix must not collide two items — pings dedup on ids)
|
|
255
|
-
out.push({
|
|
256
|
-
id: `health:${i.text}`,
|
|
257
|
-
text: i.text,
|
|
258
|
-
severity: i.kind === 'fail' ? 'red' : i.kind === 'warn' ? 'yellow' : undefined,
|
|
259
|
-
md: i.fixes.length > 0 ? wireMd(i.fixes.map((f) => `fix: ${f}`)) : undefined,
|
|
260
|
-
})
|
|
261
|
-
}
|
|
262
|
-
const r = await evaluateContracts(activeContracts(c), contractsConfigFrom(c, root))
|
|
354
|
+
const disk = CheckCtx.open({ root, config: c })
|
|
355
|
+
const r = await evaluateContracts(activeContracts(c), contractsConfigFrom(c, disk), disk)
|
|
263
356
|
for (const l of r.lines) {
|
|
264
|
-
if (l.kind === 'fail') out.push({ id: `health:contract:${l.text}`, severity: 'red', text: l.text })
|
|
265
|
-
else if (l.kind === 'warn') out.push({ id: `health:contractwarn:${l.text}`, severity: 'yellow', text: l.text })
|
|
357
|
+
if (l.kind === 'fail') out.push({ id: `health:contract:${l.text}`, type: 'contract', path: `health/contracts/${slug(l.text)}`, virtual: true, severity: 'red', text: l.text })
|
|
358
|
+
else if (l.kind === 'warn') out.push({ id: `health:contractwarn:${l.text}`, type: 'contract', path: `health/contracts/${slug(l.text)}`, virtual: true, severity: 'yellow', text: l.text })
|
|
266
359
|
}
|
|
267
360
|
return out
|
|
268
361
|
},
|
|
269
362
|
}
|
|
270
363
|
}
|
|
271
364
|
|
|
365
|
+
/** one listing for every caller while it is in flight: the hub lists every
|
|
366
|
+
* collection at once, and the secrets read the checks listing too */
|
|
367
|
+
const sharedList = (col: Collection): Collection => {
|
|
368
|
+
let inFlight: Promise<Instance[]> | null = null
|
|
369
|
+
return {
|
|
370
|
+
...col,
|
|
371
|
+
list: () => (inFlight ??= col.list().finally(() => { inFlight = null })),
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
272
375
|
export function buildCollections(root: string, c: RepoConfig, kit: ResolvedKit | null, o?: { bootVersion?: string; libVersion?: () => string }): Collection[] {
|
|
376
|
+
const checks = sharedList(checksCollection(root, c))
|
|
273
377
|
return [
|
|
378
|
+
checks,
|
|
274
379
|
repoHealthCollection(root, c, o),
|
|
275
380
|
asksCollection(root, c),
|
|
276
|
-
credsCollection(root, c),
|
|
381
|
+
credsCollection(root, c, { verdicts: () => verdictsOf(checks) }),
|
|
277
382
|
mediaKitCollection(root, kit),
|
|
278
383
|
socialPostsCollection(root),
|
|
384
|
+
reflectionsCollection(root),
|
|
385
|
+
testsCollection(root, c),
|
|
279
386
|
]
|
|
280
387
|
}
|