rvlib-shipkit 0.35.0 → 0.35.1

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.35.1
4
+
5
+ - **SK013's green line claimed a replacement that could not be named.** With the guard declared but not yet installed, the rule WARNed correctly and then printed `closes 14/14 raw runner(s), with the replacement named` directly underneath it. Both lines were about the same file and only one of them was true. The summary now reports what the file actually delivers: the guard is `declared, not yet runnable` until its script resolves.
6
+
3
7
  ## 0.35.0
4
8
 
5
9
  - **`shipkit pb collection` mints imports of a package that exists.** The pocketbase data layer and the Mantine shell are one package now, `rvlib-pb`: the generated `columns.tsx`, `grid.tsx` and `index.ts` import `Column`, `LiveGrid` and `defineCollectionUI` from `rvlib-pb/ui`, and `LiveList` from `rvlib-pb`. The two tiers are different subpaths on purpose, so a generated grid that took its type from the ui subpath would typecheck and would put `@mantine/core` in the tsconfig of anything that reads it. This generator ships inside the tarball and writes into repos it never sees, so a stale specifier here keeps producing broken scaffolds long after a rename lands: the suite now asserts every import string the generator emits, which nothing did before.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rvlib-shipkit",
3
- "version": "0.35.0",
3
+ "version": "0.35.1",
4
4
  "description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -32,6 +32,12 @@ export type DiscoveredCheck = {
32
32
  /** `export const never = 'why'`, read statically — a DECIDED never
33
33
  * cacheable, which takes the check off the burn-down list. */
34
34
  never: string | null
35
+ /** `export const gate = false`, read statically — this check is NOT part of
36
+ * `shipkit ci`. `ci` answers "is my change good"; a check that answers
37
+ * something else, and that can sit red for months without anything being
38
+ * wrong, does not belong in the answer. `shipkit check` still runs it.
39
+ * Default true: a check is in the gate unless it opts out by name. */
40
+ gate: boolean
35
41
  }
36
42
 
37
43
  // read without importing: checks run as subprocesses, and discovery must stay
@@ -44,6 +50,10 @@ const BLOCKING_RE = /^\s*export\s+const\s+blocking\s*(?::\s*boolean\s*)?=\s*(tru
44
50
  const INPUTS_RE = /^\s*export\s+const\s+inputs\s*(?::[^=]*)?=\s*\[([^\]]*)\]/m
45
51
  const TTL_RE = /^\s*export\s+const\s+ttl\s*(?::\s*string\s*)?=\s*['"]([^'"]+)['"]/m
46
52
  const NEVER_RE = /^\s*export\s+const\s+never\s*(?::\s*string\s*)?=\s*['"]([^'"]+)['"]/m
53
+ // `export const gate = false` takes a check out of `shipkit ci`. Only the
54
+ // literal false opts out: anything unreadable stays in the gate, because a
55
+ // scanner that guesses its way to SKIPPING a check is how coverage disappears
56
+ const GATE_RE = /^\s*export\s+const\s+gate\s*(?::\s*boolean\s*)?=\s*false\s*$/m
47
57
  const STRING_ITEM_RE = /['"]([^'"]+)['"]/g
48
58
 
49
59
  export function discoverChecks(root: string): DiscoveredCheck[] {
@@ -69,6 +79,7 @@ export function discoverChecks(root: string): DiscoveredCheck[] {
69
79
  let inputs: string[] | null = null
70
80
  let ttl: string | null = null
71
81
  let never: string | null = null
82
+ let gate = true
72
83
  try {
73
84
  const src = readFileSync(f, 'utf8')
74
85
  const m = src.match(BLOCKING_RE)
@@ -80,7 +91,8 @@ export function discoverChecks(root: string): DiscoveredCheck[] {
80
91
  }
81
92
  ttl = src.match(TTL_RE)?.[1] ?? null
82
93
  never = src.match(NEVER_RE)?.[1] ?? null
94
+ gate = src.match(GATE_RE) == null
83
95
  } catch {}
84
- return { path: f, name, blocking, inputs, ttl, never }
96
+ return { path: f, name, blocking, inputs, ttl, never, gate }
85
97
  })
86
98
  }
@@ -50,6 +50,11 @@ export type CheckOpts = {
50
50
  * spends a third-party API call per publish. Undeclared checks are still
51
51
  * collected, because an unclassified check stays fatal. */
52
52
  onlyBlocking?: boolean
53
+ /** the caller is the GATE (`shipkit ci`), so checks that declared
54
+ * `export const gate = false` are skipped. `shipkit check` leaves it unset
55
+ * and runs everything: the health surface is where a human todo list
56
+ * belongs, and the gate is not. */
57
+ gateOnly?: boolean
53
58
  /** the caller narrowed the run to these ids (`ci --only`). Empty or absent
54
59
  * means everything. A `--only` that narrowed TASKS while every check still
55
60
  * ran made the flag actively harmful: `ci --only cycles`, a 0.03s task,
@@ -201,6 +206,12 @@ export async function collectChecks(root: string, o: CheckOpts = {}): Promise<Ch
201
206
  // file wins, then the repo default, then nothing: an unclassified
202
207
  // check is an error, never a guess
203
208
  if (!matchesOnly(o.only, d.name, rel)) continue
209
+ // `ci` answers "is my change good". A check that answers something else,
210
+ // and that can sit red for months with nothing wrong, is not part of
211
+ // that answer at any interval. `shipkit check` still runs it, and
212
+ // naming it in `--only` still runs it, so opting out is never a way to
213
+ // make a check unreachable.
214
+ if (o.gateOnly === true && !d.gate && o.only == null) continue
204
215
  const blocking = d.blocking ?? c.checkDefaults?.blocking
205
216
  if (blocking == null) undeclared.push(rel)
206
217
  if (o.onlyBlocking === true && blocking !== true) continue
@@ -271,7 +271,7 @@ async function runCiInScope(root: string, o: CiOpts = {}): Promise<number> {
271
271
  if (survey != null) await survey
272
272
  const checks = o.skipChecks === true
273
273
  ? { lines: [], blocked: false, advisoryFailures: [], undeclared: [] }
274
- : await collectChecks(root, { status: false, config: c, frozen: o.frozen, onlyBlocking: o.onlyBlockingChecks, only: o.onlyIds, fresh: o.fresh, ledger, reportHome: false })
274
+ : await collectChecks(root, { status: false, config: c, frozen: o.frozen, onlyBlocking: o.onlyBlockingChecks, only: o.onlyIds, fresh: o.fresh, ledger, reportHome: false, gateOnly: true })
275
275
  if (priorShare == null) delete process.env[SHARE_ENV]; else process.env[SHARE_ENV] = priorShare
276
276
  for (const f of [sharePath, `${sharePath}.lock`]) { try { rmSync(f, { force: true }) } catch {} }
277
277
  const color = process.stdout.isTTY && process.env.NO_COLOR == null
@@ -103,10 +103,12 @@ export const oneWayInPolicy: Rule = {
103
103
  if (missing.length > 0)
104
104
  t.fail(`${SETTINGS_PATH}: ${missing.length} raw runner(s) still permitted (${missing.slice(0, 3).join(', ')}${missing.length > 3 ? ', …' : ''}) — those calls never reach shipkit, so nothing is cached and no run can be attached to`)
105
105
  const guard = guardHookCommand(s)
106
+ let guardRuns = false
106
107
  if (guard == null)
107
108
  t.fail(`${SETTINGS_PATH}: no PreToolUse guard — a bare deny tells the model only that it was denied, so it goes looking for another runner instead of the shipkit command`)
108
109
  else {
109
110
  const missing = guardScriptMissing(env.root, guard)
111
+ guardRuns = missing == null
110
112
  // the hook is DECLARED either way, so it starts working the moment
111
113
  // the dependency lands: no second fix run, nothing to remember. An
112
114
  // uninstalled dependency is therefore a WARN naming the command
@@ -124,7 +126,11 @@ export const oneWayInPolicy: Rule = {
124
126
  `point it at this repo's own copy of hooks/one-way-in-guard.ts, or delete the hook and let the deny list stand alone`,
125
127
  )
126
128
  }
127
- if (!t.failed) t.ok(`${SETTINGS_PATH} closes ${want.length}/${RAW_RUNNERS.length} raw runner(s), with the replacement named`)
129
+ // the summary states only what the file actually delivers: claiming the
130
+ // replacement is named while the guard cannot run is the green that
131
+ // measured nothing, one line under the WARN that says otherwise
132
+ if (!t.failed)
133
+ t.ok(`${SETTINGS_PATH} closes ${want.length}/${RAW_RUNNERS.length} raw runner(s)${guardRuns ? ', with the replacement named' : ' — the guard that names the replacement is declared, not yet runnable'}`)
128
134
  },
129
135
  // MERGES, never rewrites: this file also carries the attribution silencer,
130
136
  // a repo's own allow list and its own hooks, and clobbering those to fix a