@shrkcrft/cli 0.1.0-alpha.23 → 0.1.0-alpha.25

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 (62) hide show
  1. package/dist/commands/changelog-data.d.ts +25 -0
  2. package/dist/commands/changelog-data.d.ts.map +1 -0
  3. package/dist/commands/changelog-data.js +70 -0
  4. package/dist/commands/changelog.command.d.ts +3 -0
  5. package/dist/commands/changelog.command.d.ts.map +1 -0
  6. package/dist/commands/changelog.command.js +100 -0
  7. package/dist/commands/changes.command.d.ts.map +1 -1
  8. package/dist/commands/changes.command.js +4 -0
  9. package/dist/commands/check.command.d.ts.map +1 -1
  10. package/dist/commands/check.command.js +182 -17
  11. package/dist/commands/command-catalog.d.ts +11 -0
  12. package/dist/commands/command-catalog.d.ts.map +1 -1
  13. package/dist/commands/command-catalog.js +99 -0
  14. package/dist/commands/compress.command.d.ts.map +1 -1
  15. package/dist/commands/compress.command.js +15 -1
  16. package/dist/commands/constructs.command.d.ts.map +1 -1
  17. package/dist/commands/constructs.command.js +49 -14
  18. package/dist/commands/context.command.d.ts.map +1 -1
  19. package/dist/commands/context.command.js +31 -19
  20. package/dist/commands/finish.command.d.ts +3 -0
  21. package/dist/commands/finish.command.d.ts.map +1 -0
  22. package/dist/commands/finish.command.js +70 -0
  23. package/dist/commands/gate.command.d.ts.map +1 -1
  24. package/dist/commands/gate.command.js +6 -1
  25. package/dist/commands/gen.command.d.ts.map +1 -1
  26. package/dist/commands/gen.command.js +65 -9
  27. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  28. package/dist/commands/graph-code-subverbs.js +56 -25
  29. package/dist/commands/help.command.d.ts.map +1 -1
  30. package/dist/commands/help.command.js +20 -2
  31. package/dist/commands/impact.command.d.ts.map +1 -1
  32. package/dist/commands/impact.command.js +17 -22
  33. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  34. package/dist/commands/policy-lint.command.js +46 -8
  35. package/dist/commands/registry.command.d.ts.map +1 -1
  36. package/dist/commands/registry.command.js +71 -13
  37. package/dist/commands/reuse.command.d.ts.map +1 -1
  38. package/dist/commands/reuse.command.js +80 -14
  39. package/dist/commands/smart-context.command.d.ts.map +1 -1
  40. package/dist/commands/smart-context.command.js +34 -42
  41. package/dist/commands/task.command.d.ts.map +1 -1
  42. package/dist/commands/task.command.js +33 -16
  43. package/dist/commands/trace.command.d.ts.map +1 -1
  44. package/dist/commands/trace.command.js +73 -3
  45. package/dist/commands/wiring.command.d.ts +12 -0
  46. package/dist/commands/wiring.command.d.ts.map +1 -0
  47. package/dist/commands/wiring.command.js +384 -0
  48. package/dist/diff/collect-changed-paths.d.ts +5 -3
  49. package/dist/diff/collect-changed-paths.d.ts.map +1 -1
  50. package/dist/diff/collect-changed-paths.js +73 -36
  51. package/dist/diff/deleted-orphans.d.ts +42 -0
  52. package/dist/diff/deleted-orphans.d.ts.map +1 -0
  53. package/dist/diff/deleted-orphans.js +46 -0
  54. package/dist/finish/run-finish.d.ts +62 -0
  55. package/dist/finish/run-finish.d.ts.map +1 -0
  56. package/dist/finish/run-finish.js +239 -0
  57. package/dist/main.d.ts.map +1 -1
  58. package/dist/main.js +6 -0
  59. package/dist/validation/typecheck-emitted.d.ts +36 -0
  60. package/dist/validation/typecheck-emitted.d.ts.map +1 -0
  61. package/dist/validation/typecheck-emitted.js +109 -0
  62. package/package.json +33 -33
@@ -1 +1 @@
1
- {"version":3,"file":"graph-code-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/graph-code-subverbs.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAqD,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AA+J5G,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBrE;AA4FD,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkEtE;AAiBD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA2F1E;AAID,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+EpE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+FtE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAwEtE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA0MvE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA8HtE;AAID;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkGvE;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgHpE"}
1
+ {"version":3,"file":"graph-code-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/graph-code-subverbs.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAiE,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAgLxH,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBrE;AA4FD,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkFtE;AAiBD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA2F1E;AAID,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+EpE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+FtE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAwEtE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA2MvE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+HtE;AAID;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAiGvE;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgHpE"}
@@ -12,7 +12,7 @@ import { FrameworkQueryApi, FrameworkStore } from '@shrkcrft/framework-scanners'
12
12
  import { existsSync } from 'node:fs';
13
13
  import * as nodePath from 'node:path';
14
14
  import { compactArrayToColumnar } from '@shrkcrft/compress';
15
- import { flagBool, flagPositiveInt, flagString, resolveCwd } from "../command-registry.js";
15
+ import { flagBool, flagNumber, flagPositiveInt, flagString, resolveCwd } from "../command-registry.js";
16
16
  import { asJson, header, kv } from "../output/format-output.js";
17
17
  import { maybeRunInWatchMode } from "../output/watch-loop.js";
18
18
  /**
@@ -67,6 +67,24 @@ const CORRUPT_HINT = 'code graph store is corrupt — run `shrk graph index` to
67
67
  * runGraphCallers). Kept in sync with the MCP `get_graph_context` tool.
68
68
  */
69
69
  const CONTEXT_LIST_CAP = 50;
70
+ /**
71
+ * Resolve a `--limit N` per-list cap for the graph read commands. Absent → the
72
+ * command's `fallback` (the historical default — 50 for `context`, 200 for
73
+ * `callers`/`impact`); `--limit 0` (or any non-positive) → unbounded so a
74
+ * consumer can pull the full set off a high-fan-in barrel/hub in one call.
75
+ * Returns `Infinity` for the unbounded case: `arr.slice(0, Infinity)` yields
76
+ * every row and `len > Infinity` is `false`, so the `<list>Truncated` flag stays
77
+ * honest (never "truncated" when nothing was dropped). Malformed `--limit abc`
78
+ * (NaN) degrades to `fallback`, matching `flagPositiveInt`.
79
+ */
80
+ function resolveListLimit(args, fallback) {
81
+ const v = flagNumber(args, 'limit');
82
+ if (v === undefined)
83
+ return fallback;
84
+ if (v <= 0)
85
+ return Number.POSITIVE_INFINITY;
86
+ return Math.floor(v);
87
+ }
70
88
  /**
71
89
  * Refresh-by-default: incrementally reindex changed/deleted files BEFORE
72
90
  * querying so an agent's just-saved edits are reflected, then print a one-line
@@ -267,7 +285,14 @@ export async function runGraphCycles(args) {
267
285
  const api = loadGuarded(() => GraphQueryApi.fromStore(cwd), wantJson);
268
286
  if (!api)
269
287
  return 1;
270
- const allCycles = api.cycles();
288
+ // Type-only import edges (`import type`, `export type … from`) are erased at
289
+ // emit time, so they cannot cause a runtime cycle — excluded by default.
290
+ // `--include-type-edges` opts back in for auditing. The type-only-loop delta
291
+ // is reported as a non-blocking bucket.
292
+ const includeTypeEdges = flagBool(args, 'include-type-edges');
293
+ const allCycles = api.cycles({ includeTypeEdges });
294
+ const runtimeCycles = includeTypeEdges ? api.cycles() : allCycles;
295
+ const typeOnlyLoopCount = Math.max(0, api.cycles({ includeTypeEdges: true }).length - runtimeCycles.length);
271
296
  const filtered = allCycles.filter((c) => c.size >= minSize);
272
297
  const limited = filtered.slice(0, limit);
273
298
  if (wantJson) {
@@ -275,6 +300,8 @@ export async function runGraphCycles(args) {
275
300
  ok: true,
276
301
  total: filtered.length,
277
302
  truncated: filtered.length > limit,
303
+ includeTypeEdges,
304
+ typeOnlyLoopCount,
278
305
  cycles: limited.map((c) => ({
279
306
  size: c.size,
280
307
  paths: c.paths ?? c.nodeIds.map((id) => id.replace(/^file:/, '')),
@@ -284,8 +311,11 @@ export async function runGraphCycles(args) {
284
311
  }
285
312
  process.stdout.write(header('Graph cycles'));
286
313
  process.stdout.write(kv('total', String(filtered.length)) + '\n');
314
+ if (!includeTypeEdges && typeOnlyLoopCount > 0) {
315
+ process.stdout.write(kv('type-only loops', `${typeOnlyLoopCount} (excluded — compile-time only; --include-type-edges to audit)`) + '\n');
316
+ }
287
317
  if (filtered.length === 0) {
288
- process.stdout.write('\nNo cycles in the file-import graph. ✓\n');
318
+ process.stdout.write(`\nNo ${includeTypeEdges ? '' : 'runtime '}cycles in the file-import graph. ✓\n`);
289
319
  return 0;
290
320
  }
291
321
  process.stdout.write(kv('shown', `${limited.length}/${filtered.length}`) + '\n');
@@ -659,10 +689,11 @@ export async function runGraphContext(args) {
659
689
  const wantJson = flagBool(args, 'json');
660
690
  const target = args.positional[1];
661
691
  if (!target) {
662
- process.stderr.write('Usage: shrk graph context <fileOrSymbol> [--depth N] [--no-bridge] [--no-framework]\n');
692
+ process.stderr.write('Usage: shrk graph context <fileOrSymbol> [--depth N] [--limit N|0] [--no-bridge] [--no-framework]\n');
663
693
  return 2;
664
694
  }
665
695
  const depth = Math.min(3, flagPositiveInt(args, 'depth', 1));
696
+ const cap = resolveListLimit(args, CONTEXT_LIST_CAP);
666
697
  const includeBridge = !flagBool(args, 'no-bridge');
667
698
  const includeFramework = !flagBool(args, 'no-framework');
668
699
  maybeRefresh(args, cwd);
@@ -708,13 +739,13 @@ export async function runGraphContext(args) {
708
739
  const importsFromAll = neighbours.out.filter((o) => o.edge.kind === 'imports-file');
709
740
  const importedByAll = neighbours.in.filter((i) => i.edge.kind === 'imports-file');
710
741
  const importsFromList = importsFromAll
711
- .slice(0, CONTEXT_LIST_CAP)
742
+ .slice(0, cap)
712
743
  .map((o) => ('target' in o ? targetSummary(o.target) : { id: 'unknown', resolved: false }));
713
744
  const importedByList = importedByAll
714
- .slice(0, CONTEXT_LIST_CAP)
745
+ .slice(0, cap)
715
746
  .map((i) => ('source' in i ? sourceSummary(i.source) : { id: 'unknown', resolved: false }));
716
- const referencedByList = references.slice(0, CONTEXT_LIST_CAP).map(nodeSummary);
717
- const calledByList = callers.slice(0, CONTEXT_LIST_CAP).map(nodeSummary);
747
+ const referencedByList = references.slice(0, cap).map(nodeSummary);
748
+ const calledByList = callers.slice(0, cap).map(nodeSummary);
718
749
  // Staleness over the anchor + every referenced file: drop dead paths from the
719
750
  // usage lists, flag changed ones.
720
751
  const ctxPathOf = (x) => x.path;
@@ -733,19 +764,19 @@ export async function runGraphContext(args) {
733
764
  depth,
734
765
  importsFrom: ctxDropDel(importsFromList),
735
766
  totalImportsFrom: importsFromAll.length,
736
- importsFromTruncated: importsFromAll.length > CONTEXT_LIST_CAP,
767
+ importsFromTruncated: importsFromAll.length > cap,
737
768
  importedBy: ctxDropDel(importedByList),
738
769
  totalImportedBy: importedByAll.length,
739
- importedByTruncated: importedByAll.length > CONTEXT_LIST_CAP,
740
- symbols: symbols.slice(0, 50).map(nodeSummary),
770
+ importedByTruncated: importedByAll.length > cap,
771
+ symbols: symbols.slice(0, cap).map(nodeSummary),
741
772
  referencedBy: ctxDropDel(referencedByList),
742
773
  totalReferencedBy: references.length,
743
- referencedByTruncated: references.length > CONTEXT_LIST_CAP,
774
+ referencedByTruncated: references.length > cap,
744
775
  calledBy: ctxDropDel(calledByList),
745
776
  totalCalledBy: callers.length,
746
- calledByTruncated: callers.length > CONTEXT_LIST_CAP,
747
- ...(subtypes.length > 0 ? { subtypes: subtypes.slice(0, 50).map(nodeSummary) } : {}),
748
- ...(supertypes.length > 0 ? { supertypes: supertypes.slice(0, 50).map(nodeSummary) } : {}),
777
+ calledByTruncated: callers.length > cap,
778
+ ...(subtypes.length > 0 ? { subtypes: subtypes.slice(0, cap).map(nodeSummary) } : {}),
779
+ ...(supertypes.length > 0 ? { supertypes: supertypes.slice(0, cap).map(nodeSummary) } : {}),
749
780
  ...(fresh.field ?? {}),
750
781
  bridge: bridgeFor
751
782
  ? {
@@ -860,11 +891,11 @@ export async function runGraphImpact(args) {
860
891
  const wantFull = flagBool(args, 'full');
861
892
  const target = args.positional[1];
862
893
  if (!target) {
863
- process.stderr.write('Usage: shrk graph impact <fileOrSymbol> [--max-depth N] [--limit N] [--full]\n');
894
+ process.stderr.write('Usage: shrk graph impact <fileOrSymbol> [--max-depth N] [--limit N|0] [--full]\n');
864
895
  return 2;
865
896
  }
866
897
  const maxDepth = Math.min(10, flagPositiveInt(args, 'max-depth', 5));
867
- const limit = flagPositiveInt(args, 'limit', 200);
898
+ const limit = resolveListLimit(args, 200);
868
899
  maybeRefresh(args, cwd);
869
900
  // --full → delegate to the impact-engine for a richer v3 payload.
870
901
  if (wantFull) {
@@ -949,7 +980,8 @@ export async function runGraphImpact(args) {
949
980
  schema: 'sharkcraft.graph-impact/v1',
950
981
  anchor: nodeSummary(anchor),
951
982
  maxDepth,
952
- limit,
983
+ // 0 signals "unbounded" (`--limit 0`); JSON can't carry Infinity.
984
+ limit: Number.isFinite(limit) ? limit : 0,
953
985
  truncated: closure.truncated,
954
986
  directDependents: liveDirect,
955
987
  transitiveDependents: liveTransitive,
@@ -1052,16 +1084,15 @@ export async function runGraphCallers(args) {
1052
1084
  const wantJson = flagBool(args, 'json');
1053
1085
  const target = args.positional[1];
1054
1086
  if (!target) {
1055
- process.stderr.write('Usage: shrk graph callers <symbol> [--mode call|reference] [--limit N] [--no-refresh]\n');
1087
+ process.stderr.write('Usage: shrk graph callers <symbol> [--mode call|reference] [--limit N|0] [--no-refresh]\n');
1056
1088
  return 2;
1057
1089
  }
1058
1090
  const mode = (flagString(args, 'mode') ?? 'call');
1059
- // --limit N: cap the returned call sites (default 200). `total` still reports
1060
- // the true uncapped count, so a truncated result stays honest. Guard against
1061
- // non-numeric input — `Number('foo')` is NaN and `slice(0, NaN)` would zero
1062
- // the callers list while `total` kept showing the real count.
1063
- const parsedLimit = Number.parseInt(flagString(args, 'limit') ?? '200', 10);
1064
- const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 200;
1091
+ // --limit N: cap the returned call sites (default 200; `--limit 0` = all).
1092
+ // `total` still reports the true uncapped count, so a truncated result stays
1093
+ // honest. resolveListLimit guards non-numeric input (NaN fallback) so a
1094
+ // fat-fingered `--limit foo` can't zero the list via `slice(0, NaN)`.
1095
+ const limit = resolveListLimit(args, 200);
1065
1096
  maybeRefresh(args, cwd);
1066
1097
  const api = loadOrFail(cwd, wantJson);
1067
1098
  if (!api)
@@ -1 +1 @@
1
- {"version":3,"file":"help.command.d.ts","sourceRoot":"","sources":["../../src/commands/help.command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAqB9D;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAwC1C;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe;;;;cAK3C;QAAE,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAA;KAAE,GAAG,MAAM;EA2HpF"}
1
+ {"version":3,"file":"help.command.d.ts","sourceRoot":"","sources":["../../src/commands/help.command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AA4B9D;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAwC1C;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe;;;;cAK3C;QAAE,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAA;KAAE,GAAG,MAAM;EAwIpF"}
@@ -1,5 +1,11 @@
1
1
  import { header } from "../output/format-output.js";
2
- import { COMMAND_CATALOG, defaultShowInHelp } from "./command-catalog.js";
2
+ import { COMMAND_CATALOG, defaultShowInHelp, listExplainFamily } from "./command-catalog.js";
3
+ /** First sentence of a catalog description, for the compact explain-family list. */
4
+ function firstSentence(description) {
5
+ const dot = description.indexOf('. ');
6
+ const head = dot > 0 ? description.slice(0, dot + 1) : description;
7
+ return head.length > 100 ? head.slice(0, 97).trimEnd() + '…' : head;
8
+ }
3
9
  const EXTRA_HELP_LINES = Object.freeze({
4
10
  graph: [
5
11
  '',
@@ -60,7 +66,7 @@ export function renderStartScreen() {
60
66
  lines.push('Discover the rest (everything stays callable — this screen shows ~17 of ~70 verbs):');
61
67
  lines.push(' $ shrk surface list — full catalog by tier');
62
68
  lines.push(' $ shrk help <command> — usage for a specific command');
63
- lines.push(' $ shrk --full-help — long, exhaustive help');
69
+ lines.push(' $ shrk --full-help — long, exhaustive help (incl. the explain/dry-run family)');
64
70
  lines.push(' $ shrk --about — what shrk is and is not');
65
71
  lines.push('');
66
72
  lines.push('Free-form input is fine — `shrk "<task>"` routes to `shrk recommend`.');
@@ -188,6 +194,18 @@ export function makeHelpCommand(registry) {
188
194
  process.stdout.write(` ${group} ${s.name.padEnd(10)} — ${s.description}\n`);
189
195
  }
190
196
  }
197
+ // Explain / dry-run family — several carry an Advanced surface and are
198
+ // filtered out of the listings above, so surface them explicitly. These
199
+ // show you what a gate, ranker, or graph SEES before you act; an agent
200
+ // would otherwise find them only by guessing (the #4.4 friction).
201
+ const explainFamily = listExplainFamily();
202
+ if (explainFamily.length > 0) {
203
+ process.stdout.write(header('Inspect / explain (dry-run what a gate, ranker, or graph sees)'));
204
+ for (const e of explainFamily) {
205
+ process.stdout.write(` ${e.command.padEnd(24)} — ${firstSentence(e.description)}\n`);
206
+ }
207
+ process.stdout.write(' (also: shrk check wiring --explain <ruleId>)\n');
208
+ }
191
209
  process.stdout.write('\nRun `shrk help <command>` for detailed usage.\n');
192
210
  return 0;
193
211
  },
@@ -1 +1 @@
1
- {"version":3,"file":"impact.command.d.ts","sourceRoot":"","sources":["../../src/commands/impact.command.ts"],"names":[],"mappings":"AAsBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAibhC,eAAO,MAAM,aAAa,EAAE,eAsW3B,CAAC"}
1
+ {"version":3,"file":"impact.command.d.ts","sourceRoot":"","sources":["../../src/commands/impact.command.ts"],"names":[],"mappings":"AAsBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA2ahC,eAAO,MAAM,aAAa,EAAE,eAsW3B,CAAC"}
@@ -4,7 +4,7 @@ import { analyzeImpact, analyzeTestImpact, findSymbolInProject, FuzzyImpactSourc
4
4
  import { flagBool, flagNumber, flagString, flagList, resolveCwd, } from "../command-registry.js";
5
5
  import { asJson, header, kv } from "../output/format-output.js";
6
6
  import { fuzzyImpactAmbiguousHints, renderFailureHints } from "../output/failure-hints.js";
7
- import { collectChangedPaths } from "../diff/collect-changed-paths.js";
7
+ import { computeDeletedOrphans } from "../diff/deleted-orphans.js";
8
8
  function collectFiles(args, cwd) {
9
9
  const diagnostics = [];
10
10
  const explicitFiles = flagList(args, 'files');
@@ -297,17 +297,24 @@ async function runDeletedOrphans(args) {
297
297
  const cwd = resolveCwd(args);
298
298
  const wantJson = flagBool(args, 'json') || flagString(args, 'format') === 'json';
299
299
  const sinceRef = flagString(args, 'since');
300
- const changed = collectChangedPaths({ cwd, ...(sinceRef ? { ref: sinceRef } : {}) });
301
- if (!changed.isAvailable) {
300
+ const staged = flagBool(args, 'staged');
301
+ const scan = await computeDeletedOrphans(cwd, {
302
+ ...(sinceRef ? { since: sinceRef } : {}),
303
+ ...(staged ? { staged: true } : {}),
304
+ });
305
+ if (!scan.ok) {
302
306
  if (wantJson) {
303
- process.stdout.write(asJson({ ok: false, error: changed.error }) + '\n');
307
+ process.stdout.write(asJson({ ok: false, error: scan.error }) + '\n');
308
+ }
309
+ else if (scan.reason === 'diff-unavailable') {
310
+ process.stderr.write(`Cannot resolve diff: ${scan.error ?? 'unknown'}\n`);
304
311
  }
305
312
  else {
306
- process.stderr.write(`Cannot resolve diff: ${changed.error ?? 'unknown'}\n`);
313
+ process.stderr.write(`${scan.error ?? 'orphan check unavailable'}\n`);
307
314
  }
308
315
  return 2;
309
316
  }
310
- const deleted = changed.deleted;
317
+ const deleted = scan.deleted;
311
318
  if (deleted.length === 0) {
312
319
  if (wantJson) {
313
320
  process.stdout.write(asJson({
@@ -315,33 +322,21 @@ async function runDeletedOrphans(args) {
315
322
  resolvedDeleted: [],
316
323
  unresolvedDeleted: [],
317
324
  orphans: [],
318
- diagnostics: [`no deleted files in diff vs ${changed.ref}`],
325
+ diagnostics: [`no deleted files in diff vs ${scan.ref}`],
319
326
  }) + '\n');
320
327
  return 0;
321
328
  }
322
329
  process.stdout.write(header('Deleted-symbol orphans'));
323
- process.stdout.write(`No deleted files in diff vs ${changed.ref}.\n`);
330
+ process.stdout.write(`No deleted files in diff vs ${scan.ref}.\n`);
324
331
  return 0;
325
332
  }
326
- const { GraphStore, GraphQueryApi } = await import('@shrkcrft/graph');
327
- if (!new GraphStore(cwd).exists()) {
328
- const msg = 'code-graph store missing — run `shrk graph index` first.';
329
- if (wantJson) {
330
- process.stdout.write(asJson({ ok: false, error: msg }) + '\n');
331
- }
332
- else {
333
- process.stderr.write(msg + '\n');
334
- }
335
- return 2;
336
- }
337
- const { findDeletedOrphans } = await import('@shrkcrft/impact-engine');
338
- const report = findDeletedOrphans(GraphQueryApi.fromStore(cwd), deleted);
333
+ const report = scan.report;
339
334
  if (wantJson) {
340
335
  process.stdout.write(asJson(report) + '\n');
341
336
  return report.orphans.length > 0 ? 1 : 0;
342
337
  }
343
338
  process.stdout.write(header('Deleted-symbol orphans'));
344
- process.stdout.write(`Deleted files (vs ${changed.ref}): ${deleted.length}\n`);
339
+ process.stdout.write(`Deleted files (vs ${scan.ref}): ${deleted.length}\n`);
345
340
  if (report.orphans.length === 0) {
346
341
  process.stdout.write('no orphaned importers\n');
347
342
  for (const d of report.diagnostics.slice(0, 5))
@@ -1 +1 @@
1
- {"version":3,"file":"policy-lint.command.d.ts","sourceRoot":"","sources":["../../src/commands/policy-lint.command.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAKhC,eAAO,MAAM,iBAAiB,EAAE,eAkJ/B,CAAC"}
1
+ {"version":3,"file":"policy-lint.command.d.ts","sourceRoot":"","sources":["../../src/commands/policy-lint.command.ts"],"names":[],"mappings":"AAIA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAKhC,eAAO,MAAM,iBAAiB,EAAE,eA6L/B,CAAC"}
@@ -1,17 +1,21 @@
1
1
  import * as nodePath from 'node:path';
2
2
  import { runPolicyLint } from '@shrkcrft/boundaries';
3
- import { resolveChangedFiles, resolveProjectConfig } from '@shrkcrft/inspector';
3
+ import { classifyChangedScope, resolveChangedFiles, resolveProjectConfig } from '@shrkcrft/inspector';
4
4
  import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
5
5
  import { asJson, header, kv } from "../output/format-output.js";
6
6
  const VALID_SURFACES = new Set(['template', 'style', 'ts']);
7
7
  export const policyLintCommand = {
8
8
  name: 'policy-lint',
9
9
  description: 'Lint template/markup, stylesheet, and AOT-invisible TS surfaces against data-defined policyRules[] (e.g. flag raw markup when a primitive exists). Sees `.html` files AND inline `template:` strings — surfaces tsc/AOT cannot. Deterministic; no AI.',
10
- usage: 'shrk [--cwd <dir>] policy-lint [--surface template|style|ts] [--changed-only] [--since <ref>] [--only <ids>] [--json]',
10
+ usage: 'shrk [--cwd <dir>] policy-lint [--surface template|style|ts] [--changed-only] [--new-only] [--since <ref>] [--only <ids>] [--json]\n (--changed-only SCANS just the changed files; --new-only scans the whole tree but shows only findings the change introduced, hiding pre-existing baseline debt)',
11
11
  async run(args) {
12
12
  const cwd = resolveCwd(args);
13
13
  const wantJson = flagBool(args, 'json');
14
14
  const changedOnly = flagBool(args, 'changed-only');
15
+ // --new-only: scan the WHOLE tree, then show only findings the current change
16
+ // introduced (baseline debt is bucketed as hidden, not printed) — the
17
+ // finding-level complement to --changed-only's file-level scoping.
18
+ const newOnly = flagBool(args, 'new-only');
15
19
  const since = flagString(args, 'since');
16
20
  const only = flagString(args, 'only');
17
21
  const surfaceRaw = flagString(args, 'surface');
@@ -60,11 +64,11 @@ export const policyLintCommand = {
60
64
  }
61
65
  }
62
66
  let changedFiles;
63
- if (changedOnly || since) {
67
+ if (changedOnly || newOnly || since) {
64
68
  changedFiles = resolveChangedFiles({
65
69
  projectRoot: cwd,
66
70
  ...(since ? { since } : {}),
67
- ...(changedOnly && !since ? { includeWorktree: true } : {}),
71
+ ...((changedOnly || newOnly) && !since ? { includeWorktree: true } : {}),
68
72
  }).files;
69
73
  }
70
74
  // Don't lint SharkCraft's own asset/config dir by default (its .ts files
@@ -73,17 +77,46 @@ export const policyLintCommand = {
73
77
  const excludeDirs = sharkcraftRel && !sharkcraftRel.startsWith('..') ? [sharkcraftRel] : [];
74
78
  const reportRaw = runPolicyLint(cwd, rules, {
75
79
  ...(surfaces ? { surfaces } : {}),
76
- ...(changedOnly || since ? { changedOnly: true, changedFiles: changedFiles ?? [] } : {}),
80
+ // --changed-only narrows the SCAN; --new-only scans the full tree so it can
81
+ // still diff findings (it filters after, below).
82
+ ...((changedOnly || since) && !newOnly ? { changedOnly: true, changedFiles: changedFiles ?? [] } : {}),
77
83
  ...(only ? { only: only.split(',').map((s) => s.trim()).filter(Boolean) } : {}),
78
84
  ...(excludeDirs.length > 0 ? { excludeDirs } : {}),
79
85
  });
80
86
  // Surface pack-plane merge notes (missing/invalid pack policy files, dropped
81
87
  // collisions) in the same diagnostics array the engine already emits.
82
- const report = planeDiagnostics.length > 0
88
+ let report = planeDiagnostics.length > 0
83
89
  ? { ...reportRaw, diagnostics: [...reportRaw.diagnostics, ...planeDiagnostics] }
84
90
  : reportRaw;
91
+ // --new-only: partition the full-tree findings against the changed set and
92
+ // keep only the ones the current change introduced; pre-existing baseline
93
+ // debt is bucketed as hidden (reported as a count, never printed as green).
94
+ let hiddenBaseline = 0;
95
+ if (newOnly) {
96
+ const keyOf = (f) => `${f.ruleId}|${f.file}:${f.line}|${f.match}`;
97
+ const classification = classifyChangedScope({
98
+ projectRoot: cwd,
99
+ current: report.findings.map((f) => ({
100
+ key: keyOf(f),
101
+ file: f.file,
102
+ code: f.ruleId,
103
+ severity: f.severity,
104
+ message: f.message,
105
+ })),
106
+ changedFiles: changedFiles ?? [],
107
+ });
108
+ const newKeys = new Set(classification.newIssues.map((n) => n.key));
109
+ const newFindings = report.findings.filter((f) => newKeys.has(keyOf(f)));
110
+ hiddenBaseline = report.findings.length - newFindings.length;
111
+ const verdict = newFindings.some((f) => f.severity === 'error')
112
+ ? 'errors'
113
+ : newFindings.length > 0
114
+ ? report.verdict
115
+ : 'pass';
116
+ report = { ...report, findings: newFindings, verdict };
117
+ }
85
118
  if (wantJson) {
86
- process.stdout.write(asJson(report) + '\n');
119
+ process.stdout.write(asJson({ ...report, ...(newOnly ? { newOnly: true, hiddenBaseline } : {}) }) + '\n');
87
120
  return report.verdict === 'errors' ? 1 : 0;
88
121
  }
89
122
  process.stdout.write(header('Policy lint'));
@@ -99,13 +132,18 @@ export const policyLintCommand = {
99
132
  const errors = report.findings.filter((f) => f.severity === 'error').length;
100
133
  const warnings = report.findings.filter((f) => f.severity === 'warning').length;
101
134
  process.stdout.write(kv('findings', `${errors} error(s), ${warnings} warning(s)`) + '\n');
135
+ if (newOnly) {
136
+ process.stdout.write(kv('scope', `new-only (${hiddenBaseline} pre-existing finding(s) hidden — run without --new-only to see all)`) + '\n');
137
+ }
102
138
  if (report.diagnostics.length > 0) {
103
139
  process.stdout.write('\nMisconfigured rules:\n');
104
140
  for (const d of report.diagnostics)
105
141
  process.stdout.write(` ! ${d}\n`);
106
142
  }
107
143
  if (report.findings.length === 0 && report.diagnostics.length === 0) {
108
- process.stdout.write('\nNo policy violations on the scanned surfaces. ✓\n');
144
+ process.stdout.write(newOnly
145
+ ? `\nNo NEW policy violations from this change${hiddenBaseline > 0 ? ` (${hiddenBaseline} pre-existing hidden)` : ''}. ✓\n`
146
+ : '\nNo policy violations on the scanned surfaces. ✓\n');
109
147
  return 0;
110
148
  }
111
149
  // Group findings by rule.
@@ -1 +1 @@
1
- {"version":3,"file":"registry.command.d.ts","sourceRoot":"","sources":["../../src/commands/registry.command.ts"],"names":[],"mappings":"AAwBA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC,eAAO,MAAM,wBAAwB,EAAE,eAetC,CAAC;AAsGF,eAAO,MAAM,eAAe,EAAE,eAuB7B,CAAC"}
1
+ {"version":3,"file":"registry.command.d.ts","sourceRoot":"","sources":["../../src/commands/registry.command.ts"],"names":[],"mappings":"AA4BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC,eAAO,MAAM,wBAAwB,EAAE,eAsCtC,CAAC;AAsIF,eAAO,MAAM,eAAe,EAAE,eA4B7B,CAAC"}
@@ -4,30 +4,57 @@
4
4
  * shrk registry lifecycle [--json] # register/remove symmetry
5
5
  * shrk registry <name> list [--json] # every declared id
6
6
  * shrk registry <name> exists <id> [--json] # is the id taken? (exit 1 if not)
7
+ * [--resolve] # map a synonym → canonical id first
8
+ * [--fail-if-taken] # guard: non-zero when taken (free → 0)
9
+ * [--fail-if-missing] # guard: non-zero when NOT registered
7
10
  * shrk registry <name> where <id> [--json] # declaration (+ consumer) sites
8
11
  *
9
12
  * `<name>` resolves a `registries[]` declaration in sharkcraft.config.ts — one
10
13
  * deterministic multi-root scan that answers "is this id taken / where is it"
11
14
  * without an agent re-running a fragile grep.
12
15
  */
13
- import { buildRegistryLifecycleReport, renderRegistryLifecycleReportText, resolveProjectConfig, } from '@shrkcrft/inspector';
16
+ import { buildRegistryLifecycleReport, renderRegistryLifecycleReportText, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
14
17
  import { scanRegistry, registryExists, registryWhere, } from '@shrkcrft/boundaries';
15
18
  import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
16
19
  import { asJson } from "../output/format-output.js";
17
20
  export const registryLifecycleCommand = {
18
21
  name: 'lifecycle',
19
22
  description: 'Scan the workspace for register/remove symmetry. Read-only.',
20
- usage: 'shrk registry lifecycle [--scope <dir>] [--json]',
23
+ usage: 'shrk registry lifecycle [--scope <dir>] [--changed-only] [--since <ref>] [--json]',
21
24
  async run(args) {
22
25
  const cwd = resolveCwd(args);
23
26
  const scope = flagString(args, 'scope');
24
- const report = buildRegistryLifecycleReport({ projectRoot: cwd, ...(scope ? { scope } : {}) });
27
+ const changedOnly = flagBool(args, 'changed-only');
28
+ const since = flagString(args, 'since');
29
+ let files;
30
+ if (changedOnly || since) {
31
+ const changed = resolveChangedFiles({
32
+ projectRoot: cwd,
33
+ ...(since ? { since } : {}),
34
+ ...(changedOnly && !since ? { includeWorktree: true } : {}),
35
+ });
36
+ files = changed.files;
37
+ }
38
+ // Full-tree walk honors the project's skipDirs override.
39
+ let skipDirs;
40
+ if (files === undefined) {
41
+ const loaded = await resolveProjectConfig(cwd);
42
+ if (loaded.ok)
43
+ skipDirs = loaded.value.config.registryLifecycle?.skipDirs;
44
+ }
45
+ const report = buildRegistryLifecycleReport({
46
+ projectRoot: cwd,
47
+ ...(files !== undefined ? { files } : {}),
48
+ ...(scope ? { scope } : {}),
49
+ ...(skipDirs ? { skipDirs } : {}),
50
+ });
51
+ const exit = report.timedOut ? 2 : report.missingRemovers.length === 0 ? 0 : 1;
25
52
  if (flagBool(args, 'json')) {
26
53
  process.stdout.write(asJson(report) + '\n');
27
- return report.missingRemovers.length === 0 ? 0 : 1;
54
+ return exit;
28
55
  }
29
56
  process.stdout.write(renderRegistryLifecycleReportText(report));
30
- return report.missingRemovers.length === 0 ? 0 : 1;
57
+ return exit;
31
58
  },
32
59
  };
33
60
  async function loadRegistries(cwd) {
@@ -87,15 +114,42 @@ async function runRegistryInventory(args, name) {
87
114
  }
88
115
  if (action === 'exists') {
89
116
  if (!id) {
90
- process.stderr.write(`Usage: shrk registry ${name} exists <id>\n`);
117
+ process.stderr.write(`Usage: shrk registry ${name} exists <id> [--resolve] [--fail-if-taken|--fail-if-missing]\n`);
91
118
  return 2;
92
119
  }
93
- const exists = registryExists(inventory, id);
94
- if (json)
95
- process.stdout.write(asJson({ name: inventory.name, id, exists }) + '\n');
96
- else
97
- process.stdout.write(`${exists ? 'yes' : 'no'} — "${id}" is ${exists ? 'declared' : 'NOT declared'} in registry "${inventory.name}".\n`);
98
- return exists ? 0 : 1;
120
+ const failIfTaken = flagBool(args, 'fail-if-taken');
121
+ const failIfMissing = flagBool(args, 'fail-if-missing');
122
+ if (failIfTaken && failIfMissing) {
123
+ process.stderr.write('Pass at most one of --fail-if-taken / --fail-if-missing.\n');
124
+ return 2;
125
+ }
126
+ // `--resolve` maps a human noun to the canonical registered id via the
127
+ // registry's `aliases` map before the existence test — so a duplicate guard
128
+ // can't return a false "free" on a synonym of an already-taken slug.
129
+ const doResolve = flagBool(args, 'resolve');
130
+ const canonical = doResolve ? (decl.aliases?.[id] ?? id) : id;
131
+ const resolved = canonical !== id;
132
+ const exists = registryExists(inventory, canonical);
133
+ // Exit-code convention:
134
+ // --fail-if-taken → non-zero when the id is already registered (free → 0),
135
+ // so `exists <id> --fail-if-taken && <author>` is a natural guard.
136
+ // --fail-if-missing → non-zero when the id is NOT registered (the consume-side check).
137
+ // neither → the historical query convention (taken → 0, free → 1).
138
+ const code = failIfTaken ? (exists ? 1 : 0) : exists ? 0 : 1;
139
+ if (json) {
140
+ process.stdout.write(asJson({
141
+ name: inventory.name,
142
+ id,
143
+ ...(resolved ? { resolvedId: canonical } : {}),
144
+ exists,
145
+ exitCode: code,
146
+ }) + '\n');
147
+ return code;
148
+ }
149
+ if (resolved)
150
+ process.stdout.write(`resolved "${id}" → "${canonical}" (alias)\n`);
151
+ process.stdout.write(`${exists ? 'yes' : 'no'} — "${canonical}" is ${exists ? 'declared' : 'NOT declared'} in registry "${inventory.name}".\n`);
152
+ return code;
99
153
  }
100
154
  if (action === 'where') {
101
155
  if (!id) {
@@ -124,7 +178,11 @@ async function runRegistryInventory(args, name) {
124
178
  export const registryCommand = {
125
179
  name: 'registry',
126
180
  description: 'Registry inspections: lifecycle symmetry + declared-registry inventory. Read-only.',
127
- usage: 'shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id>',
181
+ usage: 'shrk registry lifecycle | <name> list | <name> exists <id> [--resolve] [--fail-if-taken|--fail-if-missing] | <name> where <id>',
182
+ // Guard-mode + query flags take no value — declare them so `exists <id>
183
+ // --fail-if-taken` (flag last) and `exists --resolve <id>` (flag first) both
184
+ // keep the id as a positional instead of swallowing it.
185
+ booleanFlags: new Set(['json', 'resolve', 'fail-if-taken', 'fail-if-missing']),
128
186
  async run(args) {
129
187
  const sub = args.positional[0];
130
188
  if (sub === 'lifecycle') {
@@ -1 +1 @@
1
- {"version":3,"file":"reuse.command.d.ts","sourceRoot":"","sources":["../../src/commands/reuse.command.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA4DhC,eAAO,MAAM,YAAY,EAAE,eAqK1B,CAAC"}
1
+ {"version":3,"file":"reuse.command.d.ts","sourceRoot":"","sources":["../../src/commands/reuse.command.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA2GhC,eAAO,MAAM,YAAY,EAAE,eA0N1B,CAAC"}