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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/dist/commands/changelog-data.d.ts +25 -0
  2. package/dist/commands/changelog-data.d.ts.map +1 -0
  3. package/dist/commands/changelog-data.js +70 -0
  4. package/dist/commands/changelog.command.d.ts +3 -0
  5. package/dist/commands/changelog.command.d.ts.map +1 -0
  6. package/dist/commands/changelog.command.js +100 -0
  7. package/dist/commands/changes.command.d.ts.map +1 -1
  8. package/dist/commands/changes.command.js +4 -0
  9. package/dist/commands/check.command.d.ts.map +1 -1
  10. package/dist/commands/check.command.js +182 -17
  11. package/dist/commands/command-catalog.d.ts +11 -0
  12. package/dist/commands/command-catalog.d.ts.map +1 -1
  13. package/dist/commands/command-catalog.js +99 -0
  14. package/dist/commands/compress.command.d.ts.map +1 -1
  15. package/dist/commands/compress.command.js +15 -1
  16. package/dist/commands/constructs.command.d.ts.map +1 -1
  17. package/dist/commands/constructs.command.js +49 -14
  18. package/dist/commands/context.command.d.ts.map +1 -1
  19. package/dist/commands/context.command.js +31 -19
  20. package/dist/commands/finish.command.d.ts +3 -0
  21. package/dist/commands/finish.command.d.ts.map +1 -0
  22. package/dist/commands/finish.command.js +70 -0
  23. package/dist/commands/gate.command.d.ts.map +1 -1
  24. package/dist/commands/gate.command.js +6 -1
  25. package/dist/commands/gen.command.d.ts.map +1 -1
  26. package/dist/commands/gen.command.js +65 -9
  27. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  28. package/dist/commands/graph-code-subverbs.js +56 -25
  29. package/dist/commands/help.command.d.ts.map +1 -1
  30. package/dist/commands/help.command.js +20 -2
  31. package/dist/commands/impact.command.d.ts.map +1 -1
  32. package/dist/commands/impact.command.js +17 -22
  33. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  34. package/dist/commands/policy-lint.command.js +46 -8
  35. package/dist/commands/registry.command.d.ts.map +1 -1
  36. package/dist/commands/registry.command.js +71 -13
  37. package/dist/commands/reuse.command.d.ts.map +1 -1
  38. package/dist/commands/reuse.command.js +80 -14
  39. package/dist/commands/smart-context.command.d.ts.map +1 -1
  40. package/dist/commands/smart-context.command.js +34 -42
  41. package/dist/commands/task.command.d.ts.map +1 -1
  42. package/dist/commands/task.command.js +33 -16
  43. package/dist/commands/trace.command.d.ts.map +1 -1
  44. package/dist/commands/trace.command.js +73 -3
  45. package/dist/commands/wiring.command.d.ts +12 -0
  46. package/dist/commands/wiring.command.d.ts.map +1 -0
  47. package/dist/commands/wiring.command.js +384 -0
  48. package/dist/diff/collect-changed-paths.d.ts +5 -3
  49. package/dist/diff/collect-changed-paths.d.ts.map +1 -1
  50. package/dist/diff/collect-changed-paths.js +73 -36
  51. package/dist/diff/deleted-orphans.d.ts +42 -0
  52. package/dist/diff/deleted-orphans.d.ts.map +1 -0
  53. package/dist/diff/deleted-orphans.js +46 -0
  54. package/dist/finish/run-finish.d.ts +62 -0
  55. package/dist/finish/run-finish.d.ts.map +1 -0
  56. package/dist/finish/run-finish.js +239 -0
  57. package/dist/main.d.ts.map +1 -1
  58. package/dist/main.js +6 -0
  59. package/dist/validation/typecheck-emitted.d.ts +36 -0
  60. package/dist/validation/typecheck-emitted.d.ts.map +1 -0
  61. package/dist/validation/typecheck-emitted.js +109 -0
  62. package/package.json +33 -33
@@ -390,6 +390,14 @@ export const COMMAND_CATALOG = Object.freeze([
390
390
  surface: CommandSurface.Common,
391
391
  taskRole: CommandTaskRole.Validate,
392
392
  }),
393
+ entry({
394
+ command: 'check orphans',
395
+ description: 'Write-safety guard: after deleting file(s)/export(s), finds surviving files that still import them or reference a symbol they declared — alias-resolved, incl. barrel re-exports the type checker misses. Reverse-closure over the diff vs --since (or --staged) against the code-graph snapshot; each survivor reported with file:line. [--since <ref>] [--staged] [--json]',
396
+ category: 'core',
397
+ safetyLevel: SafetyLevel.ReadOnly,
398
+ surface: CommandSurface.Common,
399
+ taskRole: CommandTaskRole.Validate,
400
+ }),
393
401
  entry({
394
402
  command: 'policy-lint',
395
403
  description: 'Lint template/markup, stylesheet, and AOT-invisible TS surfaces against config-defined policyRules[] — sees `.html` files AND inline `template:` strings that tsc/AOT cannot. Deterministic; no AI. [--surface template|style|ts] [--changed-only] [--only <ids>] [--json]',
@@ -398,6 +406,46 @@ export const COMMAND_CATALOG = Object.freeze([
398
406
  surface: CommandSurface.Common,
399
407
  taskRole: CommandTaskRole.Validate,
400
408
  }),
409
+ entry({
410
+ command: 'wiring explain',
411
+ description: 'Dry-run ONE configured wiringRule and print the declared set + registered set it extracts (token + file:line), the alias-resolved set-difference, and the verdict — the author-loop view of what `check wiring` sees, without re-running the gate. [--json]',
412
+ category: 'core',
413
+ safetyLevel: SafetyLevel.ReadOnly,
414
+ surface: CommandSurface.Advanced,
415
+ taskRole: CommandTaskRole.Explain,
416
+ }),
417
+ entry({
418
+ command: 'wiring test',
419
+ description: 'Dry-run an EPHEMERAL candidate wiring rule (a .json file or inline JSON) against the live tree — see the declared/registered sets + diff it would produce before committing it to sharkcraft.config.ts. Never writes config. [--json]',
420
+ category: 'core',
421
+ safetyLevel: SafetyLevel.ReadOnly,
422
+ surface: CommandSurface.Advanced,
423
+ taskRole: CommandTaskRole.Explain,
424
+ }),
425
+ entry({
426
+ command: 'wiring chain',
427
+ description: 'Registration/DI graph query: the declared → provided → consumed chain of a token across files/layers, alias-resolved, with file:line at each hop + a verdict (wired / unprovided / orphan). Models runtime wiring imports can\'t see. Idioms from sharkcraft.config.ts registrationGraph[]. [--json]',
428
+ category: 'analysis',
429
+ safetyLevel: SafetyLevel.ReadOnly,
430
+ surface: CommandSurface.Advanced,
431
+ taskRole: CommandTaskRole.Inspect,
432
+ }),
433
+ entry({
434
+ command: 'wiring unprovided',
435
+ description: 'Registration/DI graph query: tokens DECLARED or INJECTED but never PROVIDED — the silent-at-runtime class (typecheck/AOT-green, resolves to undefined at runtime). The write-safety check imports/grep structurally can\'t do. [--json]',
436
+ category: 'analysis',
437
+ safetyLevel: SafetyLevel.ReadOnly,
438
+ surface: CommandSurface.Advanced,
439
+ taskRole: CommandTaskRole.Validate,
440
+ }),
441
+ entry({
442
+ command: 'wiring orphans',
443
+ description: 'Registration/DI graph query: tokens PROVIDED/registered that nothing CONSUMES — a build-clean dead registration (or a sign the consumer was renamed/removed). [--json]',
444
+ category: 'analysis',
445
+ safetyLevel: SafetyLevel.ReadOnly,
446
+ surface: CommandSurface.Advanced,
447
+ taskRole: CommandTaskRole.Inspect,
448
+ }),
401
449
  entry({
402
450
  command: 'reuse',
403
451
  description: 'Intent → the canonical primitive to reuse: matches your intent against config reusePrimitives[] then resolves the symbol through the code graph (transitive star-barrels) to its import path, sibling exports, and real consumer files to copy. Read-only; no AI.',
@@ -415,6 +463,14 @@ export const COMMAND_CATALOG = Object.freeze([
415
463
  surface: CommandSurface.Common,
416
464
  taskRole: CommandTaskRole.Validate,
417
465
  }),
466
+ entry({
467
+ command: 'finish',
468
+ description: 'Composite "is this changeset safe to finish?" gate: EXECUTES boundaries + import-hygiene + wiring + policy + deleted-orphans changed-only inline, plus an impact summary, and returns ONE pass/fail. The trustworthy "done?" call after editing — superset of diff-check; honors 0-rules→skipped. Read-only. [files… | --staged | --since <ref>] [--json]',
469
+ category: 'core',
470
+ safetyLevel: SafetyLevel.ReadOnly,
471
+ surface: CommandSurface.Common,
472
+ taskRole: CommandTaskRole.Validate,
473
+ }),
418
474
  entry({
419
475
  command: 'review',
420
476
  description: 'PR-review packet — changed files, affected rules, missing tests heuristic.',
@@ -1104,6 +1160,14 @@ export const COMMAND_CATALOG = Object.freeze([
1104
1160
  safetyLevel: SafetyLevel.ReadOnly,
1105
1161
  surface: CommandSurface.Common,
1106
1162
  }),
1163
+ entry({
1164
+ command: 'changelog',
1165
+ description: 'The shrk command-surface delta of the running build (added / changed / removed verbs & flags). `--since <version>` for the cumulative delta; offline, authoritative for this exact build.',
1166
+ category: 'meta',
1167
+ safetyLevel: SafetyLevel.ReadOnly,
1168
+ surface: CommandSurface.Common,
1169
+ intendedAudience: [CommandAudience.Human, CommandAudience.Agent],
1170
+ }),
1107
1171
  entry({
1108
1172
  command: 'completion',
1109
1173
  description: 'Print a sourcable shell-completion script for the `shrk` CLI (bash | zsh | fish). Pipe into your shell rc.',
@@ -1658,6 +1722,14 @@ export const COMMAND_CATALOG = Object.freeze([
1658
1722
  safetyLevel: SafetyLevel.ReadOnly,
1659
1723
  mcpAvailable: true,
1660
1724
  }),
1725
+ entry({
1726
+ command: 'trace literal',
1727
+ description: 'Trace an EXACT string literal across the codebase, classified by direction: declare → register → consume sites of a cross-fence contract (kind slug, permission id, route key) the type system can\'t link and grep can\'t classify. Resolves `const X = "lit"` aliases. Generalizes `registry … where` to any literal — no pre-declared registry. [--glob <g>] [--no-aliases] [--limit N] [--json]',
1728
+ category: 'analysis',
1729
+ safetyLevel: SafetyLevel.ReadOnly,
1730
+ surface: CommandSurface.Advanced,
1731
+ taskRole: CommandTaskRole.Inspect,
1732
+ }),
1661
1733
  entry({
1662
1734
  command: 'feedback',
1663
1735
  description: 'Feedback ingestion (ingest|summarize|actions|convert-to-backlog). Read-only.',
@@ -3664,6 +3736,7 @@ const PRIMARY_VERBS_ALLOWLIST = new Set([
3664
3736
  'gen',
3665
3737
  'apply',
3666
3738
  'check',
3739
+ 'finish',
3667
3740
  'quality',
3668
3741
  'plan',
3669
3742
  'fix',
@@ -3734,6 +3807,32 @@ export function defaultShowInHelp(e) {
3734
3807
  const surface = commandSurface(e);
3735
3808
  return surface === CommandSurface.Primary || surface === CommandSurface.Common;
3736
3809
  }
3810
+ /**
3811
+ * The "explain / dry-run" command family: entries that show you what a gate,
3812
+ * ranker, or graph *sees* before you act (`search tuning explain`, `wiring
3813
+ * explain`/`wiring test`, `boundaries explain`, `surface explain`, …). Several
3814
+ * carry an Advanced surface, so {@link defaultShowInHelp} hides them despite
3815
+ * being high-value — which is exactly why an agent finds them only by guessing.
3816
+ * `shrk --full-help` lists this family in a dedicated section so they stop
3817
+ * being hidden. Returns callable entries only (retired/deprecated/alias and
3818
+ * R46-pruned are excluded), sorted by command.
3819
+ */
3820
+ export function listExplainFamily() {
3821
+ return COMMAND_CATALOG.filter((e) => {
3822
+ const lc = commandLifecycle(e);
3823
+ if (lc === CommandLifecycle.Deprecated ||
3824
+ lc === CommandLifecycle.Retired ||
3825
+ lc === CommandLifecycle.Alias) {
3826
+ return false;
3827
+ }
3828
+ if (R46_OVERLAY[e.command])
3829
+ return false;
3830
+ const role = commandTaskRole(e);
3831
+ return role === CommandTaskRole.Explain || /(^|\s)explain$/.test(e.command);
3832
+ })
3833
+ .slice()
3834
+ .sort((a, b) => a.command.localeCompare(b.command));
3835
+ }
3737
3836
  /**
3738
3837
  * Short "Use this when…" line derived from `surface`, `taskRole`,
3739
3838
  * `preferredCommand`, `replacedBy`, and `machineOnly`. Empty string when
@@ -1 +1 @@
1
- {"version":3,"file":"compress.command.d.ts","sourceRoot":"","sources":["../../src/commands/compress.command.ts"],"names":[],"mappings":"AAQA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAqEhC,eAAO,MAAM,eAAe,EAAE,eAoG7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,eA4B3B,CAAC"}
1
+ {"version":3,"file":"compress.command.d.ts","sourceRoot":"","sources":["../../src/commands/compress.command.ts"],"names":[],"mappings":"AAQA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAqEhC,eAAO,MAAM,eAAe,EAAE,eAqH7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,eA4B3B,CAAC"}
@@ -60,7 +60,7 @@ const COMPRESS_BOOLEAN_FLAGS = new Set([
60
60
  ]);
61
61
  export const compressCommand = {
62
62
  name: 'compress',
63
- description: 'Compress a blob (file or stdin) deterministically to cut tokens — JSON→table, logs/search/diffs→signal. Reversible via `shrk expand`.',
63
+ 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
64
  usage: 'shrk [--cwd <dir>] compress [<file>|-] [--stdin] [--type <content-type>] [--query <text>] [--max <n>] [--lossless] [--no-cache] [--json]',
65
65
  booleanFlags: COMPRESS_BOOLEAN_FLAGS,
66
66
  run(args) {
@@ -118,6 +118,13 @@ export const compressCommand = {
118
118
  tokensSaved: result.savings.saved,
119
119
  savedRatio: result.savings.ratio,
120
120
  tokensAreEstimated: true,
121
+ // Explicit fidelity signal so a caller never mistakes a lossy code
122
+ // outline for a line-accurate read.
123
+ fidelity: result.strategy === ECompressionStrategy.Code
124
+ ? 'lossy-outline (not line-accurate; use Read or --lossless to inspect)'
125
+ : result.lossy
126
+ ? 'lossy'
127
+ : 'lossless',
121
128
  queryApplied,
122
129
  ccrKey: result.ccrKey ?? null,
123
130
  note: result.note,
@@ -136,6 +143,13 @@ export const compressCommand = {
136
143
  process.stdout.write(result.compressed + '\n');
137
144
  const cached = result.ccrKey ? ` · original cached as ${result.ccrKey} (shrk expand ${result.ccrKey})` : '';
138
145
  process.stderr.write(`${result.strategy}: ~${result.savings.before} → ~${result.savings.after} tokens (−${pct}%, est.)${cached}\n`);
146
+ // Fidelity banner: the code outline elides function bodies, so it is a
147
+ // footgun as a cheaper Read. Say so loudly — same failure mode as a lossy
148
+ // result that doesn't *look* lossy.
149
+ if (result.strategy === ECompressionStrategy.Code) {
150
+ process.stderr.write('note: code outline is LOSSY (bodies elided) — for bulk context, not line-accurate reading; ' +
151
+ 'use `Read` or `--lossless` for inspection.\n');
152
+ }
139
153
  // Token-economy guard: when auto-detect declines to compress a non-trivial
140
154
  // input, a silent `−0%` re-emit looks like success. Nudge toward `--type`
141
155
  // (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');
@@ -0,0 +1,3 @@
1
+ import { type ICommandHandler } from '../command-registry.js';
2
+ export declare const finishCommand: ICommandHandler;
3
+ //# sourceMappingURL=finish.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finish.command.d.ts","sourceRoot":"","sources":["../../src/commands/finish.command.ts"],"names":[],"mappings":"AACA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA8DhC,eAAO,MAAM,aAAa,EAAE,eAmB3B,CAAC"}
@@ -0,0 +1,70 @@
1
+ import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
2
+ import { asJson, bullet, header, kv } from "../output/format-output.js";
3
+ import { runFinishGates } from "../finish/run-finish.js";
4
+ function resolveScope(args, cwd) {
5
+ const staged = flagBool(args, 'staged');
6
+ const since = flagString(args, 'since');
7
+ const filesRaw = flagString(args, 'files');
8
+ const files = filesRaw
9
+ ? filesRaw.split(',').map((s) => s.trim()).filter((s) => s.length > 0)
10
+ : args.positional.filter((s) => s.length > 0);
11
+ if (files.length > 0)
12
+ return { mode: 'files', options: { projectRoot: cwd, files } };
13
+ if (staged)
14
+ return { mode: 'staged', options: { projectRoot: cwd, staged: true } };
15
+ if (since)
16
+ return { mode: 'since', options: { projectRoot: cwd, since } };
17
+ return { mode: 'worktree', options: { projectRoot: cwd, includeWorktree: true } };
18
+ }
19
+ const STATUS_GLYPH = {
20
+ pass: '✓',
21
+ fail: '✗',
22
+ skipped: '–',
23
+ };
24
+ function renderText(report) {
25
+ process.stdout.write(header('Finish — is this changeset safe to complete?'));
26
+ process.stdout.write(kv('scope', `${report.scope.mode} (${report.scope.fileCount} file${report.scope.fileCount === 1 ? '' : 's'})`) +
27
+ '\n');
28
+ for (const g of report.gates) {
29
+ process.stdout.write(` ${STATUS_GLYPH[g.status]} ${g.name.padEnd(11)} ${g.status.padEnd(8)} ${g.detail}\n`);
30
+ }
31
+ if (report.impact.ran) {
32
+ process.stdout.write(kv('impact', `risk=${report.impact.risk}, ${report.impact.directDependents} direct / ${report.impact.transitiveDependents} transitive dependents`) +
33
+ '\n');
34
+ }
35
+ else if (report.impact.note) {
36
+ process.stdout.write(kv('impact', `(skipped — ${report.impact.note})`) + '\n');
37
+ }
38
+ process.stdout.write(kv('verdict', report.verdict) + '\n\n');
39
+ process.stdout.write(report.summary + '\n');
40
+ const failing = report.gates.filter((g) => g.status === 'fail');
41
+ for (const g of failing) {
42
+ process.stdout.write(`\n${g.name} — failing items:\n`);
43
+ for (const item of g.items.slice(0, 15)) {
44
+ const loc = item.file ? `${item.file}${item.line ? `:${item.line}` : ''}` : '';
45
+ process.stdout.write(bullet(`${loc ? loc + ' — ' : ''}${item.message}`) + '\n');
46
+ }
47
+ if (g.items.length > 15) {
48
+ process.stdout.write(` … and ${g.items.length - 15} more (pass --json for the full list).\n`);
49
+ }
50
+ }
51
+ process.stdout.write(`\nNext: ${report.nextAction}\n`);
52
+ }
53
+ export const finishCommand = {
54
+ name: 'finish',
55
+ description: 'Composite "is this changeset safe to finish?" gate: EXECUTES every deterministic changed-only check inline — boundaries + import-hygiene + wiring + policy + deleted-orphans — plus an impact summary, and returns ONE pass/fail. The single trustworthy "done?" call after editing (superset of `diff-check`; honors 0-rules→skipped). Read-only.',
56
+ usage: 'shrk [--cwd <dir>] finish [files... | --files a.ts,b.ts | --staged | --since <ref>] [--json]',
57
+ booleanFlags: new Set(['json', 'staged']),
58
+ async run(args) {
59
+ const cwd = resolveCwd(args);
60
+ const wantJson = flagBool(args, 'json');
61
+ const { mode, options } = resolveScope(args, cwd);
62
+ const report = await runFinishGates({ cwd, mode, scope: options });
63
+ if (wantJson) {
64
+ process.stdout.write(asJson(report) + '\n');
65
+ return report.verdict === 'fail' ? 1 : 0;
66
+ }
67
+ renderText(report);
68
+ return report.verdict === 'fail' ? 1 : 0;
69
+ },
70
+ };
@@ -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":"AAaA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,EAAE,eAsMzB,CAAC"}
@@ -143,7 +143,12 @@ export const gateCommand = {
143
143
  : {}),
144
144
  impact: {
145
145
  ...(sinceRef ? { sinceRef } : {}),
146
- ...(failOn ? { failOn } : {}),
146
+ // Blast-radius risk is inherently PRE-EXISTING structure (touching a hub
147
+ // is risky but not a new failure this change introduced), so the composite
148
+ // gate treats it as ADVISORY by default — `failOn: []` warns instead of
149
+ // redding, keeping the verdict change-attributable. `--fail-on critical`
150
+ // opts into a hard fail; `--strict` escalates the advisory warn.
151
+ failOn: failOn ?? [],
147
152
  // Scope the impact gate to the changeset too: with `--since` we keep the
148
153
  // gitref diff; with `--changed-only` / `--staged` / `--files` (and no
149
154
  // `--since`) we analyze the resolved changed-file set directly.
@@ -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
  };