@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,377 @@
1
+ /**
2
+ * `shrk gates` — the rule-authoring trust layer.
3
+ *
4
+ * shrk gates list [--plane <p>] # every data-defined rule, across every plane
5
+ * shrk gates coverage [--strict] # what each rule MATCHED; flags every rule matching 0
6
+ * shrk gates explain <id> # the concrete inputs one rule resolved
7
+ *
8
+ * Every rule engine in shrk is only as trustworthy as the author's ability to
9
+ * see what a rule actually matched, and the dominant real-world failure is a
10
+ * stale selector that silently matches nothing — a "pass" that checked zero
11
+ * files. `gates coverage` is the detector: run it in CI and a rule quietly
12
+ * dying becomes a failure in its own right.
13
+ *
14
+ * Distinct from `shrk gate` (singular), which RUNS the quality-gate pipeline.
15
+ * This verb inspects the data-defined RULES themselves.
16
+ */
17
+ import * as nodePath from 'node:path';
18
+ import { explainWiring, inspectSource, scanRegistry } from '@shrkcrft/boundaries';
19
+ import { resolveProjectConfig } from '@shrkcrft/inspector';
20
+ import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
21
+ import { ExitCode } from "../exit-codes.js";
22
+ import { asJson, header, kv } from "../output/format-output.js";
23
+ import { collectGateRules, GATE_PLANES, } from "../gates/gate-rule-view.js";
24
+ import { buildGateCoverage } from "../gates/rule-coverage.js";
25
+ import { buildGateEnvelope } from "../gates/gate-envelope.js";
26
+ import { baselineExplainCommand } from "./baseline.command.js";
27
+ import { generatedExplainCommand } from "./generated.command.js";
28
+ import { renderPolicyExplain, runPolicyExplain } from "./policy-lint.command.js";
29
+ import { renderWiringExplain } from "./wiring.command.js";
30
+ const SCHEMA = 'sharkcraft.gates/v1';
31
+ async function prepare(args) {
32
+ const cwd = resolveCwd(args);
33
+ const json = flagBool(args, 'json');
34
+ const loaded = await resolveProjectConfig(cwd);
35
+ if (!loaded.ok) {
36
+ const msg = loaded.error.message;
37
+ if (json)
38
+ process.stdout.write(asJson({ schema: SCHEMA, error: msg }) + '\n');
39
+ else
40
+ process.stderr.write(`Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
41
+ return { ok: false, code: ExitCode.UsageError };
42
+ }
43
+ const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
44
+ return {
45
+ ok: true,
46
+ value: {
47
+ cwd,
48
+ rules: collectGateRules(loaded.value.config),
49
+ excludeDirs: rel && !rel.startsWith('..') ? [rel] : [],
50
+ planeDiagnostics: loaded.value.planeDiagnostics,
51
+ },
52
+ };
53
+ }
54
+ /** Parse `--plane`, refusing an unknown value rather than silently matching nothing. */
55
+ function parsePlanes(args) {
56
+ const raw = flagString(args, 'plane');
57
+ if (!raw)
58
+ return { ok: true };
59
+ const parts = raw.split(',').map((s) => s.trim()).filter(Boolean);
60
+ const bad = parts.filter((p) => !GATE_PLANES.includes(p));
61
+ if (bad.length > 0) {
62
+ process.stderr.write(`Unknown --plane "${bad.join(', ')}". Use ${GATE_PLANES.join(' | ')}.\n`);
63
+ return { ok: false };
64
+ }
65
+ return { ok: true, planes: new Set(parts) };
66
+ }
67
+ function writeNoRules(json) {
68
+ if (json) {
69
+ process.stdout.write(asJson({ schema: SCHEMA, rules: [], total: 0 }) + '\n');
70
+ return ExitCode.NotVerified;
71
+ }
72
+ process.stdout.write(header('Gate rules'));
73
+ process.stdout.write(' No data-defined rules declared. These planes live in sharkcraft.config.ts:\n' +
74
+ ' wiringRules[] declared-here → registered-there completeness\n' +
75
+ ' policyRules[] forbidden content the compiler never sees\n' +
76
+ ' registries[] id inventories (`shrk registry <name> list`)\n' +
77
+ ' registrationGraph[] DI/registration idioms (`shrk wiring chain`)\n' +
78
+ ' baselines[] committed ledgers that must not silently drift\n' +
79
+ ' generatedArtifacts[] generated files that must not be hand-edited\n');
80
+ return ExitCode.NotVerified;
81
+ }
82
+ export const gatesListCommand = {
83
+ name: 'list',
84
+ description: 'Every data-defined rule across every plane, with its severity and empty-match policy.',
85
+ usage: 'shrk gates list [--plane wiring|policy|registry|registration|baseline|generated] [--json]',
86
+ booleanFlags: new Set(['json']),
87
+ async run(args) {
88
+ const prep = await prepare(args);
89
+ if (!prep.ok)
90
+ return prep.code;
91
+ const planes = parsePlanes(args);
92
+ if (!planes.ok)
93
+ return ExitCode.UsageError;
94
+ const json = flagBool(args, 'json');
95
+ const rules = planes.planes
96
+ ? prep.value.rules.filter((r) => planes.planes.has(r.plane))
97
+ : prep.value.rules;
98
+ if (rules.length === 0)
99
+ return writeNoRules(json);
100
+ if (json) {
101
+ process.stdout.write(asJson({
102
+ schema: SCHEMA,
103
+ total: rules.length,
104
+ rules: rules.map((r) => ({
105
+ id: r.id,
106
+ plane: r.plane,
107
+ description: r.description ?? null,
108
+ severity: r.severity,
109
+ failOnEmpty: r.failOnEmpty,
110
+ selfTest: r.selfTest ?? null,
111
+ })),
112
+ diagnostics: prep.value.planeDiagnostics,
113
+ }) + '\n');
114
+ return ExitCode.VerifiedPass;
115
+ }
116
+ process.stdout.write(header(`Gate rules (${rules.length})`));
117
+ for (const plane of GATE_PLANES) {
118
+ const inPlane = rules.filter((r) => r.plane === plane);
119
+ if (inPlane.length === 0)
120
+ continue;
121
+ process.stdout.write(`\n${plane} (${inPlane.length})\n`);
122
+ for (const r of inPlane) {
123
+ const flags = [
124
+ r.severity === 'warning' ? 'warning' : undefined,
125
+ r.failOnEmpty ? 'failOnEmpty' : undefined,
126
+ r.selfTest ? 'selfTest' : undefined,
127
+ ].filter(Boolean);
128
+ process.stdout.write(` • ${r.id}${flags.length > 0 ? ` [${flags.join(', ')}]` : ''}\n`);
129
+ if (r.description)
130
+ process.stdout.write(` ${r.description}\n`);
131
+ }
132
+ }
133
+ for (const d of prep.value.planeDiagnostics)
134
+ process.stdout.write(` ! ${d}\n`);
135
+ process.stdout.write('\nRun `shrk gates coverage` to see what each one actually matches.\n');
136
+ return ExitCode.VerifiedPass;
137
+ },
138
+ };
139
+ export const gatesCoverageCommand = {
140
+ name: 'coverage',
141
+ description: 'What every rule MATCHED against the live tree — the stale-selector detector. A rule matching 0 files/ids is a bug in the rule, never a pass. Also runs each rule\'s declared selfTest expectations.',
142
+ usage: 'shrk gates coverage [--plane <p>] [--strict] [--json]',
143
+ booleanFlags: new Set(['json', 'strict']),
144
+ async run(args) {
145
+ const prep = await prepare(args);
146
+ if (!prep.ok)
147
+ return prep.code;
148
+ const planes = parsePlanes(args);
149
+ if (!planes.ok)
150
+ return ExitCode.UsageError;
151
+ const json = flagBool(args, 'json');
152
+ const rules = planes.planes
153
+ ? prep.value.rules.filter((r) => planes.planes.has(r.plane))
154
+ : prep.value.rules;
155
+ if (rules.length === 0)
156
+ return writeNoRules(json);
157
+ const report = buildGateCoverage(prep.value.cwd, rules, prep.value.excludeDirs);
158
+ // A rule that matched nothing is NOT-VERIFIED (2) by default — it neither
159
+ // passed nor failed, it never ran. `failOnEmpty` on the rule (or the global
160
+ // --strict promotion) turns that into a hard failure.
161
+ const hardFailures = report.rules.filter((r) => r.status === 'error' || r.status === 'failed-expectation' || (r.status === 'empty' && r.failOnEmpty));
162
+ const softEmpty = report.rules.filter((r) => r.status === 'empty' && !r.failOnEmpty);
163
+ const exit = hardFailures.length > 0
164
+ ? ExitCode.Failure
165
+ : softEmpty.length > 0
166
+ ? ExitCode.NotVerified
167
+ : ExitCode.VerifiedPass;
168
+ if (json) {
169
+ process.stdout.write(asJson({
170
+ ...report,
171
+ hardFailures: hardFailures.length,
172
+ exitCode: exit,
173
+ gate: buildGateEnvelope('gates coverage', exit, report.rules.map((r) => ({
174
+ id: r.id,
175
+ type: r.plane,
176
+ status: r.status === 'ok'
177
+ ? 'passed'
178
+ : r.status === 'empty'
179
+ ? r.failOnEmpty
180
+ ? 'failed'
181
+ : 'skipped'
182
+ : r.status === 'error'
183
+ ? 'error'
184
+ : 'failed',
185
+ severity: r.failOnEmpty ? 'error' : 'warning',
186
+ counts: { files: r.filesMatched, units: r.unitsMatched },
187
+ violations: r.expectationFailures.map((f) => ({ id: r.id, message: f })),
188
+ ...(r.status === 'empty' ? { skipReason: `matched 0 ${r.unitLabel}` } : {}),
189
+ ...(r.error ? { error: r.error } : {}),
190
+ }))),
191
+ }) + '\n');
192
+ return exit;
193
+ }
194
+ process.stdout.write(header('Gate-rule coverage'));
195
+ process.stdout.write(kv('rules', String(report.total)) + '\n');
196
+ process.stdout.write(kv('matched nothing', `${report.empty}${report.empty > 0 ? ' ← stale selector suspects' : ''}`) + '\n');
197
+ if (report.errored > 0)
198
+ process.stdout.write(kv('misconfigured', String(report.errored)) + '\n');
199
+ if (report.expectationFailures > 0) {
200
+ process.stdout.write(kv('broken selfTest', String(report.expectationFailures)) + '\n');
201
+ }
202
+ process.stdout.write('\n');
203
+ for (const r of report.rules) {
204
+ const mark = r.status === 'ok' ? '✓' : r.status === 'empty' ? (r.failOnEmpty ? '✗' : '–') : '✗';
205
+ process.stdout.write(` ${mark} [${r.plane}] ${r.id} — ${r.unitsMatched} ${r.unitLabel} across ${r.filesMatched} file(s)\n`);
206
+ if (r.sampleIds.length > 0) {
207
+ process.stdout.write(` e.g. ${r.sampleIds.join(', ')}\n`);
208
+ }
209
+ if (r.status === 'empty') {
210
+ process.stdout.write(` ${r.failOnEmpty ? 'FAILED' : 'SKIPPED'} — matched nothing; the selector is probably stale\n`);
211
+ }
212
+ if (r.error)
213
+ process.stdout.write(` ! ${r.error}\n`);
214
+ for (const f of r.expectationFailures)
215
+ process.stdout.write(` ! selfTest: ${f}\n`);
216
+ }
217
+ if (exit === ExitCode.VerifiedPass) {
218
+ process.stdout.write('\nEvery rule is connected to something. ✓\n');
219
+ }
220
+ else if (exit === ExitCode.NotVerified) {
221
+ process.stdout.write(`\n${softEmpty.length} rule(s) matched nothing — NOT a pass. Fix the selector, or set \`failOnEmpty: true\`\n` +
222
+ 'once the rule is known to have real subjects (then this becomes a hard failure).\n');
223
+ }
224
+ return exit;
225
+ },
226
+ };
227
+ /** Render a registry inventory as the trust-layer explain view. */
228
+ function explainRegistry(cwd, decl, excludeDirs) {
229
+ const inventory = scanRegistry(cwd, decl, { excludeDirs });
230
+ const insp = inspectSource(cwd, decl.source, excludeDirs);
231
+ process.stdout.write(kv('files scanned', String(insp.filesScanned)) + '\n');
232
+ process.stdout.write(kv('ids', String(inventory.entries.length)) + '\n');
233
+ for (const e of inventory.entries.slice(0, 60)) {
234
+ process.stdout.write(` • ${e.id} (${e.sites.map((s) => `${s.file}:${s.line}`).join(', ')})\n`);
235
+ }
236
+ if (inventory.entries.length > 60) {
237
+ process.stdout.write(` … (${inventory.entries.length - 60} more)\n`);
238
+ }
239
+ for (const d of inventory.diagnostics)
240
+ process.stdout.write(` ! ${d}\n`);
241
+ }
242
+ /** Render the three sides of a registration idiom. */
243
+ function explainRegistration(cwd, idiom, excludeDirs) {
244
+ for (const [label, source] of [
245
+ ['declared', idiom.declared],
246
+ ['provided', idiom.provided],
247
+ ['consumed', idiom.consumed],
248
+ ]) {
249
+ const insp = inspectSource(cwd, source, excludeDirs);
250
+ process.stdout.write(kv(label, `${insp.ids.length} token(s) across ${insp.filesScanned} file(s)`) + '\n');
251
+ if (insp.error)
252
+ process.stdout.write(` ! ${insp.error}\n`);
253
+ for (const s of insp.sites.slice(0, 20)) {
254
+ process.stdout.write(` ${s.token} (${s.file}:${s.line})\n`);
255
+ }
256
+ if (insp.sites.length > 20)
257
+ process.stdout.write(` … (${insp.sites.length - 20} more)\n`);
258
+ }
259
+ process.stdout.write(`\n Query one token's chain with \`shrk wiring chain <token>\`.\n`);
260
+ }
261
+ export const gatesExplainCommand = {
262
+ name: 'explain',
263
+ description: 'The universal introspection: for a rule of ANY plane, print the concrete inputs it resolved — files matched, ids extracted with file:line, and the computed diff.',
264
+ usage: 'shrk gates explain <id> [--json]',
265
+ booleanFlags: new Set(['json']),
266
+ async run(args) {
267
+ const id = args.positional[0] ?? flagString(args, 'id');
268
+ if (!id) {
269
+ process.stderr.write('Usage: shrk gates explain <id> [--json]\n');
270
+ return ExitCode.UsageError;
271
+ }
272
+ const prep = await prepare(args);
273
+ if (!prep.ok)
274
+ return prep.code;
275
+ const matches = prep.value.rules.filter((r) => r.id === id);
276
+ if (matches.length === 0) {
277
+ process.stderr.write(`No gate rule "${id}". Run \`shrk gates list\` to see the ${prep.value.rules.length} declared rule(s).\n`);
278
+ return ExitCode.UsageError;
279
+ }
280
+ // An id may legitimately exist on two planes (a wiring rule and a registry
281
+ // can share a name); `--plane` disambiguates instead of guessing.
282
+ const planes = parsePlanes(args);
283
+ if (!planes.ok)
284
+ return ExitCode.UsageError;
285
+ const candidates = planes.planes
286
+ ? matches.filter((r) => planes.planes.has(r.plane))
287
+ : matches;
288
+ if (candidates.length > 1) {
289
+ process.stderr.write(`"${id}" exists on ${candidates.length} planes (${candidates.map((c) => c.plane).join(', ')}). ` +
290
+ 'Disambiguate with --plane <p>.\n');
291
+ return ExitCode.UsageError;
292
+ }
293
+ const view = candidates[0];
294
+ if (!view) {
295
+ process.stderr.write(`No gate rule "${id}" on the requested plane.\n`);
296
+ return ExitCode.UsageError;
297
+ }
298
+ const json = flagBool(args, 'json');
299
+ // The two shell-executing planes own their explain output (and their trust
300
+ // rules), so delegate rather than re-implement — one behaviour, one place.
301
+ if (view.plane === 'baseline') {
302
+ args.flags.set('id', view.id);
303
+ return baselineExplainCommand.run(args);
304
+ }
305
+ if (view.plane === 'generated') {
306
+ args.flags.set('id', view.id);
307
+ return generatedExplainCommand.run(args);
308
+ }
309
+ if (view.plane === 'wiring') {
310
+ const explain = explainWiring(prep.value.cwd, view.raw, {
311
+ excludeDirs: prep.value.excludeDirs,
312
+ });
313
+ renderWiringExplain(explain, json);
314
+ return ExitCode.VerifiedPass;
315
+ }
316
+ if (view.plane === 'policy') {
317
+ const explain = runPolicyExplain(prep.value.cwd, view.raw, prep.value.excludeDirs);
318
+ renderPolicyExplain(explain, json);
319
+ return ExitCode.VerifiedPass;
320
+ }
321
+ if (json) {
322
+ const source = view.plane === 'registry'
323
+ ? view.raw.source
324
+ : view.raw.declared;
325
+ process.stdout.write(asJson({
326
+ schema: 'sharkcraft.gates-explain/v1',
327
+ id: view.id,
328
+ plane: view.plane,
329
+ ...inspectSource(prep.value.cwd, source, prep.value.excludeDirs),
330
+ }) + '\n');
331
+ return ExitCode.VerifiedPass;
332
+ }
333
+ process.stdout.write(header(`${view.plane} rule: ${view.id}`));
334
+ if (view.description)
335
+ process.stdout.write(` ${view.description}\n`);
336
+ if (view.plane === 'registry') {
337
+ explainRegistry(prep.value.cwd, view.raw, prep.value.excludeDirs);
338
+ }
339
+ else {
340
+ explainRegistration(prep.value.cwd, view.raw, prep.value.excludeDirs);
341
+ }
342
+ return ExitCode.VerifiedPass;
343
+ },
344
+ };
345
+ /**
346
+ * Try to explain `id` as a data-defined rule on ANY plane.
347
+ *
348
+ * Returns the exit code when the id resolves to exactly one declared rule, or
349
+ * `undefined` when it is not a rule id at all — which lets `shrk explain` keep
350
+ * its original topic-search behaviour for everything else. This is the D2
351
+ * unification: a user holding a rule id no longer has to know which plane owns
352
+ * it, and no existing invocation changes meaning.
353
+ */
354
+ export async function tryExplainGateRule(args, id) {
355
+ const cwd = resolveCwd(args);
356
+ const loaded = await resolveProjectConfig(cwd);
357
+ if (!loaded.ok)
358
+ return undefined;
359
+ const rules = collectGateRules(loaded.value.config);
360
+ if (!rules.some((r) => r.id === id))
361
+ return undefined;
362
+ const forwarded = { ...args, positional: [id] };
363
+ return gatesExplainCommand.run(forwarded);
364
+ }
365
+ export const gatesCommand = {
366
+ name: 'gates',
367
+ description: 'Rule-authoring trust layer: list every data-defined rule, show what each one MATCHED (the stale-selector detector), and explain any one of them. Read-only. Not `shrk gate`, which runs the quality-gate pipeline.',
368
+ usage: 'shrk gates list | coverage [--strict] | explain <id>',
369
+ booleanFlags: new Set(['json', 'strict']),
370
+ async run(args) {
371
+ const sub = args.positional[0];
372
+ process.stderr.write((sub ? `Unknown subcommand "${sub}". ` : '') +
373
+ 'Usage: shrk gates list | coverage [--plane <p>] [--strict] | explain <id>\n' +
374
+ '(`shrk gate`, singular, runs the quality-gate pipeline — a different verb.)\n');
375
+ return ExitCode.UsageError;
376
+ },
377
+ };
@@ -0,0 +1,6 @@
1
+ import { type ICommandHandler } from '../command-registry.js';
2
+ export declare const generatedListCommand: ICommandHandler;
3
+ export declare const generatedCheckCommand: ICommandHandler;
4
+ export declare const generatedUpdateCommand: ICommandHandler;
5
+ export declare const generatedExplainCommand: ICommandHandler;
6
+ //# sourceMappingURL=generated.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generated.command.d.ts","sourceRoot":"","sources":["../../src/commands/generated.command.ts"],"names":[],"mappings":"AA+BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAyShC,eAAO,MAAM,oBAAoB,EAAE,eA2ClC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAuHnC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eAmDpC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eAiErC,CAAC"}