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

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 (94) hide show
  1. package/dist/command-registry.d.ts +9 -0
  2. package/dist/command-registry.d.ts.map +1 -1
  3. package/dist/command-registry.js +15 -0
  4. package/dist/commands/ai-status.command.d.ts.map +1 -1
  5. package/dist/commands/ai-status.command.js +10 -0
  6. package/dist/commands/apply.command.d.ts.map +1 -1
  7. package/dist/commands/apply.command.js +9 -2
  8. package/dist/commands/ask.command.d.ts.map +1 -1
  9. package/dist/commands/ask.command.js +2 -1
  10. package/dist/commands/changes.command.d.ts.map +1 -1
  11. package/dist/commands/changes.command.js +14 -4
  12. package/dist/commands/check.command.d.ts.map +1 -1
  13. package/dist/commands/check.command.js +119 -3
  14. package/dist/commands/command-catalog.d.ts.map +1 -1
  15. package/dist/commands/command-catalog.js +224 -0
  16. package/dist/commands/commands.command.d.ts +15 -0
  17. package/dist/commands/commands.command.d.ts.map +1 -1
  18. package/dist/commands/commands.command.js +162 -3
  19. package/dist/commands/compress.command.d.ts.map +1 -1
  20. package/dist/commands/compress.command.js +39 -4
  21. package/dist/commands/constructs.command.d.ts.map +1 -1
  22. package/dist/commands/constructs.command.js +40 -9
  23. package/dist/commands/context.command.d.ts.map +1 -1
  24. package/dist/commands/context.command.js +3 -1
  25. package/dist/commands/daily.commands.d.ts.map +1 -1
  26. package/dist/commands/daily.commands.js +48 -5
  27. package/dist/commands/diff-check.command.d.ts.map +1 -1
  28. package/dist/commands/diff-check.command.js +6 -2
  29. package/dist/commands/feedback.command.d.ts.map +1 -1
  30. package/dist/commands/feedback.command.js +4 -0
  31. package/dist/commands/framework.command.d.ts.map +1 -1
  32. package/dist/commands/framework.command.js +20 -3
  33. package/dist/commands/gate.command.d.ts.map +1 -1
  34. package/dist/commands/gate.command.js +99 -2
  35. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  36. package/dist/commands/graph-code-subverbs.js +183 -68
  37. package/dist/commands/graph.command.d.ts.map +1 -1
  38. package/dist/commands/graph.command.js +6 -0
  39. package/dist/commands/impact.command.d.ts.map +1 -1
  40. package/dist/commands/impact.command.js +91 -1
  41. package/dist/commands/knowledge.command.d.ts.map +1 -1
  42. package/dist/commands/knowledge.command.js +21 -1
  43. package/dist/commands/onboard.command.d.ts.map +1 -1
  44. package/dist/commands/onboard.command.js +11 -2
  45. package/dist/commands/orchestrate.command.d.ts.map +1 -1
  46. package/dist/commands/orchestrate.command.js +6 -1
  47. package/dist/commands/pack-author.command.d.ts.map +1 -1
  48. package/dist/commands/pack-author.command.js +13 -1
  49. package/dist/commands/packs.command.d.ts.map +1 -1
  50. package/dist/commands/packs.command.js +95 -17
  51. package/dist/commands/paths.command.d.ts.map +1 -1
  52. package/dist/commands/paths.command.js +8 -1
  53. package/dist/commands/pipelines.command.d.ts.map +1 -1
  54. package/dist/commands/pipelines.command.js +2 -1
  55. package/dist/commands/plan-context.command.d.ts.map +1 -1
  56. package/dist/commands/plan-context.command.js +6 -3
  57. package/dist/commands/plan-simulate.command.js +1 -1
  58. package/dist/commands/policy-lint.command.d.ts +3 -0
  59. package/dist/commands/policy-lint.command.d.ts.map +1 -0
  60. package/dist/commands/policy-lint.command.js +135 -0
  61. package/dist/commands/recommend.command.d.ts.map +1 -1
  62. package/dist/commands/recommend.command.js +10 -2
  63. package/dist/commands/registry.command.d.ts.map +1 -1
  64. package/dist/commands/registry.command.js +117 -9
  65. package/dist/commands/report.command.js +1 -1
  66. package/dist/commands/reuse.command.d.ts +3 -0
  67. package/dist/commands/reuse.command.d.ts.map +1 -0
  68. package/dist/commands/reuse.command.js +202 -0
  69. package/dist/commands/review.command.d.ts.map +1 -1
  70. package/dist/commands/review.command.js +20 -2
  71. package/dist/commands/rule-graph-subverbs.d.ts.map +1 -1
  72. package/dist/commands/rule-graph-subverbs.js +31 -4
  73. package/dist/commands/search-structural.command.d.ts.map +1 -1
  74. package/dist/commands/search-structural.command.js +2 -2
  75. package/dist/commands/search.command.d.ts.map +1 -1
  76. package/dist/commands/search.command.js +19 -1
  77. package/dist/commands/simulate.command.d.ts.map +1 -1
  78. package/dist/commands/simulate.command.js +6 -1
  79. package/dist/commands/smart-context.command.d.ts.map +1 -1
  80. package/dist/commands/smart-context.command.js +87 -4
  81. package/dist/commands/spec.command.d.ts.map +1 -1
  82. package/dist/commands/spec.command.js +73 -13
  83. package/dist/commands/task-context.command.d.ts.map +1 -1
  84. package/dist/commands/task-context.command.js +26 -2
  85. package/dist/commands/test.command.d.ts.map +1 -1
  86. package/dist/commands/test.command.js +17 -0
  87. package/dist/commands/trace.command.d.ts.map +1 -1
  88. package/dist/commands/trace.command.js +14 -1
  89. package/dist/main.d.ts.map +1 -1
  90. package/dist/main.js +19 -1
  91. package/dist/status/freshness.d.ts +30 -0
  92. package/dist/status/freshness.d.ts.map +1 -0
  93. package/dist/status/freshness.js +81 -0
  94. package/package.json +33 -33
@@ -0,0 +1,135 @@
1
+ import * as nodePath from 'node:path';
2
+ import { runPolicyLint } from '@shrkcrft/boundaries';
3
+ import { resolveChangedFiles, resolveProjectConfig } from '@shrkcrft/inspector';
4
+ import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
5
+ import { asJson, header, kv } from "../output/format-output.js";
6
+ const VALID_SURFACES = new Set(['template', 'style', 'ts']);
7
+ export const policyLintCommand = {
8
+ name: 'policy-lint',
9
+ description: 'Lint template/markup, stylesheet, and AOT-invisible TS surfaces against data-defined policyRules[] (e.g. flag raw markup when a primitive exists). Sees `.html` files AND inline `template:` strings — surfaces tsc/AOT cannot. Deterministic; no AI.',
10
+ usage: 'shrk [--cwd <dir>] policy-lint [--surface template|style|ts] [--changed-only] [--since <ref>] [--only <ids>] [--json]',
11
+ async run(args) {
12
+ const cwd = resolveCwd(args);
13
+ const wantJson = flagBool(args, 'json');
14
+ const changedOnly = flagBool(args, 'changed-only');
15
+ const since = flagString(args, 'since');
16
+ const only = flagString(args, 'only');
17
+ const surfaceRaw = flagString(args, 'surface');
18
+ let surfaces;
19
+ if (surfaceRaw) {
20
+ const parts = surfaceRaw.split(',').map((s) => s.trim()).filter(Boolean);
21
+ const bad = parts.filter((s) => !VALID_SURFACES.has(s));
22
+ if (bad.length > 0) {
23
+ process.stderr.write(`Unknown --surface "${bad.join(', ')}". Use template | style | ts.\n`);
24
+ return 2;
25
+ }
26
+ surfaces = parts;
27
+ }
28
+ // Distinguish invalid config from valid-with-no-rules (no silent fail-open).
29
+ const loaded = await resolveProjectConfig(cwd);
30
+ if (!loaded.ok) {
31
+ const msg = loaded.error.message;
32
+ if (wantJson) {
33
+ process.stdout.write(asJson({ schema: 'sharkcraft.policy-lint/v1', error: msg, rules: [], findings: [], diagnostics: [msg], evaluated: 0, verdict: 'errors' }) + '\n');
34
+ return 1;
35
+ }
36
+ process.stdout.write(header('Policy lint'));
37
+ process.stdout.write(` ✗ Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
38
+ return 1;
39
+ }
40
+ const rules = loaded.value.config.policyRules ?? [];
41
+ const planeDiagnostics = loaded.value.planeDiagnostics;
42
+ if (rules.length === 0) {
43
+ if (wantJson) {
44
+ process.stdout.write(asJson({ schema: 'sharkcraft.policy-lint/v1', rules: [], findings: [], diagnostics: [], evaluated: 0, verdict: 'pass' }) + '\n');
45
+ return 0;
46
+ }
47
+ process.stdout.write(header('Policy lint'));
48
+ process.stdout.write(' No policy rules configured. Declare `policyRules[]` in sharkcraft.config.ts to lint\n' +
49
+ ' templates / styles / AOT-invisible TS shapes (see docs/policy-lint.md).\n');
50
+ return 0;
51
+ }
52
+ // A typo'd --only id must not silently select nothing and report green.
53
+ if (only) {
54
+ const requested = only.split(',').map((s) => s.trim()).filter(Boolean);
55
+ const known = new Set(rules.map((r) => r.id));
56
+ const unknown = requested.filter((id) => !known.has(id));
57
+ if (unknown.length > 0) {
58
+ process.stderr.write(`Unknown --only rule id(s): ${unknown.join(', ')}. Configured: ${[...known].join(', ') || '(none)'}\n`);
59
+ return 2;
60
+ }
61
+ }
62
+ let changedFiles;
63
+ if (changedOnly || since) {
64
+ changedFiles = resolveChangedFiles({
65
+ projectRoot: cwd,
66
+ ...(since ? { since } : {}),
67
+ ...(changedOnly && !since ? { includeWorktree: true } : {}),
68
+ }).files;
69
+ }
70
+ // Don't lint SharkCraft's own asset/config dir by default (its .ts files
71
+ // hold the rule definitions themselves, which can self-match).
72
+ const sharkcraftRel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
73
+ const excludeDirs = sharkcraftRel && !sharkcraftRel.startsWith('..') ? [sharkcraftRel] : [];
74
+ const reportRaw = runPolicyLint(cwd, rules, {
75
+ ...(surfaces ? { surfaces } : {}),
76
+ ...(changedOnly || since ? { changedOnly: true, changedFiles: changedFiles ?? [] } : {}),
77
+ ...(only ? { only: only.split(',').map((s) => s.trim()).filter(Boolean) } : {}),
78
+ ...(excludeDirs.length > 0 ? { excludeDirs } : {}),
79
+ });
80
+ // Surface pack-plane merge notes (missing/invalid pack policy files, dropped
81
+ // collisions) in the same diagnostics array the engine already emits.
82
+ const report = planeDiagnostics.length > 0
83
+ ? { ...reportRaw, diagnostics: [...reportRaw.diagnostics, ...planeDiagnostics] }
84
+ : reportRaw;
85
+ if (wantJson) {
86
+ process.stdout.write(asJson(report) + '\n');
87
+ return report.verdict === 'errors' ? 1 : 0;
88
+ }
89
+ process.stdout.write(header('Policy lint'));
90
+ // `evaluated` counts rules that actually scanned ≥1 file. When 0 rules
91
+ // evaluated but rules ARE configured, say so loudly — "scanned nothing" must
92
+ // never read as the green "no policy violations" pass.
93
+ if (report.evaluated === 0) {
94
+ process.stdout.write(` ! Nothing evaluated — ${report.rules.length} rule(s) configured but none matched files in scope` +
95
+ (changedOnly || since ? ' (changed-only).\n' : '.\n'));
96
+ return 0;
97
+ }
98
+ process.stdout.write(kv('rules evaluated', `${report.evaluated} of ${report.rules.length}`) + '\n');
99
+ const errors = report.findings.filter((f) => f.severity === 'error').length;
100
+ const warnings = report.findings.filter((f) => f.severity === 'warning').length;
101
+ process.stdout.write(kv('findings', `${errors} error(s), ${warnings} warning(s)`) + '\n');
102
+ if (report.diagnostics.length > 0) {
103
+ process.stdout.write('\nMisconfigured rules:\n');
104
+ for (const d of report.diagnostics)
105
+ process.stdout.write(` ! ${d}\n`);
106
+ }
107
+ if (report.findings.length === 0 && report.diagnostics.length === 0) {
108
+ process.stdout.write('\nNo policy violations on the scanned surfaces. ✓\n');
109
+ return 0;
110
+ }
111
+ // Group findings by rule.
112
+ const byRule = new Map();
113
+ for (const f of report.findings) {
114
+ const arr = byRule.get(f.ruleId) ?? [];
115
+ arr.push(f);
116
+ byRule.set(f.ruleId, arr);
117
+ }
118
+ for (const r of report.rules) {
119
+ const fs = byRule.get(r.ruleId);
120
+ if (!fs || fs.length === 0)
121
+ continue;
122
+ process.stdout.write(`\n[${r.severity}] ${r.ruleId} (${r.surface}) — ${fs[0].message}\n`);
123
+ for (const f of fs.slice(0, 50)) {
124
+ const tag = f.inlineTemplate ? ' [inline template]' : '';
125
+ process.stdout.write(` • ${f.match} (${f.file}:${f.line})${tag}\n`);
126
+ }
127
+ if (fs.length > 50)
128
+ process.stdout.write(` … (${fs.length - 50} more)\n`);
129
+ const suggest = fs.find((f) => f.suggest)?.suggest;
130
+ if (suggest)
131
+ process.stdout.write(` → ${suggest}\n`);
132
+ }
133
+ return report.verdict === 'errors' ? 1 : 0;
134
+ },
135
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"recommend.command.d.ts","sourceRoot":"","sources":["../../src/commands/recommend.command.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAShC,eAAO,MAAM,gBAAgB,EAAE,eAkP9B,CAAC;AA0HF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAY3D;AAcD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAcxD"}
1
+ {"version":3,"file":"recommend.command.d.ts","sourceRoot":"","sources":["../../src/commands/recommend.command.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAShC,eAAO,MAAM,gBAAgB,EAAE,eA4P9B,CAAC;AA0HF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAY3D;AAcD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAcxD"}
@@ -114,10 +114,18 @@ export const recommendCommand = {
114
114
  }));
115
115
  const promotedSet = new Set(promoted.map((p) => p.command));
116
116
  const isGenericDefault = (c) => /^(bun run )?shrk (review|report|impact)\b/i.test(c);
117
+ // The recommender (inspector) already arbitrates the keyword recipe
118
+ // against the shared ranker and may have promoted a `shrk gen <template>`
119
+ // / `shrk task` headline (its `why` starts with "Ranker matched"). That
120
+ // ranker-confirmed scaffold is the engine's corrected route — keep it
121
+ // ahead of even the routing-hint promotion so the headline survives.
122
+ const isRankerPromoted = (r) => /^Ranker matched /.test(r.why);
117
123
  const existing = report.recommendations;
118
- const keptNonGeneric = existing.filter((r) => !promotedSet.has(r.command) && !isGenericDefault(r.command));
119
- const keptGeneric = existing.filter((r) => !promotedSet.has(r.command) && isGenericDefault(r.command));
124
+ const keptRankerPromoted = existing.filter((r) => !promotedSet.has(r.command) && isRankerPromoted(r));
125
+ const keptNonGeneric = existing.filter((r) => !promotedSet.has(r.command) && !isRankerPromoted(r) && !isGenericDefault(r.command));
126
+ const keptGeneric = existing.filter((r) => !promotedSet.has(r.command) && !isRankerPromoted(r) && isGenericDefault(r.command));
120
127
  report.recommendations = [
128
+ ...keptRankerPromoted,
121
129
  ...promoted,
122
130
  ...keptNonGeneric,
123
131
  ...keptGeneric,
@@ -1 +1 @@
1
- {"version":3,"file":"registry.command.d.ts","sourceRoot":"","sources":["../../src/commands/registry.command.ts"],"names":[],"mappings":"AASA,OAAO,EAGL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC,eAAO,MAAM,wBAAwB,EAAE,eActC,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,eAa7B,CAAC"}
1
+ {"version":3,"file":"registry.command.d.ts","sourceRoot":"","sources":["../../src/commands/registry.command.ts"],"names":[],"mappings":"AAwBA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC,eAAO,MAAM,wBAAwB,EAAE,eAetC,CAAC;AAsGF,eAAO,MAAM,eAAe,EAAE,eAuB7B,CAAC"}
@@ -1,18 +1,27 @@
1
1
  /**
2
2
  * `shrk registry` commands.
3
3
  *
4
- * shrk registry lifecycle [--json]
4
+ * shrk registry lifecycle [--json] # register/remove symmetry
5
+ * shrk registry <name> list [--json] # every declared id
6
+ * shrk registry <name> exists <id> [--json] # is the id taken? (exit 1 if not)
7
+ * shrk registry <name> where <id> [--json] # declaration (+ consumer) sites
8
+ *
9
+ * `<name>` resolves a `registries[]` declaration in sharkcraft.config.ts — one
10
+ * deterministic multi-root scan that answers "is this id taken / where is it"
11
+ * without an agent re-running a fragile grep.
5
12
  */
6
- import { buildRegistryLifecycleReport, renderRegistryLifecycleReportText, } from '@shrkcrft/inspector';
7
- import { flagBool, resolveCwd, } from "../command-registry.js";
13
+ import { buildRegistryLifecycleReport, renderRegistryLifecycleReportText, resolveProjectConfig, } from '@shrkcrft/inspector';
14
+ import { scanRegistry, registryExists, registryWhere, } from '@shrkcrft/boundaries';
15
+ import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
8
16
  import { asJson } from "../output/format-output.js";
9
17
  export const registryLifecycleCommand = {
10
18
  name: 'lifecycle',
11
- description: 'Scan the workspace for register*/remove* symmetry. Read-only.',
12
- usage: 'shrk registry lifecycle [--json]',
19
+ description: 'Scan the workspace for register/remove symmetry. Read-only.',
20
+ usage: 'shrk registry lifecycle [--scope <dir>] [--json]',
13
21
  async run(args) {
14
22
  const cwd = resolveCwd(args);
15
- const report = buildRegistryLifecycleReport({ projectRoot: cwd });
23
+ const scope = flagString(args, 'scope');
24
+ const report = buildRegistryLifecycleReport({ projectRoot: cwd, ...(scope ? { scope } : {}) });
16
25
  if (flagBool(args, 'json')) {
17
26
  process.stdout.write(asJson(report) + '\n');
18
27
  return report.missingRemovers.length === 0 ? 0 : 1;
@@ -21,17 +30,116 @@ export const registryLifecycleCommand = {
21
30
  return report.missingRemovers.length === 0 ? 0 : 1;
22
31
  },
23
32
  };
33
+ async function loadRegistries(cwd) {
34
+ const loaded = await resolveProjectConfig(cwd);
35
+ if (!loaded.ok)
36
+ return { ok: false, message: loaded.error.message };
37
+ return {
38
+ ok: true,
39
+ registries: loaded.value.config.registries ?? [],
40
+ planeDiagnostics: loaded.value.planeDiagnostics,
41
+ };
42
+ }
43
+ function findRegistry(registries, name) {
44
+ return registries.find((r) => r.name === name);
45
+ }
46
+ /** Run a `list | exists | where` query against a declared registry. */
47
+ async function runRegistryInventory(args, name) {
48
+ const cwd = resolveCwd(args);
49
+ const json = flagBool(args, 'json');
50
+ const action = args.positional[1];
51
+ const id = args.positional[2];
52
+ const loaded = await loadRegistries(cwd);
53
+ if (!loaded.ok) {
54
+ if (json)
55
+ process.stdout.write(asJson({ error: loaded.message }) + '\n');
56
+ else
57
+ process.stderr.write(`Could not load config: ${loaded.message}\n Run \`shrk doctor\` for details.\n`);
58
+ return 1;
59
+ }
60
+ const decl = findRegistry(loaded.registries, name);
61
+ if (!decl) {
62
+ const names = loaded.registries.map((r) => r.name);
63
+ const avail = names.length > 0 ? `Declared registries: ${names.join(', ')}.` : 'No registries declared in sharkcraft.config.ts `registries[]`.';
64
+ if (json)
65
+ process.stdout.write(asJson({ error: `unknown registry "${name}"`, available: names }) + '\n');
66
+ else
67
+ process.stderr.write(`No registry named "${name}". ${avail}\n`);
68
+ return 2;
69
+ }
70
+ const inventory = scanRegistry(cwd, decl);
71
+ if (action === 'list' || action === undefined) {
72
+ // Fold pack-plane merge notes (missing/invalid pack registry files, dropped
73
+ // collisions) into the inventory's own scan diagnostics.
74
+ const diagnostics = [...inventory.diagnostics, ...loaded.planeDiagnostics];
75
+ if (json) {
76
+ process.stdout.write(asJson({ name: inventory.name, count: inventory.entries.length, ids: inventory.entries.map((e) => e.id), diagnostics }) + '\n');
77
+ return 0;
78
+ }
79
+ process.stdout.write(`Registry "${inventory.name}"${inventory.description ? ' — ' + inventory.description : ''}\n`);
80
+ process.stdout.write(` ${inventory.entries.length} id(s)\n`);
81
+ for (const e of inventory.entries) {
82
+ process.stdout.write(` • ${e.id} (${e.sites.length} site${e.sites.length === 1 ? '' : 's'})\n`);
83
+ }
84
+ for (const d of diagnostics)
85
+ process.stdout.write(` ! ${d}\n`);
86
+ return 0;
87
+ }
88
+ if (action === 'exists') {
89
+ if (!id) {
90
+ process.stderr.write(`Usage: shrk registry ${name} exists <id>\n`);
91
+ return 2;
92
+ }
93
+ const exists = registryExists(inventory, id);
94
+ if (json)
95
+ process.stdout.write(asJson({ name: inventory.name, id, exists }) + '\n');
96
+ else
97
+ process.stdout.write(`${exists ? 'yes' : 'no'} — "${id}" is ${exists ? 'declared' : 'NOT declared'} in registry "${inventory.name}".\n`);
98
+ return exists ? 0 : 1;
99
+ }
100
+ if (action === 'where') {
101
+ if (!id) {
102
+ process.stderr.write(`Usage: shrk registry ${name} where <id>\n`);
103
+ return 2;
104
+ }
105
+ const entry = registryWhere(inventory, id);
106
+ if (json) {
107
+ process.stdout.write(asJson({ name: inventory.name, id, found: entry !== undefined, entry: entry ?? null }) + '\n');
108
+ return entry ? 0 : 1;
109
+ }
110
+ if (!entry) {
111
+ process.stdout.write(`"${id}" is not declared in registry "${inventory.name}".\n`);
112
+ return 1;
113
+ }
114
+ process.stdout.write(`"${id}" in registry "${inventory.name}":\n`);
115
+ for (const s of entry.sites)
116
+ process.stdout.write(` declared ${s.file}:${s.line}\n`);
117
+ for (const s of entry.consumerSites ?? [])
118
+ process.stdout.write(` consumed ${s.file}:${s.line}\n`);
119
+ return 0;
120
+ }
121
+ process.stderr.write(`Unknown action "${action}". Usage: shrk registry ${name} list | exists <id> | where <id>\n`);
122
+ return 2;
123
+ }
24
124
  export const registryCommand = {
25
125
  name: 'registry',
26
- description: 'Registry inspections (lifecycle symmetry, etc.). Read-only.',
27
- usage: 'shrk registry lifecycle',
126
+ description: 'Registry inspections: lifecycle symmetry + declared-registry inventory. Read-only.',
127
+ usage: 'shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id>',
28
128
  async run(args) {
29
129
  const sub = args.positional[0];
30
130
  if (sub === 'lifecycle') {
31
131
  args.positional = args.positional.slice(1);
32
132
  return registryLifecycleCommand.run(args);
33
133
  }
34
- process.stderr.write('Usage: shrk registry lifecycle\n');
134
+ if (sub !== undefined && sub.length > 0) {
135
+ // `<name> list | exists <id> | where <id>` — sub is the registry name.
136
+ return runRegistryInventory(args, sub);
137
+ }
138
+ const cwd = resolveCwd(args);
139
+ const loaded = await loadRegistries(cwd);
140
+ const names = loaded.ok ? loaded.registries.map((r) => r.name) : [];
141
+ process.stderr.write('Usage: shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id>\n' +
142
+ (names.length > 0 ? `Declared registries: ${names.join(', ')}.\n` : 'No registries declared (sharkcraft.config.ts `registries[]`).\n'));
35
143
  return 2;
36
144
  },
37
145
  };
@@ -8,7 +8,7 @@ const VALID_FORMATS = new Set(['text', 'markdown', 'html', 'json']);
8
8
  export const reportCommand = {
9
9
  name: 'report',
10
10
  description: 'Render runtime reports in text / markdown / html / json. Subcommands: adoption / session / quality / safety / review / coverage / drift / graph.',
11
- usage: 'shrk report <adoption|session <id>|quality|safety|review <packet.json>|coverage|drift|graph> [--format text|markdown|html|json] [--output <path>] [--collapse-long-sections] [--max-items N] [--include-raw-json] [--json]',
11
+ usage: 'shrk report <adoption|session <id>|quality|safety|review <packet.json>|coverage|drift|graph> [--format text|markdown|html|json] [--output <path>] [--collapse-long-sections] [--max-items N] [--json]',
12
12
  async run(args) {
13
13
  const sub = args.positional[0];
14
14
  const sliced = { ...args, positional: args.positional.slice(1) };
@@ -0,0 +1,3 @@
1
+ import { type ICommandHandler } from '../command-registry.js';
2
+ export declare const reuseCommand: ICommandHandler;
3
+ //# sourceMappingURL=reuse.command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reuse.command.d.ts","sourceRoot":"","sources":["../../src/commands/reuse.command.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA4DhC,eAAO,MAAM,YAAY,EAAE,eAqK1B,CAAC"}
@@ -0,0 +1,202 @@
1
+ import { GraphStore, GraphQueryApi } from '@shrkcrft/graph';
2
+ import { resolveProjectConfig } from '@shrkcrft/inspector';
3
+ import { flagBool, flagNumber, resolveCwd, } from "../command-registry.js";
4
+ import { asJson, header } from "../output/format-output.js";
5
+ const STOP = new Set([
6
+ 'the', 'a', 'an', 'to', 'for', 'of', 'and', 'or', 'with', 'in', 'on', 'into', 'my',
7
+ 'add', 'use', 'using', 'create', 'make', 'new', 'build', 'want', 'need', 'how', 'do',
8
+ ]);
9
+ function tokenize(s) {
10
+ // Min length 3: 2-char tokens (`ui`, `id`) substring-match unrelated text
11
+ // (`guidance`, `valid`) and produce false-positive primitive matches.
12
+ return [
13
+ ...new Set(s
14
+ .toLowerCase()
15
+ .replace(/[^a-z0-9]+/g, ' ')
16
+ .split(' ')
17
+ .filter((t) => t.length >= 3 && !STOP.has(t))),
18
+ ];
19
+ }
20
+ function scorePrimitive(p, tokens) {
21
+ // Substring match (so intent "debounce" matches symbol `useDebounce`); the
22
+ // 3-char minimum above keeps it from over-matching on tiny fragments.
23
+ const hay = [p.symbol, ...(p.roles ?? []), ...(p.keywords ?? []), p.description ?? '']
24
+ .join(' ')
25
+ .toLowerCase();
26
+ let s = 0;
27
+ for (const t of tokens)
28
+ if (hay.includes(t))
29
+ s += 1;
30
+ // A role the intent mentions is a strong signal; re-weight role hits.
31
+ for (const role of p.roles ?? []) {
32
+ const rl = role.toLowerCase();
33
+ if (rl.length >= 3 && tokens.some((t) => rl.includes(t)))
34
+ s += 0.5;
35
+ }
36
+ return s;
37
+ }
38
+ const INDEX_RE = /(^|\/)index\.[cm]?[jt]sx?$/;
39
+ export const reuseCommand = {
40
+ name: 'reuse',
41
+ description: 'Intent → the canonical primitive to reuse. Matches your intent against configured reusePrimitives[], then resolves the symbol in the code graph to its declaration, public import path, sibling exports, and real consumer files to copy. Deterministic; no AI.',
42
+ usage: 'shrk reuse "<what I want to build>" [--limit N] [--json]',
43
+ async run(args) {
44
+ const cwd = resolveCwd(args);
45
+ const wantJson = flagBool(args, 'json');
46
+ const limit = flagNumber(args, 'limit') ?? 3;
47
+ const intent = args.positional.join(' ').trim();
48
+ if (!intent) {
49
+ process.stderr.write('Usage: shrk reuse "<what I want to build>" [--limit N] [--json]\n');
50
+ return 2;
51
+ }
52
+ const loaded = await resolveProjectConfig(cwd);
53
+ if (!loaded.ok) {
54
+ const msg = loaded.error.message;
55
+ if (wantJson) {
56
+ process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, error: msg, results: [] }) + '\n');
57
+ return 1;
58
+ }
59
+ process.stdout.write(header(`Reuse: "${intent}"`));
60
+ process.stdout.write(` ✗ Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
61
+ return 1;
62
+ }
63
+ const primitives = loaded.value.config.reusePrimitives ?? [];
64
+ const planeDiagnostics = loaded.value.planeDiagnostics;
65
+ // reuse has no pre-existing diagnostics surface; expose pack-plane merge
66
+ // notes (missing/invalid pack primitive files, dropped collisions) so a
67
+ // pack contribution that failed to load isn't silently invisible.
68
+ const planeJson = planeDiagnostics.length > 0 ? { planeDiagnostics } : {};
69
+ const writePlaneNotes = () => {
70
+ for (const d of planeDiagnostics)
71
+ process.stdout.write(` ! ${d}\n`);
72
+ };
73
+ if (primitives.length === 0) {
74
+ if (wantJson) {
75
+ process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, results: [], ...planeJson }) + '\n');
76
+ return 0;
77
+ }
78
+ process.stdout.write(header(`Reuse: "${intent}"`));
79
+ process.stdout.write(' No reuse primitives configured. Declare `reusePrimitives[]` in sharkcraft.config.ts\n' +
80
+ ' to map roles/intents to canonical symbols (see docs/reuse.md).\n');
81
+ writePlaneNotes();
82
+ return 0;
83
+ }
84
+ 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));
90
+ const store = new GraphStore(cwd);
91
+ const api = store.exists() ? GraphQueryApi.fromStore(cwd) : null;
92
+ if (ranked.length === 0) {
93
+ const roles = [...new Set(primitives.flatMap((p) => p.roles))].sort();
94
+ if (wantJson) {
95
+ process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, results: [], availableRoles: roles, ...planeJson }) + '\n');
96
+ return 0;
97
+ }
98
+ process.stdout.write(header(`Reuse: "${intent}"`));
99
+ process.stdout.write(' No primitive matched. Available roles:\n');
100
+ for (const r of roles.slice(0, 40))
101
+ process.stdout.write(` • ${r}\n`);
102
+ writePlaneNotes();
103
+ return 0;
104
+ }
105
+ const results = ranked.map(({ p, score }) => {
106
+ const r = { symbol: p.symbol, score, roles: p.roles, siblings: [], consumers: [] };
107
+ if (p.description)
108
+ r.description = p.description;
109
+ if (p.importPath)
110
+ r.importPath = p.importPath;
111
+ if (api) {
112
+ // Disambiguate same-named declarations deterministically: prefer
113
+ // exported symbols, then shallowest path; disclose the alternates.
114
+ const candidates = api
115
+ .findSymbol(p.symbol, { exact: true })
116
+ .slice()
117
+ .sort((a, b) => (a.path ?? '').localeCompare(b.path ?? ''));
118
+ const exported = candidates.filter((c) => c.data?.['isExported'] === true);
119
+ const pool = exported.length > 0 ? exported : candidates;
120
+ const sym = pool[0];
121
+ if (sym) {
122
+ if (sym.path)
123
+ r.declaredIn = sym.path;
124
+ if (sym.line)
125
+ r.declaredLine = sym.line;
126
+ if (sym.path) {
127
+ const fileNode = api.findFile(sym.path);
128
+ if (fileNode) {
129
+ r.siblings = api
130
+ .symbolsIn(fileNode.id)
131
+ .filter((s) => s.data?.['isExported'] === true && s.label && s.label !== p.symbol)
132
+ .map((s) => s.label)
133
+ .slice(0, 8);
134
+ // When no public importPath is configured, surface a re-exporting
135
+ // barrel as a hint (we never fabricate a module specifier from a
136
+ // deep file path — that would be a broken/unusable import).
137
+ if (!r.importPath) {
138
+ const barrel = api.importersOf(fileNode.id).find((n) => n.path && INDEX_RE.test(n.path));
139
+ if (barrel?.path)
140
+ r.reExportedVia = barrel.path;
141
+ }
142
+ }
143
+ }
144
+ r.consumers = api
145
+ .referenceSitesOf(sym.id)
146
+ .slice(0, 5)
147
+ .map((s) => ({ path: s.node.path ?? s.node.id, ...(s.line ? { line: s.line } : {}) }));
148
+ const alts = pool.slice(1).map((c) => c.path).filter((x) => !!x);
149
+ if (alts.length > 0)
150
+ r.alternates = alts;
151
+ }
152
+ else {
153
+ r.notFound = true;
154
+ }
155
+ }
156
+ // The import line is emitted ONLY from a configured importPath (a clean,
157
+ // copy-pasteable specifier). Without it we show declaration + barrel hint.
158
+ if (r.importPath)
159
+ r.importLine = `import { ${p.symbol} } from '${r.importPath}';`;
160
+ return r;
161
+ });
162
+ if (wantJson) {
163
+ process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, graphIndexed: !!api, results, ...planeJson }) + '\n');
164
+ return 0;
165
+ }
166
+ process.stdout.write(header(`Reuse: "${intent}"`));
167
+ writePlaneNotes();
168
+ if (!api) {
169
+ process.stdout.write(' (code graph missing — import path/siblings/consumers limited; run `shrk graph index`)\n');
170
+ }
171
+ let i = 0;
172
+ for (const r of results) {
173
+ i += 1;
174
+ process.stdout.write(`\n${i}. ${r.symbol}\n`);
175
+ if (r.description)
176
+ process.stdout.write(` ${r.description}\n`);
177
+ if (r.notFound) {
178
+ process.stdout.write(' ⚠ symbol not found in the code graph — verify reusePrimitives[].symbol (typo/rename?) or run `shrk graph index`\n');
179
+ }
180
+ if (r.importLine) {
181
+ process.stdout.write(` import: ${r.importLine}\n`);
182
+ }
183
+ else if (r.reExportedVia) {
184
+ process.stdout.write(` re-exported via: ${r.reExportedVia} (set reusePrimitives[].importPath for a copy-paste import)\n`);
185
+ }
186
+ if (r.declaredIn) {
187
+ process.stdout.write(` declared in: ${r.declaredIn}${r.declaredLine ? ':' + r.declaredLine : ''}\n`);
188
+ }
189
+ if (r.alternates && r.alternates.length > 0) {
190
+ process.stdout.write(` ⚠ name also declared in: ${r.alternates.join(', ')}\n`);
191
+ }
192
+ if (r.siblings.length > 0)
193
+ process.stdout.write(` sibling exports: ${r.siblings.join(', ')}\n`);
194
+ if (r.consumers.length > 0) {
195
+ process.stdout.write(' consumers to copy:\n');
196
+ for (const c of r.consumers)
197
+ process.stdout.write(` - ${c.path}${c.line ? ':' + c.line : ''}\n`);
198
+ }
199
+ }
200
+ return 0;
201
+ },
202
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"review.command.d.ts","sourceRoot":"","sources":["../../src/commands/review.command.ts"],"names":[],"mappings":"AAgBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA6GhC,eAAO,MAAM,aAAa,EAAE,eA+E3B,CAAC"}
1
+ {"version":3,"file":"review.command.d.ts","sourceRoot":"","sources":["../../src/commands/review.command.ts"],"names":[],"mappings":"AAgBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA6GhC,eAAO,MAAM,aAAa,EAAE,eAiG3B,CAAC"}
@@ -77,7 +77,20 @@ function scaffoldOptionsFromArgs(args) {
77
77
  export const reviewCommand = {
78
78
  name: 'review',
79
79
  description: 'Build a PR-review packet: changed files (via git), affected path conventions, relevant rules/templates/pipelines, boundary violations on those files, missing-test heuristic, verification commands. Also: `shrk review scaffold github-action` prints a workflow template with optional `--with-boundaries / --with-coverage / --with-drift / --artifact-only / --comment-placeholder` flags. `shrk review render-comment <packet.json>` renders a Markdown PR comment from a saved packet.',
80
- usage: 'shrk [--cwd <dir>] review [--since <ref>] [--staged] [--files a,b] [--json] | shrk review scaffold github-action [...] | shrk review render-comment <packet.json> [--output <file>]',
80
+ usage: 'shrk [--cwd <dir>] review [files... | --files a,b] [--since <ref>] [--staged] [--no-untracked] [--json] | shrk review scaffold github-action [...] | shrk review render-comment <packet.json> [--output <file>]',
81
+ booleanFlags: new Set([
82
+ 'json',
83
+ 'staged',
84
+ 'untracked',
85
+ 'no-untracked',
86
+ 'v2',
87
+ 'v3',
88
+ 'with-boundaries',
89
+ 'with-coverage',
90
+ 'with-drift',
91
+ 'artifact-only',
92
+ 'comment-placeholder',
93
+ ]),
81
94
  async run(args) {
82
95
  if (args.positional[0] === 'scaffold') {
83
96
  const which = args.positional[1] ?? 'github-action';
@@ -97,11 +110,16 @@ export const reviewCommand = {
97
110
  }
98
111
  const inspection = await inspectSharkcraft({ cwd: resolveCwd(args) });
99
112
  const since = flagString(args, 'since');
100
- const files = flagList(args, 'files');
113
+ // Accept files via `--files a,b` OR as bare positional args
114
+ // (`shrk review a.ts b.ts`). Positionals were previously dropped silently,
115
+ // yielding a confident "0 changed files" with exit 0.
116
+ const filesFlag = flagList(args, 'files');
117
+ const files = filesFlag.length ? filesFlag : args.positional;
101
118
  const packet = buildReviewPacket(inspection, {
102
119
  ...(since ? { since } : {}),
103
120
  ...(flagBool(args, 'staged') ? { staged: true } : {}),
104
121
  ...(files.length ? { files } : {}),
122
+ ...(flagBool(args, 'no-untracked') ? { untracked: false } : {}),
105
123
  });
106
124
  if (flagBool(args, 'json')) {
107
125
  process.stdout.write(asJson(packet) + '\n');
@@ -1 +1 @@
1
- {"version":3,"file":"rule-graph-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/rule-graph-subverbs.ts"],"names":[],"mappings":"AAQA,OAAO,EAAwB,KAAK,eAAe,EAAmB,MAAM,wBAAwB,CAAC;AAGrG,eAAO,MAAM,gBAAgB,EAAE,eAc9B,CAAC"}
1
+ {"version":3,"file":"rule-graph-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/rule-graph-subverbs.ts"],"names":[],"mappings":"AASA,OAAO,EAAwB,KAAK,eAAe,EAAmB,MAAM,wBAAwB,CAAC;AAGrG,eAAO,MAAM,gBAAgB,EAAE,eAc9B,CAAC"}
@@ -5,7 +5,8 @@
5
5
  * because the verbs operate on a separate `.sharkcraft/bridge/` store
6
6
  * and don't conceptually belong under `shrk graph`.
7
7
  */
8
- import { buildBridge, RuleGraphQueryApi } from '@shrkcrft/rule-graph';
8
+ import { buildBridge, BridgeStore, RuleGraphQueryApi } from '@shrkcrft/rule-graph';
9
+ import { detectGraphFreshness } from '@shrkcrft/graph';
9
10
  import { flagBool, resolveCwd } from "../command-registry.js";
10
11
  import { asJson, header, kv } from "../output/format-output.js";
11
12
  export const ruleGraphCommand = {
@@ -54,13 +55,39 @@ async function runStatus(args) {
54
55
  return 1;
55
56
  }
56
57
  const api = RuleGraphQueryApi.fromStores(cwd);
57
- // No public counters from the API itself just confirm it loads.
58
+ // The bridge maps code-graph files asset registries, so it is stale once
59
+ // the underlying code graph has drifted. Reuse the already-available graph
60
+ // freshness signal (the same one `graph status` reports) rather than a
61
+ // hardcoded 'fresh'. `lastBuiltAt` comes from the bridge manifest.
62
+ let lastBuiltAt = null;
63
+ try {
64
+ lastBuiltAt = new BridgeStore(cwd).loadSnapshot().manifest.lastBuiltAt;
65
+ }
66
+ catch {
67
+ lastBuiltAt = null;
68
+ }
69
+ const fresh = detectGraphFreshness(cwd);
70
+ const behind = fresh.modified.length + fresh.added.length + fresh.deleted.length;
71
+ const state = behind > 0 ? 'stale' : 'fresh';
58
72
  if (wantJson) {
59
- process.stdout.write(asJson({ ok: true, state: 'fresh' }) + '\n');
73
+ process.stdout.write(asJson({
74
+ ok: true,
75
+ state,
76
+ lastBuiltAt,
77
+ modifiedSinceIndex: fresh.modified.length,
78
+ newSinceIndex: fresh.added.length,
79
+ deletedSinceIndex: fresh.deleted.length,
80
+ ...(state === 'stale' ? { nextCommand: 'shrk rule-graph index' } : {}),
81
+ }) + '\n');
60
82
  return 0;
61
83
  }
62
84
  process.stdout.write(header('Rule-graph status'));
63
- process.stdout.write(kv('state', 'fresh') + '\n');
85
+ if (lastBuiltAt)
86
+ process.stdout.write(kv('last built', lastBuiltAt) + '\n');
87
+ process.stdout.write(kv('state', state) + '\n');
88
+ if (state === 'stale') {
89
+ process.stdout.write(`! stale — ${fresh.modified.length} modified, ${fresh.added.length} new, ${fresh.deleted.length} deleted in the code graph since the bridge was built; re-run \`shrk rule-graph index\`\n`);
90
+ }
64
91
  void api;
65
92
  return 0;
66
93
  }