@shrkcrft/cli 0.1.0-alpha.27 → 0.1.0-alpha.29

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.
Files changed (46) hide show
  1. package/dist/commands/baseline.command.d.ts +8 -0
  2. package/dist/commands/baseline.command.d.ts.map +1 -0
  3. package/dist/commands/baseline.command.js +542 -0
  4. package/dist/commands/changelog-data.d.ts.map +1 -1
  5. package/dist/commands/changelog-data.js +42 -0
  6. package/dist/commands/check.command.d.ts.map +1 -1
  7. package/dist/commands/check.command.js +147 -12
  8. package/dist/commands/command-catalog.d.ts.map +1 -1
  9. package/dist/commands/command-catalog.js +120 -0
  10. package/dist/commands/daily.commands.d.ts.map +1 -1
  11. package/dist/commands/daily.commands.js +11 -1
  12. package/dist/commands/gates.command.d.ts +16 -0
  13. package/dist/commands/gates.command.d.ts.map +1 -0
  14. package/dist/commands/gates.command.js +377 -0
  15. package/dist/commands/generated.command.d.ts +6 -0
  16. package/dist/commands/generated.command.d.ts.map +1 -0
  17. package/dist/commands/generated.command.js +544 -0
  18. package/dist/commands/help.command.d.ts.map +1 -1
  19. package/dist/commands/help.command.js +64 -2
  20. package/dist/commands/ingest.command.d.ts +11 -0
  21. package/dist/commands/ingest.command.d.ts.map +1 -1
  22. package/dist/commands/ingest.command.js +49 -23
  23. package/dist/commands/policy-lint.command.d.ts +37 -0
  24. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  25. package/dist/commands/policy-lint.command.js +167 -8
  26. package/dist/commands/registry.command.d.ts.map +1 -1
  27. package/dist/commands/registry.command.js +70 -13
  28. package/dist/commands/wiring.command.d.ts.map +1 -1
  29. package/dist/commands/wiring.command.js +25 -0
  30. package/dist/exit-codes.d.ts +27 -7
  31. package/dist/exit-codes.d.ts.map +1 -1
  32. package/dist/exit-codes.js +47 -8
  33. package/dist/finish/run-finish.d.ts.map +1 -1
  34. package/dist/finish/run-finish.js +9 -3
  35. package/dist/gates/gate-envelope.d.ts +64 -0
  36. package/dist/gates/gate-envelope.d.ts.map +1 -0
  37. package/dist/gates/gate-envelope.js +26 -0
  38. package/dist/gates/gate-rule-view.d.ts +35 -0
  39. package/dist/gates/gate-rule-view.d.ts.map +1 -0
  40. package/dist/gates/gate-rule-view.js +81 -0
  41. package/dist/gates/rule-coverage.d.ts +53 -0
  42. package/dist/gates/rule-coverage.d.ts.map +1 -0
  43. package/dist/gates/rule-coverage.js +165 -0
  44. package/dist/main.d.ts.map +1 -1
  45. package/dist/main.js +27 -3
  46. package/package.json +33 -33
@@ -0,0 +1,8 @@
1
+ import { type ICommandHandler } from '../command-registry.js';
2
+ export declare const baselineListCommand: ICommandHandler;
3
+ export declare const baselineCheckCommand: ICommandHandler;
4
+ export declare const baselineDiffCommand: ICommandHandler;
5
+ export declare const baselineUpdateCommand: ICommandHandler;
6
+ export declare const baselineExplainCommand: ICommandHandler;
7
+ export declare const baselineCommand: ICommandHandler;
8
+ //# sourceMappingURL=baseline.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"baseline.command.d.ts","sourceRoot":"","sources":["../../src/commands/baseline.command.ts"],"names":[],"mappings":"AA8BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA6ShC,eAAO,MAAM,mBAAmB,EAAE,eAyCjC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,eAyGlC,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,eA8BjC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAwDnC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eA2DpC,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,eAc7B,CAAC"}
@@ -0,0 +1,542 @@
1
+ /**
2
+ * `shrk baseline` — the committed-baseline drift engine.
3
+ *
4
+ * shrk baseline list # every declared baseline
5
+ * shrk baseline check [--id X] # recompute + diff vs committed
6
+ * shrk baseline diff [--id X] # human-readable +added/−removed (never fails)
7
+ * shrk baseline update [--id X] # the explicit, reviewable bless step
8
+ * shrk baseline explain --id X # what it will run/extract, without judging
9
+ *
10
+ * Rules come from `sharkcraft.config.ts baselines[]`. A `compute.kind:
11
+ * "command"` baseline SPAWNS a shell command — which is why the pack-plane
12
+ * merge seam (`resolveProjectConfig`) drops any pack-contributed baseline that
13
+ * declares one. Everything reaching this command with a `run` therefore came
14
+ * from the repo's OWN config, the same trust boundary `verificationCommands`
15
+ * uses.
16
+ */
17
+ import { spawnSync } from 'node:child_process';
18
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
19
+ import * as nodePath from 'node:path';
20
+ import { failsWhenEmpty } from '@shrkcrft/core';
21
+ import { baselineCount, baselineFails, computeBaselineFromExtractor, diffBaseline, matchesAny, } from '@shrkcrft/boundaries';
22
+ import { resolveChangedFiles, resolveProjectConfig } from '@shrkcrft/inspector';
23
+ import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
24
+ import { ExitCode } from "../exit-codes.js";
25
+ import { asJson, header, kv } from "../output/format-output.js";
26
+ import { buildGateEnvelope } from "../gates/gate-envelope.js";
27
+ const SCHEMA = 'sharkcraft.baseline/v1';
28
+ const DEFAULT_TIMEOUT_MS = 60_000;
29
+ async function loadBaselines(cwd) {
30
+ const loaded = await resolveProjectConfig(cwd);
31
+ if (!loaded.ok)
32
+ return { ok: false, message: loaded.error.message };
33
+ const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
34
+ return {
35
+ ok: true,
36
+ value: {
37
+ rules: loaded.value.config.baselines ?? [],
38
+ planeDiagnostics: loaded.value.planeDiagnostics,
39
+ sharkcraftDirRel: rel && !rel.startsWith('..') ? rel : '',
40
+ },
41
+ };
42
+ }
43
+ /** Narrow to `--id`, refusing an unknown id rather than silently selecting nothing. */
44
+ function selectRules(rules, id) {
45
+ if (!id)
46
+ return { ok: true, rules };
47
+ const wanted = id.split(',').map((s) => s.trim()).filter(Boolean);
48
+ const known = new Set(rules.map((r) => r.id));
49
+ const unknown = wanted.filter((w) => !known.has(w));
50
+ if (unknown.length > 0) {
51
+ return {
52
+ ok: false,
53
+ message: `Unknown baseline id(s): ${unknown.join(', ')}. Declared: ${[...known].join(', ') || '(none)'}`,
54
+ };
55
+ }
56
+ return { ok: true, rules: rules.filter((r) => wanted.includes(r.id)) };
57
+ }
58
+ function computeCurrent(cwd, rule, excludeDirs) {
59
+ if (rule.compute.kind === 'extractor') {
60
+ const source = rule.compute.source;
61
+ if (!source)
62
+ return { text: '', error: 'compute.kind "extractor" but no `source` declared' };
63
+ const res = computeBaselineFromExtractor(cwd, source, excludeDirs);
64
+ return res.error
65
+ ? { text: '', error: res.error, filesScanned: res.filesScanned }
66
+ : { text: res.text, filesScanned: res.filesScanned };
67
+ }
68
+ const run = rule.compute.run;
69
+ if (!run || run.trim() === '') {
70
+ return { text: '', error: 'compute.kind "command" but no `run` declared' };
71
+ }
72
+ const child = spawnSync(run, {
73
+ cwd,
74
+ shell: true,
75
+ encoding: 'utf8',
76
+ timeout: rule.compute.timeoutMs ?? DEFAULT_TIMEOUT_MS,
77
+ maxBuffer: 16 * 1024 * 1024,
78
+ });
79
+ if (child.error)
80
+ return { text: '', error: `compute command failed to start: ${child.error.message}` };
81
+ if (child.status !== 0) {
82
+ const tail = String(child.stderr ?? '').trim().split('\n').slice(-3).join(' | ');
83
+ return {
84
+ text: '',
85
+ error: `compute command exited ${child.status ?? 'null'}${tail ? ` — ${tail}` : ''}`,
86
+ };
87
+ }
88
+ return { text: String(child.stdout ?? '') };
89
+ }
90
+ function evaluateRule(cwd, rule, excludeDirs, changedFiles) {
91
+ // --changed-only is honest about what it CANNOT scope: a command compute with
92
+ // no `watchFiles` has no file footprint, so it is reported as skipped rather
93
+ // than quietly passing.
94
+ if (changedFiles !== undefined) {
95
+ const globs = rule.watchFiles && rule.watchFiles.length > 0
96
+ ? rule.watchFiles
97
+ : rule.compute.kind === 'extractor'
98
+ ? (rule.compute.source?.files ?? [])
99
+ : undefined;
100
+ if (globs === undefined) {
101
+ return {
102
+ rule,
103
+ status: 'skipped',
104
+ committedCount: 0,
105
+ currentCount: 0,
106
+ skipReason: 'command compute with no `watchFiles` cannot be scoped to a changeset',
107
+ };
108
+ }
109
+ if (!changedFiles.some((f) => matchesAny(f, globs))) {
110
+ return {
111
+ rule,
112
+ status: 'skipped',
113
+ committedCount: 0,
114
+ currentCount: 0,
115
+ skipReason: 'no watched file changed',
116
+ };
117
+ }
118
+ }
119
+ const abs = nodePath.resolve(cwd, rule.baseline);
120
+ if (!existsSync(abs)) {
121
+ // No artifact yet. `check` must still fail (nothing to compare against),
122
+ // but the CURRENT side is knowable and is exactly what the author needs to
123
+ // see before blessing — so compute it rather than reporting a false `0`.
124
+ const first = computeCurrent(cwd, rule, excludeDirs);
125
+ return {
126
+ rule,
127
+ status: 'error',
128
+ committedCount: 0,
129
+ ...(first.error ? {} : { current: first.text }),
130
+ currentCount: first.error ? 0 : baselineCount(rule, first.text),
131
+ missingBaseline: true,
132
+ error: first.error ??
133
+ `committed baseline ${rule.baseline} does not exist — create it with \`shrk baseline update --id ${rule.id}\``,
134
+ };
135
+ }
136
+ let committed;
137
+ try {
138
+ committed = readFileSync(abs, 'utf8');
139
+ }
140
+ catch (e) {
141
+ return {
142
+ rule,
143
+ status: 'error',
144
+ committedCount: 0,
145
+ currentCount: 0,
146
+ error: `could not read ${rule.baseline}: ${e.message}`,
147
+ };
148
+ }
149
+ const computed = computeCurrent(cwd, rule, excludeDirs);
150
+ if (computed.error) {
151
+ return { rule, status: 'error', committed, committedCount: 0, currentCount: 0, error: computed.error };
152
+ }
153
+ const committedCount = baselineCount(rule, committed);
154
+ const currentCount = baselineCount(rule, computed.text);
155
+ // A recompute that produced NOTHING compared nothing — and left as a pass it
156
+ // would "match" an empty baseline forever, the silent-green this plane exists
157
+ // to prevent. But this only holds when the COMMITTED side is empty too: if
158
+ // the baseline has entries and the recompute has none, that is real drift
159
+ // (everything vanished) and must be reported as such, not swallowed as a skip.
160
+ if (currentCount === 0 && committedCount === 0) {
161
+ return {
162
+ rule,
163
+ status: failsWhenEmpty(rule) ? 'failed' : 'skipped',
164
+ committed,
165
+ current: computed.text,
166
+ committedCount,
167
+ currentCount,
168
+ skipReason: 'the recompute produced no entries (and the committed baseline is empty too)',
169
+ };
170
+ }
171
+ const diff = diffBaseline(rule, committed, computed.text);
172
+ return {
173
+ rule,
174
+ status: baselineFails(rule, diff) ? 'failed' : 'passed',
175
+ diff,
176
+ committed,
177
+ current: computed.text,
178
+ committedCount,
179
+ currentCount,
180
+ // A total wipe is far more often a broken compute than a real emptying —
181
+ // say so next to the diff so it is not blessed by reflex.
182
+ ...(currentCount === 0 ? { emptyCompute: true } : {}),
183
+ };
184
+ }
185
+ function hintFor(rule) {
186
+ return rule.hint ?? `review the diff, then bless it with \`shrk baseline update --id ${rule.id}\``;
187
+ }
188
+ function outcomeJson(o) {
189
+ return {
190
+ id: o.rule.id,
191
+ ...(o.rule.description ? { description: o.rule.description } : {}),
192
+ baseline: o.rule.baseline,
193
+ severity: o.rule.severity ?? 'error',
194
+ direction: o.rule.direction ?? 'two-way',
195
+ status: o.status,
196
+ committedCount: o.committedCount,
197
+ currentCount: o.currentCount,
198
+ ...(o.diff ? { diff: { added: o.diff.added, removed: o.diff.removed, mode: o.diff.mode, canonical: o.diff.canonical } } : {}),
199
+ ...(o.error ? { error: o.error } : {}),
200
+ ...(o.skipReason ? { skipReason: o.skipReason } : {}),
201
+ ...(o.emptyCompute ? { emptyCompute: true } : {}),
202
+ ...(o.missingBaseline ? { missingBaseline: true } : {}),
203
+ hint: hintFor(o.rule),
204
+ };
205
+ }
206
+ /** Print one outcome's ±diff, capped so a huge drift stays readable. */
207
+ function writeDiff(o, cap = 25) {
208
+ if (!o.diff)
209
+ return;
210
+ for (const a of o.diff.added.slice(0, cap))
211
+ process.stdout.write(` + ${a}\n`);
212
+ if (o.diff.added.length > cap)
213
+ process.stdout.write(` + … (${o.diff.added.length - cap} more)\n`);
214
+ for (const r of o.diff.removed.slice(0, cap))
215
+ process.stdout.write(` - ${r}\n`);
216
+ if (o.diff.removed.length > cap)
217
+ process.stdout.write(` - … (${o.diff.removed.length - cap} more)\n`);
218
+ }
219
+ /** Shared prologue: load config, select rules, resolve the changed scope. */
220
+ async function prepare(args, opts) {
221
+ const cwd = resolveCwd(args);
222
+ const json = flagBool(args, 'json');
223
+ const loaded = await loadBaselines(cwd);
224
+ if (!loaded.ok) {
225
+ if (json)
226
+ process.stdout.write(asJson({ schema: SCHEMA, error: loaded.message }) + '\n');
227
+ else
228
+ process.stderr.write(`Could not load config: ${loaded.message}\n Run \`shrk doctor\` for details.\n`);
229
+ return { ok: false, code: ExitCode.UsageError };
230
+ }
231
+ const selected = selectRules(loaded.value.rules, flagString(args, 'id') ?? undefined);
232
+ if (!selected.ok) {
233
+ process.stderr.write(selected.message + '\n');
234
+ return { ok: false, code: ExitCode.UsageError };
235
+ }
236
+ let changedFiles;
237
+ if (opts.changedAware) {
238
+ const since = flagString(args, 'since');
239
+ if (flagBool(args, 'changed-only') || since) {
240
+ changedFiles = resolveChangedFiles({
241
+ projectRoot: cwd,
242
+ ...(since ? { since } : {}),
243
+ ...(!since ? { includeWorktree: true } : {}),
244
+ }).files;
245
+ }
246
+ }
247
+ return {
248
+ ok: true,
249
+ cwd,
250
+ rules: selected.rules,
251
+ all: loaded.value.rules,
252
+ excludeDirs: loaded.value.sharkcraftDirRel ? [loaded.value.sharkcraftDirRel] : [],
253
+ ...(changedFiles !== undefined ? { changedFiles } : {}),
254
+ planeDiagnostics: loaded.value.planeDiagnostics,
255
+ };
256
+ }
257
+ /** The "no baselines declared" landing, shared by every subverb. */
258
+ function writeNoRules(json) {
259
+ if (json) {
260
+ process.stdout.write(asJson({ schema: SCHEMA, results: [], evaluated: 0, verdict: 'not-verified' }) + '\n');
261
+ return ExitCode.NotVerified;
262
+ }
263
+ process.stdout.write(header('Baselines'));
264
+ process.stdout.write(' No baselines declared. Add `baselines[]` to sharkcraft.config.ts to replace a\n' +
265
+ ' hand-rolled "committed file + recompute script + drift test" trio with one\n' +
266
+ ' two-way engine (see docs/baseline-drift.md).\n');
267
+ return ExitCode.NotVerified;
268
+ }
269
+ export const baselineListCommand = {
270
+ name: 'list',
271
+ description: 'List every declared baseline: what it pins, how it recomputes, which direction fails.',
272
+ usage: 'shrk baseline list [--json]',
273
+ booleanFlags: new Set(['json']),
274
+ async run(args) {
275
+ const prep = await prepare(args, { changedAware: false });
276
+ if (!prep.ok)
277
+ return prep.code;
278
+ const json = flagBool(args, 'json');
279
+ if (prep.all.length === 0)
280
+ return writeNoRules(json);
281
+ if (json) {
282
+ process.stdout.write(asJson({
283
+ schema: SCHEMA,
284
+ baselines: prep.all.map((r) => ({
285
+ id: r.id,
286
+ description: r.description ?? null,
287
+ baseline: r.baseline,
288
+ compute: r.compute.kind,
289
+ direction: r.direction ?? 'two-way',
290
+ keyBy: r.keyBy ?? null,
291
+ failOnEmpty: r.failOnEmpty === true,
292
+ })),
293
+ diagnostics: prep.planeDiagnostics,
294
+ }) + '\n');
295
+ return ExitCode.VerifiedPass;
296
+ }
297
+ process.stdout.write(header(`Baselines (${prep.all.length})`));
298
+ for (const r of prep.all) {
299
+ process.stdout.write(` • ${r.id} → ${r.baseline}\n`);
300
+ process.stdout.write(` compute ${r.compute.kind}${r.compute.kind === 'command' ? ` (${r.compute.run})` : ''}` +
301
+ ` · direction ${r.direction ?? 'two-way'}${r.keyBy ? ` · keyBy ${r.keyBy}` : ''}` +
302
+ `${r.failOnEmpty ? ' · failOnEmpty' : ''}\n`);
303
+ if (r.description)
304
+ process.stdout.write(` ${r.description}\n`);
305
+ }
306
+ for (const d of prep.planeDiagnostics)
307
+ process.stdout.write(` ! ${d}\n`);
308
+ return ExitCode.VerifiedPass;
309
+ },
310
+ };
311
+ export const baselineCheckCommand = {
312
+ name: 'check',
313
+ description: 'Recompute every declared baseline and fail on drift. Two-way by default — a LOST entry fails exactly like a gained one.',
314
+ usage: 'shrk baseline check [--id <ids>] [--changed-only] [--since <ref>] [--json]',
315
+ booleanFlags: new Set(['json', 'changed-only']),
316
+ async run(args) {
317
+ const prep = await prepare(args, { changedAware: true });
318
+ if (!prep.ok)
319
+ return prep.code;
320
+ const json = flagBool(args, 'json');
321
+ if (prep.rules.length === 0)
322
+ return writeNoRules(json);
323
+ const outcomes = prep.rules.map((r) => evaluateRule(prep.cwd, r, prep.excludeDirs, prep.changedFiles));
324
+ const failed = outcomes.filter((o) => o.status === 'failed' || (o.status === 'error' && (o.rule.severity ?? 'error') === 'error'));
325
+ const evaluated = outcomes.filter((o) => o.status !== 'skipped').length;
326
+ // 0 only when a NON-EMPTY scope was actually compared; 2 when nothing was.
327
+ // A skipped baseline is "partially verified", never a green 0.
328
+ const skippedCount = outcomes.filter((o) => o.status === 'skipped').length;
329
+ const exit = failed.length > 0
330
+ ? ExitCode.Failure
331
+ : evaluated === 0 || skippedCount > 0
332
+ ? ExitCode.NotVerified
333
+ : ExitCode.VerifiedPass;
334
+ if (json) {
335
+ process.stdout.write(asJson({
336
+ schema: SCHEMA,
337
+ results: outcomes.map(outcomeJson),
338
+ evaluated,
339
+ skipped: outcomes.filter((o) => o.status === 'skipped').length,
340
+ verdict: failed.length > 0 ? 'errors' : evaluated === 0 ? 'not-verified' : 'pass',
341
+ diagnostics: prep.planeDiagnostics,
342
+ gate: buildGateEnvelope('baseline check', exit, outcomes.map((o) => ({
343
+ id: o.rule.id,
344
+ type: 'baseline',
345
+ status: o.status,
346
+ severity: o.rule.severity ?? 'error',
347
+ counts: { committed: o.committedCount, current: o.currentCount },
348
+ violations: [
349
+ ...(o.diff?.added ?? []).map((id) => ({ id, message: 'added', hint: hintFor(o.rule) })),
350
+ ...(o.diff?.removed ?? []).map((id) => ({ id, message: 'removed', hint: hintFor(o.rule) })),
351
+ ],
352
+ ...(o.skipReason ? { skipReason: o.skipReason } : {}),
353
+ ...(o.error ? { error: o.error } : {}),
354
+ }))),
355
+ }) + '\n');
356
+ return exit;
357
+ }
358
+ process.stdout.write(header('Baseline drift'));
359
+ process.stdout.write(kv('evaluated', `${evaluated} of ${prep.rules.length}`) + '\n');
360
+ for (const o of outcomes) {
361
+ if (o.status === 'passed') {
362
+ process.stdout.write(` ✓ ${o.rule.id} (${o.currentCount} entries, no drift)\n`);
363
+ continue;
364
+ }
365
+ if (o.status === 'skipped') {
366
+ process.stdout.write(` – ${o.rule.id} SKIPPED — ${o.skipReason}\n`);
367
+ continue;
368
+ }
369
+ if (o.status === 'error') {
370
+ process.stdout.write(` ! ${o.rule.id} ${o.error}\n`);
371
+ if (o.missingBaseline && o.currentCount > 0) {
372
+ process.stdout.write(` the compute currently yields ${o.currentCount} entr${o.currentCount === 1 ? 'y' : 'ies'} — ` +
373
+ `run \`shrk baseline update --id ${o.rule.id}\` to bless them.\n`);
374
+ }
375
+ continue;
376
+ }
377
+ const added = o.diff?.added.length ?? 0;
378
+ const removed = o.diff?.removed.length ?? 0;
379
+ process.stdout.write(` ✗ ${o.rule.id} DRIFT — ${added} added, ${removed} removed ` +
380
+ `(${o.committedCount} committed → ${o.currentCount} now, ${o.diff?.mode})\n`);
381
+ writeDiff(o);
382
+ if (o.emptyCompute) {
383
+ process.stdout.write(' ! the recompute produced 0 entries — check the compute before blessing this.\n');
384
+ }
385
+ process.stdout.write(` → ${hintFor(o.rule)}\n`);
386
+ }
387
+ for (const d of prep.planeDiagnostics)
388
+ process.stdout.write(` ! ${d}\n`);
389
+ if (exit === ExitCode.NotVerified) {
390
+ process.stdout.write('\nNothing was compared — this is NOT a pass. Every selected baseline was skipped.\n');
391
+ }
392
+ else if (exit === ExitCode.VerifiedPass) {
393
+ process.stdout.write('\nEvery baseline matches its committed artifact. ✓\n');
394
+ }
395
+ return exit;
396
+ },
397
+ };
398
+ export const baselineDiffCommand = {
399
+ name: 'diff',
400
+ description: 'Show the +added / −removed entries for each baseline without failing — the inspection verb (always exits 0 when it ran).',
401
+ usage: 'shrk baseline diff [--id <ids>] [--json]',
402
+ booleanFlags: new Set(['json']),
403
+ async run(args) {
404
+ const prep = await prepare(args, { changedAware: false });
405
+ if (!prep.ok)
406
+ return prep.code;
407
+ const json = flagBool(args, 'json');
408
+ if (prep.rules.length === 0)
409
+ return writeNoRules(json);
410
+ const outcomes = prep.rules.map((r) => evaluateRule(prep.cwd, r, prep.excludeDirs, undefined));
411
+ if (json) {
412
+ process.stdout.write(asJson({ schema: SCHEMA, results: outcomes.map(outcomeJson), inspection: true }) + '\n');
413
+ return ExitCode.VerifiedPass;
414
+ }
415
+ process.stdout.write(header('Baseline diff (inspection — never fails)'));
416
+ for (const o of outcomes) {
417
+ const added = o.diff?.added.length ?? 0;
418
+ const removed = o.diff?.removed.length ?? 0;
419
+ process.stdout.write(`\n${o.rule.id} (${o.rule.baseline}) ${o.status === 'error' ? `! ${o.error}` : `+${added} / -${removed}`}\n`);
420
+ writeDiff(o, 100);
421
+ }
422
+ return ExitCode.VerifiedPass;
423
+ },
424
+ };
425
+ export const baselineUpdateCommand = {
426
+ name: 'update',
427
+ description: 'Rewrite the committed baseline from the current value — the explicit, reviewable bless step. Writes files.',
428
+ usage: 'shrk baseline update [--id <ids>] [--dry-run] [--json]',
429
+ booleanFlags: new Set(['json', 'dry-run']),
430
+ async run(args) {
431
+ const prep = await prepare(args, { changedAware: false });
432
+ if (!prep.ok)
433
+ return prep.code;
434
+ const json = flagBool(args, 'json');
435
+ const dryRun = flagBool(args, 'dry-run');
436
+ if (prep.rules.length === 0)
437
+ return writeNoRules(json);
438
+ const written = [];
439
+ const errors = [];
440
+ for (const rule of prep.rules) {
441
+ const computed = computeCurrent(prep.cwd, rule, prep.excludeDirs);
442
+ if (computed.error) {
443
+ errors.push({ id: rule.id, error: computed.error });
444
+ continue;
445
+ }
446
+ if (computed.text.trim() === '' && failsWhenEmpty(rule)) {
447
+ errors.push({
448
+ id: rule.id,
449
+ error: 'refusing to write an EMPTY baseline for a `failOnEmpty` rule — fix the compute first',
450
+ });
451
+ continue;
452
+ }
453
+ const abs = nodePath.resolve(prep.cwd, rule.baseline);
454
+ const previous = existsSync(abs) ? readFileSync(abs, 'utf8') : undefined;
455
+ const changed = previous !== computed.text;
456
+ if (!dryRun && changed) {
457
+ mkdirSync(nodePath.dirname(abs), { recursive: true });
458
+ writeFileSync(abs, computed.text, 'utf8');
459
+ }
460
+ written.push({ id: rule.id, path: rule.baseline, bytes: computed.text.length, changed });
461
+ }
462
+ if (json) {
463
+ process.stdout.write(asJson({ schema: SCHEMA, dryRun, written, errors }) + '\n');
464
+ return errors.length > 0 ? ExitCode.Failure : ExitCode.VerifiedPass;
465
+ }
466
+ process.stdout.write(header(dryRun ? 'Baseline update (dry run)' : 'Baseline update'));
467
+ for (const w of written) {
468
+ process.stdout.write(` ${w.changed ? (dryRun ? 'would write' : 'wrote') : 'unchanged '} ${w.path} (${w.bytes} bytes)\n`);
469
+ }
470
+ for (const e of errors)
471
+ process.stdout.write(` ! ${e.id}: ${e.error}\n`);
472
+ if (written.some((w) => w.changed) && !dryRun) {
473
+ process.stdout.write('\nReview the diff before committing — this is the bless step.\n');
474
+ }
475
+ return errors.length > 0 ? ExitCode.Failure : ExitCode.VerifiedPass;
476
+ },
477
+ };
478
+ export const baselineExplainCommand = {
479
+ name: 'explain',
480
+ description: 'Show what ONE baseline will compute and compare — the command or extractor, the canonical form, both entry counts and the diff — without turning it into a verdict.',
481
+ usage: 'shrk baseline explain --id <id> [--json]',
482
+ booleanFlags: new Set(['json']),
483
+ async run(args) {
484
+ const id = flagString(args, 'id') ?? args.positional[0];
485
+ if (!id) {
486
+ process.stderr.write('Usage: shrk baseline explain --id <id>\n');
487
+ return ExitCode.UsageError;
488
+ }
489
+ const prep = await prepare(args, { changedAware: false });
490
+ if (!prep.ok)
491
+ return prep.code;
492
+ const rule = prep.all.find((r) => r.id === id);
493
+ if (!rule) {
494
+ process.stderr.write(`No baseline "${id}". Declared: ${prep.all.map((r) => r.id).join(', ') || '(none)'}\n`);
495
+ return ExitCode.UsageError;
496
+ }
497
+ const outcome = evaluateRule(prep.cwd, rule, prep.excludeDirs, undefined);
498
+ if (flagBool(args, 'json')) {
499
+ process.stdout.write(asJson({
500
+ schema: 'sharkcraft.baseline-explain/v1',
501
+ ...outcomeJson(outcome),
502
+ compute: rule.compute,
503
+ watchFiles: rule.watchFiles ?? null,
504
+ }) + '\n');
505
+ return ExitCode.VerifiedPass;
506
+ }
507
+ process.stdout.write(header(`Baseline: ${rule.id}`));
508
+ if (rule.description)
509
+ process.stdout.write(` ${rule.description}\n`);
510
+ process.stdout.write(kv('committed', rule.baseline) + '\n');
511
+ process.stdout.write(kv('compute', rule.compute.kind === 'command' ? `command · ${rule.compute.run}` : `extractor · ${rule.compute.source?.extract ?? 'sugar'}`) + '\n');
512
+ process.stdout.write(kv('direction', rule.direction ?? 'two-way') + '\n');
513
+ process.stdout.write(kv('canonical', outcome.diff?.canonical ?? rule.compute.canonical ?? 'auto') + '\n');
514
+ if (rule.keyBy)
515
+ process.stdout.write(kv('keyBy', rule.keyBy) + '\n');
516
+ process.stdout.write(kv('entries', outcome.missingBaseline
517
+ ? `committed (none yet) → ${outcome.currentCount} now`
518
+ : `${outcome.committedCount} committed → ${outcome.currentCount} now`) + '\n');
519
+ process.stdout.write(kv('status', outcome.status) + '\n');
520
+ if (outcome.error)
521
+ process.stdout.write(` ! ${outcome.error}\n`);
522
+ if (outcome.skipReason)
523
+ process.stdout.write(` – ${outcome.skipReason}\n`);
524
+ if (outcome.diff && (outcome.diff.added.length > 0 || outcome.diff.removed.length > 0)) {
525
+ process.stdout.write('\n diff:\n');
526
+ writeDiff(outcome, 100);
527
+ }
528
+ return ExitCode.VerifiedPass;
529
+ },
530
+ };
531
+ export const baselineCommand = {
532
+ name: 'baseline',
533
+ description: 'Committed-baseline drift engine: recompute a ledger/digest/allow-list and fail on drift in BOTH directions. Read-only except `update`.',
534
+ usage: 'shrk baseline list | check | diff | update | explain --id <id>',
535
+ booleanFlags: new Set(['json', 'changed-only', 'dry-run']),
536
+ async run(args) {
537
+ const sub = args.positional[0];
538
+ process.stderr.write((sub ? `Unknown subcommand "${sub}". ` : '') +
539
+ 'Usage: shrk baseline list | check [--id X] | diff | update | explain --id <id>\n');
540
+ return ExitCode.UsageError;
541
+ },
542
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"changelog-data.d.ts","sourceRoot":"","sources":["../../src/commands/changelog-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,eAAO,MAAM,sBAAsB,EAAE,SAAS,oBAAoB,EAwGjE,CAAC"}
1
+ {"version":3,"file":"changelog-data.d.ts","sourceRoot":"","sources":["../../src/commands/changelog-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,eAAO,MAAM,sBAAsB,EAAE,SAAS,oBAAoB,EAkJjE,CAAC"}
@@ -112,4 +112,46 @@ export const RELEASE_SURFACE_DELTAS = [
112
112
  ],
113
113
  removed: [],
114
114
  },
115
+ {
116
+ version: '0.1.0-alpha.28',
117
+ title: "The plane the compiler can't see — six data-defined gate planes on one extraction DSL",
118
+ added: [
119
+ '`shrk gates list | coverage | explain <id>` — the rule-authoring trust layer across EVERY data-defined plane (wiring / policy / registry / registration / baseline / generated). `coverage` is the stale-selector detector: it reports what each rule actually MATCHED and flags every rule matching 0, so a rule quietly dying is itself a CI failure.',
120
+ '`shrk baseline list | check | diff | update | explain` — the committed-ledger drift engine (`baselines[]`). TWO-WAY by default: a silently LOST entry fails exactly like a gained one. `update` is a separate, explicit bless verb.',
121
+ '`shrk generated list | check | update | explain` — generated-artifact drift + provenance (`generatedArtifacts[]`). Regenerates into a temp dir and diffs BOTH ways (hand-edited file AND a regen that writes a subset), plus the "do not edit" header contract. `--headers-only` never spawns.',
122
+ '`shrk policy-lint explain <ruleId>` — every hit with file:line, INCLUDING the hits an exemption or the scan zone dropped, each labelled with which one applied.',
123
+ '`shrk registry <name> duplicates` — ids declared in more than one place, with every declaration site (load-order roulette the compiler cannot see).',
124
+ 'Extraction DSL on every rule source: `extract` with 9 kinds (`regex-capture`, `array-members`, `object-keys`, `enum-members`, `export-names`, `call-args`, `decorator-args`, `string-union-members`, `json-path`) plus `anchor` / `argIndex` / `capture` and the `match` + `exclude` allow/deny pair.',
125
+ 'Wiring relations: `mode: disjoint`, `registeredMode: intersection`, multi-hop `chain`, `{id}` message templating, and `failOnEmpty` / `selfTest` on every plane.',
126
+ 'Policy rules: `scan: all|code|strings|comments` (lexical zone classifier), `exemptFiles`, `exemptLines`.',
127
+ ],
128
+ changed: [
129
+ 'Loud-skip contract: a rule whose SOURCE side matches 0 files / extracts 0 ids is `skipped` and the check exits `2` (NOT verified), never a green `0`; `failOnEmpty: true` promotes it to `1`. An empty SINK is deliberately NOT a skip — it stays a failure, annotated `emptySink`, so a real total-miss is never downgraded.',
130
+ '`shrk policy-lint` exits `2` when it evaluated nothing, and reports exemption-suppressed hits as a count instead of silently deleting them.',
131
+ "`shrk finish`'s import sub-gate lists only the findings that DRIVE the verdict — allowlisted (`info`) entries no longer pad the capped fix-list and push a real error out of view.",
132
+ 'MCP: the pack-helper tool is now `get_pack_helper` (was `get_helper`, which collided with the helper-registry tool and made one of the two unreachable via `tools/call`). Completes the dedup that already renamed `list_helpers` → `list_pack_helpers`.',
133
+ 'Pack safety: `baselines[]` / `generatedArtifacts[]` are pack-distributable, but the merge seam DROPS any pack-contributed element declaring a shell command (`compute.run` / `regen`) — mirroring the "pack-contributed verification commands are NOT auto-run" contract.',
134
+ ],
135
+ removed: [],
136
+ },
137
+ {
138
+ version: '0.1.0-alpha.29',
139
+ title: 'The exit code has to agree with the sentence',
140
+ added: [
141
+ '`shrk check wiring --fix` — deterministic autofix for the mechanically-unambiguous `declared-but-not-registered` case: append the missing id to its sink array. Dry-run by default, `--write` applies. Refuses ambiguity (N sinks, non-array sink, non-unique file/array, chain rules, parity violations) and lists what it left alone WITH the reason — it never guesses.',
142
+ '`shrk explain <ruleId>` resolves a rule id across ALL planes and dispatches to the right explainer; a non-rule token still gets the original topic search. The per-plane forms remain.',
143
+ 'A shared `gate` envelope (`sharkcraft.gate/v1`) inside every gate verb\'s `--json`: `verb` / `exit` / normalized per-rule `{id,type,status,severity,counts,violations,skipReason}`. Additive — the per-plane payloads are unchanged. See docs/gate-json.md.',
144
+ 'Global `--no-hints` silences the advisory piped-exit note (the structured `--exit-trailer` channel is unaffected).',
145
+ 'Exit code `3` = usage error (unloadable config / unknown rule id / bad flag value) on the gate verbs, split out of `2` so "the gate proved nothing" and "the gate never started" are distinguishable.',
146
+ ],
147
+ changed: [
148
+ 'A SKIPPED rule is no longer masked by a passing sibling. `failOnEmpty` now defaults to TRUE for `error`-severity rules (an error rule matching zero subjects is a bug in the rule, not a pass) and any skip with no failures exits `2`, never `0`. `warning`-severity rules still default to `false`. BEHAVIOUR CHANGE: an error-severity rule that matches nothing now exits `1` — fix the selector or set `failOnEmpty: false`.',
149
+ '`shrk help <multi-word verb>` resolves catalog-documented paths (`check wiring`, `wiring unprovided`, …) that are dispatched from a parent handler and therefore never appear in the command trie; it also lists sibling verbs. Previously answered "Unknown command".',
150
+ '`shrk baseline explain` always computes the CURRENT side, so a rule with no committed artifact yet reports `committed (none yet) → N now` instead of a false `0 now`. `baseline check` names what would be blessed.',
151
+ '`shrk registry` accepts both argument orders — `registry list <name>` now works alongside `registry <name> list`; a verb-first call with an unknown name names the correct grammar.',
152
+ '`shrk policy-lint` returns `3` (usage) rather than `1` (violations) when the config cannot be loaded, and `2` when it scanned nothing.',
153
+ 'The piped-exit note is emitted at most once per process.',
154
+ ],
155
+ removed: [],
156
+ },
115
157
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"check.command.d.ts","sourceRoot":"","sources":["../../src/commands/check.command.ts"],"names":[],"mappings":"AAoBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAu4BhC,eAAO,MAAM,YAAY,EAAE,eA+G1B,CAAC"}
1
+ {"version":3,"file":"check.command.d.ts","sourceRoot":"","sources":["../../src/commands/check.command.ts"],"names":[],"mappings":"AAoBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA2iChC,eAAO,MAAM,YAAY,EAAE,eA+G1B,CAAC"}