rvlib-shipkit 1.2.0 → 1.4.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 +104 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +15 -10
- package/src/check.ts +26 -10
- package/src/checks/SK001-package-json-shape.check.ts +34 -5
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +103 -61
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK014-prose-ste.check.ts +16 -6
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK016-legacy-check-shapes.check.ts +11 -5
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
- package/src/checks/SK024-identity-colors.check.ts +5 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +50 -0
- package/src/checks/SK028-readme.check.ts +83 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Ci.ts +5 -0
- package/src/cli/cmd_CrossRepoActivity.ts +119 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +9 -1
- package/src/collections/checks.ts +3 -2
- package/src/collections/collection-types.ts +15 -0
- package/src/collections/providers.ts +7 -3
- package/src/collections/test-time.ts +10 -1
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-runner.ts +66 -17
- package/src/core/discover-checks.ts +10 -5
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +1 -1
- package/src/core/layout.ts +2 -0
- package/src/core/motherhome.ts +88 -66
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +14 -5
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +8 -7
- package/src/core/run-ci.ts +46 -15
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/wire.ts +49 -9
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +138 -0
- package/src/home/activity.ts +256 -0
- package/src/home/global-units.ts +3 -2
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +10 -2
- package/src/home/home-routes.ts +34 -6
- package/src/home/home-start.ts +33 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +40 -7
- package/src/home/process-rows.ts +222 -0
- package/src/home/resources.ts +99 -20
- package/src/home/run-daemon.ts +12 -7
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +6 -3
- package/src/home/supervise.ts +16 -1
- package/src/home/widget-text.ts +119 -0
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +32 -5
- package/src/ledger/db.ts +1 -1
- package/src/ledger/gate-checks.ts +20 -6
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +39 -10
- package/src/ledger/store.ts +30 -2
- package/src/ledger/world-cache.ts +63 -14
- package/src/plan/cadence.ts +21 -5
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +138 -13
- package/src/plan/repo-source.ts +120 -8
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +17 -4
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-words.ts +2 -0
- package/src/serve/hub-app/checks-svc.ts +2 -1
- package/src/serve/hub-app/col-tables-view.tsx +124 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +28 -0
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/hash-route.ts +7 -1
- package/src/serve/hub-app/header-bar.tsx +79 -104
- package/src/serve/hub-app/home-st.ts +50 -13
- package/src/serve/hub-app/home-strip.css +81 -23
- package/src/serve/hub-app/home-strip.tsx +243 -102
- package/src/serve/hub-app/home.html +4 -0
- package/src/serve/hub-app/hub-app.tsx +103 -161
- package/src/serve/hub-app/hub-types.ts +6 -3
- package/src/serve/hub-app/hub.css +101 -39
- package/src/serve/hub-app/icons.tsx +27 -3
- package/src/serve/hub-app/index.html +4 -0
- package/src/serve/hub-app/keys.ts +44 -9
- package/src/serve/hub-app/live-svc.ts +24 -64
- package/src/serve/hub-app/machine-st.ts +65 -13
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/next-run.ts +1 -1
- package/src/serve/hub-app/palette-st.ts +13 -9
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +21 -85
- package/src/serve/hub-app/planned-view.tsx +12 -7
- package/src/serve/hub-app/planned.css +5 -2
- package/src/serve/hub-app/project-list.css +43 -21
- package/src/serve/hub-app/project-list.tsx +122 -23
- package/src/serve/hub-app/projects-svc.ts +120 -12
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +26 -7
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/tests-view.tsx +31 -4
- package/src/serve/hub-app/timeline-chart.tsx +31 -27
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +2 -2
- package/src/serve/hub-app/timeline-view.tsx +29 -9
- package/src/serve/hub-runtime.ts +17 -9
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +100 -20
- package/src/serve/project-gutter.ts +34 -13
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/serve-hub.ts +13 -8
- package/src/serve/tracked-watch.ts +33 -1
- package/src/serve/warm-cell.ts +49 -16
- package/src/shipkit-files.ts +20 -4
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/checks/SK023-stores-under-cap.check.ts +0 -49
- package/src/checks/contracts.check.ts +0 -20
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// shipkit overview: the overview page in the console, top to bottom as the
|
|
2
|
+
// page: the monitor widgets as the daemon last rendered them, their html read
|
|
3
|
+
// as text by widget-text.ts, then the processes through
|
|
4
|
+
// globalProcessesAnswer (the rows `shipkit gp` prints: pm, the daemon and the
|
|
5
|
+
// web first, then the global processes). Nothing renders a widget here: the
|
|
6
|
+
// daemon holds the render, and while it is down the monitor is unknown.
|
|
7
|
+
import { Command, Option } from 'clipanion'
|
|
8
|
+
import { daemonUrl } from '../home/home-paths.ts'
|
|
9
|
+
import type { WidgetsAnswer } from '../home/home-routes.ts'
|
|
10
|
+
import type { ChildView, PmAnswer } from '../home/pm.ts'
|
|
11
|
+
import { splitWidgetHead, widgetText } from '../home/widget-text.ts'
|
|
12
|
+
import { askDaemon, askPm, globalProcessesAnswer, localProcessDeps, renderGlobalProcesses, type GlobalProcessesAnswer, type GlobalProcessesDeps } from './cmd_GlobalProcesses.ts'
|
|
13
|
+
|
|
14
|
+
export type OverviewAnswer = {
|
|
15
|
+
now: number
|
|
16
|
+
/** pm as it answered: its own pid and version, its children; null when it did not */
|
|
17
|
+
pm: { pid: number; since: number | null; version: string; children: ChildView[] } | null
|
|
18
|
+
/** the daemon's identity, asked of the daemon itself; null when it does not answer */
|
|
19
|
+
daemon: { version: string } | null
|
|
20
|
+
/** the global processes, exactly as `shipkit global-processes --json` */
|
|
21
|
+
processes: GlobalProcessesAnswer
|
|
22
|
+
/** the daemon's widgets route payload; null when the daemon does not answer */
|
|
23
|
+
widgets: WidgetsAnswer | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type OverviewDeps = Pick<GlobalProcessesDeps, 'declared' | 'order'> & {
|
|
27
|
+
pm: () => Promise<PmAnswer | null>
|
|
28
|
+
daemon: () => Promise<{ version: string } | null>
|
|
29
|
+
widgets: () => Promise<WidgetsAnswer | null>
|
|
30
|
+
now: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function getJson<T>(url: string): Promise<T | null> {
|
|
34
|
+
try {
|
|
35
|
+
const r = await fetch(url, { signal: AbortSignal.timeout(3_000) })
|
|
36
|
+
return r.ok ? ((await r.json()) as T) : null
|
|
37
|
+
} catch {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** the daemon's widgets route payload, or null */
|
|
43
|
+
export const askWidgets = (url = daemonUrl()): Promise<WidgetsAnswer | null> => getJson<WidgetsAnswer>(`${url}/api/home/widgets.json`)
|
|
44
|
+
|
|
45
|
+
export async function overviewAnswer(d: OverviewDeps): Promise<OverviewAnswer> {
|
|
46
|
+
const [pm, daemon, widgets] = await Promise.all([d.pm(), d.daemon(), d.widgets()])
|
|
47
|
+
const processes = await globalProcessesAnswer({ pm: async () => pm, daemon: async () => daemon, declared: d.declared, order: d.order, now: d.now })
|
|
48
|
+
return { now: d.now, pm: pm == null ? null : { pid: pm.pid, since: pm.since ?? null, version: pm.version, children: pm.children }, daemon, processes, widgets }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const indent = (lines: string[], by = ' '): string[] => lines.map((l) => by + l)
|
|
52
|
+
|
|
53
|
+
/** the page as text: the monitor, then the processes (shipkit's own first),
|
|
54
|
+
* each section saying what is unknown and why when pm or the daemon does not
|
|
55
|
+
* answer */
|
|
56
|
+
export function renderOverview(a: OverviewAnswer): string[] {
|
|
57
|
+
const out: string[] = ['monitor']
|
|
58
|
+
if (a.widgets == null) out.push(` the daemon does not answer on ${daemonUrl()}, and it renders the widgets: the monitor is unknown`)
|
|
59
|
+
else {
|
|
60
|
+
if (a.widgets.widgets.length === 0 && a.widgets.errors.length === 0) out.push(' no widget declared: a repo declares one in a <name>.shipkit.ts file, as export default widget({...})')
|
|
61
|
+
for (const x of a.widgets.widgets) {
|
|
62
|
+
const { head, body } = splitWidgetHead(x.html ?? '')
|
|
63
|
+
out.push(` ${x.title}${head == null ? '' : ` · ${widgetText(head).join(' ')}`}`)
|
|
64
|
+
if (x.error != null) out.push(` WARN ${x.error}`)
|
|
65
|
+
if (x.html == null) out.push(' not rendered yet')
|
|
66
|
+
else out.push(...indent(widgetText(body), ' '))
|
|
67
|
+
}
|
|
68
|
+
for (const e of a.widgets.errors) out.push(` WARN ${e}`)
|
|
69
|
+
}
|
|
70
|
+
out.push('processes', ...indent(renderGlobalProcesses(a.processes)))
|
|
71
|
+
if (a.daemon == null) out.push(` the daemon does not answer on ${daemonUrl()}`)
|
|
72
|
+
return out
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export class OverviewCommand extends Command {
|
|
76
|
+
static override paths = [['overview']]
|
|
77
|
+
|
|
78
|
+
static override usage = Command.Usage({
|
|
79
|
+
category: 'Home',
|
|
80
|
+
description: 'the overview page in the console: every monitor widget as text, from the html the daemon last rendered, then the processes as `shipkit gp` prints them (pm, the daemon and the web with pid, uptime and version, then every global process). When pm or the daemon does not answer, it says which and prints what is still known. Always exits 0',
|
|
81
|
+
examples: [
|
|
82
|
+
['the overview', 'shipkit overview'],
|
|
83
|
+
['the answers the page reads, as json', 'shipkit overview --json'],
|
|
84
|
+
],
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
json = Option.Boolean('--json', false, { description: 'the answers as json: { now, pm, daemon, processes, widgets }, processes as `shipkit gp --json`, widgets the daemon route\'s payload' })
|
|
88
|
+
|
|
89
|
+
async execute(): Promise<number> {
|
|
90
|
+
const a = await overviewAnswer({ ...localProcessDeps(process.cwd()), pm: () => askPm(), daemon: () => askDaemon(), widgets: () => askWidgets(), now: Date.now() })
|
|
91
|
+
if (this.json) {
|
|
92
|
+
this.context.stdout.write(`${JSON.stringify(a, null, 2)}\n`)
|
|
93
|
+
return 0
|
|
94
|
+
}
|
|
95
|
+
for (const l of renderOverview(a)) this.context.stdout.write(`${l}\n`)
|
|
96
|
+
return 0
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// shipkit planned: the planned page in the console. The plan lives in the
|
|
2
|
+
// daemon (its marks, the running batch, the catch up spread all exist only
|
|
3
|
+
// there), so this asks the daemon's route, the one the page polls, and only
|
|
4
|
+
// renders it: the order and every word come from plan-timeline.ts, the page's
|
|
5
|
+
// own functions. Nothing is computed from disk: a plan rebuilt here would load
|
|
6
|
+
// every repo's checks and still not know what the daemon's plan holds.
|
|
7
|
+
import { Command, Option } from 'clipanion'
|
|
8
|
+
import { daemonUrl } from '../home/home-paths.ts'
|
|
9
|
+
import type { PlannedAnswer } from '../home/home-routes.ts'
|
|
10
|
+
import { markOf, spanShort, timeline, titleWords, waitingRows, waitingWords, whenWords, type Section } from '../plan/plan-timeline.ts'
|
|
11
|
+
import type { PlanRow } from '../plan/planner.ts'
|
|
12
|
+
|
|
13
|
+
/** the daemon's planned answer, or null when it does not answer */
|
|
14
|
+
export async function askPlanned(url = daemonUrl()): Promise<PlannedAnswer | null> {
|
|
15
|
+
try {
|
|
16
|
+
const r = await fetch(`${url}/api/home/planned.json`, { signal: AbortSignal.timeout(8_000) })
|
|
17
|
+
if (!r.ok) return null
|
|
18
|
+
return (await r.json()) as PlannedAnswer
|
|
19
|
+
} catch {
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** the answer for one repo: its rows and its load error only */
|
|
25
|
+
export function onlyRepo(a: PlannedAnswer, repo: string): PlannedAnswer {
|
|
26
|
+
return { ...a, rows: a.rows.filter((r) => r.repo === repo), errors: a.errors.filter((e) => e.repo === repo) }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const pad = (s: string, n: number): string => (s.length >= n ? s : s + ' '.repeat(n - s.length))
|
|
30
|
+
|
|
31
|
+
const lastWords = (r: PlanRow, now: number): string =>
|
|
32
|
+
r.last == null ? 'never ran' : `last ${r.last.status} ${spanShort(Math.max(0, now - r.last.at))} ago${r.last.streak > 1 ? ` (${r.last.streak} in a row)` : ''}`
|
|
33
|
+
|
|
34
|
+
const SECTION_WORDS: Record<Section, string> = { now: 'now', next: 'next' }
|
|
35
|
+
|
|
36
|
+
/** the page's one list as text: its three sections in its order, a line per
|
|
37
|
+
* row (when, repo, check and title, mark, last result) and the rule under
|
|
38
|
+
* it; the checks that only wait for a save are counted, or listed with `all` */
|
|
39
|
+
export function renderPlanned(a: PlannedAnswer, o: { all?: boolean } = {}): string[] {
|
|
40
|
+
const out: string[] = [...(a.held ?? [])]
|
|
41
|
+
const list = timeline(a.rows)
|
|
42
|
+
const idle = a.rows.filter((r) => r.state === 'idle')
|
|
43
|
+
const w = {
|
|
44
|
+
when: Math.max(0, ...list.map((x) => whenWords(x.row, a.now).length)),
|
|
45
|
+
what: Math.max(0, ...list.map((x) => `${x.row.repo} ${x.row.check}`.length)),
|
|
46
|
+
}
|
|
47
|
+
let prev: Section | null = null
|
|
48
|
+
for (const { section, row } of list) {
|
|
49
|
+
if (section !== prev) {
|
|
50
|
+
out.push(SECTION_WORDS[section])
|
|
51
|
+
prev = section
|
|
52
|
+
}
|
|
53
|
+
const m = markOf(row)
|
|
54
|
+
const title = titleWords(row.title, row.check)
|
|
55
|
+
const cells = [` ${pad(whenWords(row, a.now), w.when)}`, pad(`${row.repo} ${row.check}`, w.what), m.words, lastWords(row, a.now)]
|
|
56
|
+
out.push(cells.filter((c) => c !== '').join(' '))
|
|
57
|
+
out.push(` ${title == null ? '' : `${title} · `}${row.rule}`)
|
|
58
|
+
if (row.error != null) out.push(` error: ${row.error}`)
|
|
59
|
+
}
|
|
60
|
+
if (list.length === 0) out.push(a.rows.length === 0 ? 'nothing is planned' : 'nothing has a time of its own')
|
|
61
|
+
const waiting = waitingRows(a.rows)
|
|
62
|
+
if (waiting.length > 0) {
|
|
63
|
+
out.push(`${waitingWords(a.rows)}${o.all === true ? '' : ' (--all lists them)'}`)
|
|
64
|
+
if (o.all === true) for (const r of waiting) out.push(` ${r.repo} ${r.check} ${lastWords(r, a.now)} · ${r.rule}`)
|
|
65
|
+
}
|
|
66
|
+
if (o.all === true && idle.length > 0) {
|
|
67
|
+
out.push('on a save only')
|
|
68
|
+
for (const r of [...idle].sort((x, y) => x.repo.localeCompare(y.repo) || x.check.localeCompare(y.check))) out.push(` ${r.repo} ${r.check} ${lastWords(r, a.now)} · ${r.rule}`)
|
|
69
|
+
}
|
|
70
|
+
for (const e of a.errors) out.push(`WARN ${e.repo}: ${e.error}`)
|
|
71
|
+
const n = { now: list.filter((x) => x.section === 'now').length, next: list.filter((x) => x.section === 'next').length }
|
|
72
|
+
out.push(`${n.now} now, ${n.next} next, ${waiting.length} waiting, ${idle.length} on a save only${o.all === true || idle.length === 0 ? '' : ' (--all lists them)'}`)
|
|
73
|
+
return out
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class PlannedCommand extends Command {
|
|
77
|
+
static override paths = [['planned']]
|
|
78
|
+
|
|
79
|
+
static override usage = Command.Usage({
|
|
80
|
+
category: 'Home',
|
|
81
|
+
description: 'the planned page in the console: every check the machine runs on its own time, running now, then next, each with its repo, check and title, when, the rule that planned it and its last result. Asks the daemon, which holds the plan. With the daemon down it says so and exits 0',
|
|
82
|
+
examples: [
|
|
83
|
+
['the plan, as the page lists it', 'shipkit planned'],
|
|
84
|
+
['one repo only', 'shipkit planned --repo my-app'],
|
|
85
|
+
['also the checks that only a save runs', 'shipkit planned --all'],
|
|
86
|
+
['the route\'s exact payload', 'shipkit planned --json'],
|
|
87
|
+
],
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
json = Option.Boolean('--json', false, { description: 'the daemon route\'s exact payload: { now, rows, errors, running, held }' })
|
|
91
|
+
repo = Option.String('--repo', { required: false, description: 'only this repo\'s rows' })
|
|
92
|
+
all = Option.Boolean('--all', false, { description: 'also list the checks that only a save runs (the page leaves them out)' })
|
|
93
|
+
|
|
94
|
+
async execute(): Promise<number> {
|
|
95
|
+
const got = await askPlanned()
|
|
96
|
+
if (got == null) {
|
|
97
|
+
const why = `the daemon does not answer on ${daemonUrl()}, and the plan lives there: no plan is known (\`shipkit pm\` starts it)`
|
|
98
|
+
if (this.json) {
|
|
99
|
+
this.context.stdout.write('null\n')
|
|
100
|
+
this.context.stderr.write(`${why}\n`)
|
|
101
|
+
} else this.context.stdout.write(`${why}\n`)
|
|
102
|
+
return 0
|
|
103
|
+
}
|
|
104
|
+
const a = this.repo == null ? got : onlyRepo(got, this.repo)
|
|
105
|
+
if (this.json) {
|
|
106
|
+
this.context.stdout.write(`${JSON.stringify(a, null, 2)}\n`)
|
|
107
|
+
return 0
|
|
108
|
+
}
|
|
109
|
+
if (this.repo != null && a.rows.length === 0 && a.errors.length === 0) {
|
|
110
|
+
const known = [...new Set(got.rows.map((r) => r.repo))].sort()
|
|
111
|
+
this.context.stdout.write(`no planned check in ${this.repo}; the plan holds ${known.join(', ') || 'no repo'}\n`)
|
|
112
|
+
return 0
|
|
113
|
+
}
|
|
114
|
+
for (const l of renderPlanned(a, { all: this.all })) this.context.stdout.write(`${l}\n`)
|
|
115
|
+
return 0
|
|
116
|
+
}
|
|
117
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -9,7 +9,11 @@ import { AssetsAddCommand, AssetsCheckCommand, AssetsFillCommand, AssetsGenComma
|
|
|
9
9
|
import { BumpCommand } from './cli/cmd_Bump.ts'
|
|
10
10
|
import { CheckCommand } from './cli/cmd_Check.ts'
|
|
11
11
|
import { CommandsCommand } from './cli/cmd_Commands.ts'
|
|
12
|
+
import { aliasArgs, CrossRepoActivityCommand } from './cli/cmd_CrossRepoActivity.ts'
|
|
12
13
|
import { ConfigKeysCommand } from './cli/cmd_ConfigKeys.ts'
|
|
14
|
+
import { GlobalProcessesCommand } from './cli/cmd_GlobalProcesses.ts'
|
|
15
|
+
import { PlannedCommand } from './cli/cmd_Planned.ts'
|
|
16
|
+
import { OverviewCommand } from './cli/cmd_Overview.ts'
|
|
13
17
|
import { CiCommand } from './cli/cmd_Ci.ts'
|
|
14
18
|
import { ContractsCommand } from './cli/cmd_Contracts.ts'
|
|
15
19
|
import { FleetCommand } from './cli/cmd_Fleet.ts'
|
|
@@ -86,6 +90,10 @@ export function buildCli(): Cli {
|
|
|
86
90
|
cli.register(SmokeCommand)
|
|
87
91
|
cli.register(ReflectCommand)
|
|
88
92
|
cli.register(KeysCommand)
|
|
93
|
+
cli.register(GlobalProcessesCommand)
|
|
94
|
+
cli.register(CrossRepoActivityCommand)
|
|
95
|
+
cli.register(PlannedCommand)
|
|
96
|
+
cli.register(OverviewCommand)
|
|
89
97
|
cli.register(ChecksCommand)
|
|
90
98
|
cli.register(UnitsAliasCommand)
|
|
91
99
|
cli.register(CommandsCommand)
|
|
@@ -100,7 +108,7 @@ export function buildCli(): Cli {
|
|
|
100
108
|
// this one (one-lib.ts), so the machine's version runs in every repo. Then pm
|
|
101
109
|
// is started when it is down (autostart.ts), before the command runs
|
|
102
110
|
if (import.meta.main) {
|
|
103
|
-
const argv = process.argv.slice(2)
|
|
111
|
+
const argv = aliasArgs(process.argv.slice(2))
|
|
104
112
|
const { handoff } = await import('./home/handoff.ts')
|
|
105
113
|
handoff(argv)
|
|
106
114
|
const { useOneLib } = await import('./core/one-lib.ts')
|
|
@@ -18,7 +18,7 @@ import { homedir } from 'os'
|
|
|
18
18
|
import { isAbsolute, join, relative, resolve } from 'path'
|
|
19
19
|
import { rawName, routesToShipkit } from '../core/canonical-scripts.ts'
|
|
20
20
|
import { FLOOR_IDS } from '../core/floor.ts'
|
|
21
|
-
import { formatLine,
|
|
21
|
+
import { FOREVER_MS, formatLine, ttlMsOf } from '../core/wire.ts'
|
|
22
22
|
import { gateInventory, type CheckRow } from '../ledger/gate-checks.ts'
|
|
23
23
|
import { readLastLines, readRuns, runsPath, type LastLine, type RunRow } from '../ledger/run-log.ts'
|
|
24
24
|
import { checkStatsFor, type CheckStat } from '../ledger/check-stats.ts'
|
|
@@ -166,7 +166,8 @@ function instanceOf(root: string, c: RepoConfig, row: CheckRow, stat: CheckStat
|
|
|
166
166
|
export function keepsUntil(ttl: string | undefined, stat: Pick<CheckStat, 'measuredAt' | 'measuredStatus'> | undefined): number | null {
|
|
167
167
|
if (ttl == null || ttl === '' || stat?.measuredAt == null) return null
|
|
168
168
|
let ms = 0
|
|
169
|
-
try { ms =
|
|
169
|
+
try { ms = ttlMsOf(ttl) } catch { return null }
|
|
170
|
+
if (ms >= FOREVER_MS && stat.measuredStatus === 'pass') return null
|
|
170
171
|
if (ms <= 0) return null
|
|
171
172
|
return stat.measuredAt + (stat.measuredStatus === 'pass' ? ms : Math.min(ms, NEGATIVE_TTL_CAP_MS))
|
|
172
173
|
}
|
|
@@ -161,6 +161,19 @@ export type TestRun = {
|
|
|
161
161
|
* declaration names a file that is not on disk */
|
|
162
162
|
export type TestStanding = 'runs' | 'ignored' | 'orphan' | 'opaque' | 'missing'
|
|
163
163
|
|
|
164
|
+
/** an ask as its line on the asks page reads it: the frontmatter of its file */
|
|
165
|
+
export type AskFacts = {
|
|
166
|
+
kind: string
|
|
167
|
+
/** `YYYY-MM-DD`, `?` when the file carries none */
|
|
168
|
+
created: string
|
|
169
|
+
/** `YYYY-MM-DD` for a done ask, null otherwise or when it was not stamped */
|
|
170
|
+
closed: string | null
|
|
171
|
+
/** what it blocks, `-` for nothing named */
|
|
172
|
+
blocks: string
|
|
173
|
+
/** the shell probe that closes it once it exits 0, null for a manual ask */
|
|
174
|
+
probe: string | null
|
|
175
|
+
}
|
|
176
|
+
|
|
164
177
|
/** one test file, as its row and its page read it */
|
|
165
178
|
export type TestFacts = {
|
|
166
179
|
/** repo relative */
|
|
@@ -243,6 +256,8 @@ export type Instance = {
|
|
|
243
256
|
check?: CheckFacts
|
|
244
257
|
/** test files only: where it runs and what it last cost */
|
|
245
258
|
test?: TestFacts
|
|
259
|
+
/** asks only: what its frontmatter says */
|
|
260
|
+
ask?: AskFacts
|
|
246
261
|
/** an open ask owns the action (dedup = acted-on once, never hidden):
|
|
247
262
|
* the instance stays red HERE but verbs and pings live with the ask */
|
|
248
263
|
askOwned?: boolean
|
|
@@ -17,12 +17,12 @@ import { assetDir, listRolls, readPick } from '../assets/roll-workspace.ts'
|
|
|
17
17
|
// runtime and the canvas binding, ~400ms of module load, and the hub's
|
|
18
18
|
// collection list is built on every request by callers that mostly never heal
|
|
19
19
|
import type { healIdentity } from '../assets/run-assets.ts'
|
|
20
|
-
import { doneAsks, openAsks } from '../asks/asks-queue.ts'
|
|
20
|
+
import { doneAsks, openAsks, type Ask } from '../asks/asks-queue.ts'
|
|
21
21
|
import { evaluateContracts } from '../contracts/contracts-engine.ts'
|
|
22
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 { CheckFacts, Collection, Instance } from './collection-types.ts'
|
|
25
|
+
import type { AskFacts, CheckFacts, Collection, Instance } from './collection-types.ts'
|
|
26
26
|
import { mdBody } from './md-body.ts'
|
|
27
27
|
import { socialPostsCollection } from './posts.ts'
|
|
28
28
|
import { assetState, secretState, type SecretLevel, type SecretProbe } from './state-rules.ts'
|
|
@@ -31,7 +31,7 @@ import { testsCollection } from './tests.ts'
|
|
|
31
31
|
import { checksCollection } from './checks.ts'
|
|
32
32
|
import { LAYOUT } from '../core/layout.ts'
|
|
33
33
|
import { isBudgetLine } from '../core/budget.ts'
|
|
34
|
-
import { GH_LOGIN_CRED } from '../
|
|
34
|
+
import { GH_LOGIN_CRED } from '../core/gh-login.ts'
|
|
35
35
|
|
|
36
36
|
const askDirOf = (root: string, c: RepoConfig): string => join(root, c.asks?.dir ?? LAYOUT.asks)
|
|
37
37
|
// read path must not mint an asks/ dir (listAsks mkdirs on read)
|
|
@@ -119,6 +119,8 @@ const slug = (t: string): string => t.toLowerCase().replace(/[^a-z0-9]+/g, '-').
|
|
|
119
119
|
/** wire lines re-rendered as a fenced block for an instance's md */
|
|
120
120
|
const wireMd = (lines: string[]): string => `\`\`\`\n${lines.join('\n')}\n\`\`\``
|
|
121
121
|
|
|
122
|
+
const askFacts = (a: Ask): AskFacts => ({ kind: a.kind, created: a.created, closed: a.closed, blocks: a.blocks, probe: a.check })
|
|
123
|
+
|
|
122
124
|
export function asksCollection(root: string, c: RepoConfig): Collection {
|
|
123
125
|
return {
|
|
124
126
|
id: 'asks',
|
|
@@ -141,6 +143,7 @@ export function asksCollection(root: string, c: RepoConfig): Collection {
|
|
|
141
143
|
file: `${rel}/${a.file}`,
|
|
142
144
|
md: mdBody(join(root, rel, a.file)),
|
|
143
145
|
remarks: lastRemarks(join(root, rel, a.file)),
|
|
146
|
+
ask: askFacts(a),
|
|
144
147
|
}))
|
|
145
148
|
// the done/ archive rides along QUIET (no severity) — past answers
|
|
146
149
|
// stay consultable and remarkable; newest first, after every open ask
|
|
@@ -155,6 +158,7 @@ export function asksCollection(root: string, c: RepoConfig): Collection {
|
|
|
155
158
|
file: `${rel}/done/${a.file}`,
|
|
156
159
|
md: mdBody(join(root, rel, 'done', a.file)),
|
|
157
160
|
remarks: lastRemarks(join(root, rel, 'done', a.file)),
|
|
161
|
+
ask: askFacts(a),
|
|
158
162
|
}))
|
|
159
163
|
return [...open, ...done]
|
|
160
164
|
},
|
|
@@ -127,9 +127,15 @@ export type TestCheckRow = {
|
|
|
127
127
|
shape: Shape | null
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/** one file of any test check, named with the check that runs it */
|
|
131
|
+
export type TestFileRow = FileTime & { check: string }
|
|
132
|
+
|
|
130
133
|
export type TestsRootView = {
|
|
131
134
|
/** slowest wall first, the never ran ones last */
|
|
132
135
|
checks: TestCheckRow[]
|
|
136
|
+
/** every file of every test check, slowest first, the never ran ones
|
|
137
|
+
* last: each check's own list, merged */
|
|
138
|
+
files: TestFileRow[]
|
|
133
139
|
failing: FailingFile[]
|
|
134
140
|
}
|
|
135
141
|
|
|
@@ -270,8 +276,10 @@ function lastRunOf(checkId: string, at: number, files: FileTime[], sumMs: number
|
|
|
270
276
|
|
|
271
277
|
export function shapeTestsRoot(o: TestTimeInput): TestsRootView {
|
|
272
278
|
const ids = [...new Set(o.facts.filter((f) => f.standing === 'runs' && f.checkId != null).map((f) => f.checkId!))]
|
|
279
|
+
const files: TestFileRow[] = []
|
|
273
280
|
const checks = ids.map((checkId): TestCheckRow => {
|
|
274
281
|
const v = shapeTestCheck(checkId, o)
|
|
282
|
+
for (const f of v.files) files.push({ ...f, check: checkId })
|
|
275
283
|
const top = v.files.find((f) => f.ms != null)
|
|
276
284
|
return {
|
|
277
285
|
checkId,
|
|
@@ -284,5 +292,6 @@ export function shapeTestsRoot(o: TestTimeInput): TestsRootView {
|
|
|
284
292
|
shape: v.last?.shape ?? null,
|
|
285
293
|
}
|
|
286
294
|
}).sort((a, b) => (b.wallMs ?? -1) - (a.wallMs ?? -1) || a.checkId.localeCompare(b.checkId))
|
|
287
|
-
|
|
295
|
+
files.sort((a, b) => (b.ms ?? -1) - (a.ms ?? -1) || a.file.localeCompare(b.file))
|
|
296
|
+
return { checks, files, failing: failingOf(o.facts) }
|
|
288
297
|
}
|
|
@@ -10,21 +10,22 @@ import { tagCheckLines } from '../check.ts'
|
|
|
10
10
|
import { defineScan } from '../scan.ts'
|
|
11
11
|
import { allContracts } from './contract-manifests.ts'
|
|
12
12
|
import type { ContractManifest } from './contract-types.ts'
|
|
13
|
+
import { webDeploys, webRootOf, type WebDisk } from './web-surface.ts'
|
|
13
14
|
import { evaluateContracts, type ContractsConfig, type ContractsResult } from './contracts-engine.ts'
|
|
14
15
|
|
|
15
|
-
export function contractsConfigFrom(c: RepoConfig, d: Pick<ScanCtx, 'root'
|
|
16
|
+
export function contractsConfigFrom(c: RepoConfig, d: WebDisk & Pick<ScanCtx, 'root'>): ContractsConfig {
|
|
16
17
|
const venues = c.venues ?? []
|
|
17
18
|
const social = c.social ?? []
|
|
18
|
-
const declared = [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
]
|
|
19
|
+
const declared = [...venues, ...(social.length > 0 ? ['social'] : [])]
|
|
20
|
+
const deploys = webDeploys(d)
|
|
21
|
+
// a public repo whose only surface is an npm package has no page to serve an identity on
|
|
22
|
+
const npmOnly = c.release?.npm === true && deploys.length === 0 && venues.length === 0
|
|
23
23
|
return {
|
|
24
24
|
root: d.root,
|
|
25
25
|
declared,
|
|
26
26
|
social,
|
|
27
|
-
publicSurface: venues.length > 0 ||
|
|
27
|
+
publicSurface: venues.length > 0 || deploys.length > 0 || (c.visibility === 'public' && !npmOnly),
|
|
28
|
+
webRoot: webRootOf(c.contracts?.webRoot, deploys),
|
|
28
29
|
exempt: c.contracts?.exempt,
|
|
29
30
|
docsBase: c.docsBase,
|
|
30
31
|
}
|
|
@@ -45,7 +46,7 @@ export function applyContractsResult(t: CheckCtx, r: ContractsResult, tag?: { id
|
|
|
45
46
|
for (const l of lines) t.lines.push(l)
|
|
46
47
|
if (r.exitCode === 1) t.failed = true
|
|
47
48
|
if (r.lines.some((l) => l.kind === 'warn')) t.warned = true
|
|
48
|
-
if (r.lines.
|
|
49
|
+
if (r.lines.every((l) => l.kind === 'note')) {
|
|
49
50
|
const text = 'no contracts apply (nothing declared, nothing detected)'
|
|
50
51
|
t.ok(tag?.id != null ? `[${tag.id}] ${text}` : text)
|
|
51
52
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The data language `*.contract.ts` manifest files are written in: a venue
|
|
2
2
|
// contract = folder + artifact rules + detectors. Evaluation engine lands in
|
|
3
3
|
// ./contracts-engine.ts (0.1). All rule paths are REPO-ROOT-relative so a
|
|
4
|
-
// contract can require files outside its own folder (e.g.
|
|
4
|
+
// contract can require files outside its own folder (e.g. {web}/og.png, where
|
|
5
|
+
// `{web}` is the repo's web root, contracts/web-surface.ts).
|
|
5
6
|
// `warnOnly` demotes a rule to WARN — for mandates gated on a later step.
|
|
6
7
|
|
|
7
8
|
export type RuleOpts = { warnOnly?: boolean }
|
|
@@ -25,9 +26,13 @@ export type Detector =
|
|
|
25
26
|
| { kind: 'script'; pattern: string }
|
|
26
27
|
| { kind: 'fileContains'; path: string; pattern: string }
|
|
27
28
|
| { kind: 'dirExists'; path: string }
|
|
29
|
+
/** a file matching the glob holds a line matching the (multiline) pattern */
|
|
30
|
+
| { kind: 'fileMatches'; glob: string; pattern: string }
|
|
28
31
|
|
|
29
32
|
export type ContractEnv = {
|
|
30
33
|
root: string
|
|
34
|
+
/** the dir the site serves, what `{web}` in a rule path stands for */
|
|
35
|
+
webRoot: string
|
|
31
36
|
venues: string[]
|
|
32
37
|
social: string[]
|
|
33
38
|
exists: (relPath: string) => boolean
|
|
@@ -47,7 +52,7 @@ export type ContractManifest = {
|
|
|
47
52
|
owner: string
|
|
48
53
|
/** `declared` = required once the repo names this venue. `always` = every repo, behind `gate` */
|
|
49
54
|
appliesTo: 'declared' | 'always'
|
|
50
|
-
/** `always` contracts only:
|
|
55
|
+
/** `always` contracts only: applies once a public surface exists, silent before */
|
|
51
56
|
gate?: 'public-surface'
|
|
52
57
|
/** signals that this venue is in play although the repo never declared it, so config cannot lie */
|
|
53
58
|
detect: Detector[]
|
|
@@ -65,6 +70,7 @@ export const custom = (id: string, probe: CustomRule['probe'], p?: RuleOpts): Cu
|
|
|
65
70
|
export const script = (pattern: string): Detector => ({ kind: 'script', pattern })
|
|
66
71
|
export const fileContains = (path: string, pattern: string): Detector => ({ kind: 'fileContains', path, pattern })
|
|
67
72
|
export const dirExists = (path: string): Detector => ({ kind: 'dirExists', path })
|
|
73
|
+
export const fileMatches = (glob: string, pattern: string): Detector => ({ kind: 'fileMatches', glob, pattern })
|
|
68
74
|
|
|
69
75
|
export function validateManifest(m: ContractManifest): string[] {
|
|
70
76
|
const errs: string[] = []
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// - undeclared venue whose detectors fire → FAIL (config can't lie,
|
|
5
5
|
// folders can't rot)
|
|
6
6
|
// - exempt venues skip VISIBLY with their reason; never silence
|
|
7
|
-
// - 'always' contracts with gate 'public-surface':
|
|
8
|
-
//
|
|
7
|
+
// - 'always' contracts with gate 'public-surface': apply once the repo has a
|
|
8
|
+
// public surface, silent before (nothing is served yet).
|
|
9
|
+
// - a rule path's `{web}` is the repo's web root (contracts/web-surface.ts).
|
|
9
10
|
// Every FAIL carries a fix: pointer = docsBase + manifest.owner.
|
|
10
11
|
//
|
|
11
12
|
// It reads ONLY through the disk it is handed (a check context), so what a
|
|
@@ -23,8 +24,10 @@ export type ContractsConfig = {
|
|
|
23
24
|
declared: string[]
|
|
24
25
|
/** social channel list — handed to custom rules via ContractEnv */
|
|
25
26
|
social: string[]
|
|
26
|
-
/** repo has a public surface (deploy
|
|
27
|
+
/** repo has a public surface (a deploy, a declared venue, a public repo) — gates 'always' contracts */
|
|
27
28
|
publicSurface: boolean
|
|
29
|
+
/** the dir the site serves, substituted for `{web}` in rule paths (default 'public') */
|
|
30
|
+
webRoot?: string
|
|
28
31
|
/** venue → reason; skips the venue VISIBLY */
|
|
29
32
|
exempt?: Record<string, string>
|
|
30
33
|
/** prefix for fix: pointers, e.g. a docs checkout path or URL */
|
|
@@ -84,6 +87,14 @@ export function globFiles(pattern: string, d: ContractDisk): string[] {
|
|
|
84
87
|
/** the text of a file, null when no FILE stands there */
|
|
85
88
|
const textOf = (d: ContractDisk, rel: string): string | null => (d.exists(rel, 'file') ? d.read(rel) : null)
|
|
86
89
|
|
|
90
|
+
/** a rule with `{web}` replaced by the web root, `.` meaning the repo root */
|
|
91
|
+
export function atWebRoot(rule: ArtifactRule, webRoot: string): ArtifactRule {
|
|
92
|
+
const sub = (s: string): string => s.replaceAll('{web}/', webRoot === '.' ? '' : `${webRoot}/`)
|
|
93
|
+
if ('path' in rule) return { ...rule, path: sub(rule.path) }
|
|
94
|
+
if ('pattern' in rule) return { ...rule, pattern: sub(rule.pattern) }
|
|
95
|
+
return rule
|
|
96
|
+
}
|
|
97
|
+
|
|
87
98
|
async function checkRule(rule: ArtifactRule, env: ContractEnv, d: ContractDisk): Promise<string[]> {
|
|
88
99
|
const problems: string[] = []
|
|
89
100
|
switch (rule.kind) {
|
|
@@ -145,6 +156,10 @@ function detectorFires(m: ContractManifest, d: ContractDisk): string | null {
|
|
|
145
156
|
for (const det of m.detect) {
|
|
146
157
|
if (det.kind === 'dirExists') {
|
|
147
158
|
if (d.exists(det.path)) return `${det.path}/ exists`
|
|
159
|
+
} else if (det.kind === 'fileMatches') {
|
|
160
|
+
const re = new RegExp(det.pattern, 'm')
|
|
161
|
+
const hit = globFiles(det.glob, d).find((f) => re.test(textOf(d, f) ?? ''))
|
|
162
|
+
if (hit != null) return `${hit} matches /${det.pattern}/`
|
|
148
163
|
} else if (det.kind === 'fileContains') {
|
|
149
164
|
const text = textOf(d, det.path)
|
|
150
165
|
if (text != null && new RegExp(det.pattern).test(text)) return `${det.path} matches /${det.pattern}/`
|
|
@@ -166,8 +181,10 @@ export async function evaluateContracts(manifests: ContractManifest[], cfg: Cont
|
|
|
166
181
|
const lines: Line[] = []
|
|
167
182
|
const outcomes: ContractOutcome[] = []
|
|
168
183
|
let anyFail = false
|
|
184
|
+
const webRoot = cfg.webRoot ?? 'public'
|
|
169
185
|
const env: ContractEnv = {
|
|
170
186
|
root: cfg.root,
|
|
187
|
+
webRoot,
|
|
171
188
|
venues: cfg.declared,
|
|
172
189
|
social: cfg.social,
|
|
173
190
|
exists: (rel) => d.exists(rel),
|
|
@@ -180,7 +197,7 @@ export async function evaluateContracts(manifests: ContractManifest[], cfg: Cont
|
|
|
180
197
|
for (const m of manifests) {
|
|
181
198
|
const exemptReason = cfg.exempt?.[m.venue]
|
|
182
199
|
if (exemptReason != null) {
|
|
183
|
-
lines.push({ kind: 'skip', text: `${m.venue}
|
|
200
|
+
lines.push({ kind: 'skip', text: `${m.venue}: exempt: "${exemptReason}"` })
|
|
184
201
|
outcomes.push({ venue: m.venue, status: 'exempt', problems: 0 })
|
|
185
202
|
continue
|
|
186
203
|
}
|
|
@@ -189,28 +206,32 @@ export async function evaluateContracts(manifests: ContractManifest[], cfg: Cont
|
|
|
189
206
|
const signal = detectorFires(m, d)
|
|
190
207
|
if (signal != null) {
|
|
191
208
|
anyFail = true
|
|
192
|
-
lines.push({ kind: 'fail', text: `${m.venue}
|
|
209
|
+
lines.push({ kind: 'fail', text: `${m.venue}: ships (${signal}) but venue is not declared — declare it or remove the signal` })
|
|
193
210
|
lines.push(...fix(m, 'declare + fill the contract'))
|
|
194
211
|
outcomes.push({ venue: m.venue, status: 'fail', problems: 1 })
|
|
195
212
|
} else {
|
|
213
|
+
// a folder with nothing out yet (local drafts) is work in progress, never a ship
|
|
214
|
+
if (d.exists(m.dir, 'dir')) lines.push({ kind: 'note', text: `${m.venue}: ${m.dir}/ holds work that is not out yet, nothing to check until the venue is declared` })
|
|
196
215
|
outcomes.push({ venue: m.venue, status: 'not-applicable', problems: 0 })
|
|
197
216
|
}
|
|
198
217
|
continue
|
|
199
218
|
}
|
|
200
|
-
//
|
|
201
|
-
|
|
219
|
+
// an 'always' contract gated on a public surface means nothing before one exists
|
|
220
|
+
if (m.appliesTo === 'always' && m.gate === 'public-surface' && !cfg.publicSurface) {
|
|
221
|
+
outcomes.push({ venue: m.venue, status: 'not-applicable', problems: 0 })
|
|
222
|
+
continue
|
|
223
|
+
}
|
|
202
224
|
let problems = 0
|
|
203
225
|
let warned = 0
|
|
204
226
|
for (const rule of m.artifacts) {
|
|
205
|
-
const found = await checkRule(rule, env, d)
|
|
206
|
-
const asWarn = warnAll || rule.warnOnly === true
|
|
227
|
+
const found = await checkRule(atWebRoot(rule, webRoot), env, d)
|
|
207
228
|
for (const p of found) {
|
|
208
|
-
if (
|
|
229
|
+
if (rule.warnOnly === true) {
|
|
209
230
|
warned++
|
|
210
|
-
lines.push({ kind: 'warn', text: `${m.venue}
|
|
231
|
+
lines.push({ kind: 'warn', text: `${m.venue}: ${p}` })
|
|
211
232
|
} else {
|
|
212
233
|
problems++
|
|
213
|
-
lines.push({ kind: 'fail', text: `${m.venue}
|
|
234
|
+
lines.push({ kind: 'fail', text: `${m.venue}: ${p}` })
|
|
214
235
|
}
|
|
215
236
|
}
|
|
216
237
|
}
|
|
@@ -224,7 +245,7 @@ export async function evaluateContracts(manifests: ContractManifest[], cfg: Cont
|
|
|
224
245
|
} else if (warned > 0) {
|
|
225
246
|
outcomes.push({ venue: m.venue, status: 'warn', problems: warned })
|
|
226
247
|
} else {
|
|
227
|
-
lines.push({ kind: 'ok', text: `${m.venue}
|
|
248
|
+
lines.push({ kind: 'ok', text: `${m.venue}: ${m.dir}/ complete (${m.artifacts.length} rule(s))` })
|
|
228
249
|
outcomes.push({ venue: m.venue, status: 'ok', problems: 0 })
|
|
229
250
|
}
|
|
230
251
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// normative WHAT for .shipkit/identity/ — every repo with a public surface owns
|
|
2
|
-
// its brand source + committed favicon/logo/OG outputs
|
|
3
|
-
//
|
|
2
|
+
// its brand source + committed favicon/logo/OG outputs, at the web root its
|
|
3
|
+
// deploy serves (`{web}`). gate: applies once a public surface exists (a
|
|
4
|
+
// deploy, a declared venue, a public repo that is more than an npm package).
|
|
4
5
|
import { defineContract, file, glob, png } from 'rvlib-shipkit/src/contracts/contract-types.ts'
|
|
5
6
|
|
|
6
7
|
export default defineContract({
|
|
@@ -12,8 +13,8 @@ export default defineContract({
|
|
|
12
13
|
detect: [],
|
|
13
14
|
artifacts: [
|
|
14
15
|
file('.shipkit/identity/brand.ts'),
|
|
15
|
-
png('
|
|
16
|
-
png('
|
|
17
|
-
glob('
|
|
16
|
+
png('{web}/favicon.png', 512, 512),
|
|
17
|
+
png('{web}/og.png', 1200, 630, { atLeast: true }),
|
|
18
|
+
glob('{web}/logo.*', { min: 1 }),
|
|
18
19
|
],
|
|
19
20
|
})
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
// normative WHAT for .shipkit/social/ (posts ONLY — the Discord server contract is
|
|
2
2
|
// discord.contract.ts). Declared = the repo config lists social channels.
|
|
3
|
-
|
|
3
|
+
// Undeclared, it ships only once a post is out (`status: posted`): a folder
|
|
4
|
+
// of local drafts is work in progress, never a ship.
|
|
5
|
+
import { custom, defineContract, fileMatches } from 'rvlib-shipkit/src/contracts/contract-types.ts'
|
|
4
6
|
|
|
5
7
|
export default defineContract({
|
|
6
8
|
venue: 'social',
|
|
7
9
|
dir: '.shipkit/social',
|
|
8
10
|
owner: 'distribute/post-publicly.md',
|
|
9
11
|
appliesTo: 'declared',
|
|
10
|
-
detect: [
|
|
12
|
+
detect: [fileMatches('.shipkit/social/**/*.md', '^status:\\s*posted\\b')],
|
|
11
13
|
artifacts: [
|
|
12
14
|
custom('declared-channel-dirs', (env) =>
|
|
13
15
|
env.social
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// normative WHAT for .shipkit/steam/ — the WHY + workflow live in the sibling page.
|
|
2
2
|
// Capsule sizes as of 2026-07 — Valve churns them: re-verify at use, bump here.
|
|
3
|
-
import {
|
|
3
|
+
import { custom, defineContract, dirExists, file, fileContains, glob, md, png, script } from 'rvlib-shipkit/src/contracts/contract-types.ts'
|
|
4
4
|
|
|
5
5
|
export default defineContract({
|
|
6
6
|
venue: 'steam',
|
|
@@ -22,7 +22,11 @@ export default defineContract({
|
|
|
22
22
|
// .ico, dims not png-parseable — presence-checked only
|
|
23
23
|
file('.shipkit/steam/capsules/client-icon.ico'),
|
|
24
24
|
glob('.shipkit/steam/screenshots/*.png', { min: 5, width: 1920, height: 1080 }),
|
|
25
|
-
// vdf scripts
|
|
26
|
-
|
|
25
|
+
// vdf scripts need the Steamworks appid (recorded in steam_appid.txt,
|
|
26
|
+
// Valve's own file name): before it, the line names that blocker
|
|
27
|
+
custom('vdf', (env) => {
|
|
28
|
+
if (!env.exists('.shipkit/steam/steam_appid.txt')) return ['no Steamworks appid yet (.shipkit/steam/steam_appid.txt): the vdf/ build scripts wait on it']
|
|
29
|
+
return env.exists('.shipkit/steam/vdf') ? [] : ['.shipkit/steam/vdf/ missing: the appid exists, write the app build and depot scripts']
|
|
30
|
+
}, { warnOnly: true }),
|
|
27
31
|
],
|
|
28
32
|
})
|