rvlib-shipkit 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +105 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/asks/asks-queue.ts +4 -2
- package/src/assets/kits/identity-kit.ts +15 -13
- package/src/assets/run-assets.ts +12 -2
- package/src/check.ts +73 -98
- package/src/checks/SK001-package-json-shape.check.ts +26 -4
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +20 -6
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
- package/src/checks/SK028-readme.check.ts +10 -2
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +10 -6
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +4 -3
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +24 -4
- package/src/collections/providers.ts +6 -2
- package/src/contracts/apply-contracts.ts +9 -8
- package/src/contracts/contract-types.ts +8 -2
- package/src/contracts/contracts-engine.ts +34 -13
- package/src/contracts/manifests/identity.contract.ts +6 -5
- package/src/contracts/manifests/social.contract.ts +4 -2
- package/src/contracts/manifests/steam.contract.ts +7 -3
- package/src/contracts/web-surface.ts +59 -0
- package/src/core/budget.ts +19 -9
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +63 -25
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +31 -7
- package/src/core/layout.ts +2 -0
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/motherhome.ts +88 -66
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +14 -14
- package/src/core/run-ci.ts +11 -7
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +34 -9
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/activity-shape.ts +4 -1
- package/src/home/activity.ts +22 -15
- package/src/home/gui-app/ShipkitGui.swift +144 -25
- package/src/home/gui-app.ts +93 -30
- package/src/home/home-launchd.ts +30 -6
- package/src/home/home-routes.ts +14 -2
- package/src/home/home-start.ts +33 -5
- package/src/home/hubs.ts +5 -5
- package/src/home/machine-stores.ts +4 -2
- package/src/home/open-app.ts +23 -10
- package/src/home/pm-config.ts +15 -11
- package/src/home/pm-inputs.ts +76 -0
- package/src/home/pm.ts +11 -3
- package/src/home/process-rows.ts +23 -6
- package/src/home/resources.ts +67 -15
- package/src/home/run-daemon.ts +6 -1
- package/src/home/run-pm.ts +30 -5
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/home/widgets.ts +32 -3
- package/src/hooks/commit-message.ts +2 -3
- package/src/hooks/run-hooks.ts +60 -6
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +115 -61
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/store.ts +11 -0
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/ledger/world-cache.ts +17 -15
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +36 -56
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +4 -4
- package/src/serve/hub-app/admin-view.tsx +13 -11
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +16 -36
- package/src/serve/hub-app/col-tables-view.tsx +131 -0
- package/src/serve/hub-app/col-tables.ts +115 -0
- package/src/serve/hub-app/cols-svc.ts +33 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +7 -1
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +15 -3
- package/src/serve/hub-app/home-strip.css +69 -19
- package/src/serve/hub-app/home-strip.tsx +192 -118
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +21 -5
- package/src/serve/hub-app/hub-types.ts +11 -5
- package/src/serve/hub-app/hub.css +88 -44
- package/src/serve/hub-app/icons.tsx +15 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +11 -8
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +34 -49
- package/src/serve/hub-app/palette-st.ts +5 -5
- package/src/serve/hub-app/palette-view.tsx +1 -1
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +38 -0
- package/src/serve/hub-app/project-list.tsx +3 -3
- package/src/serve/hub-app/projects-svc.ts +15 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +117 -0
- package/src/serve/hub-app/repo-st.ts +47 -10
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/timeline-view.tsx +14 -11
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +3 -16
- package/src/serve/project-gutter.ts +1 -2
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/warm-cell.ts +49 -16
- package/src/suite/judge-suites.ts +42 -17
- package/src/suite/suite-cli.ts +5 -8
- package/src/wiki/lint-md.ts +2 -0
- package/src/wiki/scan-md.ts +37 -10
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// shipkit when — what runs when: the legend (every context, every value) and
|
|
2
|
+
// one row per check with its declared value on each axis. The same data the
|
|
3
|
+
// hub's when page draws (core/runs-matrix.ts), so the terminal and the page
|
|
4
|
+
// cannot disagree.
|
|
5
|
+
import { Command, Option } from 'clipanion'
|
|
6
|
+
import { loadRepoConfig } from '../repo-config.ts'
|
|
7
|
+
import { gateInventory } from '../ledger/gate-checks.ts'
|
|
8
|
+
import { legendMarkdown, MATRIX_AXES, matrixLines, RUNS_LEGEND, runsCellsOf, type MatrixAxis } from '../core/runs-matrix.ts'
|
|
9
|
+
import { spliceMarkedBlock } from '../core/doc-block.ts'
|
|
10
|
+
|
|
11
|
+
export const LEGEND_BEGIN = '<!-- shipkit:when-legend -->'
|
|
12
|
+
export const LEGEND_END = '<!-- /shipkit:when-legend -->'
|
|
13
|
+
|
|
14
|
+
export class WhenCommand extends Command {
|
|
15
|
+
static override paths = [['when']]
|
|
16
|
+
|
|
17
|
+
static override usage = Command.Usage({
|
|
18
|
+
category: 'Health',
|
|
19
|
+
description: 'what runs when: every check on every context, with the legend',
|
|
20
|
+
details: 'Every check on every context (commit, ci, full, release, health, save, clock, fix) and how its answer is cached, after the legend of every axis and value. Reports, never gates.',
|
|
21
|
+
examples: [
|
|
22
|
+
['the legend, then every check', 'sk when'],
|
|
23
|
+
['only what the quick gate runs', 'sk when --only onCi'],
|
|
24
|
+
['the matrix without the legend', 'sk when --no-legend'],
|
|
25
|
+
['machine-readable', 'sk when --json'],
|
|
26
|
+
['write the legend into a docs page between its markers', 'sk when --out wiki/build/shipkit/runs.md'],
|
|
27
|
+
['fail if that page is stale (lint mode)', 'sk when --out wiki/build/shipkit/runs.md --check'],
|
|
28
|
+
],
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
only = Option.String('--only', { required: false, description: `only the rows one context runs: ${MATRIX_AXES.join(', ')}` })
|
|
32
|
+
legend = Option.Boolean('--legend', true, { description: 'print the legend first. Default on, --no-legend drops it' })
|
|
33
|
+
out = Option.String('--out', { required: false, description: 'host file carrying the shipkit:when-legend markers. The legend between them is rewritten' })
|
|
34
|
+
check = Option.Boolean('--check', false, { description: 'with --out: exit 1 if the legend is stale, write nothing' })
|
|
35
|
+
json = Option.Boolean('--json', false, { description: 'machine-readable { legend, rows: [{ id, title, from, blocking, build, runs: { cells, cache, off, derivedWhy } }] }' })
|
|
36
|
+
|
|
37
|
+
async execute(): Promise<number> {
|
|
38
|
+
if (this.out != null) return await this.writeLegend(this.out)
|
|
39
|
+
const root = process.cwd()
|
|
40
|
+
const c = await loadRepoConfig(root)
|
|
41
|
+
if (this.only != null && !(MATRIX_AXES as readonly string[]).includes(this.only)) {
|
|
42
|
+
this.context.stdout.write(`FAIL: --only takes one of ${MATRIX_AXES.join(', ')}, got '${this.only}'\n`)
|
|
43
|
+
return 1
|
|
44
|
+
}
|
|
45
|
+
const rows = (await gateInventory(root, c)).map((r) => ({ id: r.decl.id, title: r.title, from: r.from, blocking: r.blocking, build: r.build, runs: runsCellsOf(r) }))
|
|
46
|
+
if (this.json) {
|
|
47
|
+
this.context.stdout.write(`${JSON.stringify({ legend: RUNS_LEGEND, rows })}\n`)
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
50
|
+
for (const l of matrixLines(rows, { legend: this.legend, only: this.only as MatrixAxis | undefined })) this.context.stdout.write(`${l}\n`)
|
|
51
|
+
return 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** the legend only depends on the binary, never on the repo */
|
|
55
|
+
private async writeLegend(path: string): Promise<number> {
|
|
56
|
+
const { existsSync, readFileSync, writeFileSync } = await import('fs')
|
|
57
|
+
if (!existsSync(path)) { this.context.stdout.write(`FAIL: ${path} does not exist\n`); return 1 }
|
|
58
|
+
const host = readFileSync(path, 'utf8')
|
|
59
|
+
const next = spliceMarkedBlock(host, legendMarkdown(), LEGEND_BEGIN, LEGEND_END)
|
|
60
|
+
if (next == null) { this.context.stdout.write(`FAIL: ${path} has no ${LEGEND_BEGIN} / ${LEGEND_END} markers\n`); return 1 }
|
|
61
|
+
if (next === host) { this.context.stdout.write(`ok: ${path} legend current\n`); return 0 }
|
|
62
|
+
if (this.check) {
|
|
63
|
+
this.context.stdout.write(`FAIL: ${path} legend is STALE, the binary's axes or values differ from the page\n`)
|
|
64
|
+
this.context.stdout.write(` fix: shipkit when --out ${path}\n`)
|
|
65
|
+
return 1
|
|
66
|
+
}
|
|
67
|
+
writeFileSync(path, next)
|
|
68
|
+
this.context.stdout.write(`ok: ${path} legend rewritten\n`)
|
|
69
|
+
return 0
|
|
70
|
+
}
|
|
71
|
+
}
|
package/src/cli/cmd_Wiki.ts
CHANGED
|
@@ -10,8 +10,8 @@ export class WikiServeCommand extends Command {
|
|
|
10
10
|
category: 'Wiki',
|
|
11
11
|
description: 'live md link-graph server — reload = rescan',
|
|
12
12
|
examples: [
|
|
13
|
-
['serve the cwd wiki', '
|
|
14
|
-
['serve a subtree on a port', '
|
|
13
|
+
['serve the cwd wiki', 'sk wiki serve'],
|
|
14
|
+
['serve a subtree on a port', 'sk wiki serve --root agent --index process.md --port 6162'],
|
|
15
15
|
],
|
|
16
16
|
})
|
|
17
17
|
|
|
@@ -38,8 +38,8 @@ export class WikiLintCommand extends Command {
|
|
|
38
38
|
category: 'Wiki',
|
|
39
39
|
description: 'fail on orphan pages and dangling refs (exit 1)',
|
|
40
40
|
examples: [
|
|
41
|
-
['lint the cwd wiki', '
|
|
42
|
-
['lint a subtree', '
|
|
41
|
+
['lint the cwd wiki', 'sk wiki lint'],
|
|
42
|
+
['lint a subtree', 'sk wiki lint --root agent --index process.md'],
|
|
43
43
|
],
|
|
44
44
|
})
|
|
45
45
|
|
package/src/cli.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { CheckCommand } from './cli/cmd_Check.ts'
|
|
|
11
11
|
import { CommandsCommand } from './cli/cmd_Commands.ts'
|
|
12
12
|
import { aliasArgs, CrossRepoActivityCommand } from './cli/cmd_CrossRepoActivity.ts'
|
|
13
13
|
import { ConfigKeysCommand } from './cli/cmd_ConfigKeys.ts'
|
|
14
|
-
import { GlobalProcessesCommand } from './cli/cmd_GlobalProcesses.ts'
|
|
14
|
+
import { GlobalProcessesCommand, GlobalProcessRestartCommand, GlobalProcessStartCommand, GlobalProcessStopCommand } from './cli/cmd_GlobalProcesses.ts'
|
|
15
15
|
import { PlannedCommand } from './cli/cmd_Planned.ts'
|
|
16
16
|
import { OverviewCommand } from './cli/cmd_Overview.ts'
|
|
17
17
|
import { CiCommand } from './cli/cmd_Ci.ts'
|
|
@@ -29,6 +29,7 @@ import { ReflectCommand } from './cli/cmd_Reflect.ts'
|
|
|
29
29
|
import { ReleaseCommand } from './cli/cmd_Release.ts'
|
|
30
30
|
import { ReleasesCommand } from './cli/cmd_Releases.ts'
|
|
31
31
|
import { ChecksCommand, UnitsAliasCommand } from './cli/cmd_Checks.ts'
|
|
32
|
+
import { WhenCommand } from './cli/cmd_When.ts'
|
|
32
33
|
import { ServeCommand } from './cli/cmd_Serve.ts'
|
|
33
34
|
import { SmokeCommand } from './cli/cmd_Smoke.ts'
|
|
34
35
|
import { StatusCommand } from './cli/cmd_Status.ts'
|
|
@@ -43,7 +44,7 @@ import { WikiLintCommand, WikiServeCommand } from './cli/cmd_Wiki.ts'
|
|
|
43
44
|
export function buildCli(): Cli {
|
|
44
45
|
const cli = new Cli({
|
|
45
46
|
binaryLabel: 'shipkit — repo toolkit',
|
|
46
|
-
binaryName: '
|
|
47
|
+
binaryName: 'sk',
|
|
47
48
|
binaryVersion: LIB_VERSION,
|
|
48
49
|
enableCapture: true, // console output pipes into the command streams (testable)
|
|
49
50
|
enableColors: true,
|
|
@@ -91,10 +92,14 @@ export function buildCli(): Cli {
|
|
|
91
92
|
cli.register(ReflectCommand)
|
|
92
93
|
cli.register(KeysCommand)
|
|
93
94
|
cli.register(GlobalProcessesCommand)
|
|
95
|
+
cli.register(GlobalProcessStartCommand)
|
|
96
|
+
cli.register(GlobalProcessStopCommand)
|
|
97
|
+
cli.register(GlobalProcessRestartCommand)
|
|
94
98
|
cli.register(CrossRepoActivityCommand)
|
|
95
99
|
cli.register(PlannedCommand)
|
|
96
100
|
cli.register(OverviewCommand)
|
|
97
101
|
cli.register(ChecksCommand)
|
|
102
|
+
cli.register(WhenCommand)
|
|
98
103
|
cli.register(UnitsAliasCommand)
|
|
99
104
|
cli.register(CommandsCommand)
|
|
100
105
|
cli.register(ConfigKeysCommand)
|
|
@@ -20,6 +20,7 @@ import { rawName, routesToShipkit } from '../core/canonical-scripts.ts'
|
|
|
20
20
|
import { FLOOR_IDS } from '../core/floor.ts'
|
|
21
21
|
import { FOREVER_MS, formatLine, ttlMsOf } from '../core/wire.ts'
|
|
22
22
|
import { gateInventory, type CheckRow } from '../ledger/gate-checks.ts'
|
|
23
|
+
import { runsCellsOf } from '../core/runs-matrix.ts'
|
|
23
24
|
import { readLastLines, readRuns, runsPath, type LastLine, type RunRow } from '../ledger/run-log.ts'
|
|
24
25
|
import { checkStatsFor, type CheckStat } from '../ledger/check-stats.ts'
|
|
25
26
|
import { NEGATIVE_TTL_CAP_MS } from '../ledger/world-cache.ts'
|
|
@@ -114,6 +115,7 @@ function instanceOf(root: string, c: RepoConfig, row: CheckRow, stat: CheckStat
|
|
|
114
115
|
description: row.description,
|
|
115
116
|
lastLines: wireLines,
|
|
116
117
|
gate: row.gate,
|
|
118
|
+
runs: runsCellsOf(row),
|
|
117
119
|
build: row.build,
|
|
118
120
|
blocking: row.blocking,
|
|
119
121
|
advisory,
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// the docs wiki this lib ships beside.
|
|
8
8
|
|
|
9
9
|
import type { WhenAt, WhenSource } from '../core/when.ts'
|
|
10
|
+
import type { RunsCells } from '../core/runs-matrix.ts'
|
|
10
11
|
import type { CheckFrom } from '../ledger/gate-checks.ts'
|
|
11
12
|
|
|
12
13
|
export type NeedSeverity = 'red' | 'yellow'
|
|
@@ -22,14 +23,15 @@ export const RUNS_ID = 'runs'
|
|
|
22
23
|
* row, the runs page and the to fix list all open the same page by it */
|
|
23
24
|
export const testId = (file: string): string => `test:${file}`
|
|
24
25
|
|
|
25
|
-
/**
|
|
26
|
+
/** the path segment of a test file no check reaches, where a check id sits
|
|
27
|
+
* for the others, so the filter finds a test file by its check */
|
|
26
28
|
export const NO_CHECK = 'in no check'
|
|
27
29
|
|
|
28
|
-
/** the resource KIND, never the collection it came from:
|
|
29
|
-
*
|
|
30
|
+
/** the resource KIND, never the collection it came from: it decides the
|
|
31
|
+
* section a row lives under (hub-app/tree-build.ts KIND_SECTION). There is no
|
|
30
32
|
* catch all kind: a row belongs to an owner, and what belongs to the hub
|
|
31
33
|
* itself is its status (serve/hub-problems.ts), never a row */
|
|
32
|
-
export const RESOURCE_TYPES = ['ask', 'secret', 'asset', 'post', 'workflow', 'wiki', 'contract', 'check', 'test', 'reflection'] as const
|
|
34
|
+
export const RESOURCE_TYPES = ['ask', 'secret', 'asset', 'post', 'workflow', 'wiki', 'contract', 'check', 'test', 'reflection', 'process'] as const
|
|
33
35
|
|
|
34
36
|
export type ResourceType = (typeof RESOURCE_TYPES)[number]
|
|
35
37
|
|
|
@@ -53,6 +55,9 @@ export type CheckFacts = {
|
|
|
53
55
|
lastLines: string[]
|
|
54
56
|
/** does `shipkit ci` run it? false = proactive, `shipkit check` only */
|
|
55
57
|
gate: boolean
|
|
58
|
+
/** every context it runs in and how it caches, as the when page draws
|
|
59
|
+
* it (core/runs-matrix.ts) */
|
|
60
|
+
runs: RunsCells
|
|
56
61
|
/** a BUILD STEP: the gate runs it, the health pass does not (derived from
|
|
57
62
|
* a floor slot or the repo's `tasks: []`, never declared by a check) */
|
|
58
63
|
build: boolean
|
|
@@ -161,6 +166,19 @@ export type TestRun = {
|
|
|
161
166
|
* declaration names a file that is not on disk */
|
|
162
167
|
export type TestStanding = 'runs' | 'ignored' | 'orphan' | 'opaque' | 'missing'
|
|
163
168
|
|
|
169
|
+
/** an ask as its line on the asks page reads it: the frontmatter of its file */
|
|
170
|
+
export type AskFacts = {
|
|
171
|
+
kind: string
|
|
172
|
+
/** `YYYY-MM-DD`, `?` when the file carries none */
|
|
173
|
+
created: string
|
|
174
|
+
/** `YYYY-MM-DD` for a done ask, null otherwise or when it was not stamped */
|
|
175
|
+
closed: string | null
|
|
176
|
+
/** what it blocks, `-` for nothing named */
|
|
177
|
+
blocks: string
|
|
178
|
+
/** the shell probe that closes it once it exits 0, null for a manual ask */
|
|
179
|
+
probe: string | null
|
|
180
|
+
}
|
|
181
|
+
|
|
164
182
|
/** one test file, as its row and its page read it */
|
|
165
183
|
export type TestFacts = {
|
|
166
184
|
/** repo relative */
|
|
@@ -243,6 +261,8 @@ export type Instance = {
|
|
|
243
261
|
check?: CheckFacts
|
|
244
262
|
/** test files only: where it runs and what it last cost */
|
|
245
263
|
test?: TestFacts
|
|
264
|
+
/** asks only: what its frontmatter says */
|
|
265
|
+
ask?: AskFacts
|
|
246
266
|
/** an open ask owns the action (dedup = acted-on once, never hidden):
|
|
247
267
|
* the instance stays red HERE but verbs and pings live with the ask */
|
|
248
268
|
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'
|
|
@@ -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
|
},
|
|
@@ -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
|
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// What a repo serves on the web, read from how it deploys: a GitHub Pages
|
|
2
|
+
// workflow, a wrangler config, or deploy.sh. The web root is where the
|
|
3
|
+
// identity outputs (favicon, logo, OG image) must land to be served, so it
|
|
4
|
+
// comes from the deploy, never a hardcoded `public/`.
|
|
5
|
+
// pure over the disk it is handed: SK002 is cached on what it reads through it
|
|
6
|
+
import type { ScanCtx } from '../core/check-ctx.ts'
|
|
7
|
+
|
|
8
|
+
export type WebDisk = Pick<ScanCtx, 'read' | 'exists' | 'entries'>
|
|
9
|
+
/** `source`: the file that says the repo deploys; `root`: the served dir, null when that file does not name one */
|
|
10
|
+
export type WebDeploy = { source: string; root: string | null }
|
|
11
|
+
|
|
12
|
+
const WORKFLOWS = '.github/workflows'
|
|
13
|
+
const PAGES = /actions\/(?:deploy-pages|upload-pages-artifact)|peaceiris\/actions-gh-pages|cloudflare\/(?:pages|wrangler)-action|wrangler\s+pages\s+deploy/
|
|
14
|
+
const WRANGLER = ['wrangler.toml', 'wrangler.json', 'wrangler.jsonc']
|
|
15
|
+
export const DEFAULT_WEB_ROOT = 'public'
|
|
16
|
+
|
|
17
|
+
const clean = (dir: string): string => dir.replace(/^["']|["']$/g, '').replace(/^\.\//, '').replace(/\/+$/, '') || '.'
|
|
18
|
+
|
|
19
|
+
/** the served dir a deploy file names, null when it names none */
|
|
20
|
+
function rootIn(text: string): string | null {
|
|
21
|
+
const pick = (re: RegExp): string | null => {
|
|
22
|
+
const m = text.match(re)
|
|
23
|
+
return m?.[1] != null ? clean(m[1]) : null
|
|
24
|
+
}
|
|
25
|
+
return (
|
|
26
|
+
pick(/upload-pages-artifact@[^\n]*\n(?:[ \t]+[^\n]*\n)*?[ \t]+path:\s*(\S+)/) ??
|
|
27
|
+
pick(/publish_dir:\s*(\S+)/) ??
|
|
28
|
+
pick(/wrangler\s+pages\s+deploy\s+([^\s-]\S*)/) ??
|
|
29
|
+
pick(/pages_build_output_dir["']?\s*[:=]\s*["']([^"']+)["']/) ??
|
|
30
|
+
pick(/\[assets\][^[]*?directory\s*=\s*["']([^"']+)["']/) ??
|
|
31
|
+
pick(/"assets"\s*:\s*\{[^}]*"directory"\s*:\s*"([^"]+)"/) ??
|
|
32
|
+
pick(/\[site\][^[]*?bucket\s*=\s*["']([^"']+)["']/)
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** every deploy this repo declares, in the order a web root is read from */
|
|
37
|
+
export function webDeploys(d: WebDisk): WebDeploy[] {
|
|
38
|
+
const out: WebDeploy[] = []
|
|
39
|
+
for (const e of d.entries(WORKFLOWS)) {
|
|
40
|
+
if (e.kind !== 'file' || !/\.ya?ml$/.test(e.name)) continue
|
|
41
|
+
const text = d.read(`${WORKFLOWS}/${e.name}`) ?? ''
|
|
42
|
+
if (PAGES.test(text)) out.push({ source: `${WORKFLOWS}/${e.name}`, root: rootIn(text) })
|
|
43
|
+
}
|
|
44
|
+
for (const f of WRANGLER) {
|
|
45
|
+
const text = d.exists(f, 'file') ? d.read(f) : null
|
|
46
|
+
if (text != null) out.push({ source: f, root: rootIn(text) })
|
|
47
|
+
}
|
|
48
|
+
const sh = d.exists('deploy.sh', 'file') ? d.read('deploy.sh') : null
|
|
49
|
+
if (sh != null) out.push({ source: 'deploy.sh', root: rootIn(sh) })
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** where the identity outputs go: the config's `contracts.webRoot`, else the
|
|
54
|
+
* first dir a deploy names, else `public` */
|
|
55
|
+
export function webRootOf(configured: string | undefined, deploys: WebDeploy[]): string {
|
|
56
|
+
if (configured != null) return clean(configured)
|
|
57
|
+
return deploys.find((x) => x.root != null)?.root ?? DEFAULT_WEB_ROOT
|
|
58
|
+
}
|
|
59
|
+
|
package/src/core/budget.ts
CHANGED
|
@@ -11,12 +11,14 @@ import type { MachineLoad } from '../suite/judge-suites.ts'
|
|
|
11
11
|
* 2s at their 90th percentile, and the few past it shell out to a tool */
|
|
12
12
|
export const IN_PROCESS_BUDGET_MS = 2_000
|
|
13
13
|
|
|
14
|
-
/** a subprocess (a build step, a test suite, a `cmd` check) or a world probe
|
|
14
|
+
/** a subprocess (a build step, a test suite, a `cmd` check) or a world probe
|
|
15
|
+
* (a `ttl` check, or a `never:` one, whose answer lives outside the repo).
|
|
15
16
|
* Read off the same record: every typecheck stays under 6s at its 90th
|
|
16
17
|
* percentile, and the quick test suites under 6s but one */
|
|
17
18
|
export const SUBPROCESS_BUDGET_MS = 10_000
|
|
18
19
|
|
|
19
|
-
/** what a check declares about itself that picks its default
|
|
20
|
+
/** what a check declares about itself that picks its default. `world`: it
|
|
21
|
+
* declares `ttl` or `never`, so it reads the world rather than the repo */
|
|
20
22
|
export type BudgetKind = { build: boolean; cmd: boolean; world: boolean }
|
|
21
23
|
|
|
22
24
|
export const defaultBudget = (k: BudgetKind): number => (k.build || k.cmd || k.world ? SUBPROCESS_BUDGET_MS : IN_PROCESS_BUDGET_MS)
|
|
@@ -36,7 +38,15 @@ export const unknownBudgetIds = (ids: readonly string[], budgets: Record<string,
|
|
|
36
38
|
export const budgetValueProblem = (v: unknown): string | null =>
|
|
37
39
|
typeof v === 'number' && Number.isInteger(v) && v > 0 ? null : `must be a whole number of milliseconds above 0, got ${JSON.stringify(v)}`
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
/** `<load1>/<cores>` in this variable pins the reading: a test of a budget
|
|
42
|
+
* verdict must not depend on what else the machine runs */
|
|
43
|
+
export const MACHINE_LOAD_ENV = 'SHIPKIT_MACHINE_LOAD'
|
|
44
|
+
|
|
45
|
+
export const machineLoad = (): MachineLoad => {
|
|
46
|
+
const pinned = process.env[MACHINE_LOAD_ENV]?.match(/^(\d+(?:\.\d+)?)\/(\d+)$/)
|
|
47
|
+
if (pinned != null) return { load1: Number(pinned[1]), cores: Number(pinned[2]) }
|
|
48
|
+
return { load1: loadavg()[0] ?? 0, cores: availableParallelism() }
|
|
49
|
+
}
|
|
40
50
|
|
|
41
51
|
/** `10s` for a whole second count, `150ms` otherwise */
|
|
42
52
|
export const fmtBudget = (ms: number): string => (ms % 1000 === 0 ? `${ms / 1000}s` : `${ms}ms`)
|
|
@@ -49,12 +59,12 @@ export const isBudgetLine = (text: string): boolean => /^\S+ (passed|warned) in
|
|
|
49
59
|
/** a test check sends the reader to the page that breaks its time down */
|
|
50
60
|
const isTestCheck = (id: string): boolean => id === 'test' || id.startsWith('test:')
|
|
51
61
|
|
|
52
|
-
/** the WARN a pass over its budget prints, or null inside it.
|
|
53
|
-
*
|
|
54
|
-
* is the
|
|
62
|
+
/** the WARN a pass over its budget prints, or null inside it. Null too with
|
|
63
|
+
* the load at or past the core count: the budget judges wall time, and on a
|
|
64
|
+
* saturated machine the wall is the machine's, not the check's (a suite's
|
|
65
|
+
* own verdict judges CPU instead, suite/judge-suites.ts) */
|
|
55
66
|
export function budgetLine(id: string, ms: number, budgetMs: number, load: MachineLoad, verb: 'passed' | 'warned' = 'passed'): string | null {
|
|
56
|
-
if (ms <= budgetMs) return null
|
|
57
|
-
const loaded = load.load1 >= load.cores ? ` under load ${load.load1.toFixed(1)} on ${load.cores} cores,` : ''
|
|
67
|
+
if (ms <= budgetMs || load.load1 >= load.cores) return null
|
|
58
68
|
const where = isTestCheck(id) ? 'see the tests page for where the time goes' : 'its page in the hub shows every run'
|
|
59
|
-
return `${id} ${verb} in ${(ms / 1000).toFixed(1)}s
|
|
69
|
+
return `${id} ${verb} in ${(ms / 1000).toFixed(1)}s, over its ${fmtBudget(budgetMs)} budget: ${where}`
|
|
60
70
|
}
|