@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.
- package/dist/commands/changelog-data.d.ts +25 -0
- package/dist/commands/changelog-data.d.ts.map +1 -0
- package/dist/commands/changelog-data.js +70 -0
- package/dist/commands/changelog.command.d.ts +3 -0
- package/dist/commands/changelog.command.d.ts.map +1 -0
- package/dist/commands/changelog.command.js +100 -0
- package/dist/commands/changes.command.d.ts.map +1 -1
- package/dist/commands/changes.command.js +4 -0
- package/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +182 -17
- package/dist/commands/command-catalog.d.ts +11 -0
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +99 -0
- package/dist/commands/compress.command.d.ts.map +1 -1
- package/dist/commands/compress.command.js +15 -1
- package/dist/commands/constructs.command.d.ts.map +1 -1
- package/dist/commands/constructs.command.js +49 -14
- package/dist/commands/context.command.d.ts.map +1 -1
- package/dist/commands/context.command.js +31 -19
- package/dist/commands/finish.command.d.ts +3 -0
- package/dist/commands/finish.command.d.ts.map +1 -0
- package/dist/commands/finish.command.js +70 -0
- package/dist/commands/gate.command.d.ts.map +1 -1
- package/dist/commands/gate.command.js +6 -1
- package/dist/commands/gen.command.d.ts.map +1 -1
- package/dist/commands/gen.command.js +65 -9
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +56 -25
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +20 -2
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +17 -22
- package/dist/commands/policy-lint.command.d.ts.map +1 -1
- package/dist/commands/policy-lint.command.js +46 -8
- package/dist/commands/registry.command.d.ts.map +1 -1
- package/dist/commands/registry.command.js +71 -13
- package/dist/commands/reuse.command.d.ts.map +1 -1
- package/dist/commands/reuse.command.js +80 -14
- package/dist/commands/smart-context.command.d.ts.map +1 -1
- package/dist/commands/smart-context.command.js +34 -42
- package/dist/commands/task.command.d.ts.map +1 -1
- package/dist/commands/task.command.js +33 -16
- package/dist/commands/trace.command.d.ts.map +1 -1
- package/dist/commands/trace.command.js +73 -3
- package/dist/commands/wiring.command.d.ts +12 -0
- package/dist/commands/wiring.command.d.ts.map +1 -0
- package/dist/commands/wiring.command.js +384 -0
- package/dist/diff/collect-changed-paths.d.ts +5 -3
- package/dist/diff/collect-changed-paths.d.ts.map +1 -1
- package/dist/diff/collect-changed-paths.js +73 -36
- package/dist/diff/deleted-orphans.d.ts +42 -0
- package/dist/diff/deleted-orphans.d.ts.map +1 -0
- package/dist/diff/deleted-orphans.js +46 -0
- package/dist/finish/run-finish.d.ts +62 -0
- package/dist/finish/run-finish.d.ts.map +1 -0
- package/dist/finish/run-finish.js +239 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +6 -0
- package/dist/validation/typecheck-emitted.d.ts +36 -0
- package/dist/validation/typecheck-emitted.d.ts.map +1 -0
- package/dist/validation/typecheck-emitted.js +109 -0
- 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,
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
745
|
+
.slice(0, cap)
|
|
715
746
|
.map((i) => ('source' in i ? sourceSummary(i.source) : { id: 'unknown', resolved: false }));
|
|
716
|
-
const referencedByList = references.slice(0,
|
|
717
|
-
const calledByList = callers.slice(0,
|
|
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 >
|
|
767
|
+
importsFromTruncated: importsFromAll.length > cap,
|
|
737
768
|
importedBy: ctxDropDel(importedByList),
|
|
738
769
|
totalImportedBy: importedByAll.length,
|
|
739
|
-
importedByTruncated: importedByAll.length >
|
|
740
|
-
symbols: symbols.slice(0,
|
|
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 >
|
|
774
|
+
referencedByTruncated: references.length > cap,
|
|
744
775
|
calledBy: ctxDropDel(calledByList),
|
|
745
776
|
totalCalledBy: callers.length,
|
|
746
|
-
calledByTruncated: callers.length >
|
|
747
|
-
...(subtypes.length > 0 ? { subtypes: subtypes.slice(0,
|
|
748
|
-
...(supertypes.length > 0 ? { supertypes: supertypes.slice(0,
|
|
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 =
|
|
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
|
|
1060
|
-
// the true uncapped count, so a truncated result stays
|
|
1061
|
-
// non-numeric input
|
|
1062
|
-
//
|
|
1063
|
-
const
|
|
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;
|
|
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;
|
|
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 {
|
|
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
|
|
301
|
-
|
|
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:
|
|
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(
|
|
313
|
+
process.stderr.write(`${scan.error ?? 'orphan check unavailable'}\n`);
|
|
307
314
|
}
|
|
308
315
|
return 2;
|
|
309
316
|
}
|
|
310
|
-
const 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 ${
|
|
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 ${
|
|
330
|
+
process.stdout.write(`No deleted files in diff vs ${scan.ref}.\n`);
|
|
324
331
|
return 0;
|
|
325
332
|
}
|
|
326
|
-
const
|
|
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 ${
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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":"
|
|
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
|
|
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
|
|
54
|
+
return exit;
|
|
28
55
|
}
|
|
29
56
|
process.stdout.write(renderRegistryLifecycleReportText(report));
|
|
30
|
-
return
|
|
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
|
|
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
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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;
|
|
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"}
|