@shrkcrft/cli 0.1.0-alpha.24 → 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 (40) 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 +73 -15
  11. package/dist/commands/command-catalog.d.ts.map +1 -1
  12. package/dist/commands/command-catalog.js +8 -0
  13. package/dist/commands/compress.command.d.ts.map +1 -1
  14. package/dist/commands/compress.command.js +15 -1
  15. package/dist/commands/constructs.command.d.ts.map +1 -1
  16. package/dist/commands/constructs.command.js +49 -14
  17. package/dist/commands/context.command.d.ts.map +1 -1
  18. package/dist/commands/context.command.js +31 -19
  19. package/dist/commands/gate.command.d.ts.map +1 -1
  20. package/dist/commands/gate.command.js +6 -1
  21. package/dist/commands/gen.command.d.ts.map +1 -1
  22. package/dist/commands/gen.command.js +65 -9
  23. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  24. package/dist/commands/graph-code-subverbs.js +14 -2
  25. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  26. package/dist/commands/policy-lint.command.js +46 -8
  27. package/dist/commands/registry.command.d.ts.map +1 -1
  28. package/dist/commands/registry.command.js +71 -13
  29. package/dist/commands/reuse.command.d.ts.map +1 -1
  30. package/dist/commands/reuse.command.js +80 -14
  31. package/dist/commands/smart-context.command.d.ts.map +1 -1
  32. package/dist/commands/smart-context.command.js +20 -5
  33. package/dist/commands/task.command.d.ts.map +1 -1
  34. package/dist/commands/task.command.js +33 -16
  35. package/dist/main.d.ts.map +1 -1
  36. package/dist/main.js +2 -0
  37. package/dist/validation/typecheck-emitted.d.ts +36 -0
  38. package/dist/validation/typecheck-emitted.d.ts.map +1 -0
  39. package/dist/validation/typecheck-emitted.js +109 -0
  40. package/package.json +33 -33
@@ -20,20 +20,44 @@ function tokenize(s) {
20
20
  function scorePrimitive(p, tokens) {
21
21
  // Substring match (so intent "debounce" matches symbol `useDebounce`); the
22
22
  // 3-char minimum above keeps it from over-matching on tiny fragments.
23
+ const symbolLower = p.symbol.toLowerCase();
23
24
  const hay = [p.symbol, ...(p.roles ?? []), ...(p.keywords ?? []), p.description ?? '']
24
25
  .join(' ')
25
26
  .toLowerCase();
26
27
  let s = 0;
27
- for (const t of tokens)
28
- if (hay.includes(t))
28
+ const matched = [];
29
+ let symbolHit = false;
30
+ for (const t of tokens) {
31
+ if (hay.includes(t)) {
29
32
  s += 1;
33
+ matched.push(t);
34
+ if (symbolLower.includes(t))
35
+ symbolHit = true;
36
+ }
37
+ }
30
38
  // A role the intent mentions is a strong signal; re-weight role hits.
31
39
  for (const role of p.roles ?? []) {
32
40
  const rl = role.toLowerCase();
33
41
  if (rl.length >= 3 && tokens.some((t) => rl.includes(t)))
34
42
  s += 0.5;
35
43
  }
36
- return s;
44
+ return { score: s, matched, symbolHit };
45
+ }
46
+ /**
47
+ * Confidence floor: a keyword collision is not a match. A hit is only "confident"
48
+ * when it matched the symbol name itself, OR matched ≥2 distinct query tokens, OR
49
+ * the query was a single token and that token hit. A single generic keyword hit on
50
+ * a multi-token intent (the "nearest collision on an unrelated entry" failure mode)
51
+ * is a weak match — surfaced as a did-you-mean, never as a confident answer.
52
+ */
53
+ function isConfidentMatch(detail, queryTokenCount) {
54
+ if (detail.matched.length === 0)
55
+ return false;
56
+ if (detail.symbolHit)
57
+ return true;
58
+ if (detail.matched.length >= 2)
59
+ return true;
60
+ return queryTokenCount <= 1;
37
61
  }
38
62
  const INDEX_RE = /(^|\/)index\.[cm]?[jt]sx?$/;
39
63
  export const reuseCommand = {
@@ -82,14 +106,17 @@ export const reuseCommand = {
82
106
  return 0;
83
107
  }
84
108
  const tokens = tokenize(intent);
85
- const ranked = primitives
86
- .map((p) => ({ p, score: scorePrimitive(p, tokens) }))
87
- .filter((x) => x.score > 0)
88
- .sort((a, b) => b.score - a.score || a.p.symbol.localeCompare(b.p.symbol))
89
- .slice(0, Math.max(1, limit));
109
+ const confidenceOf = (matched) => tokens.length === 0 ? 0 : matched / tokens.length;
110
+ const scored = primitives
111
+ .map((p) => ({ p, detail: scorePrimitive(p, tokens) }))
112
+ .filter((x) => x.detail.score > 0)
113
+ .sort((a, b) => b.detail.score - a.detail.score || a.p.symbol.localeCompare(b.p.symbol));
114
+ const confident = scored.filter((x) => isConfidentMatch(x.detail, tokens.length));
115
+ const ranked = confident.slice(0, Math.max(1, limit));
90
116
  const store = new GraphStore(cwd);
91
117
  const api = store.exists() ? GraphQueryApi.fromStore(cwd) : null;
92
- if (ranked.length === 0) {
118
+ // Zero keyword overlap: nothing matched at all → surface available roles.
119
+ if (scored.length === 0) {
93
120
  const roles = [...new Set(primitives.flatMap((p) => p.roles))].sort();
94
121
  if (wantJson) {
95
122
  process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, results: [], availableRoles: roles, ...planeJson }) + '\n');
@@ -102,8 +129,41 @@ export const reuseCommand = {
102
129
  writePlaneNotes();
103
130
  return 0;
104
131
  }
105
- const results = ranked.map(({ p, score }) => {
106
- const r = { symbol: p.symbol, score, roles: p.roles, siblings: [], consumers: [] };
132
+ // Weak overlap only (a single generic keyword collision on an unrelated
133
+ // entry): below the confidence floor. A miss must look like a miss — never
134
+ // return the nearest collision as a confident answer. Offer did-you-mean.
135
+ if (ranked.length === 0) {
136
+ const didYouMean = scored.slice(0, 5).map((x) => ({
137
+ symbol: x.p.symbol,
138
+ score: x.detail.score,
139
+ confidence: confidenceOf(x.detail.matched.length),
140
+ matched: x.detail.matched,
141
+ roles: x.p.roles,
142
+ }));
143
+ if (wantJson) {
144
+ process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, confident: false, results: [], didYouMean, ...planeJson }) + '\n');
145
+ return 0;
146
+ }
147
+ process.stdout.write(header(`Reuse: "${intent}"`));
148
+ process.stdout.write(' No confident match — the intent only weakly overlaps existing primitives.\n' +
149
+ ' Did you mean (weak, verify before reusing):\n');
150
+ for (const s of didYouMean) {
151
+ process.stdout.write(` • ${s.symbol} (score ${s.score}, ${Math.round(s.confidence * 100)}% of intent; matched: ${s.matched.join(', ') || '—'})\n`);
152
+ }
153
+ writePlaneNotes();
154
+ return 0;
155
+ }
156
+ const results = ranked.map(({ p, detail }) => {
157
+ const score = detail.score;
158
+ const r = {
159
+ symbol: p.symbol,
160
+ score,
161
+ confidence: confidenceOf(detail.matched.length),
162
+ matched: detail.matched,
163
+ roles: p.roles,
164
+ siblings: [],
165
+ consumers: [],
166
+ };
107
167
  if (p.description)
108
168
  r.description = p.description;
109
169
  if (p.importPath)
@@ -141,8 +201,9 @@ export const reuseCommand = {
141
201
  }
142
202
  }
143
203
  }
144
- r.consumers = api
145
- .referenceSitesOf(sym.id)
204
+ const sites = api.referenceSitesOf(sym.id);
205
+ r.consumerTotal = sites.length;
206
+ r.consumers = sites
146
207
  .slice(0, 5)
147
208
  .map((s) => ({ path: s.node.path ?? s.node.id, ...(s.line ? { line: s.line } : {}) }));
148
209
  const alts = pool.slice(1).map((c) => c.path).filter((x) => !!x);
@@ -174,6 +235,7 @@ export const reuseCommand = {
174
235
  process.stdout.write(`\n${i}. ${r.symbol}\n`);
175
236
  if (r.description)
176
237
  process.stdout.write(` ${r.description}\n`);
238
+ process.stdout.write(` match: score ${r.score} (${Math.round(r.confidence * 100)}% of intent; matched: ${r.matched.join(', ') || '—'})\n`);
177
239
  if (r.notFound) {
178
240
  process.stdout.write(' ⚠ symbol not found in the code graph — verify reusePrimitives[].symbol (typo/rename?) or run `shrk graph index`\n');
179
241
  }
@@ -192,7 +254,11 @@ export const reuseCommand = {
192
254
  if (r.siblings.length > 0)
193
255
  process.stdout.write(` sibling exports: ${r.siblings.join(', ')}\n`);
194
256
  if (r.consumers.length > 0) {
195
- process.stdout.write(' consumers to copy:\n');
257
+ const total = r.consumerTotal ?? r.consumers.length;
258
+ const label = total > r.consumers.length
259
+ ? ` consumers to copy (${total} total, showing ${r.consumers.length}):\n`
260
+ : ` consumers to copy (${total} total):\n`;
261
+ process.stdout.write(label);
196
262
  for (const c of r.consumers)
197
263
  process.stdout.write(` - ${c.path}${c.line ? ':' + c.line : ''}\n`);
198
264
  }
@@ -1 +1 @@
1
- {"version":3,"file":"smart-context.command.d.ts","sourceRoot":"","sources":["../../src/commands/smart-context.command.ts"],"names":[],"mappings":"AA2BA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAmFhC,eAAO,MAAM,mBAAmB,EAAE,eAmQjC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,EAAE,eAsF1C,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,EAAE,eAwBrC,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,EAAE,eAkCrC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAmH/C,CAAC;AA2JF;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAoH/C,CAAC;AA2JF;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAgG/C,CAAC;AAyPF,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,EAAE,eAuHhD,CAAC;AAMF,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,EAAE,eAsCjD,CAAC;AAqJF,UAAU,eAAe;IACvB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,SAAS,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,eAAe,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAWzF"}
1
+ {"version":3,"file":"smart-context.command.d.ts","sourceRoot":"","sources":["../../src/commands/smart-context.command.ts"],"names":[],"mappings":"AA2BA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAmFhC,eAAO,MAAM,mBAAmB,EAAE,eAiQjC,CAAC;AAEF,+EAA+E;AAC/E,eAAO,MAAM,4BAA4B,EAAE,eAsF1C,CAAC;AAEF,sDAAsD;AACtD,eAAO,MAAM,uBAAuB,EAAE,eAwBrC,CAAC;AAEF,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,EAAE,eAkCrC,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAmH/C,CAAC;AA2JF;;;;;;;GAOG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAoH/C,CAAC;AA2JF;;;;;;GAMG;AACH,eAAO,MAAM,iCAAiC,EAAE,eAgG/C,CAAC;AAyPF,4EAA4E;AAC5E,eAAO,MAAM,kCAAkC,EAAE,eAuHhD,CAAC;AAMF,iFAAiF;AACjF,eAAO,MAAM,mCAAmC,EAAE,eAsCjD,CAAC;AAqJF,UAAU,eAAe;IACvB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,SAAS,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,eAAe,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAWzF"}
@@ -52,11 +52,13 @@ export const smartContextCommand = {
52
52
  name: 'smart-context',
53
53
  booleanFlags: SMART_CONTEXT_BOOLEAN_FLAGS,
54
54
  description: 'Build deterministic context and ask an AI provider to synthesise an enriched brief (default), structured plan (--plan), or two-stage development plan (--ai-plan).',
55
- usage: 'shrk smart-context "<task>" [--plus] [--budget <seconds>] [--plan] [--ai-plan] [--save] [--provider auto|ollama|llamacpp] [--enhance|--no-enhance] [--enhance-passes N] [--instructions <path>] [--no-instructions] [--model <id>] [--max-tokens N] [--stage1-max-tokens N] [--seed-tokens N] [--expansion-tokens N] [--expansion-limit N] [--log-prompt] [--save-conversation[=<path>]] [--dry-run] [--debug] [--json]',
55
+ usage: 'shrk smart-context "<task>" | --task "<task>" [--plus] [--budget <seconds>] [--plan] [--ai-plan] [--save] [--provider auto|ollama|llamacpp] [--enhance|--no-enhance] [--enhance-passes N] [--instructions <path>] [--no-instructions] [--model <id>] [--max-tokens N] [--stage1-max-tokens N] [--seed-tokens N] [--expansion-tokens N] [--expansion-limit N] [--log-prompt] [--save-conversation[=<path>]] [--dry-run] [--debug] [--json]',
56
56
  async run(args) {
57
- const task = args.positional.join(' ').trim();
57
+ // Accept the documented `--task` form as well as the positional form, so the
58
+ // CLI surface matches the docs (which advertise `--task` broadly).
59
+ const task = (args.positional.join(' ').trim() || flagString(args, 'task') || '').trim();
58
60
  if (!task) {
59
- process.stderr.write('Usage: shrk smart-context "<task>" [--plan] [--ai-plan] [--save]\n');
61
+ process.stderr.write('Usage: shrk smart-context "<task>" [--task "<task>"] [--plan] [--ai-plan] [--save]\n');
60
62
  return 2;
61
63
  }
62
64
  // Isolate the LLM / native-runtime work in a child process. On macOS the
@@ -165,7 +167,7 @@ export const smartContextCommand = {
165
167
  (originalContext.length > 0 && enhanced.value.content.trim() === originalContext);
166
168
  if (fullyDegraded) {
167
169
  if (!opts.json) {
168
- process.stderr.write('[smart-context] enhancement fully degraded returning deterministic context only.\n');
170
+ process.stderr.write(degradedRetrievalBanner(seed, 'enhancement fully degraded') + '\n');
169
171
  }
170
172
  const fallbackEnvelope = buildEnvelope({
171
173
  task,
@@ -226,7 +228,7 @@ export const smartContextCommand = {
226
228
  // exit 1. The agent that asked for fast grounding still gets usable
227
229
  // rules / paths / templates / candidate files. The error is advisory on
228
230
  // stderr (never stdout, so --json stays valid).
229
- process.stderr.write('[smart-context] provider unavailable returning deterministic context only.\n');
231
+ process.stderr.write(degradedRetrievalBanner(seed, 'provider unavailable') + '\n');
230
232
  const fallbackEnvelope = buildEnvelope({
231
233
  task,
232
234
  seed,
@@ -1452,6 +1454,19 @@ export function renderIndexFreshnessWarning(f) {
1452
1454
  `(${f.stale} changed, ${f.missing} deleted, ${f.untracked} new).${pruned} ` +
1453
1455
  'Verify the file suggestions above before editing — run `shrk smart-context --refresh` to rebuild.');
1454
1456
  }
1457
+ /**
1458
+ * Loud one-liner for the degraded / provider-unavailable path. The returned
1459
+ * content is the deterministic seed (rules/paths/templates/candidate files), NOT
1460
+ * task-scoped semantic retrieval — so say so, and how far the index is behind,
1461
+ * so a caller never trusts a stale generic dump as if it were curated retrieval.
1462
+ */
1463
+ function degradedRetrievalBanner(seed, cause) {
1464
+ const behind = seed.indexFreshness?.behind ?? 0;
1465
+ const behindClause = behind > 0 ? `, semantic index ${behind} file(s) behind the working tree` : '';
1466
+ return (`[smart-context] semantic retrieval unavailable (${cause})${behindClause} — ` +
1467
+ 'falling back to the deterministic seed (rules/paths/templates/candidate files), NOT task-scoped retrieval. ' +
1468
+ 'Run `shrk smart-context --refresh` to rebuild the index.');
1469
+ }
1455
1470
  async function buildSmartContextSeed(input) {
1456
1471
  const { cwd, task, inspection, options } = input;
1457
1472
  const overview = buildProjectOverview(inspection.workspace, inspection.config?.projectName);
@@ -1 +1 @@
1
- {"version":3,"file":"task.command.d.ts","sourceRoot":"","sources":["../../src/commands/task.command.ts"],"names":[],"mappings":"AAiBA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA2DhC,eAAO,MAAM,WAAW,EAAE,eAoQzB,CAAC"}
1
+ {"version":3,"file":"task.command.d.ts","sourceRoot":"","sources":["../../src/commands/task.command.ts"],"names":[],"mappings":"AAiBA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAkEhC,eAAO,MAAM,WAAW,EAAE,eAgRzB,CAAC"}
@@ -3,6 +3,12 @@ import { SpecStatus } from '@shrkcrft/generator';
3
3
  import { flagBool, flagNumber, flagList, resolveCwd, } from "../command-registry.js";
4
4
  import { asJson, header, kv } from "../output/format-output.js";
5
5
  import { buildTaskNextReport } from "../task-next/task-next-ranker.js";
6
+ /**
7
+ * Budget for the DEFAULT full human-text packet when no explicit `--max-tokens`
8
+ * is set — large enough to hold every planned context section so orientation
9
+ * isn't silently thinner than the sibling verbs. JSON/terse callers keep 3500.
10
+ */
11
+ const WIDE_TASK_BUDGET = 100_000;
6
12
  function compactTaskPacket(p) {
7
13
  return {
8
14
  task: p.task,
@@ -139,14 +145,26 @@ export const taskCommand = {
139
145
  return 2;
140
146
  }
141
147
  const inspection = await inspectSharkcraft({ cwd: resolveCwd(args) });
142
- const maxTokens = flagNumber(args, 'max-tokens') ?? 3500;
143
148
  const scope = flagList(args, 'scope');
144
149
  const explainRanking = flagBool(args, 'explain-ranking') || flagBool(args, 'json');
145
- // `--full` (or `--verbose`, which already affects text rendering) opts
146
- // out of the compact packet pass it when an agent genuinely needs
147
- // the full ranking + uncapped action-hint aggregates. Default is the
148
- // tight packet (5 rules / 3 templates / 5 hints per field).
149
- const compact = !flagBool(args, 'full') && !flagBool(args, 'verbose');
150
+ // Orientation renders the full packet by default now (parity with why /
151
+ // reuse / knowledge get / `shrk context`) so the agent's first read isn't
152
+ // thinner than every adjacent command. `--summary`/`--brief` (or
153
+ // `--commands-first`/`--actions-only`) opts back into the terse view.
154
+ const wantsJson = flagBool(args, 'json');
155
+ const terse = flagBool(args, 'summary') ||
156
+ flagBool(args, 'brief') ||
157
+ flagBool(args, 'commands-first') ||
158
+ flagBool(args, 'actions-only');
159
+ // Compact packet (5 rules / 3 templates / 5 hints per field) still backs the
160
+ // default JSON shape and the terse text view; the full text view is uncapped.
161
+ const compact = wantsJson
162
+ ? !flagBool(args, 'full') && !flagBool(args, 'verbose')
163
+ : terse;
164
+ // Auto-widen the context budget for the default full human view so no rich
165
+ // section is dropped purely to fit a tight budget; JSON/terse keep 3500.
166
+ const wideView = !wantsJson && !terse;
167
+ const maxTokens = flagNumber(args, 'max-tokens') ?? (wideView ? WIDE_TASK_BUDGET : 3500);
150
168
  const packet = buildTaskPacket(inspection, task, {
151
169
  maxTokens,
152
170
  ...(scope.length ? { scope } : {}),
@@ -185,14 +203,13 @@ export const taskCommand = {
185
203
  process.stdout.write(kv('detected profiles', packet.detectedProfiles.join(', ') || '(none)') + '\n');
186
204
  process.stdout.write(kv('context tokens', `${packet.context.totalTokens} / ${packet.context.maxTokens}`) + '\n');
187
205
  process.stdout.write(kv('total token est.', String(packet.tokenEstimate)) + '\n');
188
- // Commands-first summary at the top so the agent sees the action
189
- // path before the long context body. `--commands-first` collapses the
190
- // output to just commands + uncertainty.
191
- // Text mode defaults to commands-first; pass `--verbose` or
192
- // `--full` to print the full packet. JSON output is unchanged.
206
+ // Commands-first summary at the top so the agent sees the action path
207
+ // before the long context body. The full packet then prints by default;
208
+ // `--commands-first`/`--actions-only`/`--summary` collapse to just the
209
+ // commands + uncertainty footer.
193
210
  const commandsFirst = flagBool(args, 'commands-first');
194
211
  const actionsOnly = flagBool(args, 'actions-only');
195
- const verbose = flagBool(args, 'verbose') || flagBool(args, 'full');
212
+ const summaryOnly = flagBool(args, 'summary') || flagBool(args, 'brief');
196
213
  if (packet.recommendedCliCommands.length > 0) {
197
214
  process.stdout.write('\nTop commands (command-first):\n');
198
215
  for (const c of packet.recommendedCliCommands.slice(0, 5)) {
@@ -203,11 +220,11 @@ export const taskCommand = {
203
220
  process.stdout.write('\nSuggested generation:\n');
204
221
  process.stdout.write(` $ ${packet.suggestedGen.dryRunCommand}\n`);
205
222
  }
206
- if (commandsFirst || actionsOnly || !verbose) {
207
- if (!verbose && !commandsFirst && !actionsOnly) {
208
- process.stdout.write('\n(text mode is summary-only pass --verbose for the full packet, --json for machine output.)\n');
223
+ if (commandsFirst || actionsOnly || summaryOnly) {
224
+ if (summaryOnly && !commandsFirst && !actionsOnly) {
225
+ process.stdout.write('\n(summary mode — omit --summary for the full packet, --json for machine output.)\n');
209
226
  }
210
- // Render uncertainty and stop — caller asked for action-only output.
227
+ // Render uncertainty and stop — caller asked for terse output.
211
228
  process.stdout.write('\n' + renderUncertaintyText(uncertainty) + '\n');
212
229
  return 0;
213
230
  }
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAKhB,MAAM,uBAAuB,CAAC;AA4X/B,wBAAgB,aAAa,IAAI,eAAe,CAgY/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAqID;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAoDxE"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAKhB,MAAM,uBAAuB,CAAC;AA6X/B,wBAAgB,aAAa,IAAI,eAAe,CAiY/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAqID;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAoDxE"}
package/dist/main.js CHANGED
@@ -73,6 +73,7 @@ import { movePlanCommand } from "./commands/move-plan.command.js";
73
73
  import { watchCommand, watchListCommand, watchPruneCommand, watchStopCommand } from "./commands/watch.command.js";
74
74
  import { mcpCommand } from "./commands/mcp.command.js";
75
75
  import { versionCommand } from "./commands/version.command.js";
76
+ import { changelogCommand } from "./commands/changelog.command.js";
76
77
  import { makeHelpCommand } from "./commands/help.command.js";
77
78
  import { qualityCommand } from "./commands/quality.command.js";
78
79
  import { ciCommand } from "./commands/ci.command.js";
@@ -249,6 +250,7 @@ export function buildRegistry() {
249
250
  registry.registerSubcommand('watch', watchPruneCommand);
250
251
  registry.register(mcpCommand);
251
252
  registry.register(versionCommand);
253
+ registry.register(changelogCommand);
252
254
  registry.register(qualityCommand);
253
255
  registry.register(ciCommand);
254
256
  registry.register(eslintCommand);
@@ -0,0 +1,36 @@
1
+ /** A file the generator would emit, with its rendered (in-memory) contents. */
2
+ export interface IEmittedFile {
3
+ /** Absolute path the file would land at. */
4
+ absPath: string;
5
+ /** Rendered body (never written to disk by the typecheck). */
6
+ contents: string;
7
+ }
8
+ /** A single typecheck error located in an emitted file. */
9
+ export interface IEmittedTypecheckError {
10
+ file: string;
11
+ line: number;
12
+ column: number;
13
+ message: string;
14
+ }
15
+ export interface IEmittedTypecheckResult {
16
+ /** False when there were no TS/TSX files to check (e.g. a docs-only template). */
17
+ ran: boolean;
18
+ errors: readonly IEmittedTypecheckError[];
19
+ /** Human note (why it didn't run, or which tsconfig it used). */
20
+ note?: string;
21
+ }
22
+ /**
23
+ * Typecheck a set of EMITTED (not-yet-written) files against the project's
24
+ * detected tsconfig, without touching disk. Builds a `ts.Program` whose root
25
+ * names are the emitted files, over a compiler host that overlays the rendered
26
+ * bodies for those paths and reads everything else (imports, lib) from disk — so
27
+ * a scaffold that references a real project symbol resolves, and a template bug
28
+ * (bad syntax, a dangling import, a type mismatch) surfaces BEFORE apply instead
29
+ * of at the human's next build.
30
+ *
31
+ * Only diagnostics located IN the emitted files are reported; pre-existing
32
+ * errors elsewhere in the project are ignored (this is a generation gate, not a
33
+ * whole-repo typecheck).
34
+ */
35
+ export declare function typecheckEmittedFiles(projectRoot: string, files: readonly IEmittedFile[]): IEmittedTypecheckResult;
36
+ //# sourceMappingURL=typecheck-emitted.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"typecheck-emitted.d.ts","sourceRoot":"","sources":["../../src/validation/typecheck-emitted.ts"],"names":[],"mappings":"AAIA,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,4CAA4C;IAC5C,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,2DAA2D;AAC3D,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,uBAAuB;IACtC,kFAAkF;IAClF,GAAG,EAAE,OAAO,CAAC;IACb,MAAM,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAC1C,iEAAiE;IACjE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAYD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,SAAS,YAAY,EAAE,GAC7B,uBAAuB,CAmFzB"}
@@ -0,0 +1,109 @@
1
+ import * as ts from 'typescript';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { dirname, resolve } from 'node:path';
4
+ const TSCONFIG_NAMES = ['tsconfig.json', 'tsconfig.base.json'];
5
+ function findTsconfig(projectRoot) {
6
+ for (const name of TSCONFIG_NAMES) {
7
+ const p = resolve(projectRoot, name);
8
+ if (existsSync(p))
9
+ return p;
10
+ }
11
+ return null;
12
+ }
13
+ /**
14
+ * Typecheck a set of EMITTED (not-yet-written) files against the project's
15
+ * detected tsconfig, without touching disk. Builds a `ts.Program` whose root
16
+ * names are the emitted files, over a compiler host that overlays the rendered
17
+ * bodies for those paths and reads everything else (imports, lib) from disk — so
18
+ * a scaffold that references a real project symbol resolves, and a template bug
19
+ * (bad syntax, a dangling import, a type mismatch) surfaces BEFORE apply instead
20
+ * of at the human's next build.
21
+ *
22
+ * Only diagnostics located IN the emitted files are reported; pre-existing
23
+ * errors elsewhere in the project are ignored (this is a generation gate, not a
24
+ * whole-repo typecheck).
25
+ */
26
+ export function typecheckEmittedFiles(projectRoot, files) {
27
+ const tsFiles = files.filter((f) => /\.tsx?$/.test(f.absPath));
28
+ if (tsFiles.length === 0) {
29
+ return { ran: false, errors: [], note: 'no TS/TSX files in the emit set' };
30
+ }
31
+ let options = {
32
+ target: ts.ScriptTarget.ES2022,
33
+ module: ts.ModuleKind.ESNext,
34
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
35
+ strict: true,
36
+ esModuleInterop: true,
37
+ };
38
+ const tsconfigPath = findTsconfig(projectRoot);
39
+ if (tsconfigPath) {
40
+ const read = ts.readConfigFile(tsconfigPath, (p) => {
41
+ try {
42
+ return readFileSync(p, 'utf8');
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ });
48
+ if (!read.error) {
49
+ const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, dirname(tsconfigPath));
50
+ options = parsed.options;
51
+ }
52
+ }
53
+ // Force a non-emitting, lib-skipping check regardless of the project's config.
54
+ options.noEmit = true;
55
+ options.skipLibCheck = true;
56
+ options.incremental = false;
57
+ delete options.composite;
58
+ delete options.outDir;
59
+ delete options.declaration;
60
+ const overlay = new Map(tsFiles.map((f) => [resolve(f.absPath), f.contents]));
61
+ const host = ts.createCompilerHost(options, true);
62
+ const origGetSourceFile = host.getSourceFile.bind(host);
63
+ const origReadFile = host.readFile.bind(host);
64
+ const origFileExists = host.fileExists.bind(host);
65
+ host.readFile = (fileName) => {
66
+ const k = resolve(fileName);
67
+ return overlay.has(k) ? overlay.get(k) : origReadFile(fileName);
68
+ };
69
+ host.fileExists = (fileName) => overlay.has(resolve(fileName)) || origFileExists(fileName);
70
+ host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreate) => {
71
+ const k = resolve(fileName);
72
+ const body = overlay.get(k);
73
+ if (body !== undefined) {
74
+ return ts.createSourceFile(fileName, body, languageVersionOrOptions, true);
75
+ }
76
+ return origGetSourceFile(fileName, languageVersionOrOptions, onError, shouldCreate);
77
+ };
78
+ const rootNames = tsFiles.map((f) => resolve(f.absPath));
79
+ const program = ts.createProgram({ rootNames, options, host });
80
+ const errors = [];
81
+ for (const rn of rootNames) {
82
+ const sf = program.getSourceFile(rn);
83
+ if (!sf)
84
+ continue;
85
+ const diags = [...program.getSyntacticDiagnostics(sf), ...program.getSemanticDiagnostics(sf)];
86
+ for (const d of diags) {
87
+ if (d.category !== ts.DiagnosticCategory.Error)
88
+ continue;
89
+ let line = 0;
90
+ let column = 0;
91
+ if (d.file && typeof d.start === 'number') {
92
+ const lc = d.file.getLineAndCharacterOfPosition(d.start);
93
+ line = lc.line + 1;
94
+ column = lc.character + 1;
95
+ }
96
+ errors.push({
97
+ file: resolve(d.file?.fileName ?? rn),
98
+ line,
99
+ column,
100
+ message: ts.flattenDiagnosticMessageText(d.messageText, '\n'),
101
+ });
102
+ }
103
+ }
104
+ return {
105
+ ran: true,
106
+ errors,
107
+ ...(tsconfigPath ? { note: `checked against ${tsconfigPath}` } : { note: 'no tsconfig found — used defaults' }),
108
+ };
109
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/cli",
3
- "version": "0.1.0-alpha.24",
3
+ "version": "0.1.0-alpha.25",
4
4
  "description": "SharkCraft CLI (`shrk`): structured project intelligence for AI coding agents.",
5
5
  "license": "MIT",
6
6
  "author": "SharkCraft contributors",
@@ -47,38 +47,38 @@
47
47
  "typecheck": "tsc --noEmit -p tsconfig.json"
48
48
  },
49
49
  "dependencies": {
50
- "@shrkcrft/core": "^0.1.0-alpha.24",
51
- "@shrkcrft/compress": "^0.1.0-alpha.24",
52
- "@shrkcrft/config": "^0.1.0-alpha.24",
53
- "@shrkcrft/workspace": "^0.1.0-alpha.24",
54
- "@shrkcrft/knowledge": "^0.1.0-alpha.24",
55
- "@shrkcrft/context": "^0.1.0-alpha.24",
56
- "@shrkcrft/rules": "^0.1.0-alpha.24",
57
- "@shrkcrft/paths": "^0.1.0-alpha.24",
58
- "@shrkcrft/templates": "^0.1.0-alpha.24",
59
- "@shrkcrft/plugin-api": "^0.1.0-alpha.24",
60
- "@shrkcrft/dashboard": "^0.1.0-alpha.24",
61
- "@shrkcrft/dashboard-api": "^0.1.0-alpha.24",
62
- "@shrkcrft/pipelines": "^0.1.0-alpha.24",
63
- "@shrkcrft/presets": "^0.1.0-alpha.24",
64
- "@shrkcrft/boundaries": "^0.1.0-alpha.24",
65
- "@shrkcrft/graph": "^0.1.0-alpha.24",
66
- "@shrkcrft/rule-graph": "^0.1.0-alpha.24",
67
- "@shrkcrft/structural-search": "^0.1.0-alpha.24",
68
- "@shrkcrft/impact-engine": "^0.1.0-alpha.24",
69
- "@shrkcrft/context-planner": "^0.1.0-alpha.24",
70
- "@shrkcrft/architecture-guard": "^0.1.0-alpha.24",
71
- "@shrkcrft/framework-scanners": "^0.1.0-alpha.24",
72
- "@shrkcrft/api-surface-diff": "^0.1.0-alpha.24",
73
- "@shrkcrft/quality-gates": "^0.1.0-alpha.24",
74
- "@shrkcrft/migrate": "^0.1.0-alpha.24",
75
- "@shrkcrft/generator": "^0.1.0-alpha.24",
76
- "@shrkcrft/importer": "^0.1.0-alpha.24",
77
- "@shrkcrft/inspector": "^0.1.0-alpha.24",
78
- "@shrkcrft/ai": "^0.1.0-alpha.24",
79
- "@shrkcrft/embeddings": "^0.1.0-alpha.24",
80
- "@shrkcrft/shared": "^0.1.0-alpha.24",
81
- "@shrkcrft/mcp-server": "^0.1.0-alpha.24",
50
+ "@shrkcrft/core": "^0.1.0-alpha.25",
51
+ "@shrkcrft/compress": "^0.1.0-alpha.25",
52
+ "@shrkcrft/config": "^0.1.0-alpha.25",
53
+ "@shrkcrft/workspace": "^0.1.0-alpha.25",
54
+ "@shrkcrft/knowledge": "^0.1.0-alpha.25",
55
+ "@shrkcrft/context": "^0.1.0-alpha.25",
56
+ "@shrkcrft/rules": "^0.1.0-alpha.25",
57
+ "@shrkcrft/paths": "^0.1.0-alpha.25",
58
+ "@shrkcrft/templates": "^0.1.0-alpha.25",
59
+ "@shrkcrft/plugin-api": "^0.1.0-alpha.25",
60
+ "@shrkcrft/dashboard": "^0.1.0-alpha.25",
61
+ "@shrkcrft/dashboard-api": "^0.1.0-alpha.25",
62
+ "@shrkcrft/pipelines": "^0.1.0-alpha.25",
63
+ "@shrkcrft/presets": "^0.1.0-alpha.25",
64
+ "@shrkcrft/boundaries": "^0.1.0-alpha.25",
65
+ "@shrkcrft/graph": "^0.1.0-alpha.25",
66
+ "@shrkcrft/rule-graph": "^0.1.0-alpha.25",
67
+ "@shrkcrft/structural-search": "^0.1.0-alpha.25",
68
+ "@shrkcrft/impact-engine": "^0.1.0-alpha.25",
69
+ "@shrkcrft/context-planner": "^0.1.0-alpha.25",
70
+ "@shrkcrft/architecture-guard": "^0.1.0-alpha.25",
71
+ "@shrkcrft/framework-scanners": "^0.1.0-alpha.25",
72
+ "@shrkcrft/api-surface-diff": "^0.1.0-alpha.25",
73
+ "@shrkcrft/quality-gates": "^0.1.0-alpha.25",
74
+ "@shrkcrft/migrate": "^0.1.0-alpha.25",
75
+ "@shrkcrft/generator": "^0.1.0-alpha.25",
76
+ "@shrkcrft/importer": "^0.1.0-alpha.25",
77
+ "@shrkcrft/inspector": "^0.1.0-alpha.25",
78
+ "@shrkcrft/ai": "^0.1.0-alpha.25",
79
+ "@shrkcrft/embeddings": "^0.1.0-alpha.25",
80
+ "@shrkcrft/shared": "^0.1.0-alpha.25",
81
+ "@shrkcrft/mcp-server": "^0.1.0-alpha.25",
82
82
  "@huggingface/transformers": "^3.7.5"
83
83
  },
84
84
  "publishConfig": {