@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.
Files changed (52) hide show
  1. package/dist/command-registry.d.ts +10 -0
  2. package/dist/command-registry.d.ts.map +1 -1
  3. package/dist/command-registry.js +16 -0
  4. package/dist/commands/changelog-data.d.ts +25 -0
  5. package/dist/commands/changelog-data.d.ts.map +1 -0
  6. package/dist/commands/changelog-data.js +94 -0
  7. package/dist/commands/changelog.command.d.ts +3 -0
  8. package/dist/commands/changelog.command.d.ts.map +1 -0
  9. package/dist/commands/changelog.command.js +100 -0
  10. package/dist/commands/changes.command.d.ts.map +1 -1
  11. package/dist/commands/changes.command.js +4 -0
  12. package/dist/commands/check.command.d.ts.map +1 -1
  13. package/dist/commands/check.command.js +134 -19
  14. package/dist/commands/command-catalog.d.ts.map +1 -1
  15. package/dist/commands/command-catalog.js +8 -0
  16. package/dist/commands/compress.command.d.ts.map +1 -1
  17. package/dist/commands/compress.command.js +46 -2
  18. package/dist/commands/constructs.command.d.ts.map +1 -1
  19. package/dist/commands/constructs.command.js +49 -14
  20. package/dist/commands/context.command.d.ts.map +1 -1
  21. package/dist/commands/context.command.js +31 -19
  22. package/dist/commands/gate.command.d.ts.map +1 -1
  23. package/dist/commands/gate.command.js +63 -3
  24. package/dist/commands/gen.command.d.ts.map +1 -1
  25. package/dist/commands/gen.command.js +65 -9
  26. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  27. package/dist/commands/graph-code-subverbs.js +14 -2
  28. package/dist/commands/graph.command.d.ts.map +1 -1
  29. package/dist/commands/graph.command.js +68 -1
  30. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  31. package/dist/commands/policy-lint.command.js +46 -8
  32. package/dist/commands/registry-resolve.d.ts +41 -0
  33. package/dist/commands/registry-resolve.d.ts.map +1 -0
  34. package/dist/commands/registry-resolve.js +89 -0
  35. package/dist/commands/registry.command.d.ts.map +1 -1
  36. package/dist/commands/registry.command.js +86 -13
  37. package/dist/commands/reuse.command.d.ts +20 -0
  38. package/dist/commands/reuse.command.d.ts.map +1 -1
  39. package/dist/commands/reuse.command.js +156 -20
  40. package/dist/commands/smart-context.command.d.ts.map +1 -1
  41. package/dist/commands/smart-context.command.js +20 -5
  42. package/dist/commands/task.command.d.ts.map +1 -1
  43. package/dist/commands/task.command.js +33 -16
  44. package/dist/exit-codes.d.ts +49 -0
  45. package/dist/exit-codes.d.ts.map +1 -0
  46. package/dist/exit-codes.js +61 -0
  47. package/dist/main.d.ts.map +1 -1
  48. package/dist/main.js +26 -1
  49. package/dist/validation/typecheck-emitted.d.ts +36 -0
  50. package/dist/validation/typecheck-emitted.d.ts.map +1 -0
  51. package/dist/validation/typecheck-emitted.js +109 -0
  52. 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.strategy === ECompressionStrategy.Passthrough || result.savings.saved <= 0;
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;AAkHhC,eAAO,MAAM,qBAAqB,EAAE,eAqBnC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,eAoDlC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eAiGpC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eA0ErC,CAAC;AAEF,eAAO,MAAM,wBAAwB,EAAE,eAmCtC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eA4BpC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,eAkClC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eAiBrC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eAkBrC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eA4BrC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eA8BrC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eA0DpC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eA0LpC,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
- process.stderr.write(`No construct "${id}"\n`);
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
- process.stderr.write(`No construct "${id}"\n`);
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
- process.stderr.write(`No construct "${id}"\n`);
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
- process.stderr.write(`No construct "${id}"\n`);
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
- process.stderr.write(`No construct "${id}"\n`);
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
- process.stderr.write(`No construct "${id}"\n`);
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
- process.stderr.write(`No construct "${id}"\n`);
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;AAgChC,eAAO,MAAM,cAAc,EAAE,eA2I5B,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: maxTokens ?? inspection.config?.defaultMaxTokens ?? 3000,
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
- // Auto-promote commands-first for action-like tasks (rename / add /
89
- // fix / refactor / remove / migrate / explore / wire). Pass --full to
90
- // see the long context body anyway.
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
- // Default human text mode keeps the output short. The long
145
- // context body is one flag away via `--full`. JSON / commands-first /
146
- // markdown paths are unchanged.
147
- if (commandsFirst || !wantsFull) {
148
- if (!wantsFull) {
149
- process.stdout.write('\n(text mode is summary-only pass --full for the long context body, --json for machine output.)\n');
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":"AAaA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,EAAE,eAiMzB,CAAC"}
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 baseline-relative once a baseline is frozen — fails only on NEW errors; with no baseline it warns on errors rather than going perpetually red — --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]',
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
- ...(archAll ? { arch: { baselineRelative: false } } : {}),
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
- ...(failOn ? { failOn } : {}),
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;AAkBhC,eAAO,MAAM,UAAU,EAAE,eA+JxB,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 result = generate(template, {
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
- write,
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(write ? `Generation: ${template.id}` : `Dry-run: ${template.id}`));
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 (!write && !savePlanPath) {
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,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,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"}
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
- 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);
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('\nNo cycles in the file-import graph. ✓\n');
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,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA4BhC,eAAO,MAAM,YAAY,EAAE,eAuN1B,CAAC"}
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"}