rvlib-shipkit 0.38.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +371 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +6 -2
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +258 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +557 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +86 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +153 -189
- package/src/core/run-fix.ts +96 -0
- package/src/core/run-fixers.ts +53 -30
- package/src/core/self-update.ts +9 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/hooks/leak-guard.ts +10 -8
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +35 -23
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +155 -85
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +3 -2
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +216 -18
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
package/src/prose/ste.ts
CHANGED
|
@@ -26,6 +26,7 @@ export type ProseClass =
|
|
|
26
26
|
| 'sentence'
|
|
27
27
|
| 'passive'
|
|
28
28
|
| 'paragraph'
|
|
29
|
+
| 'ban'
|
|
29
30
|
|
|
30
31
|
export type ProseHit = {
|
|
31
32
|
line: number // 1-based, true to the file
|
|
@@ -36,6 +37,38 @@ export type ProseHit = {
|
|
|
36
37
|
text: string // the offending span
|
|
37
38
|
fix?: string // the replacement, when the class is mechanical
|
|
38
39
|
why: string // one line, what STE wants instead
|
|
40
|
+
/** the dictionary entry that fired, lowercased: what a repo `allow` entry
|
|
41
|
+
* names and what the hub groups by. Absent on the classes that judge a
|
|
42
|
+
* shape (sentence length, paragraph, semicolon) rather than a word */
|
|
43
|
+
word?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** a word or phrase this repo accepts: every hit on it is dropped */
|
|
47
|
+
export type ProseAllowWord = {
|
|
48
|
+
/** the word or phrase, case ignored. A dictionary entry (`should`,
|
|
49
|
+
* `leverage`) covers its inflected forms too */
|
|
50
|
+
word: string
|
|
51
|
+
/** one line: why this repo accepts it */
|
|
52
|
+
reason?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** a word or phrase this repo refuses on top of the house dictionary */
|
|
56
|
+
export type ProseBanWord = {
|
|
57
|
+
/** the word or phrase, case ignored, matched on word boundaries */
|
|
58
|
+
word: string
|
|
59
|
+
/** what to write instead. Given, `shipkit prose --fix` writes it */
|
|
60
|
+
to?: string
|
|
61
|
+
/** one line: why this repo refuses it */
|
|
62
|
+
reason?: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** the per repo word lists. The house dictionary in this file stays the
|
|
66
|
+
* default, and these override it for one repo only */
|
|
67
|
+
export type ProseWords = {
|
|
68
|
+
/** words and phrases this repo accepts */
|
|
69
|
+
allow?: ProseAllowWord[]
|
|
70
|
+
/** words and phrases this repo refuses, each with its replacement */
|
|
71
|
+
ban?: ProseBanWord[]
|
|
39
72
|
}
|
|
40
73
|
|
|
41
74
|
export type ProseOpts = {
|
|
@@ -43,6 +76,8 @@ export type ProseOpts = {
|
|
|
43
76
|
allow?: ProseClass[]
|
|
44
77
|
/** procedural limit (20) instead of the descriptive one (25) */
|
|
45
78
|
strict?: boolean
|
|
79
|
+
/** the repo's own allow and ban lists */
|
|
80
|
+
words?: ProseWords
|
|
46
81
|
}
|
|
47
82
|
|
|
48
83
|
const TIER: Record<ProseClass, ProseTier> = {
|
|
@@ -50,13 +85,16 @@ const TIER: Record<ProseClass, ProseTier> = {
|
|
|
50
85
|
abbrev: 'fix',
|
|
51
86
|
slop: 'fix',
|
|
52
87
|
modal: 'soft',
|
|
53
|
-
|
|
88
|
+
// reports, never blocks: the join carries meaning a rewrite can lose
|
|
89
|
+
semicolon: 'soft',
|
|
54
90
|
'comma-ing': 'soft',
|
|
55
91
|
perfect: 'soft',
|
|
56
92
|
vague: 'hard',
|
|
57
93
|
sentence: 'soft',
|
|
58
94
|
passive: 'soft',
|
|
59
95
|
paragraph: 'soft',
|
|
96
|
+
// the repo named the word itself, so a hit is never a guess
|
|
97
|
+
ban: 'hard',
|
|
60
98
|
}
|
|
61
99
|
|
|
62
100
|
export const PROSE_CLASSES = Object.keys(TIER) as ProseClass[]
|
|
@@ -201,8 +239,6 @@ export function maskProse(src: string): string {
|
|
|
201
239
|
return m
|
|
202
240
|
}
|
|
203
241
|
|
|
204
|
-
/** a table row is structure: word classes still apply inside a cell, sentence
|
|
205
|
-
* and paragraph limits do not. Same for a heading. */
|
|
206
242
|
/** `is unmaintained` is a state, not a passive voice: there is no verb `to
|
|
207
243
|
* unmaintain`, so no actor exists to name. The `un-` prefix catches almost
|
|
208
244
|
* the whole family; the rest are participles coined off a noun. */
|
|
@@ -215,7 +251,20 @@ const isAdjectival = (participle: string): boolean => {
|
|
|
215
251
|
return (w.startsWith('un') && w.endsWith('ed')) || COINED.has(w) || NOT_PARTICIPLES.has(w)
|
|
216
252
|
}
|
|
217
253
|
|
|
218
|
-
|
|
254
|
+
/** a table row is structure: word classes still apply inside a cell, the
|
|
255
|
+
* sentence, paragraph and semicolon rules do not. A cell is not a sentence,
|
|
256
|
+
* so "write two sentences" asks for the table to be broken, and its items
|
|
257
|
+
* are already comma separated, which leaves the semicolon as the only
|
|
258
|
+
* separator the writer has left. */
|
|
259
|
+
const isTableRow = (l: string): boolean => /^\s*\|/.test(l)
|
|
260
|
+
|
|
261
|
+
/** a list item OPENS a new paragraph: a run of items with no blank line
|
|
262
|
+
* between them is a list, never a wall of text, and counting the run as one
|
|
263
|
+
* paragraph reported a five item list as 38 sentences. Both numbered forms,
|
|
264
|
+
* at any indentation. */
|
|
265
|
+
const isListItem = (l: string): boolean => /^\s*(?:[-*+]|\d+[.)])\s/.test(l)
|
|
266
|
+
|
|
267
|
+
const isStructureLine = (l: string): boolean => isTableRow(l) || /^\s*#{1,6}\s/.test(l) || /^\s*[-|: ]+$/.test(l)
|
|
219
268
|
|
|
220
269
|
// --- sentence segmentation ----------------------------------------------
|
|
221
270
|
|
|
@@ -249,8 +298,13 @@ const capitalizeLike = (sample: string, replacement: string): string =>
|
|
|
249
298
|
|
|
250
299
|
const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
251
300
|
|
|
301
|
+
/** one spelling for a word or phrase, so `Prior To` and `prior to` are the
|
|
302
|
+
* same entry */
|
|
303
|
+
export const normWord = (w: string): string => w.trim().toLowerCase().replace(/\s+/g, ' ')
|
|
304
|
+
|
|
252
305
|
export function checkProse(src: string, opts: ProseOpts = {}): ProseHit[] {
|
|
253
306
|
const allow = new Set(opts.allow ?? [])
|
|
307
|
+
const allowWords = new Set((opts.words?.allow ?? []).map((a) => normWord(a.word)))
|
|
254
308
|
const masked = maskProse(src)
|
|
255
309
|
const hits: ProseHit[] = []
|
|
256
310
|
// line index → offset, once
|
|
@@ -267,10 +321,19 @@ export function checkProse(src: string, opts: ProseOpts = {}): ProseHit[] {
|
|
|
267
321
|
return lo + 1
|
|
268
322
|
}
|
|
269
323
|
|
|
270
|
-
const push = (cls: ProseClass, start: number, end: number, why: string, fix?: string) => {
|
|
324
|
+
const push = (cls: ProseClass, start: number, end: number, why: string, fix?: string, word?: string) => {
|
|
271
325
|
if (allow.has(cls)) return
|
|
272
326
|
const text = src.slice(start, end)
|
|
273
|
-
|
|
327
|
+
if (allowWords.size > 0 && (allowWords.has(normWord(text)) || (word != null && allowWords.has(word)))) return
|
|
328
|
+
hits.push({ line: lineOf(start), start, end, cls, tier: TIER[cls], text, fix, why, ...(word == null ? {} : { word }) })
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** the whole masked line an offset sits on, for the classes that need the
|
|
332
|
+
* line's SHAPE (a table row, a list item) and not only the span */
|
|
333
|
+
const lineAt = (off: number): string => {
|
|
334
|
+
const from = masked.lastIndexOf('\n', off) + 1
|
|
335
|
+
const to = masked.indexOf('\n', off)
|
|
336
|
+
return masked.slice(from, to < 0 ? masked.length : to)
|
|
274
337
|
}
|
|
275
338
|
|
|
276
339
|
const scan = (re: RegExp, run: (m: RegExpExecArray) => void) => {
|
|
@@ -285,11 +348,11 @@ export function checkProse(src: string, opts: ProseOpts = {}): ProseHit[] {
|
|
|
285
348
|
// --- fix tier
|
|
286
349
|
scan(new RegExp(`\\b(${Object.keys(CONTRACTIONS).map(escapeRe).join('|')})`, 'gi'), (m) => {
|
|
287
350
|
const full = CONTRACTIONS[m[0].toLowerCase()]
|
|
288
|
-
if (full != null) push('contraction', m.index, m.index + m[0].length, 'STE keeps the full form', capitalizeLike(m[0], full))
|
|
351
|
+
if (full != null) push('contraction', m.index, m.index + m[0].length, 'STE keeps the full form', capitalizeLike(m[0], full), m[0].toLowerCase())
|
|
289
352
|
})
|
|
290
353
|
scan(new RegExp(`(?<![\\w.])(${Object.keys(ABBREV).map(escapeRe).join('|')})`, 'gi'), (m) => {
|
|
291
354
|
const full = ABBREV[m[0].toLowerCase()]
|
|
292
|
-
if (full != null) push('abbrev', m.index, m.index + m[0].length, 'write the words a non-native reader can read', capitalizeLike(m[0], full))
|
|
355
|
+
if (full != null) push('abbrev', m.index, m.index + m[0].length, 'write the words a non-native reader can read', capitalizeLike(m[0], full), m[0].toLowerCase())
|
|
293
356
|
})
|
|
294
357
|
// a verb entry matches its inflected forms too, and the ending rides onto
|
|
295
358
|
// the replacement
|
|
@@ -301,12 +364,12 @@ export function checkProse(src: string, opts: ProseOpts = {}): ProseHit[] {
|
|
|
301
364
|
const stem = e.from.endsWith('e') ? e.from.slice(0, -1) : e.from
|
|
302
365
|
scan(new RegExp(`\\b(?:${escapeRe(e.from)}(es|s|ed|d)?|${escapeRe(stem)}(ing))\\b`, 'gi'), (m) => {
|
|
303
366
|
const to = inflect(e.to, (m[1] ?? m[2] ?? '').toLowerCase())
|
|
304
|
-
push('slop', m.index, m.index + m[0].length, `STE writes '${e.to}'`, capitalizeLike(m[0], to))
|
|
367
|
+
push('slop', m.index, m.index + m[0].length, `STE writes '${e.to}'`, capitalizeLike(m[0], to), e.from)
|
|
305
368
|
})
|
|
306
369
|
}
|
|
307
370
|
scan(new RegExp(`\\b(${plain.map((x) => escapeRe(x.from)).join('|')})\\b`, 'gi'), (m) => {
|
|
308
371
|
const e = plain.find((x) => x.from === m[0]!.toLowerCase())
|
|
309
|
-
if (e != null) push('slop', m.index, m.index + m[0].length, `STE writes '${e.to}'`, capitalizeLike(m[0], e.to))
|
|
372
|
+
if (e != null) push('slop', m.index, m.index + m[0].length, `STE writes '${e.to}'`, capitalizeLike(m[0], e.to), e.from)
|
|
310
373
|
})
|
|
311
374
|
// a deletion gives back exactly one space, and WHICH one depends on what
|
|
312
375
|
// follows: before punctuation the space in front goes with the word, or
|
|
@@ -323,16 +386,19 @@ export function checkProse(src: string, opts: ProseOpts = {}): ProseHit[] {
|
|
|
323
386
|
if (before === ',' && after === ',') end += 1
|
|
324
387
|
const punctuationFollows = /[.,:;!?)\]]/.test(after)
|
|
325
388
|
const fix = lead === '' ? undefined : punctuationFollows ? '' : lead
|
|
326
|
-
push('slop', m.index, end, 'the word carries no fact', fix)
|
|
389
|
+
push('slop', m.index, end, 'the word carries no fact', fix, normWord(m[2]!))
|
|
327
390
|
})
|
|
328
391
|
for (const p of PHRASE_SUBSTITUTIONS)
|
|
329
|
-
scan(new RegExp(p.re.source, 'gi'), (m) => push('slop', m.index, m.index + m[0].length, p.why, capitalizeLike(m[0], p.to)))
|
|
392
|
+
scan(new RegExp(p.re.source, 'gi'), (m) => push('slop', m.index, m.index + m[0].length, p.why, capitalizeLike(m[0], p.to), normWord(m[0].replace(/,\s*$/, ''))))
|
|
330
393
|
|
|
331
394
|
// --- hard tier
|
|
332
395
|
scan(new RegExp(`\\b(${MODALS.join('|')})\\b`, 'gi'), (m) =>
|
|
333
|
-
push('modal', m.index, m.index + m[0].length, "a requirement is 'must', a possibility is 'can', a recommendation is a fact or it is deleted"),
|
|
396
|
+
push('modal', m.index, m.index + m[0].length, "a requirement is 'must', a possibility is 'can', a recommendation is a fact or it is deleted", undefined, m[0].toLowerCase()),
|
|
334
397
|
)
|
|
335
|
-
scan(/;/g, (m) =>
|
|
398
|
+
scan(/;/g, (m) => {
|
|
399
|
+
if (isTableRow(lineAt(m.index))) return
|
|
400
|
+
push('semicolon', m.index, m.index + 1, 'write two sentences')
|
|
401
|
+
})
|
|
336
402
|
scan(/,\s+(\w+ing)\b/g, (m) => {
|
|
337
403
|
if (/^(nothing|something|anything|everything|during|string|thing)$/i.test(m[1]!)) return
|
|
338
404
|
// an `-ing` word used as an ADJECTIVE (`, missing path`) is not the
|
|
@@ -345,17 +411,31 @@ export function checkProse(src: string, opts: ProseOpts = {}): ProseHit[] {
|
|
|
345
411
|
// `Renaming a file, moving a responsibility, or changing any contract`
|
|
346
412
|
// is a coordinated list of gerund NOUNS, not the participial clause
|
|
347
413
|
// the rule bans. The tell is a second gerund behind `and` or `or`.
|
|
348
|
-
|
|
349
|
-
if (/\b(and|or)\s+\w+ing\b/.test(line)) return
|
|
414
|
+
if (/\b(and|or)\s+\w+ing\b/.test(lineAt(m.index))) return
|
|
350
415
|
push('comma-ing', m.index, m.index + m[0].length, "STE bans an '-ing' verb after a comma: start a new sentence, or write the relative clause")
|
|
351
416
|
})
|
|
352
417
|
scan(new RegExp(`\\b(has|have|had)\\s+(been|\\w+ed|${PARTICIPLES})\\b`, 'gi'), (m) =>
|
|
353
|
-
push('perfect', m.index, m.index + m[0].length, 'STE uses simple tenses only'),
|
|
418
|
+
push('perfect', m.index, m.index + m[0].length, 'STE uses simple tenses only', undefined, normWord(m[0])),
|
|
354
419
|
)
|
|
355
420
|
scan(new RegExp(`\\b(${Object.keys(VAGUE).map(escapeRe).join('|')})\\b`, 'gi'), (m) => {
|
|
356
421
|
const why = VAGUE[m[0].toLowerCase()]
|
|
357
|
-
if (why != null) push('vague', m.index, m.index + m[0].length, why)
|
|
422
|
+
if (why != null) push('vague', m.index, m.index + m[0].length, why, undefined, m[0].toLowerCase())
|
|
358
423
|
})
|
|
424
|
+
// the repo's own ban list: hard, because the repo named the word itself.
|
|
425
|
+
// A house hit on the same span gives way, so one word reports once and
|
|
426
|
+
// with the repo's own replacement
|
|
427
|
+
const banned: [number, number][] = []
|
|
428
|
+
for (const b of opts.words?.ban ?? []) {
|
|
429
|
+
const w = normWord(b.word)
|
|
430
|
+
if (w === '') continue
|
|
431
|
+
const body = w.split(' ').map(escapeRe).join('\\s+')
|
|
432
|
+
scan(new RegExp(`(?<![\\w'-])${body}(?![\\w'-])`, 'gi'), (m) => {
|
|
433
|
+
if (allow.has('ban')) return
|
|
434
|
+
banned.push([m.index, m.index + m[0].length])
|
|
435
|
+
const why = `this repo refuses '${w}'${b.to != null && b.to !== '' ? `, write '${b.to}'` : ''}${b.reason != null && b.reason !== '' ? `: ${b.reason}` : ''}`
|
|
436
|
+
push('ban', m.index, m.index + m[0].length, why, b.to != null && b.to !== '' ? capitalizeLike(m[0], b.to) : undefined, w)
|
|
437
|
+
})
|
|
438
|
+
}
|
|
359
439
|
|
|
360
440
|
// --- soft tier, line by line
|
|
361
441
|
const limit = opts.strict === true ? 20 : 25
|
|
@@ -385,21 +465,24 @@ export function checkProse(src: string, opts: ProseOpts = {}): ProseHit[] {
|
|
|
385
465
|
let pm: RegExpExecArray | null
|
|
386
466
|
while ((pm = passRe.exec(line)) != null) {
|
|
387
467
|
if (isAdjectival(pm[2]!)) continue
|
|
388
|
-
push('passive', off + pm.index, off + pm.index + pm[0].length, 'STE wants the active voice: name the actor')
|
|
389
|
-
}
|
|
390
|
-
const isBullet = /^\s*(?:[-*+]|\d+\.)\s/.test(line)
|
|
391
|
-
if (!isBullet) {
|
|
392
|
-
if (paraStart < 0) paraStart = i
|
|
468
|
+
push('passive', off + pm.index, off + pm.index + pm[0].length, 'STE wants the active voice: name the actor', undefined, normWord(pm[0]))
|
|
393
469
|
}
|
|
470
|
+
const item = isListItem(line)
|
|
471
|
+
if (item) flushParagraph(i)
|
|
472
|
+
else if (paraStart < 0) paraStart = i
|
|
394
473
|
for (const s of splitSentences(line)) {
|
|
395
474
|
const w = wordCount(s.text)
|
|
396
|
-
if (!
|
|
475
|
+
if (!item && paraStart >= 0 && w > 0) paraSentences++
|
|
397
476
|
if (w > limit)
|
|
398
477
|
push('sentence', off + s.at, off + s.at + s.text.length, `${w} words: the limit is ${limit}`)
|
|
399
478
|
}
|
|
400
479
|
}
|
|
401
480
|
flushParagraph(lines.length)
|
|
402
481
|
|
|
482
|
+
if (banned.length > 0) {
|
|
483
|
+
const underBan = (h: ProseHit): boolean => h.cls !== 'ban' && h.word != null && banned.some(([a, b]) => h.start < b && a < h.end)
|
|
484
|
+
for (let i = hits.length - 1; i >= 0; i--) if (underBan(hits[i]!)) hits.splice(i, 1)
|
|
485
|
+
}
|
|
403
486
|
return hits.sort((a, b) => a.start - b.start)
|
|
404
487
|
}
|
|
405
488
|
|
|
@@ -434,3 +517,36 @@ export function fixProse(src: string, opts: ProseOpts = {}): { text: string; app
|
|
|
434
517
|
return { text, applied }
|
|
435
518
|
}
|
|
436
519
|
|
|
520
|
+
/** what is wrong with a repo's word lists, judged the same way by the config
|
|
521
|
+
* loader and by the hub's writer: an empty word, a word listed twice, or a
|
|
522
|
+
* word both allowed and banned, which leaves the repo's intent undecidable */
|
|
523
|
+
export function proseWordsProblems(w: ProseWords | undefined): string[] {
|
|
524
|
+
if (w == null) return []
|
|
525
|
+
if (typeof w !== 'object' || Array.isArray(w)) return ['prose.words is an object: { allow?: [...], ban?: [...] }']
|
|
526
|
+
const out: string[] = []
|
|
527
|
+
for (const k of Object.keys(w)) if (k !== 'allow' && k !== 'ban') out.push(`prose.words.${k} is not a key (keys: allow, ban)`)
|
|
528
|
+
const seen = new Map<string, 'allow' | 'ban'>()
|
|
529
|
+
for (const list of ['allow', 'ban'] as const) {
|
|
530
|
+
const entries: unknown = w[list] ?? []
|
|
531
|
+
if (!Array.isArray(entries)) {
|
|
532
|
+
out.push(`prose.words.${list} is a list`)
|
|
533
|
+
continue
|
|
534
|
+
}
|
|
535
|
+
for (const e of entries as { word?: unknown; to?: unknown; reason?: unknown }[]) {
|
|
536
|
+
if (e == null || typeof e.word !== 'string' || normWord(e.word) === '') {
|
|
537
|
+
out.push(`prose.words.${list} has an entry with no word`)
|
|
538
|
+
continue
|
|
539
|
+
}
|
|
540
|
+
const fields = list === 'ban' ? ['word', 'to', 'reason'] : ['word', 'reason']
|
|
541
|
+
for (const f of Object.keys(e)) if (!fields.includes(f)) out.push(`prose.words.${list} '${e.word}' has an unknown field '${f}' (fields: ${fields.join(', ')})`)
|
|
542
|
+
for (const f of ['to', 'reason'] as const) if (e[f] != null && typeof e[f] !== 'string') out.push(`prose.words.${list} '${e.word}' ${f} is a string`)
|
|
543
|
+
const n = normWord(e.word)
|
|
544
|
+
const was = seen.get(n)
|
|
545
|
+
if (was === list) out.push(`prose.words.${list} lists '${n}' twice`)
|
|
546
|
+
else if (was != null) out.push(`prose.words lists '${n}' in both allow and ban, pick one`)
|
|
547
|
+
seen.set(n, list)
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return out
|
|
551
|
+
}
|
|
552
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// The `.shipkit/reflections/` queue: friction a repo logged against the wiki it
|
|
2
|
+
// follows, waiting for a fresh agent to fold each file into the owning page
|
|
3
|
+
// and DELETE it. Deletion is the ack, so a file that is still here is still
|
|
4
|
+
// pending. Contract: meta/wiki-doctrine in the docs wiki this lib ships
|
|
5
|
+
// beside.
|
|
6
|
+
//
|
|
7
|
+
// Two kinds. A file asking for a wiki RULE change carries six sections; a
|
|
8
|
+
// plain friction NOTE carries no shape at all. Sections match on HEADING-ish
|
|
9
|
+
// lines only: a substring scan of the whole file called any note mentioning
|
|
10
|
+
// "proposed change" a malformed request, and satisfied `scope claimed` from
|
|
11
|
+
// ordinary prose.
|
|
12
|
+
import { existsSync, readdirSync, readFileSync } from 'fs'
|
|
13
|
+
import { join } from 'path'
|
|
14
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
15
|
+
import { LEGACY_CONTRACT_PREFIX } from '../core/migrate-layout.ts'
|
|
16
|
+
|
|
17
|
+
export const REFLECTIONS_DIR = LAYOUT.reflections
|
|
18
|
+
|
|
19
|
+
/** where a repo's queue sits. A harvest across repos reads some that have not
|
|
20
|
+
* run the new shipkit yet, whose queue is still in the old folder. This read
|
|
21
|
+
* dies once `shipkit fleet` shows no repo on the old layout */
|
|
22
|
+
const queueDir = (root: string): string => {
|
|
23
|
+
const legacy = join(root, `${LEGACY_CONTRACT_PREFIX}reflections`)
|
|
24
|
+
return !existsSync(join(root, REFLECTIONS_DIR)) && existsSync(legacy) ? legacy : join(root, REFLECTIONS_DIR)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const REQUEST_SECTIONS = [
|
|
28
|
+
'what i was doing',
|
|
29
|
+
'what the wiki said',
|
|
30
|
+
'what actually happened',
|
|
31
|
+
'owns it',
|
|
32
|
+
'proposed change',
|
|
33
|
+
'scope claimed',
|
|
34
|
+
] as const
|
|
35
|
+
|
|
36
|
+
export type ReflectionKind = { isRequest: boolean; missing: string[] }
|
|
37
|
+
|
|
38
|
+
/** heading lines: markdown headings, or a line leading with **bold** (the
|
|
39
|
+
* shape as wiki-doctrine writes it) */
|
|
40
|
+
const headingLines = (text: string): string[] =>
|
|
41
|
+
text
|
|
42
|
+
.split('\n')
|
|
43
|
+
.filter((l) => /^\s*(#{1,6}\s+|\*\*|[-*]\s+\*\*)/.test(l))
|
|
44
|
+
.map((l) => l.toLowerCase())
|
|
45
|
+
|
|
46
|
+
export function classifyReflection(text: string): ReflectionKind {
|
|
47
|
+
const heads = headingLines(text)
|
|
48
|
+
const has = (section: string): boolean => heads.some((h) => h.includes(section))
|
|
49
|
+
// it announces itself by PROPOSING something, in a heading
|
|
50
|
+
if (!has('proposed change')) return { isRequest: false, missing: [] }
|
|
51
|
+
return { isRequest: true, missing: REQUEST_SECTIONS.filter((s) => !has(s)) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type Reflection = ReflectionKind & { file: string; title: string; unreadable: boolean }
|
|
55
|
+
|
|
56
|
+
/** the first heading, which is the line whoever logged it wrote to name it */
|
|
57
|
+
const firstHeading = (text: string): string | null =>
|
|
58
|
+
text.split('\n').find((l) => /^\s*#{1,6}\s+\S/.test(l))?.replace(/^\s*#{1,6}\s+/, '').trim() ?? null
|
|
59
|
+
|
|
60
|
+
/** every file still pending. README.md is the scaffold's own seed, written
|
|
61
|
+
* into every started repo, so counting it makes every fresh repo a
|
|
62
|
+
* permanent false row */
|
|
63
|
+
export function listReflections(root: string): Reflection[] {
|
|
64
|
+
let files: string[] = []
|
|
65
|
+
try {
|
|
66
|
+
files = readdirSync(queueDir(root)).filter((f) => f.endsWith('.md') && f !== 'README.md')
|
|
67
|
+
} catch {
|
|
68
|
+
return []
|
|
69
|
+
}
|
|
70
|
+
return files.sort().map((file): Reflection => {
|
|
71
|
+
let text: string | null = null
|
|
72
|
+
try {
|
|
73
|
+
text = readFileSync(join(queueDir(root), file), 'utf8')
|
|
74
|
+
} catch {
|
|
75
|
+
// reported as unreadable rather than dropped: a file nobody can
|
|
76
|
+
// read is still a file nobody harvested
|
|
77
|
+
}
|
|
78
|
+
if (text == null) return { file, title: file, isRequest: false, missing: [], unreadable: true }
|
|
79
|
+
return { file, title: firstHeading(text) ?? file, unreadable: false, ...classifyReflection(text) }
|
|
80
|
+
})
|
|
81
|
+
}
|
package/src/release/git-facts.ts
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
// Deliberately dependency-free (no config load, no ci) so a check script can
|
|
4
4
|
// import it without dragging the gate in.
|
|
5
5
|
import type { RepoConfig } from '../repo-config.ts'
|
|
6
|
+
import { gitBin } from '../core/git-bin.ts'
|
|
7
|
+
import { CI_LOG_DIR, LAYOUT } from '../core/layout.ts'
|
|
6
8
|
|
|
7
9
|
export function git(root: string, args: string[]): { code: number; out: string } {
|
|
8
|
-
const p = Bun.spawnSync([
|
|
10
|
+
const p = Bun.spawnSync([gitBin(), ...args], { cwd: root })
|
|
9
11
|
return { code: p.exitCode, out: `${p.stdout.toString()}${p.stderr.toString()}`.trim() }
|
|
10
12
|
}
|
|
11
13
|
|
|
@@ -27,16 +29,20 @@ export function deriveRemote(root: string): { remote: string } | { error: string
|
|
|
27
29
|
return { error: `branch '${branch === '' ? '(detached)' : branch}' has no upstream and ${remotes.length} remotes exist (${remotes.join(', ')}) — set one with git push -u <remote> <branch>` }
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
/** the worktree's dirty paths, minus the runner's own
|
|
31
|
-
* gate
|
|
32
|
-
* in any repo that does not gitignore it.
|
|
32
|
+
/** the worktree's dirty paths, minus the runner's own scratch (`.shipkit/tmp/`,
|
|
33
|
+
* and a `logDir` set elsewhere). running the gate writes its logs, leases and job records there, so counting them would
|
|
34
|
+
* make every gated release refuse in any repo that does not gitignore it.
|
|
35
|
+
* `-uall` lists every untracked file, so a folder git would fold into one
|
|
36
|
+
* row never hides real dirt beside the scratch */
|
|
33
37
|
export function dirtyPaths(root: string, config: RepoConfig, pathspec?: string | string[]): string {
|
|
34
|
-
const
|
|
38
|
+
const logDir = (config.logDir ?? CI_LOG_DIR).replace(/^\.\//, '').replace(/\/+$/, '')
|
|
39
|
+
const scratch = [LAYOUT.tmp, ...(logDir.startsWith(`${LAYOUT.tmp}/`) ? [] : [logDir.split('/')[0]!])]
|
|
40
|
+
const own = (p: string): boolean => scratch.some((d) => p.startsWith(`${d}/`) || p === d)
|
|
35
41
|
const specs = (typeof pathspec === 'string' ? [pathspec] : (pathspec ?? [])).filter((p) => p !== '')
|
|
36
|
-
const args = specs.length > 0 ? ['status', '--porcelain', '--', ...specs] : ['status', '--porcelain']
|
|
42
|
+
const args = specs.length > 0 ? ['status', '--porcelain', '-uall', '--', ...specs] : ['status', '--porcelain', '-uall']
|
|
37
43
|
return git(root, args).out
|
|
38
44
|
.split('\n')
|
|
39
|
-
.filter((l) => l.trim() !== '' && !l.slice(3).trim().replace(/^"|"$/g, '')
|
|
45
|
+
.filter((l) => l.trim() !== '' && !own(l.slice(3).trim().replace(/^"|"$/g, '')))
|
|
40
46
|
.join('\n')
|
|
41
47
|
}
|
|
42
48
|
|
|
@@ -66,7 +72,7 @@ export type RemoteTagRefs = { refs: Map<string, string> } | { error: string }
|
|
|
66
72
|
* because `ls-remote <pattern>` drops the peeled `^{}` line and leaves only
|
|
67
73
|
* the tag OBJECT id, which differs between two tags of the same commit. */
|
|
68
74
|
export function remoteTagRefs(root: string, remote: string): RemoteTagRefs {
|
|
69
|
-
const p = Bun.spawnSync([
|
|
75
|
+
const p = Bun.spawnSync([gitBin(), 'ls-remote', '--tags', remote], { cwd: root, env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } })
|
|
70
76
|
if (p.exitCode !== 0) return { error: `${p.stderr.toString()}${p.stdout.toString()}`.trim().split('\n').at(-1) ?? 'ls-remote failed' }
|
|
71
77
|
const refs = new Map<string, string>()
|
|
72
78
|
for (const line of p.stdout.toString().split('\n')) {
|
|
@@ -134,7 +140,7 @@ export function mergeProbe(root: string, tip: string, from: string): MergeProbe
|
|
|
134
140
|
return { state: 'unknown', reason: `${reason} (needs git 2.38+)` }
|
|
135
141
|
}
|
|
136
142
|
|
|
137
|
-
/** local tags matching any of the
|
|
143
|
+
/** local tags matching any of the releasing units' tag shapes that the remote
|
|
138
144
|
* does not have. A stranded tag is invisible where he looks (GitHub), so it
|
|
139
145
|
* is a failure, not a note. */
|
|
140
146
|
export function strandedTags(p: { root: string; globs: string[]; remote: string }): { stranded: string[]; local: string[] } | { error: string } {
|
package/src/release/run-bump.ts
CHANGED
|
@@ -33,10 +33,11 @@ import { declaredKind, hasUnreleasedNotes, promoteUnreleased } from './changelog
|
|
|
33
33
|
import { deriveRemote, dirtyPathList, dirtyPaths, git as gitAt, mergeProbe, remoteTags, tagExistsLocally } from './git-facts.ts'
|
|
34
34
|
import { unitBlockers } from './unit-gate.ts'
|
|
35
35
|
import { releaseScope } from './release-scope.ts'
|
|
36
|
-
import { formatUnitError, releaseOrder, renderTag, resolveUnits, unitById, unitPrepublishScripts, type ReleaseUnit } from './units.ts'
|
|
36
|
+
import { formatUnitError, releaseOrder, renderTag, resolveUnits, unitById, unitPrepublishScripts, unknownUnit, type ReleaseUnit } from './units.ts'
|
|
37
37
|
import { environmentBlockers, runReleaseGate, releaseUnit } from './run-release.ts'
|
|
38
38
|
import type { RepoConfig } from '../repo-config.ts'
|
|
39
39
|
import { nestedIn, nestedRefusal } from '../core/nesting.ts'
|
|
40
|
+
import { LAYOUT } from '../core/layout.ts'
|
|
40
41
|
|
|
41
42
|
export type BumpKind = 'major' | 'minor' | 'patch'
|
|
42
43
|
|
|
@@ -234,7 +235,7 @@ export async function runBump(root: string, config: RepoConfig, kind: BumpKind |
|
|
|
234
235
|
if (o.all !== true) {
|
|
235
236
|
const id = o.unit ?? 'root'
|
|
236
237
|
if (unitById(onDisk.units, id) == null) {
|
|
237
|
-
print(`FAIL:
|
|
238
|
+
print(`FAIL: ${unknownUnit(onDisk, id)}`)
|
|
238
239
|
print(' fix: shipkit releases')
|
|
239
240
|
return 1
|
|
240
241
|
}
|
|
@@ -249,7 +250,7 @@ export async function runBump(root: string, config: RepoConfig, kind: BumpKind |
|
|
|
249
250
|
|
|
250
251
|
// surfaces the toolkit itself rewrites on gate runs, safe to auto-commit
|
|
251
252
|
const machineWrittenPaths = (r: string, c: RepoConfig): Set<string> => {
|
|
252
|
-
const s = new Set([
|
|
253
|
+
const s = new Set<string>([LAYOUT.status])
|
|
253
254
|
if (c.docsBase != null) {
|
|
254
255
|
const base = c.docsBase.startsWith('~/') ? join(homedir(), c.docsBase.slice(2)) : c.docsBase
|
|
255
256
|
const page = join(base, 'build/shipkit/local-projects.md')
|
|
@@ -372,7 +373,7 @@ export async function runBump(root: string, config: RepoConfig, kind: BumpKind |
|
|
|
372
373
|
} else {
|
|
373
374
|
const id = o.unit ?? 'root'
|
|
374
375
|
const u = unitById(all, id)
|
|
375
|
-
if (u == null) return fail(
|
|
376
|
+
if (u == null) return fail(`${unknownUnit(resolved, id)} (after the merge)`, 'shipkit releases')
|
|
376
377
|
picked = [u]
|
|
377
378
|
}
|
|
378
379
|
|
|
@@ -430,7 +431,7 @@ export async function runBump(root: string, config: RepoConfig, kind: BumpKind |
|
|
|
430
431
|
// gates cost N times as much and prove the same thing. Frozen, and fatal
|
|
431
432
|
// on non-blocking failures when the repo declared requireHealthy.
|
|
432
433
|
// releaseUnit then skips it, because bump already proved this exact tree.
|
|
433
|
-
if ((await runReleaseGate(root, config, print, { fix: true })) !== 0) return fail('ci gate is red on the merged result — fix it, then bump')
|
|
434
|
+
if ((await runReleaseGate(root, config, print, { fix: true, units: picked })) !== 0) return fail('ci gate is red on the merged result — fix it, then bump')
|
|
434
435
|
// the gate's fix pass can rewrite files. writing a version on top of that
|
|
435
436
|
// produces a version commit that excludes the rewrite and a release that
|
|
436
437
|
// then refuses on a dirty tree, which is a half-released repo.
|
|
@@ -13,9 +13,10 @@ import { extractChangelogSection } from './changelog.ts'
|
|
|
13
13
|
import { deriveRemote, dirtyPaths, git, remoteTagState, tagExistsLocally, type RemoteTagState } from './git-facts.ts'
|
|
14
14
|
import { releaseScope } from './release-scope.ts'
|
|
15
15
|
import { npmPublishArgv, unitBlockers } from './unit-gate.ts'
|
|
16
|
-
import { formatUnitError, resolveUnits, unitById, type ReleaseUnit } from './units.ts'
|
|
16
|
+
import { formatUnitError, resolveUnits, unitById, unknownUnit, type ReleaseUnit } from './units.ts'
|
|
17
17
|
import type { RepoConfig } from '../repo-config.ts'
|
|
18
18
|
import { nestedIn, nestedRefusal } from '../core/nesting.ts'
|
|
19
|
+
import { declaredTestUnits, testCheckId } from '../suite/test-units.ts'
|
|
19
20
|
|
|
20
21
|
// adding a FOURTH knob here is the signal that this shape wants rethinking
|
|
21
22
|
// rather than another key (meta/gaps.md, release-path config keys accreting)
|
|
@@ -29,7 +30,7 @@ export type ReleaseConfig = {
|
|
|
29
30
|
/** publish the ROOT unit to npm. Default: its package.json is not private.
|
|
30
31
|
* A private package NEVER publishes, whatever this says. */
|
|
31
32
|
npm?: boolean
|
|
32
|
-
/** argv printing an npm auth token on stdout
|
|
33
|
+
/** argv printing an npm auth token on stdout, as in a secret-manager read.
|
|
33
34
|
* Absent = rely on ambient npm auth. Never put a token in config.
|
|
34
35
|
* Repo-wide: one account publishes every unit. */
|
|
35
36
|
npmTokenCommand?: string[]
|
|
@@ -43,10 +44,9 @@ export type ReleaseConfig = {
|
|
|
43
44
|
/** repo-relative tsconfig for the ROOT unit's own typecheck (default
|
|
44
45
|
* '<root>/tsconfig.json' when it exists) */
|
|
45
46
|
tsconfig?: string
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* the repo saying which tasks are about the REPO rather than the release. */
|
|
47
|
+
/** check ids the release gate skips: a repo-wide task (a full matrix
|
|
48
|
+
* build) that is about the REPO rather than the release. Which unit test
|
|
49
|
+
* checks a release runs is not this key's: → build/shipkit/units.md */
|
|
50
50
|
skipTasks?: string[]
|
|
51
51
|
/** `shipkit bump` run from a feature branch merges it into the release
|
|
52
52
|
* branch first, with a merge commit. Default true. A branch is a GATE, so
|
|
@@ -106,7 +106,7 @@ export type ReleaseOpts = {
|
|
|
106
106
|
* while the rules ran from another downgrades a blocking failure to a warning.
|
|
107
107
|
*
|
|
108
108
|
* it also runs the FULL test tier, which `shipkit ci` skips while you work. */
|
|
109
|
-
export async function runReleaseGate(root: string, config: RepoConfig, print: (l: string) => void, o: { fix?: boolean } = {}): Promise<number> {
|
|
109
|
+
export async function runReleaseGate(root: string, config: RepoConfig, print: (l: string) => void, o: { fix?: boolean; units?: ReleaseUnit[] } = {}): Promise<number> {
|
|
110
110
|
return await runCi(root, {
|
|
111
111
|
fix: o.fix ?? false,
|
|
112
112
|
frozen: true,
|
|
@@ -127,18 +127,32 @@ export async function runReleaseGate(root: string, config: RepoConfig, print: (l
|
|
|
127
127
|
// a DNS provider answers, and asking spends an API call per publish.
|
|
128
128
|
// requireHealthy is the repo saying it wants them fatal, so then they run.
|
|
129
129
|
onlyBlockingChecks: config.release?.requireHealthy !== true,
|
|
130
|
-
|
|
130
|
+
// a sibling unit's suite proves nothing about a sub-unit's tarball: each
|
|
131
|
+
// unit's test check runs when THAT unit ships, or when the root does
|
|
132
|
+
skipTaskIds: [...(config.release?.skipTasks ?? []), ...otherUnitTests(config, o.units)],
|
|
131
133
|
failOnAdvisory: config.release?.requireHealthy === true,
|
|
132
134
|
print: (l) => print(` ${l}`),
|
|
133
135
|
})
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
/** the test checks of every sub-unit this release does not ship. No list
|
|
139
|
+
* given means the caller did not say, and every unit's suite runs. The root
|
|
140
|
+
* depends on what it contains, so a release that ships it runs them all. A
|
|
141
|
+
* `release: false` unit is never skipped: it ships nothing of its own, so its
|
|
142
|
+
* suites exist to prove something that does, and nothing says which */
|
|
143
|
+
export function otherUnitTests(config: RepoConfig, shipping: ReleaseUnit[] | undefined): string[] {
|
|
144
|
+
if (shipping == null || shipping.some((u) => u.id === 'root')) return []
|
|
145
|
+
const ids = new Set(shipping.map((u) => u.id))
|
|
146
|
+
return declaredTestUnits(config.units).filter((u) => u.releases && !ids.has(u.id)).map((u) => testCheckId(u.id))
|
|
147
|
+
}
|
|
148
|
+
|
|
136
149
|
/** `shipkit release [unit]` — resolve the unit, then ship it. */
|
|
137
150
|
export async function runRelease(root: string, config: RepoConfig, o: ReleaseOpts = {}): Promise<number> {
|
|
138
151
|
const print = o.print ?? ((l: string) => console.log(l))
|
|
139
152
|
const nested = nestedIn(root)
|
|
140
153
|
if (nested != null) { print(`FAIL: ${nestedRefusal(nested)}`); return 1 }
|
|
141
|
-
const
|
|
154
|
+
const resolution = resolveUnits(root, config)
|
|
155
|
+
const { units, errors } = resolution
|
|
142
156
|
const id = o.unit ?? 'root'
|
|
143
157
|
// only THIS unit's own resolution has to hold: a broken sibling is not a
|
|
144
158
|
// reason to refuse a release of something that resolves fine
|
|
@@ -146,7 +160,7 @@ export async function runRelease(root: string, config: RepoConfig, o: ReleaseOpt
|
|
|
146
160
|
if (mine.length > 0) { for (const e of mine) print(`FAIL: ${formatUnitError(e)}`); return 1 }
|
|
147
161
|
const unit = unitById(units, id)
|
|
148
162
|
if (unit == null) {
|
|
149
|
-
print(`FAIL:
|
|
163
|
+
print(`FAIL: ${unknownUnit(resolution, id)}`)
|
|
150
164
|
print(' fix: shipkit releases')
|
|
151
165
|
return 1
|
|
152
166
|
}
|
|
@@ -254,7 +268,7 @@ export async function releaseUnit(root: string, config: RepoConfig, unit: Releas
|
|
|
254
268
|
if (o.skipGate === true)
|
|
255
269
|
print(o.skipGateReason != null ? `ok: ci gate ${o.skipGateReason}` : 'WARN ci gate SKIPPED by --skip-gate — publishing unverified')
|
|
256
270
|
else {
|
|
257
|
-
if ((await runReleaseGate(root, config, print)) !== 0) { print('FAIL: ci gate is red — fix it, commit, then release'); return 1 }
|
|
271
|
+
if ((await runReleaseGate(root, config, print, { units: [unit] })) !== 0) { print('FAIL: ci gate is red — fix it, commit, then release'); return 1 }
|
|
258
272
|
// frozen is a declaration, not a guarantee: any check may still write.
|
|
259
273
|
// npm packs the DIRECTORY, so a rewrite here would publish a tarball
|
|
260
274
|
// that exists in no commit while the tag describes another tree.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `shipkit releases` — where every
|
|
1
|
+
// `shipkit releases` — where every releasing unit actually stands. Report only:
|
|
2
2
|
// it never gates, never writes, and exits 0 even when everything is drifted,
|
|
3
3
|
// because it exists to end the "what is published and what is not" question,
|
|
4
4
|
// not to add another red light.
|
|
@@ -249,6 +249,6 @@ export function renderReports(reports: UnitReport[], o: { remoteError?: string |
|
|
|
249
249
|
// once rather than once per cell
|
|
250
250
|
if (o.offline === true) lines.push('note: --offline, so npm, the remote and gh were not read (the ? cells)')
|
|
251
251
|
const subs = reports.length - 1
|
|
252
|
-
lines.push(`note: ${reports.length}
|
|
252
|
+
lines.push(`note: ${reports.length} releasing unit(s) — the repo root${subs > 0 ? ` + ${subs} declared` : ' only, nothing else declared'}`)
|
|
253
253
|
return lines
|
|
254
254
|
}
|