@shrkcrft/cli 0.1.0-alpha.24 → 0.1.0-alpha.26
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/command-registry.d.ts +10 -0
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +16 -0
- 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 +94 -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 +134 -19
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +8 -0
- package/dist/commands/compress.command.d.ts.map +1 -1
- package/dist/commands/compress.command.js +46 -2
- 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/gate.command.d.ts.map +1 -1
- package/dist/commands/gate.command.js +63 -3
- 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 +14 -2
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +68 -1
- package/dist/commands/policy-lint.command.d.ts.map +1 -1
- package/dist/commands/policy-lint.command.js +46 -8
- package/dist/commands/registry-resolve.d.ts +41 -0
- package/dist/commands/registry-resolve.d.ts.map +1 -0
- package/dist/commands/registry-resolve.js +89 -0
- package/dist/commands/registry.command.d.ts.map +1 -1
- package/dist/commands/registry.command.js +86 -13
- package/dist/commands/reuse.command.d.ts +20 -0
- package/dist/commands/reuse.command.d.ts.map +1 -1
- package/dist/commands/reuse.command.js +156 -20
- package/dist/commands/smart-context.command.d.ts.map +1 -1
- package/dist/commands/smart-context.command.js +20 -5
- package/dist/commands/task.command.d.ts.map +1 -1
- package/dist/commands/task.command.js +33 -16
- package/dist/exit-codes.d.ts +49 -0
- package/dist/exit-codes.d.ts.map +1 -0
- package/dist/exit-codes.js +61 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +26 -1
- 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
|
@@ -38,6 +38,31 @@ function resolveContentType(raw) {
|
|
|
38
38
|
// legitimately have nothing to compress). Above it, a silent `−0%` re-emit is
|
|
39
39
|
// the opposite of the tool's purpose — nudge the user toward `--type`.
|
|
40
40
|
const PASSTHROUGH_HINT_MIN_BYTES = 128;
|
|
41
|
+
/**
|
|
42
|
+
* The no-win / passthrough test, shared by the `--json` envelope and the human
|
|
43
|
+
* text banner: a Passthrough strategy or a non-positive saving means the engine
|
|
44
|
+
* declined to compress and `compressed` is the verbatim original. Keeping it in
|
|
45
|
+
* one place is what makes the two output paths agree on fidelity.
|
|
46
|
+
*/
|
|
47
|
+
function isNoWin(result) {
|
|
48
|
+
return result.strategy === ECompressionStrategy.Passthrough || result.savings.saved <= 0;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The fidelity label for the human text path, mirroring the `fidelity` field
|
|
52
|
+
* the `--json` envelope surfaces: `passthrough` for a no-win re-emit, `lossy`
|
|
53
|
+
* for a reduction that drops detail (markdown line-omission or the code
|
|
54
|
+
* outline), and `lossless` otherwise. Same classification as `--json` — the
|
|
55
|
+
* text path just names the no-win case explicitly instead of via a separate
|
|
56
|
+
* `passthrough` boolean, so an agent reading stderr sees the fidelity at a
|
|
57
|
+
* glance.
|
|
58
|
+
*/
|
|
59
|
+
function textFidelity(result) {
|
|
60
|
+
if (isNoWin(result))
|
|
61
|
+
return 'passthrough';
|
|
62
|
+
if (result.strategy === ECompressionStrategy.Code || result.lossy)
|
|
63
|
+
return 'lossy';
|
|
64
|
+
return 'lossless';
|
|
65
|
+
}
|
|
41
66
|
function readInput(args) {
|
|
42
67
|
const positional = args.positional[0];
|
|
43
68
|
const useStdin = flagBool(args, 'stdin') || positional === undefined || positional === '-';
|
|
@@ -60,7 +85,7 @@ const COMPRESS_BOOLEAN_FLAGS = new Set([
|
|
|
60
85
|
]);
|
|
61
86
|
export const compressCommand = {
|
|
62
87
|
name: 'compress',
|
|
63
|
-
description: 'Compress a blob (file or stdin) deterministically to cut tokens — JSON→table, logs/search/diffs→signal. Reversible via `shrk expand`.',
|
|
88
|
+
description: 'Compress a blob (file or stdin) deterministically to cut tokens — JSON→table, logs/search/diffs→signal. Reversible via `shrk expand`. NOTE: `--type code` emits a LOSSY outline (function bodies elided) — good for fitting more code into context, NOT for line-accurate reading; use plain Read (or `--lossless`) to inspect a file.',
|
|
64
89
|
usage: 'shrk [--cwd <dir>] compress [<file>|-] [--stdin] [--type <content-type>] [--query <text>] [--max <n>] [--lossless] [--no-cache] [--json]',
|
|
65
90
|
booleanFlags: COMPRESS_BOOLEAN_FLAGS,
|
|
66
91
|
run(args) {
|
|
@@ -118,6 +143,13 @@ export const compressCommand = {
|
|
|
118
143
|
tokensSaved: result.savings.saved,
|
|
119
144
|
savedRatio: result.savings.ratio,
|
|
120
145
|
tokensAreEstimated: true,
|
|
146
|
+
// Explicit fidelity signal so a caller never mistakes a lossy code
|
|
147
|
+
// outline for a line-accurate read.
|
|
148
|
+
fidelity: result.strategy === ECompressionStrategy.Code
|
|
149
|
+
? 'lossy-outline (not line-accurate; use Read or --lossless to inspect)'
|
|
150
|
+
: result.lossy
|
|
151
|
+
? 'lossy'
|
|
152
|
+
: 'lossless',
|
|
121
153
|
queryApplied,
|
|
122
154
|
ccrKey: result.ccrKey ?? null,
|
|
123
155
|
note: result.note,
|
|
@@ -126,7 +158,7 @@ export const compressCommand = {
|
|
|
126
158
|
// VERBATIM original as `compressed`. Echoing it back inside the JSON
|
|
127
159
|
// envelope (plus scaffold) costs more tokens than the input. Signal
|
|
128
160
|
// passthrough and omit the duplicated content — the caller still has it.
|
|
129
|
-
const noWin = result
|
|
161
|
+
const noWin = isNoWin(result);
|
|
130
162
|
const payload = noWin
|
|
131
163
|
? { ...base, passthrough: true, inputBytes: Buffer.byteLength(content, 'utf8') }
|
|
132
164
|
: { ...base, compressed: result.compressed };
|
|
@@ -136,6 +168,18 @@ export const compressCommand = {
|
|
|
136
168
|
process.stdout.write(result.compressed + '\n');
|
|
137
169
|
const cached = result.ccrKey ? ` · original cached as ${result.ccrKey} (shrk expand ${result.ccrKey})` : '';
|
|
138
170
|
process.stderr.write(`${result.strategy}: ~${result.savings.before} → ~${result.savings.after} tokens (−${pct}%, est.)${cached}\n`);
|
|
171
|
+
// Fidelity parity with `--json`: print the same classification the JSON
|
|
172
|
+
// envelope carries as a one-line banner so an agent reading only stderr can
|
|
173
|
+
// see the output was lossy — and see the no-win `passthrough` case named
|
|
174
|
+
// explicitly rather than inferred from a bare `−0%` ratio.
|
|
175
|
+
process.stderr.write(`fidelity: ${textFidelity(result)}\n`);
|
|
176
|
+
// Fidelity banner: the code outline elides function bodies, so it is a
|
|
177
|
+
// footgun as a cheaper Read. Say so loudly — same failure mode as a lossy
|
|
178
|
+
// result that doesn't *look* lossy.
|
|
179
|
+
if (result.strategy === ECompressionStrategy.Code) {
|
|
180
|
+
process.stderr.write('note: code outline is LOSSY (bodies elided) — for bulk context, not line-accurate reading; ' +
|
|
181
|
+
'use `Read` or `--lossless` for inspection.\n');
|
|
182
|
+
}
|
|
139
183
|
// Token-economy guard: when auto-detect declines to compress a non-trivial
|
|
140
184
|
// input, a silent `−0%` re-emit looks like success. Nudge toward `--type`
|
|
141
185
|
// (stdout stays the verbatim blob; exit code unchanged).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constructs.command.d.ts","sourceRoot":"","sources":["../../src/commands/constructs.command.ts"],"names":[],"mappings":"AA8BA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"constructs.command.d.ts","sourceRoot":"","sources":["../../src/commands/constructs.command.ts"],"names":[],"mappings":"AA8BA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAkKhC,eAAO,MAAM,qBAAqB,EAAE,eAqBnC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,eAmDlC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eAgGpC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eAyErC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,eAkCtC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eA2BpC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,eAiClC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eAiBrC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eAkBrC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eA2BrC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eA8BrC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eA0DpC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eA0LpC,CAAC"}
|
|
@@ -4,6 +4,48 @@ import { AdoptionCheckpointStatus, buildConstructAdoptionDiff, buildConstructAdo
|
|
|
4
4
|
import { GraphQueryApi, GraphStore } from '@shrkcrft/graph';
|
|
5
5
|
import { flagBool, flagNumber, flagString, resolveCwd, } from "../command-registry.js";
|
|
6
6
|
import { asJson, header } from "../output/format-output.js";
|
|
7
|
+
/**
|
|
8
|
+
* A construct subverb was handed an id that is NOT a registered construct.
|
|
9
|
+
* `constructs *` matches a curated construct-registry ID, not a raw code symbol
|
|
10
|
+
* — so a miss must never read as an existence claim about the symbol. Probe the
|
|
11
|
+
* code graph and redirect to the tools that actually answer "where does X live /
|
|
12
|
+
* who calls it" instead of a definitive-sounding "No construct". Returns exit 1.
|
|
13
|
+
*/
|
|
14
|
+
function emitConstructMiss(id, args) {
|
|
15
|
+
const cwd = resolveCwd(args);
|
|
16
|
+
const store = new GraphStore(cwd);
|
|
17
|
+
let isCodeSymbol = false;
|
|
18
|
+
if (store.exists()) {
|
|
19
|
+
const api = GraphQueryApi.fromStore(cwd);
|
|
20
|
+
isCodeSymbol =
|
|
21
|
+
api.findSymbol(id, { exact: true }).length > 0 ||
|
|
22
|
+
api.findSymbol(id, { exact: false, limit: 1 }).length > 0;
|
|
23
|
+
}
|
|
24
|
+
if (flagBool(args, 'json')) {
|
|
25
|
+
process.stdout.write(asJson({
|
|
26
|
+
schema: 'sharkcraft.constructs-miss/v1',
|
|
27
|
+
id,
|
|
28
|
+
found: false,
|
|
29
|
+
reason: 'not-a-construct-id',
|
|
30
|
+
isCodeSymbol,
|
|
31
|
+
hint: isCodeSymbol
|
|
32
|
+
? `"${id}" is a code symbol, not a construct — try 'shrk graph callers ${id}' or 'shrk graph search ${id}'.`
|
|
33
|
+
: `"${id}" is not a registered construct ID; list them with 'shrk constructs list', or search code with 'shrk graph search ${id}'.`,
|
|
34
|
+
}) + '\n');
|
|
35
|
+
return 1;
|
|
36
|
+
}
|
|
37
|
+
if (isCodeSymbol) {
|
|
38
|
+
process.stderr.write(`"${id}" is not a construct ID — did you mean a code symbol?\n` +
|
|
39
|
+
` shrk graph callers ${id} # who references it (path:line)\n` +
|
|
40
|
+
` shrk graph search ${id} # find its declaration\n`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
process.stderr.write(`"${id}" is not a registered construct ID, and no code symbol by that name was found.\n` +
|
|
44
|
+
` shrk constructs list # the curated construct IDs\n` +
|
|
45
|
+
` shrk graph search ${id} # search the code graph\n`);
|
|
46
|
+
}
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
7
49
|
async function loadAll(args) {
|
|
8
50
|
const cwd = resolveCwd(args);
|
|
9
51
|
const inspection = await inspectSharkcraft({ cwd });
|
|
@@ -116,8 +158,7 @@ export const constructsGetCommand = {
|
|
|
116
158
|
const { constructs } = await loadAll(args);
|
|
117
159
|
const c = constructs.find((x) => x.id === id);
|
|
118
160
|
if (!c) {
|
|
119
|
-
|
|
120
|
-
return 1;
|
|
161
|
+
return emitConstructMiss(id, args);
|
|
121
162
|
}
|
|
122
163
|
if (flagBool(args, 'json')) {
|
|
123
164
|
process.stdout.write(asJson(c) + '\n');
|
|
@@ -175,8 +216,7 @@ export const constructsTraceCommand = {
|
|
|
175
216
|
const { constructs } = await loadAll(args);
|
|
176
217
|
const c = constructs.find((x) => x.id === id);
|
|
177
218
|
if (!c) {
|
|
178
|
-
|
|
179
|
-
return 1;
|
|
219
|
+
return emitConstructMiss(id, args);
|
|
180
220
|
}
|
|
181
221
|
const trace = traceConstruct(c);
|
|
182
222
|
const deep = flagBool(args, 'deep');
|
|
@@ -274,8 +314,7 @@ export const constructsImpactCommand = {
|
|
|
274
314
|
const { constructs, inspection } = await loadAll(args);
|
|
275
315
|
const c = constructs.find((x) => x.id === id);
|
|
276
316
|
if (!c) {
|
|
277
|
-
|
|
278
|
-
return 1;
|
|
317
|
+
return emitConstructMiss(id, args);
|
|
279
318
|
}
|
|
280
319
|
const trace = traceConstruct(c);
|
|
281
320
|
/**
|
|
@@ -352,8 +391,7 @@ export const constructsRelatedCommand = {
|
|
|
352
391
|
const { constructs } = await loadAll(args);
|
|
353
392
|
const c = constructs.find((x) => x.id === id);
|
|
354
393
|
if (!c) {
|
|
355
|
-
|
|
356
|
-
return 1;
|
|
394
|
+
return emitConstructMiss(id, args);
|
|
357
395
|
}
|
|
358
396
|
const related = [
|
|
359
397
|
...(c.relatedKnowledge ?? []).map((id) => ({ kind: 'knowledge', id })),
|
|
@@ -389,8 +427,7 @@ export const constructsFilesCommand = {
|
|
|
389
427
|
const { constructs } = await loadAll(args);
|
|
390
428
|
const c = constructs.find((x) => x.id === id);
|
|
391
429
|
if (!c) {
|
|
392
|
-
|
|
393
|
-
return 1;
|
|
430
|
+
return emitConstructMiss(id, args);
|
|
394
431
|
}
|
|
395
432
|
const trace = traceConstruct(c);
|
|
396
433
|
// Emit the graph-resolved files (globs expanded), not the raw declared
|
|
@@ -419,8 +456,7 @@ export const constructsApiCommand = {
|
|
|
419
456
|
const { constructs } = await loadAll(args);
|
|
420
457
|
const c = constructs.find((x) => x.id === id);
|
|
421
458
|
if (!c) {
|
|
422
|
-
|
|
423
|
-
return 1;
|
|
459
|
+
return emitConstructMiss(id, args);
|
|
424
460
|
}
|
|
425
461
|
const publicApi = c.publicApi ?? [];
|
|
426
462
|
if (flagBool(args, 'json')) {
|
|
@@ -493,8 +529,7 @@ export const constructsFacetsCommand = {
|
|
|
493
529
|
const { constructs } = await loadAll(args);
|
|
494
530
|
const c = constructs.find((x) => x.id === id);
|
|
495
531
|
if (!c) {
|
|
496
|
-
|
|
497
|
-
return 1;
|
|
532
|
+
return emitConstructMiss(id, args);
|
|
498
533
|
}
|
|
499
534
|
const facets = c.facets ?? {};
|
|
500
535
|
if (flagBool(args, 'json')) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.command.d.ts","sourceRoot":"","sources":["../../src/commands/context.command.ts"],"names":[],"mappings":"AAiBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"context.command.d.ts","sourceRoot":"","sources":["../../src/commands/context.command.ts"],"names":[],"mappings":"AAiBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA0ChC,eAAO,MAAM,cAAc,EAAE,eA8I5B,CAAC"}
|
|
@@ -6,6 +6,15 @@ import * as nodePath from 'node:path';
|
|
|
6
6
|
import { buildUniversalSearch, explainTaskRouting, recommendCommands, renderOverviewText, buildProjectOverview, } from '@shrkcrft/inspector';
|
|
7
7
|
import { flagBool, flagNumber, flagString, flagList, resolveCwd, } from "../command-registry.js";
|
|
8
8
|
import { asJson, header } from "../output/format-output.js";
|
|
9
|
+
/**
|
|
10
|
+
* Budget used for the DEFAULT full human-text orientation view when the caller
|
|
11
|
+
* set no explicit `--max-tokens`. Orientation is the cheap-first step an agent
|
|
12
|
+
* uses to form its initial model of the project — it must not silently drop the
|
|
13
|
+
* body of the richest sections (architecture, conventions, paths, workflows) the
|
|
14
|
+
* way a tight budget would. Large enough to hold every planned section; explicit
|
|
15
|
+
* `--max-tokens` and JSON/`--summary` callers still get the configured budget.
|
|
16
|
+
*/
|
|
17
|
+
const WIDE_CONTEXT_BUDGET = 100_000;
|
|
9
18
|
/**
|
|
10
19
|
* Minimal JSON shape for agent / skill consumption — the context-side mirror
|
|
11
20
|
* of `shrk task --compact`. Drops the heavy `body` and `request` echo and
|
|
@@ -65,8 +74,19 @@ export const contextCommand = {
|
|
|
65
74
|
const noPaths = flagBool(args, 'no-paths');
|
|
66
75
|
const includeDocs = flagBool(args, 'include-docs');
|
|
67
76
|
const includeCommands = flagBool(args, 'include-commands');
|
|
77
|
+
// Orientation renders the FULL body by default now (parity with why / reuse
|
|
78
|
+
// / knowledge get). `--summary`/`--brief` opts back into the terse view.
|
|
79
|
+
const wantsSummary = flagBool(args, 'summary') || flagBool(args, 'brief');
|
|
80
|
+
const wantsJsonOut = flagBool(args, 'json') || flagBool(args, 'machine-json');
|
|
68
81
|
const inspection = await inspectSharkcraft({ cwd: resolveCwd(args) });
|
|
69
82
|
const overview = buildProjectOverview(inspection.workspace, inspection.config?.projectName);
|
|
83
|
+
// Auto-widen the budget for the default full human view so no requested
|
|
84
|
+
// section is dropped purely to fit a tight budget. Explicit `--max-tokens`,
|
|
85
|
+
// JSON, and summary callers keep the configured budget.
|
|
86
|
+
const effectiveMaxTokens = maxTokens ??
|
|
87
|
+
(!wantsJsonOut && !wantsSummary
|
|
88
|
+
? WIDE_CONTEXT_BUDGET
|
|
89
|
+
: (inspection.config?.defaultMaxTokens ?? 3000));
|
|
70
90
|
const contextBoost = contextTuningBoostFor(inspection, task);
|
|
71
91
|
const result = buildContext(inspection.knowledgeEntries, {
|
|
72
92
|
task,
|
|
@@ -74,7 +94,7 @@ export const contextCommand = {
|
|
|
74
94
|
area,
|
|
75
95
|
tags,
|
|
76
96
|
scope,
|
|
77
|
-
maxTokens:
|
|
97
|
+
maxTokens: effectiveMaxTokens,
|
|
78
98
|
includeExamples: !noExamples,
|
|
79
99
|
includeTemplates: !noTemplates,
|
|
80
100
|
includeRules: !noRules,
|
|
@@ -84,14 +104,10 @@ export const contextCommand = {
|
|
|
84
104
|
projectOverview: renderOverviewText(overview),
|
|
85
105
|
...(contextBoost ? { boostFor: contextBoost } : {}),
|
|
86
106
|
});
|
|
87
|
-
// Surface top commands prominently before the long context body.
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
const actionVerbRe = /^(rename|add|fix|refactor|remove|delete|migrate|wire|explore|create|implement|update|introduce|build|extract|move|inline|generate|scaffold)\b/i;
|
|
92
|
-
const isActionLike = actionVerbRe.test(task.trim());
|
|
93
|
-
const wantsFull = flagBool(args, 'full');
|
|
94
|
-
const commandsFirst = flagBool(args, 'commands-first') || (isActionLike && !wantsFull);
|
|
107
|
+
// Surface top commands prominently before the long context body. The body
|
|
108
|
+
// itself now prints by default (parity with sibling orientation verbs);
|
|
109
|
+
// `--commands-first` keeps the terse commands-only view for action tasks.
|
|
110
|
+
const commandsOnly = flagBool(args, 'commands-first');
|
|
95
111
|
let commandRecommendations = null;
|
|
96
112
|
let routingMatches = [];
|
|
97
113
|
let searchReport = null;
|
|
@@ -141,16 +157,12 @@ export const contextCommand = {
|
|
|
141
157
|
process.stdout.write(` • ${m.hint.id} ${m.hint.title}\n`);
|
|
142
158
|
}
|
|
143
159
|
}
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
else if (isActionLike && !flagBool(args, 'commands-first')) {
|
|
152
|
-
process.stdout.write('\n(action-like task → commands-first; pass --full to see the long context body.)\n');
|
|
153
|
-
}
|
|
160
|
+
// The full context body prints by default now — parity with why / reuse /
|
|
161
|
+
// knowledge get, so the agent's first read of the project isn't thinner than
|
|
162
|
+
// every adjacent command. `--summary`/`--brief` (or `--commands-first`) opts
|
|
163
|
+
// back into the terse view.
|
|
164
|
+
if (wantsSummary || commandsOnly) {
|
|
165
|
+
process.stdout.write('\n(summary mode — omit --summary for the full context body, --json for machine output.)\n');
|
|
154
166
|
return 0;
|
|
155
167
|
}
|
|
156
168
|
process.stdout.write('\n');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.command.d.ts","sourceRoot":"","sources":["../../src/commands/gate.command.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"gate.command.d.ts","sourceRoot":"","sources":["../../src/commands/gate.command.ts"],"names":[],"mappings":"AAcA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,EAAE,eAwNzB,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { QualityGateReportStore, renderGateReportMarkdown, runQualityGates, } from '@shrkcrft/quality-gates';
|
|
2
|
+
import { ArchReportStore, runArchCheck } from '@shrkcrft/architecture-guard';
|
|
2
3
|
import { inspectSharkcraft, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
|
|
3
4
|
import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
5
|
import * as nodePath from 'node:path';
|
|
@@ -19,7 +20,7 @@ import { asJson, header, kv } from "../output/format-output.js";
|
|
|
19
20
|
export const gateCommand = {
|
|
20
21
|
name: 'gate',
|
|
21
22
|
description: 'Aggregator: runs the code-intelligence quality gates (graph freshness, architecture, impact-since-ref) and reports a single pass/fail.',
|
|
22
|
-
usage: 'shrk gate [--since <gitref>] [--changed-only] [--staged] [--files a,b,c] [--fail-on critical,high] [--arch-all] [--disable arch,impact,policy,knowledge-symbol,api-diff] [--api-baseline <path>] [--no-fail-on-breaking] [--strict] [--no-persist] [--json] [--markdown] [--output <path>]\n (the arch gate is
|
|
23
|
+
usage: 'shrk gate [--since <gitref>] [--changed-only] [--staged] [--files a,b,c] [--fail-on critical,high] [--arch-all] [--disable arch,impact,policy,knowledge-symbol,api-diff] [--api-baseline <path>] [--no-fail-on-breaking] [--strict] [--no-persist] [--json] [--markdown] [--output <path>]\n (the arch gate is change-scoped once a baseline is frozen — a NEW error blocks only when the working diff vs HEAD touched its origin file; drift in untouched files is informational — --arch-all fails on total, --strict escalates the warn)\n (--changed-only / --staged / --files / --since scope the wiring + policy + knowledge-symbol gates to the changeset; they also drive the impact gate — --since diffs the gitref, the others analyze the changed-file set)\n shrk gate scaffold-ci [--provider github|generic] [--force] [--json]\n shrk gate scaffold-hook [--provider husky|raw] [--force] [--json]\n shrk gate baseline --refreeze [--json] (operational reset: re-freeze the arch baseline to the current state — NOT the change-scoped gate)',
|
|
23
24
|
async run(args) {
|
|
24
25
|
if (args.positional[0] === 'scaffold-ci') {
|
|
25
26
|
const sliced = { ...args, positional: args.positional.slice(1) };
|
|
@@ -29,6 +30,10 @@ export const gateCommand = {
|
|
|
29
30
|
const sliced = { ...args, positional: args.positional.slice(1) };
|
|
30
31
|
return runGateScaffoldHook(sliced);
|
|
31
32
|
}
|
|
33
|
+
if (args.positional[0] === 'baseline') {
|
|
34
|
+
const sliced = { ...args, positional: args.positional.slice(1) };
|
|
35
|
+
return runGateBaseline(sliced);
|
|
36
|
+
}
|
|
32
37
|
const cwd = resolveCwd(args);
|
|
33
38
|
const wantJson = flagBool(args, 'json');
|
|
34
39
|
const wantMarkdown = flagBool(args, 'markdown');
|
|
@@ -101,6 +106,17 @@ export const gateCommand = {
|
|
|
101
106
|
const scopeOpts = wantChangedScope
|
|
102
107
|
? { changedOnly: true, changedFiles: changedFiles ?? [] }
|
|
103
108
|
: {};
|
|
109
|
+
// §3.1 — the architecture gate's "NEW" is ALWAYS change-scoped: a NEW error
|
|
110
|
+
// means one introduced by the working change (diff vs HEAD), never drift
|
|
111
|
+
// against a frozen (possibly months-old) baseline in a file the change never
|
|
112
|
+
// touched. When an explicit scope flag is passed we reuse its resolution;
|
|
113
|
+
// otherwise we default to the worktree diff vs HEAD — empty in a clean tree,
|
|
114
|
+
// so pre-existing baseline drift stays informational and can't red the gate
|
|
115
|
+
// on its own. `--arch-all` (baselineRelative:false) ignores this and fails
|
|
116
|
+
// on total errors, keeping a clean-tree CI demand expressible.
|
|
117
|
+
const archChangedFiles = wantChangedScope
|
|
118
|
+
? changedFiles ?? []
|
|
119
|
+
: resolveChangedFiles({ projectRoot: cwd, includeWorktree: true }).files;
|
|
104
120
|
// Knowledge symbol-ref integrity needs the loaded knowledge entries. The
|
|
105
121
|
// inspection is async, so we build it here and inject it; the gate stays
|
|
106
122
|
// synchronous and resolves the code graph itself. Best-effort — a failed
|
|
@@ -116,7 +132,10 @@ export const gateCommand = {
|
|
|
116
132
|
}
|
|
117
133
|
const report = runQualityGates({
|
|
118
134
|
projectRoot: cwd,
|
|
119
|
-
|
|
135
|
+
arch: {
|
|
136
|
+
...(archAll ? { baselineRelative: false } : {}),
|
|
137
|
+
changedFiles: archChangedFiles,
|
|
138
|
+
},
|
|
120
139
|
wiring: {
|
|
121
140
|
...(configError
|
|
122
141
|
? { configError }
|
|
@@ -143,7 +162,12 @@ export const gateCommand = {
|
|
|
143
162
|
: {}),
|
|
144
163
|
impact: {
|
|
145
164
|
...(sinceRef ? { sinceRef } : {}),
|
|
146
|
-
|
|
165
|
+
// Blast-radius risk is inherently PRE-EXISTING structure (touching a hub
|
|
166
|
+
// is risky but not a new failure this change introduced), so the composite
|
|
167
|
+
// gate treats it as ADVISORY by default — `failOn: []` warns instead of
|
|
168
|
+
// redding, keeping the verdict change-attributable. `--fail-on critical`
|
|
169
|
+
// opts into a hard fail; `--strict` escalates the advisory warn.
|
|
170
|
+
failOn: failOn ?? [],
|
|
147
171
|
// Scope the impact gate to the changeset too: with `--since` we keep the
|
|
148
172
|
// gitref diff; with `--changed-only` / `--staged` / `--files` (and no
|
|
149
173
|
// `--since`) we analyze the resolved changed-file set directly.
|
|
@@ -395,3 +419,39 @@ async function runGateScaffoldCi(args) {
|
|
|
395
419
|
process.stdout.write(`Scaffolded ${provider} CI runner → ${target}\n`);
|
|
396
420
|
return 0;
|
|
397
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* `shrk gate baseline --refreeze` — deliberate operational reset that re-freezes
|
|
424
|
+
* the architecture baseline to the CURRENT state, absorbing accumulated drift so
|
|
425
|
+
* the informational "baseline drift" line resets to zero.
|
|
426
|
+
*
|
|
427
|
+
* This is an operational complement, NOT the gate itself: the gate's blocking
|
|
428
|
+
* verdict is change-scoped (a NEW error blocks only when the working diff touched
|
|
429
|
+
* its origin file), so a stale baseline never reds the gate on its own. Refreeze
|
|
430
|
+
* is only for tidying the informational drift line. It mirrors `shrk arch
|
|
431
|
+
* baseline write`.
|
|
432
|
+
*/
|
|
433
|
+
async function runGateBaseline(args) {
|
|
434
|
+
const cwd = resolveCwd(args);
|
|
435
|
+
const wantJson = flagBool(args, 'json');
|
|
436
|
+
const refreeze = flagBool(args, 'refreeze');
|
|
437
|
+
if (!refreeze) {
|
|
438
|
+
process.stderr.write('Usage: shrk gate baseline --refreeze [--json]\n' +
|
|
439
|
+
' Re-freeze the architecture baseline to the current state (operational reset,\n' +
|
|
440
|
+
' not the change-scoped gate). Equivalent to `shrk arch baseline write`.\n');
|
|
441
|
+
return 2;
|
|
442
|
+
}
|
|
443
|
+
const report = runArchCheck({ projectRoot: cwd });
|
|
444
|
+
if (report.diagnostics.some((d) => d.includes('code-graph store missing'))) {
|
|
445
|
+
process.stderr.write('Cannot refreeze — graph index missing. Run `shrk graph index` first.\n');
|
|
446
|
+
return 2;
|
|
447
|
+
}
|
|
448
|
+
const store = new ArchReportStore(cwd);
|
|
449
|
+
const snap = store.writeBaseline(report);
|
|
450
|
+
if (wantJson) {
|
|
451
|
+
process.stdout.write(asJson({ ok: true, wrote: store.baselinePath, baseline: snap }) + '\n');
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
process.stdout.write(`Architecture baseline re-frozen → ${store.baselinePath}\n`);
|
|
455
|
+
process.stdout.write(kv('violations', `${snap.countsBySeverity.error} error, ${snap.countsBySeverity.warning} warning`) + '\n');
|
|
456
|
+
return 0;
|
|
457
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gen.command.d.ts","sourceRoot":"","sources":["../../src/commands/gen.command.ts"],"names":[],"mappings":"AASA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"gen.command.d.ts","sourceRoot":"","sources":["../../src/commands/gen.command.ts"],"names":[],"mappings":"AASA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAuBhC,eAAO,MAAM,UAAU,EAAE,eAsNxB,CAAC"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { inspectSharkcraft } from '@shrkcrft/inspector';
|
|
2
2
|
import { buildSavedPlan, FileChangeType, generate, OverwriteStrategy, savePlanToFile, signPlan, } from '@shrkcrft/generator';
|
|
3
3
|
import { flagBool, flagString, flagVars, resolveCwd, } from "../command-registry.js";
|
|
4
|
+
import * as nodePath from 'node:path';
|
|
4
5
|
import { asJson, header } from "../output/format-output.js";
|
|
5
6
|
import { printError } from "../output/print-error.js";
|
|
7
|
+
import { typecheckEmittedFiles, } from "../validation/typecheck-emitted.js";
|
|
6
8
|
const CHANGE_LABEL = {
|
|
7
9
|
[FileChangeType.Create]: 'CREATE',
|
|
8
10
|
[FileChangeType.Update]: 'UPDATE',
|
|
@@ -19,7 +21,7 @@ const CHANGE_LABEL = {
|
|
|
19
21
|
export const genCommand = {
|
|
20
22
|
name: 'gen',
|
|
21
23
|
description: 'Generate code from a template. Defaults to dry-run.',
|
|
22
|
-
usage: 'shrk gen <templateId> [<name>] [--var key=value ...] [--dry-run] [--write] [--force] [--save-plan <file>] [--show-content] [--json]',
|
|
24
|
+
usage: 'shrk gen <templateId> [<name>] [--var key=value ...] [--dry-run] [--write] [--force] [--save-plan <file>] [--print|--show-content] [--typecheck] [--json]\n (--typecheck compiles the emitted files against the detected tsconfig BEFORE apply — a template bug fails here, not at the human next build; --print shows the rendered bodies)',
|
|
23
25
|
async run(args) {
|
|
24
26
|
const templateId = args.positional[0];
|
|
25
27
|
const name = args.positional[1];
|
|
@@ -40,19 +42,50 @@ export const genCommand = {
|
|
|
40
42
|
: flagString(args, 'overwrite') ?? OverwriteStrategy.Never;
|
|
41
43
|
const variables = flagVars(args);
|
|
42
44
|
const savePlanPath = flagString(args, 'save-plan');
|
|
43
|
-
const
|
|
45
|
+
const wantTypecheck = flagBool(args, 'typecheck');
|
|
46
|
+
const genOpts = {
|
|
44
47
|
templateId: template.id,
|
|
45
48
|
name,
|
|
46
49
|
variables,
|
|
47
50
|
projectRoot: inspection.projectRoot,
|
|
48
51
|
overwriteStrategy: overwrite,
|
|
49
|
-
|
|
50
|
-
|
|
52
|
+
};
|
|
53
|
+
// --typecheck is a PRE-WRITE gate. Render a dry-run FIRST, compile the emitted
|
|
54
|
+
// (full-file create) TS/TSX against the detected tsconfig in memory, and only
|
|
55
|
+
// proceed to write if they compile — so an agent driving gen→apply never lands
|
|
56
|
+
// non-compiling code. On failure the write is refused (nothing touches disk).
|
|
57
|
+
// Only whole-file creates are checkable standalone (update ops are fragments).
|
|
58
|
+
let typecheckResult;
|
|
59
|
+
let effectiveWrite = write;
|
|
60
|
+
let result;
|
|
61
|
+
if (wantTypecheck) {
|
|
62
|
+
const dry = generate(template, { ...genOpts, write: false });
|
|
63
|
+
if (!dry.ok) {
|
|
64
|
+
printError(dry.error);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
const emitted = dry.value.plan.changes
|
|
68
|
+
.filter((c) => c.type === FileChangeType.Create)
|
|
69
|
+
.map((c) => ({
|
|
70
|
+
absPath: nodePath.resolve(inspection.projectRoot, c.relativePath),
|
|
71
|
+
contents: c.contents ?? '',
|
|
72
|
+
}))
|
|
73
|
+
.filter((f) => f.contents.length > 0 && /\.tsx?$/.test(f.absPath));
|
|
74
|
+
typecheckResult = typecheckEmittedFiles(inspection.projectRoot, emitted);
|
|
75
|
+
// Refuse to write non-compiling output — the whole point of the gate.
|
|
76
|
+
effectiveWrite = write && typecheckResult.errors.length === 0;
|
|
77
|
+
result = effectiveWrite ? generate(template, { ...genOpts, write: true }) : dry;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
result = generate(template, { ...genOpts, write });
|
|
81
|
+
}
|
|
51
82
|
if (!result.ok) {
|
|
52
83
|
printError(result.error);
|
|
53
84
|
return 1;
|
|
54
85
|
}
|
|
55
86
|
const { plan, summary, written } = result.value;
|
|
87
|
+
const typecheckFailed = (typecheckResult?.errors.length ?? 0) > 0;
|
|
88
|
+
const writeRefused = write && typecheckFailed;
|
|
56
89
|
// --save-plan is allowed regardless of write/dry-run, but we refuse to
|
|
57
90
|
// save plans that have conflicts (since `shrk apply` would refuse them too).
|
|
58
91
|
if (savePlanPath) {
|
|
@@ -89,10 +122,12 @@ export const genCommand = {
|
|
|
89
122
|
summary,
|
|
90
123
|
written: written.map((w) => w.relativePath),
|
|
91
124
|
savedPlanPath: savePlanPath ?? null,
|
|
125
|
+
...(typecheckResult ? { typecheck: typecheckResult } : {}),
|
|
126
|
+
...(writeRefused ? { writeRefused: true } : {}),
|
|
92
127
|
}) + '\n');
|
|
93
|
-
return plan.hasConflicts ? 1 : 0;
|
|
128
|
+
return plan.hasConflicts || typecheckFailed ? 1 : 0;
|
|
94
129
|
}
|
|
95
|
-
process.stdout.write(header(
|
|
130
|
+
process.stdout.write(header(effectiveWrite ? `Generation: ${template.id}` : `Dry-run: ${template.id}`));
|
|
96
131
|
if (plan.warnings.length) {
|
|
97
132
|
process.stdout.write('Warnings:\n');
|
|
98
133
|
for (const w of plan.warnings)
|
|
@@ -118,7 +153,7 @@ export const genCommand = {
|
|
|
118
153
|
// agent can review what a template would generate WITHOUT writing to disk
|
|
119
154
|
// (the saved plan stays content-free). The bytes are identical in dry-run
|
|
120
155
|
// and --write; this is purely additive.
|
|
121
|
-
if (flagBool(args, 'show-content')) {
|
|
156
|
+
if (flagBool(args, 'show-content') || flagBool(args, 'print')) {
|
|
122
157
|
process.stdout.write('\nVirtual content (not written to disk):\n');
|
|
123
158
|
for (const change of plan.changes) {
|
|
124
159
|
const body = change.contents ?? '';
|
|
@@ -139,10 +174,31 @@ export const genCommand = {
|
|
|
139
174
|
if (updateLike.length > 0) {
|
|
140
175
|
process.stdout.write(`\nHUMAN REVIEW REQUIRED — ${updateLike.length} update entry/entries modify existing files.\n`);
|
|
141
176
|
}
|
|
177
|
+
if (typecheckResult) {
|
|
178
|
+
if (!typecheckResult.ran) {
|
|
179
|
+
process.stdout.write(`\nTypecheck: skipped — ${typecheckResult.note ?? 'nothing to check'}.\n`);
|
|
180
|
+
}
|
|
181
|
+
else if (typecheckResult.errors.length === 0) {
|
|
182
|
+
process.stdout.write('\nTypecheck: ✓ emitted files compile against the detected tsconfig.\n');
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
process.stdout.write(`\nTypecheck: ✗ ${typecheckResult.errors.length} error(s) in the emitted files — a template bug (NOT applied):\n`);
|
|
186
|
+
for (const e of typecheckResult.errors.slice(0, 30)) {
|
|
187
|
+
const rel = nodePath.relative(inspection.projectRoot, e.file) || e.file;
|
|
188
|
+
process.stdout.write(` ${rel}:${e.line}:${e.column} ${e.message}\n`);
|
|
189
|
+
}
|
|
190
|
+
if (typecheckResult.errors.length > 30) {
|
|
191
|
+
process.stdout.write(` … (${typecheckResult.errors.length - 30} more)\n`);
|
|
192
|
+
}
|
|
193
|
+
if (writeRefused) {
|
|
194
|
+
process.stdout.write('\nWrite REFUSED — the emitted files do not typecheck (nothing written). Fix the template, then re-run.\n');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
142
198
|
process.stdout.write(`\nSummary: written=${summary.written}, skipped=${summary.skipped}, conflicts=${summary.conflicts}\n`);
|
|
143
|
-
if (!
|
|
199
|
+
if (!effectiveWrite && !savePlanPath && !writeRefused) {
|
|
144
200
|
process.stdout.write('\nRe-run with --write to apply, or --save-plan <file> + `shrk apply`.\n');
|
|
145
201
|
}
|
|
146
|
-
return plan.hasConflicts ? 1 : 0;
|
|
202
|
+
return plan.hasConflicts || typecheckFailed ? 1 : 0;
|
|
147
203
|
},
|
|
148
204
|
};
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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"}
|
|
@@ -285,7 +285,14 @@ export async function runGraphCycles(args) {
|
|
|
285
285
|
const api = loadGuarded(() => GraphQueryApi.fromStore(cwd), wantJson);
|
|
286
286
|
if (!api)
|
|
287
287
|
return 1;
|
|
288
|
-
|
|
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);
|
|
289
296
|
const filtered = allCycles.filter((c) => c.size >= minSize);
|
|
290
297
|
const limited = filtered.slice(0, limit);
|
|
291
298
|
if (wantJson) {
|
|
@@ -293,6 +300,8 @@ export async function runGraphCycles(args) {
|
|
|
293
300
|
ok: true,
|
|
294
301
|
total: filtered.length,
|
|
295
302
|
truncated: filtered.length > limit,
|
|
303
|
+
includeTypeEdges,
|
|
304
|
+
typeOnlyLoopCount,
|
|
296
305
|
cycles: limited.map((c) => ({
|
|
297
306
|
size: c.size,
|
|
298
307
|
paths: c.paths ?? c.nodeIds.map((id) => id.replace(/^file:/, '')),
|
|
@@ -302,8 +311,11 @@ export async function runGraphCycles(args) {
|
|
|
302
311
|
}
|
|
303
312
|
process.stdout.write(header('Graph cycles'));
|
|
304
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
|
+
}
|
|
305
317
|
if (filtered.length === 0) {
|
|
306
|
-
process.stdout.write(
|
|
318
|
+
process.stdout.write(`\nNo ${includeTypeEdges ? '' : 'runtime '}cycles in the file-import graph. ✓\n`);
|
|
307
319
|
return 0;
|
|
308
320
|
}
|
|
309
321
|
process.stdout.write(kv('shown', `${limited.length}/${filtered.length}`) + '\n');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.command.d.ts","sourceRoot":"","sources":["../../src/commands/graph.command.ts"],"names":[],"mappings":"AAUA,OAAO,
|
|
1
|
+
{"version":3,"file":"graph.command.d.ts","sourceRoot":"","sources":["../../src/commands/graph.command.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAqFhC,eAAO,MAAM,YAAY,EAAE,eAqO1B,CAAC"}
|