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
|
@@ -48,6 +48,7 @@ import { basename, join } from 'path'
|
|
|
48
48
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
49
49
|
import { allowMajor, breakingAdvice, isBreaking } from '../core/update-policy.ts'
|
|
50
50
|
import { isRunningCopy, rememberLatest } from '../core/self-update.ts'
|
|
51
|
+
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
51
52
|
import { mutateLease, withMutateLease } from '../core/locked-write.ts'
|
|
52
53
|
import { detectPackageManager, isRefusal, LOCK_FILES } from '../core/package-manager.ts'
|
|
53
54
|
import { check, type CheckEnv } from '../check.ts'
|
|
@@ -77,6 +78,11 @@ export const rvlibVersions = check({
|
|
|
77
78
|
ttl: '24h',
|
|
78
79
|
// it updates the rvlib deps unless the run is frozen
|
|
79
80
|
writes: true,
|
|
81
|
+
planOff: (env) => {
|
|
82
|
+
let pkg: Pkg
|
|
83
|
+
try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'no readable package.json' }
|
|
84
|
+
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).some((n) => n.startsWith('rvlib-')) ? null : 'no rvlib-* dependency'
|
|
85
|
+
},
|
|
80
86
|
cacheKey: (env) => {
|
|
81
87
|
let pkg: Pkg
|
|
82
88
|
try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'unreadable' }
|
|
@@ -84,7 +90,7 @@ export const rvlibVersions = check({
|
|
|
84
90
|
.filter((n) => n.startsWith('rvlib-'))
|
|
85
91
|
.sort()
|
|
86
92
|
.map((n) => `${n}@${installedVersion(env.root, n) ?? 'absent'}`)
|
|
87
|
-
.join(',')
|
|
93
|
+
.join(',') + (rootPublishesNpm(env.config) ? ' npm' : '')
|
|
88
94
|
},
|
|
89
95
|
run: async (c) => {
|
|
90
96
|
let pkg: Pkg
|
|
@@ -137,6 +143,12 @@ export const rvlibVersions = check({
|
|
|
137
143
|
c.warn(`newer rvlib live: ${drift} (frozen — apply with: ${cmd.join(' ')})`)
|
|
138
144
|
return
|
|
139
145
|
}
|
|
146
|
+
// a package published to npm: its package.json is its public
|
|
147
|
+
// contract, so the bump is a release decision, named and never made
|
|
148
|
+
if (rootPublishesNpm(c.config)) {
|
|
149
|
+
c.warn(`newer rvlib live: ${drift} (published to npm, never updated by a check — apply with: ${cmd.join(' ')})`)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
140
152
|
const dirty = await c.exec(['git', 'status', '--porcelain', '--', ...manifest], { cwd: c.root })
|
|
141
153
|
if (dirty.code !== 0 || dirty.out.trim() !== '') {
|
|
142
154
|
c.warn(`newer rvlib live: ${drift} — self-update deferred (${dirty.code !== 0 ? `git says: ${dirty.out.trim().split('\n')[0] ?? `exit ${dirty.code}`}` : 'package.json/lockfile dirty'}); commit first, then re-run check`)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { join, relative } from 'path'
|
|
16
16
|
import { checkUnitReader, resolveUnits } from '../release/unit-resolve.ts'
|
|
17
17
|
import { check } from '../check.ts'
|
|
18
|
+
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
18
19
|
|
|
19
20
|
type Pkg = {
|
|
20
21
|
name?: string
|
|
@@ -65,6 +66,10 @@ export const npmPublishSafety = check({
|
|
|
65
66
|
if (localHits.length === 0) c.ok('rvlib-* package — publishes by design (distribute/npm owns it)')
|
|
66
67
|
return
|
|
67
68
|
}
|
|
69
|
+
if (rootPublishesNpm(c.config)) {
|
|
70
|
+
if (localHits.length === 0) c.ok('the root unit declares release.npm: publishes by design, no local dependency protocol in any releasing unit')
|
|
71
|
+
return
|
|
72
|
+
}
|
|
68
73
|
if (pkg.private !== true) return c.note('private:true missing — SK001 reports it; publish machinery not judged until the lock is on')
|
|
69
74
|
const machinery: string[] = []
|
|
70
75
|
if (pkg.publishConfig != null) machinery.push('publishConfig')
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// own base-repo resolution prefers `upstream`, which in a fork-shaped
|
|
9
9
|
// checkout would confidently validate the WRONG repository. Flipping a
|
|
10
10
|
// repo public stays a human act behind the pre-public audit; this rule is
|
|
11
|
-
// the standing tripwire if it ever happens out of band.
|
|
12
|
-
//
|
|
11
|
+
// the standing tripwire if it ever happens out of band. A decided fact: a
|
|
12
|
+
// clean pass is kept forever, keyed on the declared value (a flip re-probes
|
|
13
|
+
// immediately, and the plan asks only while no answer exists); every
|
|
13
14
|
// unverifiable path (no git, no remote, non-github remote, gh missing or
|
|
14
15
|
// erroring, unreadable answer) is a loud skip — an UNKNOWN, never red,
|
|
15
16
|
// never cached.
|
|
@@ -17,20 +18,19 @@ import { carveGhJson, firstGhLine } from '../git-bootstrap.ts'
|
|
|
17
18
|
import { probeGitRoot } from '../core/git-root.ts'
|
|
18
19
|
import { nestedIn } from '../core/nesting.ts'
|
|
19
20
|
import { check } from '../check.ts'
|
|
21
|
+
import { declaredVisibility } from '../core/repo-facts.ts'
|
|
20
22
|
|
|
21
23
|
export const githubVisibility = check({
|
|
22
24
|
id: 'SK007',
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
// unverifiable path above is a skip, so an offline machine never goes red
|
|
26
|
-
blocking: true,
|
|
25
|
+
// a gate block does not un-leak a repo: the red shows on the needs surface
|
|
26
|
+
blocking: false,
|
|
27
27
|
title: 'github visibility',
|
|
28
28
|
description: 'the github visibility of this repo matches the one it declares',
|
|
29
29
|
owner: 'meta/contracts.md',
|
|
30
|
-
ttl: '
|
|
31
|
-
cacheKey: (env) => env.config
|
|
30
|
+
ttl: 'forever',
|
|
31
|
+
cacheKey: (env) => declaredVisibility(env.config),
|
|
32
32
|
run: async (c) => {
|
|
33
|
-
const declared = c.config
|
|
33
|
+
const declared = declaredVisibility(c.config)
|
|
34
34
|
const skip = (why: string): void => void c.lines.push({ kind: 'skip', text: `github visibility unverified — ${why} (declared: ${declared})` })
|
|
35
35
|
const firstLine = firstGhLine
|
|
36
36
|
const repo = probeGitRoot(c.root)
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
// harness then inherits every tool. The brief itself must exist. No
|
|
9
9
|
// declaration = not a reviewer repo, and the rule passes silently.
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync } from 'fs'
|
|
11
|
-
import {
|
|
12
|
-
import { dirname, isAbsolute, join } from 'path'
|
|
11
|
+
import { dirname, join } from 'path'
|
|
13
12
|
import type { RepoConfig } from '../repo-config.ts'
|
|
14
13
|
import { check, type FixResult, type CheckEnv } from '../check.ts'
|
|
15
14
|
import { LAYOUT } from '../core/layout.ts'
|
|
16
15
|
import { atomicWrite } from '../core/locked-write.ts'
|
|
16
|
+
import { configPath, fileStamp } from '../core/file-stamp.ts'
|
|
17
|
+
import { CONFIG_FILE } from '../core/check-pass.ts'
|
|
17
18
|
|
|
18
19
|
/** where a review lands when its caller named no output file:
|
|
19
20
|
* `YYYYMMDD-HHMMSS-<review-name>.md`, local time, gitignored */
|
|
@@ -48,8 +49,7 @@ export function reviewerAgentIssues(text: string, reviewer: NonNullable<RepoConf
|
|
|
48
49
|
/** the brief must EXIST: a reviewer whose mandated first action is reading a
|
|
49
50
|
* missing file flounders silently, which is the one thing this rule prevents */
|
|
50
51
|
export function briefMissing(root: string, brief: string): boolean {
|
|
51
|
-
|
|
52
|
-
return !existsSync(p)
|
|
52
|
+
return !existsSync(configPath(root, brief))
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function canonicalAgent(reviewer: NonNullable<RepoConfig['reviewer']>): string {
|
|
@@ -83,12 +83,18 @@ That brief overrides any habit of reviewing a diff for correctness alone.
|
|
|
83
83
|
export const reviewerAgent = check({
|
|
84
84
|
id: 'SK008',
|
|
85
85
|
blocking: true,
|
|
86
|
-
title: 'reviewer
|
|
86
|
+
title: 'reviewer definition',
|
|
87
87
|
description: 'the declared reviewer agent has its brief, its definition and read only tools',
|
|
88
88
|
owner: 'identity/how-we-work.md',
|
|
89
|
-
// the
|
|
90
|
-
//
|
|
91
|
-
|
|
89
|
+
// the answer is the config, the definition file and the brief. The brief
|
|
90
|
+
// is a CONFIG path that may sit outside the repo, so both files enter the
|
|
91
|
+
// key by content, wherever they live
|
|
92
|
+
inputs: [CONFIG_FILE],
|
|
93
|
+
cacheKey: (env) => {
|
|
94
|
+
const r = env.config.reviewer
|
|
95
|
+
if (r == null) return 'no reviewer'
|
|
96
|
+
return `${r.name}|${(r.tools ?? REVIEWER_TOOLS).join(',')}|def=${fileStamp(join(env.root, reviewerAgentPath(r.name)))}|${r.brief}=${fileStamp(configPath(env.root, r.brief))}`
|
|
97
|
+
},
|
|
92
98
|
run: (c) => {
|
|
93
99
|
const r = c.config.reviewer
|
|
94
100
|
if (r == null) return c.ok('no reviewer agent declared (repo.config reviewer:) — skipped')
|
|
@@ -1,18 +1,22 @@
|
|
|
1
|
-
// SK009
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
// an unpushed tip commit may be amended, and the fails say so.
|
|
1
|
+
// SK009 commit AI tells (identity/how-we-commit.md): the commit-msg hook is
|
|
2
|
+
// what refuses AI attribution, in every repo, so this check verifies the hook
|
|
3
|
+
// is ARMED: shipkit's own hooksPath, or a foreign one whose commit-msg chains
|
|
4
|
+
// the shipkit gate. It never scans history: what is pushed is accepted as it
|
|
5
|
+
// is, and only a new commit can still be stopped. A declared-public repo also
|
|
6
|
+
// needs the tracked attribution silencer, and its ref NAMES are checked (a
|
|
7
|
+
// FABLE-checkpoint tag outs the agent like a trailer); a branch nobody pushed
|
|
8
|
+
// is named "local only" and warns.
|
|
9
|
+
import { createHash } from 'crypto'
|
|
11
10
|
import { readFileSync } from 'fs'
|
|
12
|
-
import { join } from 'path'
|
|
13
|
-
import { hasAiAttribution, hasDashPunctuation } from '../hooks/commit-message.ts'
|
|
11
|
+
import { isAbsolute, join } from 'path'
|
|
14
12
|
import { nestedIn } from '../core/nesting.ts'
|
|
15
|
-
import {
|
|
13
|
+
import { gitBin } from '../core/git-bin.ts'
|
|
14
|
+
import { fileStamp } from '../core/file-stamp.ts'
|
|
15
|
+
import { CONFIG_FILE } from '../core/check-pass.ts'
|
|
16
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
17
|
+
import { check, type CheckEnv } from '../check.ts'
|
|
18
|
+
import { declaredVisibility } from '../core/repo-facts.ts'
|
|
19
|
+
import { hookBinInvocation } from '../hooks/run-hooks.ts'
|
|
16
20
|
|
|
17
21
|
// the documented silencer: .claude/settings.json attribution.commit/.pr both
|
|
18
22
|
// empty — the harness then never writes the trailer or the PR footer
|
|
@@ -25,62 +29,100 @@ const attributionSilenced = (root: string): boolean => {
|
|
|
25
29
|
}
|
|
26
30
|
}
|
|
27
31
|
|
|
32
|
+
const readOrNull = (abs: string): string | null => {
|
|
33
|
+
try {
|
|
34
|
+
return readFileSync(abs, 'utf8')
|
|
35
|
+
} catch {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** a commit-msg body that runs the shipkit gate on the message: a live line
|
|
41
|
+
* (never a comment) whose failure is not swallowed by `|| true` */
|
|
42
|
+
const chainsShipkit = (body: string): boolean =>
|
|
43
|
+
body.split('\n').some((l) => !l.trim().startsWith('#') && /\bhooks\s+commit-msg\b/.test(l) && !/\|\|\s*(?:true|:)\b/.test(l))
|
|
44
|
+
|
|
45
|
+
/** the chain line to paste into a foreign hook: a clone with no install skips
|
|
46
|
+
* the gate with a note, rather than failing every commit */
|
|
47
|
+
export const guardedChain = (bin: string): string => {
|
|
48
|
+
const cli = bin.replace(/^bun\s+/, '')
|
|
49
|
+
return `if [ -f ${cli} ]; then ${bin} hooks commit-msg "$1" || exit 1; else echo "commit-msg: rvlib-shipkit not installed (run bun install), attribution gate skipped" >&2; fi`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ref NAMES tell too. bare match only for names with no other meaning;
|
|
53
|
+
// vendor and codename words (cursor, openai, gemini, opus, fable, …) name
|
|
54
|
+
// legitimate branches and features, they only match in a checkpoint/session shape
|
|
55
|
+
const REF_TELL = /\b(?:claude|chatgpt|copilot|aider|devin)\b|(?:^|[-_/])(?:fable|sonnet|haiku|opus|gemini|codex)[-_](?:checkpoint|session|ckpt)/i
|
|
56
|
+
|
|
57
|
+
const gitOut = (root: string, args: string[]): { code: number; out: string } => {
|
|
58
|
+
const p = Bun.spawnSync([gitBin(), ...args], { cwd: root, stdout: 'pipe', stderr: 'pipe' })
|
|
59
|
+
return { code: p.exitCode, out: p.stdout.toString() }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** what the answer depends on: the declared visibility, the hooksPath and the
|
|
63
|
+
* commit-msg file it points at, and for a public repo every ref NAME and the
|
|
64
|
+
* silencer. A commit moves none of them */
|
|
65
|
+
export function historyKey(env: CheckEnv): string {
|
|
66
|
+
const nested = nestedIn(env.root)
|
|
67
|
+
if (nested != null) return `nested in ${nested}`
|
|
68
|
+
const declared = declaredVisibility(env.config)
|
|
69
|
+
const hooksPath = gitOut(env.root, ['config', 'core.hooksPath'])
|
|
70
|
+
const dir = hooksPath.code === 0 ? hooksPath.out.trim() : ''
|
|
71
|
+
const hook = `hooks=${dir}|msg=${dir === '' ? 'none' : fileStamp(isAbsolute(dir) ? join(dir, 'commit-msg') : join(env.root, dir, 'commit-msg'))}`
|
|
72
|
+
if (declared !== 'public') return `declared ${declared}|${hook}`
|
|
73
|
+
const refs = gitOut(env.root, ['for-each-ref', '--format=%(refname)', 'refs/tags', 'refs/heads', 'refs/remotes']).out
|
|
74
|
+
const digest = createHash('sha256').update(refs).digest('hex').slice(0, 16)
|
|
75
|
+
return `public|${hook}|refs=${digest}|settings=${fileStamp(join(env.root, '.claude', 'settings.json'))}`
|
|
76
|
+
}
|
|
77
|
+
|
|
28
78
|
export const commitAiTells = check({
|
|
29
79
|
id: 'SK009',
|
|
30
80
|
blocking: true,
|
|
31
81
|
title: 'commit ai tells',
|
|
32
|
-
description: 'the
|
|
82
|
+
description: 'the commit-msg hook that refuses AI attribution is armed, and no ref name tells',
|
|
33
83
|
owner: 'identity/how-we-commit.md',
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
84
|
+
// git config and ref names, which no tree id describes, so they are the key
|
|
85
|
+
inputs: [CONFIG_FILE],
|
|
86
|
+
cacheKey: historyKey,
|
|
37
87
|
run: async (c) => {
|
|
38
88
|
const nested = nestedIn(c.root)
|
|
39
|
-
if (nested != null)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
89
|
+
if (nested != null) return void c.lines.push({ kind: 'skip', text: `commit gates not checked, nested in ${nested}: the parent repo owns them` })
|
|
90
|
+
const top = await c.exec(['git', 'rev-parse', '--show-toplevel'], { cwd: c.root })
|
|
91
|
+
if (top.code !== 0) return void c.lines.push({ kind: 'skip', text: 'not a git repo: no commit to guard yet' })
|
|
92
|
+
|
|
93
|
+
const hp = await c.exec(['git', 'config', 'core.hooksPath'], { cwd: c.root })
|
|
94
|
+
const dir = hp.code === 0 ? hp.out.trim() : ''
|
|
95
|
+
const chain = guardedChain(hookBinInvocation(c.root))
|
|
96
|
+
if (dir === '' && process.env['CI'] != null && process.env['CI'] !== '') {
|
|
97
|
+
c.lines.push({ kind: 'skip', text: 'commit-msg hook not armed in this CI checkout: a CI run makes no commit, and the fix pass arms hooks on a machine only' })
|
|
98
|
+
} else if (dir === '') {
|
|
99
|
+
c.fail('the commit-msg hook is not armed, so nothing refuses an AI attribution trailer: run `shipkit hooks install` (`shipkit ci` arms it too)')
|
|
100
|
+
} else {
|
|
101
|
+
const body = isAbsolute(dir) ? readOrNull(join(dir, 'commit-msg')) : c.read(`${dir}/commit-msg`)
|
|
102
|
+
const own = dir === LAYOUT.hooks
|
|
103
|
+
if (body == null)
|
|
104
|
+
c.fail(`core.hooksPath=${dir} has no ${dir}/commit-msg, so nothing refuses an AI attribution trailer: ${own ? 'run `shipkit hooks install`' : `add one that runs \`${chain}\``}`)
|
|
105
|
+
else if (!chainsShipkit(body))
|
|
106
|
+
c.fail(`core.hooksPath=${dir}: its commit-msg (${body.split('\n').find((l) => l.trim() !== '' && !l.startsWith('#'))?.trim().slice(0, 80) ?? 'empty'}) does not run the shipkit gate, so nothing refuses AI attribution: add \`${chain}\` above its last line`)
|
|
107
|
+
else c.ok(`the commit-msg hook refuses AI attribution (${own ? dir : `${dir}, chained to the shipkit gate`})`)
|
|
50
108
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// a checkpoint/session shape
|
|
69
|
-
const refTell = /\b(?:claude|chatgpt|copilot|aider|devin)\b|(?:^|[-_/])(?:fable|sonnet|haiku|opus|gemini|codex)[-_](?:checkpoint|session|ckpt)/i
|
|
70
|
-
const refs = await c.exec(['git', 'for-each-ref', '--format=%(refname:short)', 'refs/tags', 'refs/heads', 'refs/remotes'], { cwd: c.root, timeoutMs: 30_000 })
|
|
71
|
-
if (refs.code !== 0) c.lines.push({ kind: 'skip', text: `ref names unverifiable — git for-each-ref exit ${refs.code}` })
|
|
72
|
-
const tellRefs = refs.code === 0 ? refs.out.split('\n').map((r) => r.trim()).filter((r) => r !== '' && refTell.test(r)) : []
|
|
73
|
-
if (tellRefs.length > 0)
|
|
74
|
-
c.fail(`${tellRefs.length} ref name(s) carry an AI tell in a declared-public repo (${tellRefs.slice(0, 5).join(' ')}${tellRefs.length > 5 ? ' …' : ''}) — rename or delete the refs before the flip; a pushed tag also needs the remote delete`)
|
|
75
|
-
if (attributed.length > 0)
|
|
76
|
-
c.fail(`${attributed.length}/${commits.length} commits carry AI attribution (a trailer naming an assistant / a session trailer or claude.ai url / "Generated with" / 🤖) in a declared-public repo — e.g. ${sample(attributed)}; ${remedy}`)
|
|
77
|
-
if (dashed.length > 0)
|
|
78
|
-
c.fail(`${dashed.length}/${commits.length} commit messages carry dash punctuation (em/en dash or inline " - ") in a declared-public repo — e.g. ${sample(dashed)}; ${remedy} (identity/writing-voice.md owns the punctuation rule)`)
|
|
79
|
-
const silenced = attributionSilenced(c.root)
|
|
80
|
-
if (!silenced)
|
|
81
|
-
c.fail(`.claude/settings.json does not silence AI attribution — track it with {"attribution": {"commit": "", "pr": ""}} so no agent's harness ever writes the trailer or the PR footer`)
|
|
82
|
-
if (attributed.length === 0 && dashed.length === 0 && tellRefs.length === 0 && silenced)
|
|
83
|
-
c.ok(`${commits.length} commits + ${refs.code === 0 ? refs.out.split('\n').filter((r) => r.trim() !== '').length : 0} ref names scanned, attribution silenced in .claude/settings.json — no AI tells`)
|
|
109
|
+
|
|
110
|
+
if (declaredVisibility(c.config) !== 'public') return
|
|
111
|
+
if (!attributionSilenced(c.root))
|
|
112
|
+
c.fail(`.claude/settings.json does not silence AI attribution: track it with {"attribution": {"commit": "", "pr": ""}} so no agent's harness ever writes the trailer or the PR footer`)
|
|
113
|
+
const refs = await c.exec(['git', 'for-each-ref', '--format=%(refname)', 'refs/tags', 'refs/heads', 'refs/remotes'], { cwd: c.root, timeoutMs: 30_000 })
|
|
114
|
+
if (refs.code !== 0) return void c.lines.push({ kind: 'skip', text: `ref names unverifiable: git for-each-ref exit ${refs.code}` })
|
|
115
|
+
const all = refs.out.split('\n').map((r) => r.trim()).filter((r) => r !== '')
|
|
116
|
+
const remote = new Set(all.filter((r) => r.startsWith('refs/remotes/')).map((r) => r.split('/').slice(3).join('/')))
|
|
117
|
+
const tells = all.filter((r) => REF_TELL.test(r.replace(/^refs\/(?:heads|tags|remotes)\//, '')))
|
|
118
|
+
const shown = (r: string): string => r.replace(/^refs\/(?:heads|tags|remotes)\//, '')
|
|
119
|
+
const local = tells.filter((r) => r.startsWith('refs/heads/') && !remote.has(shown(r)))
|
|
120
|
+
const out = tells.filter((r) => !local.includes(r) && !(r.startsWith('refs/heads/') && remote.has(shown(r))))
|
|
121
|
+
const list = (rs: string[], suffix = ''): string => rs.slice(0, 5).map((r) => `${shown(r)}${suffix}`).join(' ') + (rs.length > 5 ? ' …' : '')
|
|
122
|
+
if (out.length > 0)
|
|
123
|
+
c.fail(`${out.length} ref name(s) carry an AI tell in a declared-public repo (${list(out)}): rename or delete them, a pushed one on the remote too, then \`git fetch --prune\` so a remote ref deleted there leaves this clone`)
|
|
124
|
+
if (local.length > 0)
|
|
125
|
+
c.warn(`${local.length} branch name(s) carry an AI tell (${list(local, ' (local only)')}): never push them under this name, rename or delete them`)
|
|
84
126
|
},
|
|
85
127
|
})
|
|
86
128
|
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { isAbsolute, join } from 'path'
|
|
16
16
|
import { inferBin } from '../core/canonical-scripts.ts'
|
|
17
17
|
import { denyRules, RAW_RUNNERS } from '../core/one-way-in-policy.ts'
|
|
18
|
+
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
18
19
|
import type { RepoConfig } from '../repo-config.ts'
|
|
19
20
|
import { check, type CheckCtx, type FixResult } from '../check.ts'
|
|
20
21
|
|
|
@@ -95,8 +96,8 @@ async function guardScriptMissing(d: Disk, command: string): Promise<string | nu
|
|
|
95
96
|
return null // a form this rule cannot resolve is not a form it may condemn
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
/** the guard the fixer writes: the cli path the scripts run when a
|
|
99
|
-
* beside it
|
|
99
|
+
/** the guard the fixer writes: the shipkit cli path the scripts run when a
|
|
100
|
+
* guard sits beside it, else the
|
|
100
101
|
* repo's own dependency, else the copy of shipkit doing the fixing, which is
|
|
101
102
|
* the one the launcher falls back to in a repo that has none */
|
|
102
103
|
export async function guardCommandFor(d: Disk): Promise<string> {
|
|
@@ -109,6 +110,11 @@ export async function guardCommandFor(d: Disk): Promise<string> {
|
|
|
109
110
|
return guardCommand(join(import.meta.dir, '..', 'cli.ts'))
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
/** a package published to npm is worked on by strangers: this file binds
|
|
114
|
+
* their agents, and a deny list with a hook would tie them to a toolkit they
|
|
115
|
+
* never installed */
|
|
116
|
+
const PUBLIC_SKIP = `the root unit publishes to npm: ${SETTINGS_PATH} belongs to every contributor, so it is never written`
|
|
117
|
+
|
|
112
118
|
export const oneWayInPolicy = check({
|
|
113
119
|
id: 'SK013',
|
|
114
120
|
blocking: true,
|
|
@@ -119,6 +125,7 @@ export const oneWayInPolicy = check({
|
|
|
119
125
|
// the declared guard script resolves. A guard at an absolute path lives
|
|
120
126
|
// outside the repo, so an answer that probed one is never stored
|
|
121
127
|
run: async (c) => {
|
|
128
|
+
if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
|
|
122
129
|
const d = c
|
|
123
130
|
const want = denyRules(exemptSlots(c.config))
|
|
124
131
|
if (want.length === 0) return c.ok('every governed slot is exempt (repo.config oneWayIn/defaults) — nothing to deny')
|
|
@@ -165,6 +172,7 @@ export const oneWayInPolicy = check({
|
|
|
165
172
|
// a repo's own allow list and its own hooks, and clobbering those to fix a
|
|
166
173
|
// deny list would cost more than the finding it repairs
|
|
167
174
|
fix: async (c): Promise<FixResult> => {
|
|
175
|
+
if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
|
|
168
176
|
const d = c
|
|
169
177
|
const want = denyRules(exemptSlots(c.config))
|
|
170
178
|
if (want.length === 0) return { changed: false, summary: 'every governed slot is exempt — nothing to write' }
|
|
@@ -7,16 +7,17 @@
|
|
|
7
7
|
//
|
|
8
8
|
// The tier split is the whole design. `fix` and `hard` hits BLOCK: they are
|
|
9
9
|
// mechanical, and a machine reporting them is never guessing. `soft` hits
|
|
10
|
-
// (sentence length, passive voice) need a human read,
|
|
11
|
-
//
|
|
12
|
-
// down.
|
|
10
|
+
// (sentence length, passive voice, modals, semicolons, …) need a human read,
|
|
11
|
+
// so they are a note, never an attention item, and only a declared
|
|
12
|
+
// `baseline` makes them blocking, as a ratchet that can only go down.
|
|
13
13
|
//
|
|
14
14
|
// It carries NO `fix()` on purpose. `shipkit ci` runs `--fix-rules` by
|
|
15
15
|
// default, so a rule with a fixer rewrites tracked files unattended, and
|
|
16
16
|
// prose is not repo shape: a wrong package.json line is a config, a wrong
|
|
17
17
|
// sentence is content. The rewrite lives behind an explicit `shipkit prose
|
|
18
18
|
// --fix` that a human ran and can read the diff of.
|
|
19
|
-
import { classSummary, proseLines, runProseCheck } from '../prose/run-prose.ts'
|
|
19
|
+
import { classSummary, proseLines, runProseCheck, type ProseReport } from '../prose/run-prose.ts'
|
|
20
|
+
import { tierOf, type ProseClass } from '../prose/ste.ts'
|
|
20
21
|
import { check } from '../check.ts'
|
|
21
22
|
|
|
22
23
|
const OWNER = 'identity/writing-voice.md'
|
|
@@ -53,12 +54,21 @@ export const proseSte = check({
|
|
|
53
54
|
`${blocking} STE violation(s) across ${r.byFile.length}/${r.files} file(s) — ${classSummary(r)}`,
|
|
54
55
|
r.fixable > 0 ? `shipkit prose --fix rewrites ${r.fixable} of them` : undefined,
|
|
55
56
|
)
|
|
57
|
+
const soft = softSummary(r)
|
|
56
58
|
if (cfg.baseline != null && r.byTier.soft > cfg.baseline)
|
|
57
|
-
c.fail(`${r.byTier.soft} soft hits (
|
|
59
|
+
c.fail(`${r.byTier.soft} soft hits (${soft}), above the declared baseline of ${cfg.baseline}: the ratchet only goes down`)
|
|
58
60
|
else if (r.byTier.soft > 0)
|
|
59
|
-
c.
|
|
61
|
+
c.note(`${r.byTier.soft} soft hit(s) need a human read (${soft})${cfg.baseline != null ? `, within the baseline of ${cfg.baseline}` : ''}: \`shipkit prose\` lists them`)
|
|
60
62
|
if (!c.failed && blocking === 0) c.ok(`${r.files} file(s) STE-clean on the mechanical rules`)
|
|
61
63
|
}
|
|
62
64
|
})
|
|
63
65
|
|
|
66
|
+
/** every soft class with its count, most first */
|
|
67
|
+
const softSummary = (r: ProseReport): string =>
|
|
68
|
+
Object.entries(r.byClass)
|
|
69
|
+
.filter(([cls]) => tierOf(cls as ProseClass) === 'soft')
|
|
70
|
+
.sort((a, b) => b[1] - a[1])
|
|
71
|
+
.map(([cls, n]) => `${cls} ${n}`)
|
|
72
|
+
.join(', ')
|
|
73
|
+
|
|
64
74
|
export default proseSte
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// sites that write history refuse on the disk fact either way (core/nesting.ts),
|
|
4
4
|
// so this rule is about the declaration telling the truth to the next reader.
|
|
5
5
|
import { nestedIn } from '../core/nesting.ts'
|
|
6
|
+
import { CONFIG_FILE } from '../core/check-pass.ts'
|
|
6
7
|
import { check } from '../check.ts'
|
|
7
8
|
|
|
8
9
|
export const nestedRepo = check({
|
|
@@ -11,7 +12,10 @@ export const nestedRepo = check({
|
|
|
11
12
|
title: 'nested repo',
|
|
12
13
|
description: 'a config inside a larger work tree declares `nested: true`, and only there',
|
|
13
14
|
owner: 'build/shipkit/config-keys.md',
|
|
14
|
-
|
|
15
|
+
// the declaration, and where the git work tree root sits, which no tree
|
|
16
|
+
// id describes, so its path is the rest of the key
|
|
17
|
+
inputs: [CONFIG_FILE],
|
|
18
|
+
cacheKey: (env) => `nested=${env.config.nested === true}|top=${nestedIn(env.root) ?? 'root'}`,
|
|
15
19
|
run: (c) => {
|
|
16
20
|
const top = nestedIn(c.root)
|
|
17
21
|
const declared = c.config.nested === true
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// SK016 — the shapes the check file replaced, and the machine that
|
|
2
2
|
// rewrites them (build/shipkit/writing-a-check.md owns the shape itself).
|
|
3
3
|
//
|
|
4
|
-
// The window: every shape below is READ
|
|
5
|
-
// refused at the first release after `shipkit fleet` shows no repo below 0.40
|
|
4
|
+
// The window: every shape below is still READ, named once per run, and
|
|
5
|
+
// refused at the first release after `shipkit fleet` shows no repo below 0.40
|
|
6
|
+
// and none still carrying a check script.
|
|
6
7
|
// So a repo on an old declaration keeps working, learns what to write instead
|
|
7
8
|
// on every gate, and is carried across by one command, save what only a
|
|
8
9
|
// reader can port: a check script's body, a rule, a task whose body is code.
|
|
@@ -60,7 +61,7 @@ export const legacyCheckShapes = check({
|
|
|
60
61
|
if (s.placeholders.length > 0)
|
|
61
62
|
c.warn(
|
|
62
63
|
`${s.placeholders.length} check file(s) still open with the TODO description the migration wrote: ${s.placeholders.join(', ')}`,
|
|
63
|
-
'replace each TODO with one line stating what must be true',
|
|
64
|
+
'by hand: replace each TODO with one line stating what must be true',
|
|
64
65
|
)
|
|
65
66
|
if (s.count === 0) return
|
|
66
67
|
// one WARN per shape, never a bare line: the gate promises every line
|
|
@@ -78,9 +79,14 @@ export const legacyCheckShapes = check({
|
|
|
78
79
|
}
|
|
79
80
|
if (s.rules.length > 0) c.warn(`${s.rules.length} \`rules: []\` entr(ies): ${s.rules.join(', ')} — each one is a *.check.ts file whose \`run\` is its \`lint\``)
|
|
80
81
|
if (s.ruleExempt.length > 0) c.warn(`ruleExempt names ${s.ruleExempt.join(', ')} — the spelling is \`when: { id: 'off:<why>' }\``)
|
|
82
|
+
// what the fixer rewrites without guessing: the exemptions, the tasks
|
|
83
|
+
// once all of them are data, checkDefaults once no script leans on it
|
|
84
|
+
const mechanical = s.ruleExempt.length + (s.tasks.every((x) => x.data) ? s.tasks.length : 0) + (s.checkDefaults && s.checkFiles.length === 0 ? 1 : 0)
|
|
81
85
|
c.warn(
|
|
82
|
-
`${s.count} declaration(s) in a shape the check file replaced
|
|
83
|
-
|
|
86
|
+
`${s.count} declaration(s) in a shape the check file replaced: still read, and refused by the first shipkit release after \`shipkit fleet\` shows no repo below 0.40 and none still carrying a check script`,
|
|
87
|
+
mechanical > 0
|
|
88
|
+
? `shipkit check --fix SK016 rewrites ${mechanical} of them and names the rest`
|
|
89
|
+
: `nothing left is mechanical, so the port is by hand: each line above becomes a *.check.ts file (${OWNER})`,
|
|
84
90
|
)
|
|
85
91
|
},
|
|
86
92
|
fix: async (c) => await fixLegacyShapes(c),
|
|
@@ -11,6 +11,7 @@ import { basename } from 'path'
|
|
|
11
11
|
import { check } from '../check.ts'
|
|
12
12
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
13
13
|
import { checkFloorReader, resolveFloor } from '../core/floor-resolve.ts'
|
|
14
|
+
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
14
15
|
import { whenOfTask } from '../core/when.ts'
|
|
15
16
|
import { hubProblemMatcher } from '../problems/problem-line.ts'
|
|
16
17
|
import { indentAt, parseSpans, plain, removalSpan, type Node } from '../scaffold/jsonc-spans.ts'
|
|
@@ -165,6 +166,10 @@ function ownTasks(c: CheckCtx): { text: string } | 'none' | 'linked' {
|
|
|
165
166
|
return text == null ? 'none' : { text }
|
|
166
167
|
}
|
|
167
168
|
|
|
169
|
+
/** a package published to npm is worked on by strangers, whose editor
|
|
170
|
+
* runs these tasks */
|
|
171
|
+
const PUBLIC_SKIP = `the root unit publishes to npm: ${TASKS} belongs to every contributor, so it is never rewritten`
|
|
172
|
+
|
|
168
173
|
export const vscodeHubTask = check({
|
|
169
174
|
id: 'SK017',
|
|
170
175
|
// a stale editor file is behind, not broken: it warns, and the fix pass
|
|
@@ -174,6 +179,7 @@ export const vscodeHubTask = check({
|
|
|
174
179
|
description: 'the vscode hub task carries the problem matcher and no extra tsc watcher runs',
|
|
175
180
|
owner: 'build/vscode-tasks.md',
|
|
176
181
|
run: (c) => {
|
|
182
|
+
if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
|
|
177
183
|
const own = ownTasks(c)
|
|
178
184
|
if (own === 'none') return c.ok(`no ${TASKS}, so there is no hub task to wire`)
|
|
179
185
|
if (own === 'linked') return c.ok(`.vscode/ is a link to a shared dir, left alone`)
|
|
@@ -185,6 +191,7 @@ export const vscodeHubTask = check({
|
|
|
185
191
|
for (const ch of r.changes) c.fail(`${TASKS}: ${ch.replace(/^removed/, 'should remove').replace(/^dropped/, 'should drop').replace(/^wrote/, 'should write').replace(/^folded/, 'should fold')}`)
|
|
186
192
|
},
|
|
187
193
|
fix: (c) => {
|
|
194
|
+
if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
|
|
188
195
|
const own = ownTasks(c)
|
|
189
196
|
if (typeof own === 'string') return { changed: false, summary: `no ${TASKS} to repair` }
|
|
190
197
|
const r = repairTasks(own.text, { typecheckOn: typecheckOn(c) })
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// rvlib-shipkit pin is a minimum. It warns when the repo asks for a version
|
|
4
4
|
// newer than the one this process runs: a rule or an api that pin relies on
|
|
5
5
|
// is absent here until the house copy catches up.
|
|
6
|
+
import { join } from 'path'
|
|
6
7
|
import { LIB_VERSION } from '../core/lib-version.ts'
|
|
8
|
+
import { fileStamp } from '../core/file-stamp.ts'
|
|
7
9
|
import { check } from '../check.ts'
|
|
8
10
|
|
|
9
11
|
type Pkg = { version?: string; dependencies?: Record<string, string>; devDependencies?: Record<string, string> }
|
|
@@ -23,8 +25,10 @@ export const machineRunsThePin = check({
|
|
|
23
25
|
title: 'machine runs the pin',
|
|
24
26
|
description: 'the shipkit this machine runs is at least the version this repo pins',
|
|
25
27
|
owner: 'build/shipkit.md',
|
|
26
|
-
// the
|
|
27
|
-
|
|
28
|
+
// the pin, the install (gitignored) and the version this machine runs,
|
|
29
|
+
// which is no file of the repo
|
|
30
|
+
inputs: ['package.json'],
|
|
31
|
+
cacheKey: (env) => `${LIB_VERSION}|installed=${fileStamp(join(env.root, 'node_modules', NAME, 'package.json'))}`,
|
|
28
32
|
run: (c) => {
|
|
29
33
|
const pkg = c.json<Pkg>('package.json')
|
|
30
34
|
const spec = pkg?.devDependencies?.[NAME] ?? pkg?.dependencies?.[NAME]
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// surface that shows the repo (the home page header, its rail tile) draws it
|
|
4
4
|
// in its own colour. The fix derives them from the repo icon, or from a hue
|
|
5
5
|
// hashed from the name, and writes that one key, every other byte kept.
|
|
6
|
+
import { join } from 'path'
|
|
6
7
|
import { check } from '../check.ts'
|
|
8
|
+
import { fileStamp } from '../core/file-stamp.ts'
|
|
7
9
|
import { configName, CONFIG_FILE, deriveIdentity, identityRefusal, INSTALLED_CONFIG_SRC } from '../core/identity.ts'
|
|
8
10
|
import { declaresIdentity, identityOfText, identityProblem, setIdentityText } from '../core/identity-color.ts'
|
|
9
11
|
|
|
@@ -16,6 +18,9 @@ export const identityColors = check({
|
|
|
16
18
|
title: 'identity colors',
|
|
17
19
|
description: 'repo.config.ts declares identity colours: colorLight and colorDark, two hex',
|
|
18
20
|
owner: 'build/shipkit/built-in-checks.md',
|
|
21
|
+
// the config, and the installed lib that says whether it knows the key
|
|
22
|
+
inputs: [CONFIG_FILE],
|
|
23
|
+
cacheKey: (env) => `installed=${fileStamp(join(env.root, INSTALLED_CONFIG_SRC))}`,
|
|
19
24
|
run: (c) => {
|
|
20
25
|
const text = c.read(CONFIG_FILE)
|
|
21
26
|
if (text == null) return c.note(`no ${CONFIG_FILE}`)
|