@shrkcrft/inspector 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 (63) hide show
  1. package/dist/command-recommender.d.ts.map +1 -1
  2. package/dist/command-recommender.js +110 -1
  3. package/dist/context-tuning.d.ts +14 -0
  4. package/dist/context-tuning.d.ts.map +1 -0
  5. package/dist/context-tuning.js +38 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/knowledge-stale.d.ts +23 -0
  10. package/dist/knowledge-stale.d.ts.map +1 -1
  11. package/dist/knowledge-stale.js +58 -9
  12. package/dist/monorepo-onboarding.d.ts.map +1 -1
  13. package/dist/monorepo-onboarding.js +122 -12
  14. package/dist/onboarding.d.ts.map +1 -1
  15. package/dist/onboarding.js +79 -18
  16. package/dist/pack-doctor.d.ts +8 -1
  17. package/dist/pack-doctor.d.ts.map +1 -1
  18. package/dist/pack-doctor.js +110 -3
  19. package/dist/pack-quality-score.d.ts.map +1 -1
  20. package/dist/pack-quality-score.js +7 -8
  21. package/dist/pack-release-check.d.ts.map +1 -1
  22. package/dist/pack-release-check.js +23 -20
  23. package/dist/pack-signature-status.d.ts +9 -3
  24. package/dist/pack-signature-status.d.ts.map +1 -1
  25. package/dist/pack-signature-status.js +13 -2
  26. package/dist/plan-simulation.d.ts.map +1 -1
  27. package/dist/plan-simulation.js +26 -12
  28. package/dist/registry-lifecycle.d.ts +8 -0
  29. package/dist/registry-lifecycle.d.ts.map +1 -1
  30. package/dist/registry-lifecycle.js +21 -3
  31. package/dist/resolve-project-config.d.ts +26 -0
  32. package/dist/resolve-project-config.d.ts.map +1 -0
  33. package/dist/resolve-project-config.js +128 -0
  34. package/dist/resolve-verification-commands.d.ts +7 -0
  35. package/dist/resolve-verification-commands.d.ts.map +1 -1
  36. package/dist/resolve-verification-commands.js +53 -4
  37. package/dist/review-packet.d.ts +8 -0
  38. package/dist/review-packet.d.ts.map +1 -1
  39. package/dist/review-packet.js +11 -12
  40. package/dist/sharkcraft-inspector.d.ts.map +1 -1
  41. package/dist/sharkcraft-inspector.js +28 -2
  42. package/dist/spec/__tests__/spec-evidence.test.d.ts +2 -0
  43. package/dist/spec/__tests__/spec-evidence.test.d.ts.map +1 -0
  44. package/dist/spec/__tests__/spec-evidence.test.js +81 -0
  45. package/dist/spec/spec-evidence.d.ts +62 -0
  46. package/dist/spec/spec-evidence.d.ts.map +1 -0
  47. package/dist/spec/spec-evidence.js +215 -0
  48. package/dist/spec/spec-review.d.ts +1 -0
  49. package/dist/spec/spec-review.d.ts.map +1 -1
  50. package/dist/spec/spec-review.js +5 -0
  51. package/dist/symbol-index.d.ts +9 -0
  52. package/dist/symbol-index.d.ts.map +1 -1
  53. package/dist/symbol-index.js +6 -0
  54. package/dist/task-packet.d.ts.map +1 -1
  55. package/dist/task-packet.js +6 -1
  56. package/dist/task-ranker.d.ts +2 -1
  57. package/dist/task-ranker.d.ts.map +1 -1
  58. package/dist/task-ranker.js +30 -5
  59. package/dist/test-runner.d.ts.map +1 -1
  60. package/dist/test-runner.js +3 -0
  61. package/dist/why-file.d.ts.map +1 -1
  62. package/dist/why-file.js +13 -0
  63. package/package.json +17 -17
@@ -61,7 +61,17 @@ const REGISTER_PATTERNS = Object.freeze([
61
61
  /(?:export\s+)?(?:async\s+)?function\s+(register[A-Z]\w*)\s*\(/g,
62
62
  // class method DECLARATION: requires a body `{` or return-type `:` after the
63
63
  // params, which a bare call site (`registry.registerX(...)`) never has.
64
- /(?:^|\n)[\t ]*(?:public|private|protected)?\s*(?:static\s+)?(?:async\s+)?(register[A-Z]\w*)\s*\([^;]*?\)\s*[:{]/g,
64
+ // NOTE: every modifier carries its OWN single-line (`[ \t]`) trailing space
65
+ // and the leading whitespace class is single-line too — no two newline-
66
+ // spanning (`\s`) quantifiers sit adjacent. The old form had `[\t ]*` then
67
+ // `\s*` then `(?:static\s+)?` then `(?:async\s+)?` all touching, so from each
68
+ // `\n` anchor the engine re-partitioned the whole whitespace block looking for
69
+ // `register`: O(block^2) catastrophic backtracking that dominated the scan
70
+ // (~90% of runtime on a stripped source full of blanked comment/string runs).
71
+ // The only newline-spanning class is the final one, after the required `)`,
72
+ // where it cannot backtrack against a neighbour. Match-equivalent to the old
73
+ // pattern on real TS (modifiers always sit on the same line as `registerX(`).
74
+ /(?:^|\n)[ \t]*(?:(?:public|private|protected)[ \t]+)?(?:static[ \t]+)?(?:async[ \t]+)?(register[A-Z]\w*)[ \t]*\([^;]*?\)[ \t\r\n]*[:{]/g,
65
75
  // assigned arrow / function expression: `registerX = (…) =>` / `registerX = function`
66
76
  /\b(register[A-Z]\w*)\s*=\s*(?:async\s+)?(?:function\b|\([^)]*\)\s*(?::[^=]*)?=>)/g,
67
77
  ]);
@@ -220,8 +230,10 @@ function findRemoverInContent(content, candidates) {
220
230
  }
221
231
  export function buildRegistryLifecycleReport(input) {
222
232
  const { projectRoot } = input;
233
+ const scope = input.scope && input.scope.length > 0 ? input.scope : undefined;
234
+ const walkRoot = scope ? join(projectRoot, scope) : projectRoot;
223
235
  const files = [];
224
- walk(projectRoot, projectRoot, files);
236
+ walk(walkRoot, projectRoot, files);
225
237
  const limit = input.limit ?? 2000;
226
238
  const scanFiles = files.slice(0, limit);
227
239
  const matchedPairs = [];
@@ -299,6 +311,9 @@ export function buildRegistryLifecycleReport(input) {
299
311
  return {
300
312
  schema: 'sharkcraft.registry-lifecycle/v1',
301
313
  filesScanned: scanFiles.length,
314
+ totalFiles: files.length,
315
+ truncated: files.length > scanFiles.length,
316
+ ...(scope ? { scope } : {}),
302
317
  registersFound,
303
318
  matchedPairs,
304
319
  missingRemovers,
@@ -310,7 +325,10 @@ export function buildRegistryLifecycleReport(input) {
310
325
  export function renderRegistryLifecycleReportText(report) {
311
326
  const lines = [];
312
327
  lines.push('=== Registry lifecycle ===');
313
- lines.push(` files scanned ${report.filesScanned}`);
328
+ if (report.scope)
329
+ lines.push(` scope ${report.scope}`);
330
+ lines.push(` files scanned ${report.filesScanned}` +
331
+ (report.truncated ? ` ! capped (${report.totalFiles} candidates — re-run with --scope <dir> for the rest)` : ''));
314
332
  lines.push(` registers found ${report.registersFound}`);
315
333
  lines.push(` matched pairs ${report.matchedPairs.length}`);
316
334
  lines.push(` missing removers ${report.missingRemovers.length}`);
@@ -0,0 +1,26 @@
1
+ import { type AppError, type Result } from '@shrkcrft/core';
2
+ import { type LoadedConfig } from '@shrkcrft/config';
3
+ /**
4
+ * A {@link LoadedConfig} whose four data planes have had pack contributions
5
+ * merged in (local-wins), plus the human-readable notes from that merge.
6
+ */
7
+ export interface IResolvedProjectConfig extends LoadedConfig {
8
+ /**
9
+ * Notes from the pack-plane merge — missing/invalid pack files, dropped
10
+ * collisions, pack-discovery failures. Empty when there are no packs (or no
11
+ * pack contributions to the four planes). Surfaced by the readers that
12
+ * consume the merged planes (`shrk check wiring`, `registry`, `policy-lint`,
13
+ * `reuse`, `gate`).
14
+ */
15
+ readonly planeDiagnostics: readonly string[];
16
+ }
17
+ /**
18
+ * Load the project config, then merge pack-contributed `wiringRules` /
19
+ * `registries` / `policyRules` / `reusePrimitives` over the local config
20
+ * (local-wins). Returns the loader error untouched on failure, so callers keep
21
+ * the same "invalid config vs. valid-with-no-rules" distinction they had with
22
+ * {@link loadProjectConfig}. A pack-discovery failure degrades to a diagnostic
23
+ * — it never fails config resolution.
24
+ */
25
+ export declare function resolveProjectConfig(cwd: string): Promise<Result<IResolvedProjectConfig, AppError>>;
26
+ //# sourceMappingURL=resolve-project-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-project-config.d.ts","sourceRoot":"","sources":["../src/resolve-project-config.ts"],"names":[],"mappings":"AAuBA,OAAO,EAGL,KAAK,QAAQ,EAKb,KAAK,MAAM,EACZ,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAML,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAG1B;;;GAGG;AACH,MAAM,WAAW,sBAAuB,SAAQ,YAAY;IAC1D;;;;;;OAMG;IACH,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AA+GD;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,MAAM,CAAC,sBAAsB,EAAE,QAAQ,CAAC,CAAC,CAqDnD"}
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Pack-aware project-config resolution.
3
+ *
4
+ * The four "cross-file invariant as DATA" planes — `wiringRules`, `registries`,
5
+ * `policyRules`, `reusePrimitives` — can be authored inline in a repo's
6
+ * `sharkcraft.config.ts`. They can ALSO be SHIPPED by a framework pack (e.g. a
7
+ * NestJS pack contributing "every @Injectable must be registered in a module"
8
+ * as a wiring rule) via the new `wiringRuleFiles` / `registryFiles` /
9
+ * `policyRuleFiles` / `reusePrimitiveFiles` manifest slots.
10
+ *
11
+ * The merge CANNOT live in `loadProjectConfig`: config sits at layer 3 and packs
12
+ * at layer 6, so config cannot import the pack discovery. The inspector (layer
13
+ * 10, above packs) is the lowest layer that can see both, so the merge seam
14
+ * lives here.
15
+ *
16
+ * Precedence is LOCAL-WINS: a repo's own declaration always beats a pack's, and
17
+ * a pack element whose key collides with a local (or an earlier pack) one is
18
+ * dropped with a diagnostic. Pack elements are validated with the SAME exported
19
+ * zod schemas the config loader uses, so a malformed pack element is skipped
20
+ * (with a diagnostic) rather than crashing config resolution.
21
+ */
22
+ import { existsSync } from 'node:fs';
23
+ import * as nodePath from 'node:path';
24
+ import { importModuleViaLoader, ok, } from '@shrkcrft/core';
25
+ import { loadProjectConfig, PolicyRuleSchema, RegistryDeclarationSchema, ReusePrimitiveSchema, WiringRuleSchema, } from '@shrkcrft/config';
26
+ import { discoverPacks } from '@shrkcrft/packs';
27
+ /**
28
+ * Generic per-plane load + validate + merge. Seeds the merged map from the
29
+ * LOCAL array (keyed by `keyOf`), then folds in pack elements only when their
30
+ * key is free. Missing files, non-array default exports, schema-invalid
31
+ * elements, and key collisions all become diagnostics and are skipped — never a
32
+ * throw.
33
+ */
34
+ async function mergePlane(localArr, packContribs, schema, keyOf, planeLabel, diagnostics) {
35
+ const merged = new Map();
36
+ const localKeys = new Set();
37
+ for (const item of localArr) {
38
+ const key = keyOf(item);
39
+ merged.set(key, item);
40
+ localKeys.add(key);
41
+ }
42
+ for (const contrib of packContribs) {
43
+ const full = nodePath.resolve(contrib.packageRoot, contrib.rel);
44
+ if (!existsSync(full)) {
45
+ diagnostics.push(`pack ${contrib.packageName}: missing ${planeLabel} file ${contrib.rel}`);
46
+ continue;
47
+ }
48
+ let mod;
49
+ try {
50
+ mod = await importModuleViaLoader(full);
51
+ }
52
+ catch (e) {
53
+ diagnostics.push(`pack ${contrib.packageName}: failed to load ${planeLabel} file ${contrib.rel} — ${e.message}`);
54
+ continue;
55
+ }
56
+ const arr = mod.default;
57
+ if (!Array.isArray(arr)) {
58
+ diagnostics.push(`pack ${contrib.packageName}: ${planeLabel} file ${contrib.rel} default export is not an array — skipped`);
59
+ continue;
60
+ }
61
+ for (const raw of arr) {
62
+ const parsed = schema.safeParse(raw);
63
+ if (!parsed.success) {
64
+ const summary = (parsed.error?.issues ?? [])
65
+ .map((iss) => `${iss.path.join('.') || '<root>'}: ${iss.message}`)
66
+ .join('; ') || 'invalid element';
67
+ diagnostics.push(`pack ${contrib.packageName}: invalid ${planeLabel} element in ${contrib.rel} — ${summary} — skipped`);
68
+ continue;
69
+ }
70
+ const item = parsed.data;
71
+ const key = keyOf(item);
72
+ if (merged.has(key)) {
73
+ diagnostics.push(localKeys.has(key)
74
+ ? `pack ${contrib.packageName}: ${planeLabel} "${key}" already provided by local config — skipped`
75
+ : `pack ${contrib.packageName}: ${planeLabel} "${key}" already provided — skipped`);
76
+ continue;
77
+ }
78
+ merged.set(key, item);
79
+ }
80
+ }
81
+ return [...merged.values()];
82
+ }
83
+ /** Collect every pack contribution file for one manifest slot across valid packs. */
84
+ function gatherPackContribs(validPacks, slot) {
85
+ const out = [];
86
+ for (const pack of validPacks) {
87
+ // Mirror sharkcraft-inspector's pack-merge: read the slot off the
88
+ // contributions bag with a narrow cast (the new slots are all string[]).
89
+ const contributions = pack.manifest?.contributions;
90
+ const files = contributions?.[slot];
91
+ for (const rel of files ?? []) {
92
+ out.push({ packageName: pack.packageName, packageRoot: pack.packageRoot, rel });
93
+ }
94
+ }
95
+ return out;
96
+ }
97
+ /**
98
+ * Load the project config, then merge pack-contributed `wiringRules` /
99
+ * `registries` / `policyRules` / `reusePrimitives` over the local config
100
+ * (local-wins). Returns the loader error untouched on failure, so callers keep
101
+ * the same "invalid config vs. valid-with-no-rules" distinction they had with
102
+ * {@link loadProjectConfig}. A pack-discovery failure degrades to a diagnostic
103
+ * — it never fails config resolution.
104
+ */
105
+ export async function resolveProjectConfig(cwd) {
106
+ const loaded = await loadProjectConfig(cwd);
107
+ if (!loaded.ok)
108
+ return loaded;
109
+ const diagnostics = [];
110
+ const base = loaded.value;
111
+ let validPacks = [];
112
+ try {
113
+ const packs = await discoverPacks({ projectRoot: base.projectRoot });
114
+ validPacks = packs.validPacks;
115
+ }
116
+ catch (e) {
117
+ diagnostics.push(`pack discovery failed — pack-contributed planes skipped: ${e.message}`);
118
+ }
119
+ const wiringRules = await mergePlane(base.config.wiringRules ?? [], gatherPackContribs(validPacks, 'wiringRuleFiles'), WiringRuleSchema, (r) => r.id, 'wiringRule', diagnostics);
120
+ const registries = await mergePlane(base.config.registries ?? [], gatherPackContribs(validPacks, 'registryFiles'), RegistryDeclarationSchema, (r) => r.name, 'registry', diagnostics);
121
+ const policyRules = await mergePlane(base.config.policyRules ?? [], gatherPackContribs(validPacks, 'policyRuleFiles'), PolicyRuleSchema, (r) => r.id, 'policyRule', diagnostics);
122
+ const reusePrimitives = await mergePlane(base.config.reusePrimitives ?? [], gatherPackContribs(validPacks, 'reusePrimitiveFiles'), ReusePrimitiveSchema, (r) => r.symbol, 'reusePrimitive', diagnostics);
123
+ return ok({
124
+ ...base,
125
+ config: { ...base.config, wiringRules, registries, policyRules, reusePrimitives },
126
+ planeDiagnostics: diagnostics,
127
+ });
128
+ }
@@ -17,6 +17,13 @@ import type { ISharkcraftInspection } from './sharkcraft-inspector.js';
17
17
  * keeping the real post-change gates. Rules carry no verification field, so
18
18
  * they are intentionally not a source here.
19
19
  *
20
+ * Package-manager templating: any command may use a `<pm-run>` or `<pm>`
21
+ * placeholder that is substituted with the project's detected package manager
22
+ * at consume time (e.g. a pack playbook ships `<pm-run> test` and it resolves
23
+ * to `bun run test` / `npm run test` / `pnpm test` per the target repo). The
24
+ * substitution runs before the placeholder-exclusion check, so a templated
25
+ * gate survives while a truly generative `<task>` step is still dropped.
26
+ *
20
27
  * Deterministic, order-preserving, deduped. No commands are executed.
21
28
  */
22
29
  export declare function resolveVerificationCommands(inspection: ISharkcraftInspection, options?: {
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-verification-commands.d.ts","sourceRoot":"","sources":["../src/resolve-verification-commands.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,qBAAqB,EACjC,OAAO,GAAE;IACP,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C,GACL,MAAM,EAAE,CA2BV"}
1
+ {"version":3,"file":"resolve-verification-commands.d.ts","sourceRoot":"","sources":["../src/resolve-verification-commands.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAEvE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,2BAA2B,CACzC,UAAU,EAAE,qBAAqB,EACjC,OAAO,GAAE;IACP,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C,GACL,MAAM,EAAE,CA8BV"}
@@ -1,4 +1,5 @@
1
1
  import { PipelineStepType } from '@shrkcrft/pipelines';
2
+ import { PackageManager, WorkspaceProfile, } from '@shrkcrft/workspace';
2
3
  /**
3
4
  * Resolve the verification commands an agent should run after a task, grounded
4
5
  * in the pack rather than a generic default. Precedence (first non-empty wins):
@@ -17,9 +18,17 @@ import { PipelineStepType } from '@shrkcrft/pipelines';
17
18
  * keeping the real post-change gates. Rules carry no verification field, so
18
19
  * they are intentionally not a source here.
19
20
  *
21
+ * Package-manager templating: any command may use a `<pm-run>` or `<pm>`
22
+ * placeholder that is substituted with the project's detected package manager
23
+ * at consume time (e.g. a pack playbook ships `<pm-run> test` and it resolves
24
+ * to `bun run test` / `npm run test` / `pnpm test` per the target repo). The
25
+ * substitution runs before the placeholder-exclusion check, so a templated
26
+ * gate survives while a truly generative `<task>` step is still dropped.
27
+ *
20
28
  * Deterministic, order-preserving, deduped. No commands are executed.
21
29
  */
22
30
  export function resolveVerificationCommands(inspection, options = {}) {
31
+ const ws = inspection.workspace;
23
32
  const fromPipelines = [];
24
33
  for (const id of options.pipelineIds ?? []) {
25
34
  const pipeline = inspection.pipelineRegistry.get(id);
@@ -31,7 +40,7 @@ export function resolveVerificationCommands(inspection, options = {}) {
31
40
  if (step.required === false)
32
41
  continue;
33
42
  for (const raw of step.cliCommands ?? []) {
34
- const cmd = raw.trim();
43
+ const cmd = substitutePmPlaceholders(raw.trim(), ws);
35
44
  if (cmd.length > 0 && !cmd.includes('<'))
36
45
  fromPipelines.push(cmd);
37
46
  }
@@ -42,13 +51,53 @@ export function resolveVerificationCommands(inspection, options = {}) {
42
51
  const cfg = inspection.config;
43
52
  const fromConfig = [];
44
53
  for (const vc of cfg?.verificationCommands ?? []) {
45
- const cmd = vc?.command?.trim();
46
- if (cmd && cmd.length > 0)
54
+ const cmd = substitutePmPlaceholders(vc?.command?.trim() ?? '', ws);
55
+ if (cmd.length > 0)
47
56
  fromConfig.push(cmd);
48
57
  }
49
58
  if (fromConfig.length > 0)
50
59
  return dedupe(fromConfig);
51
- return dedupe(options.knowledgeDefaults ?? []);
60
+ return dedupe((options.knowledgeDefaults ?? []).map((c) => substitutePmPlaceholders(c, ws)));
61
+ }
62
+ /**
63
+ * Replace package-manager placeholders in a single command with the project's
64
+ * detected toolchain. `<pm-run>` → the run-prefix (`bun run`, `npm run`,
65
+ * `pnpm`, `yarn`); `<pm>` → the bare manager name (`bun`, `npm`, `pnpm`,
66
+ * `yarn`). A no-op (and zero workspace access) when the command carries no
67
+ * `<pm` token, so non-templated callers and stubbed inspections are unaffected.
68
+ */
69
+ function substitutePmPlaceholders(command, ws) {
70
+ if (!command.includes('<pm'))
71
+ return command;
72
+ const manager = effectivePackageManager(ws);
73
+ return command
74
+ .replaceAll('<pm-run>', packageManagerRunPrefix(manager))
75
+ .replaceAll('<pm>', bareManager(manager));
76
+ }
77
+ function effectivePackageManager(ws) {
78
+ const detected = ws?.packageManager?.manager;
79
+ if (detected && detected !== PackageManager.Unknown)
80
+ return detected;
81
+ if (ws?.profiles?.includes(WorkspaceProfile.HasBun))
82
+ return PackageManager.Bun;
83
+ return PackageManager.Npm;
84
+ }
85
+ function packageManagerRunPrefix(manager) {
86
+ switch (manager) {
87
+ case PackageManager.Bun:
88
+ return 'bun run';
89
+ case PackageManager.Pnpm:
90
+ return 'pnpm';
91
+ case PackageManager.Yarn:
92
+ return 'yarn';
93
+ case PackageManager.Npm:
94
+ return 'npm run';
95
+ default:
96
+ return 'npm run';
97
+ }
98
+ }
99
+ function bareManager(manager) {
100
+ return manager === PackageManager.Unknown ? 'npm' : manager;
52
101
  }
53
102
  function dedupe(items) {
54
103
  return [...new Set(items)];
@@ -44,6 +44,14 @@ export interface IBuildReviewPacketOptions {
44
44
  since?: string;
45
45
  staged?: boolean;
46
46
  files?: readonly string[];
47
+ /**
48
+ * Working-tree default only: when undefined/true, non-ignored untracked
49
+ * files are included — so a just-generated, never-staged source file is
50
+ * visible to the review (and to the missing-test heuristic). Set false to
51
+ * restore the legacy tracked-only `git diff` view. Ignored when
52
+ * `since`/`staged`/`files` is set.
53
+ */
54
+ untracked?: boolean;
47
55
  }
48
56
  export declare function buildReviewPacket(inspection: ISharkcraftInspection, options?: IBuildReviewPacketOptions): IReviewPacket;
49
57
  //# sourceMappingURL=review-packet.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"review-packet.d.ts","sourceRoot":"","sources":["../src/review-packet.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAIvE,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,kEAAkE;IAClE,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,8CAA8C;IAC9C,aAAa,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxE,2CAA2C;IAC3C,iBAAiB,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3D,iBAAiB,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5D,2DAA2D;IAC3D,kBAAkB,EAAE,SAAS;QAC3B,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,eAAe,EAAE,MAAM,CAAC;QACxB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,EAAE,CAAC;IACJ,+DAA+D;IAC/D,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,4DAA4D;IAC5D,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,mEAAmE;IACnE,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3B;AAyCD,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,qBAAqB,EACjC,OAAO,GAAE,yBAA8B,GACtC,aAAa,CA+Ef"}
1
+ {"version":3,"file":"review-packet.d.ts","sourceRoot":"","sources":["../src/review-packet.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAKvE,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,kEAAkE;IAClE,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,8CAA8C;IAC9C,aAAa,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACxE,2CAA2C;IAC3C,iBAAiB,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC3D,iBAAiB,EAAE,SAAS;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5D,2DAA2D;IAC3D,kBAAkB,EAAE,SAAS;QAC3B,MAAM,EAAE,MAAM,CAAC;QACf,IAAI,EAAE,MAAM,CAAC;QACb,eAAe,EAAE,MAAM,CAAC;QACxB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;KACjB,EAAE,CAAC;IACJ,+DAA+D;IAC/D,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,4DAA4D;IAC5D,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,mEAAmE;IACnE,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAwCD,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,qBAAqB,EACjC,OAAO,GAAE,yBAA8B,GACtC,aAAa,CA+Ef"}
@@ -1,24 +1,23 @@
1
- import { spawnSync } from 'node:child_process';
2
1
  import { evaluateBoundaries, loadTsconfigPaths, scanImports } from '@shrkcrft/boundaries';
3
2
  import { matchAffectedConventions } from '@shrkcrft/paths';
4
3
  import { resolveVerificationCommands } from "./resolve-verification-commands.js";
5
4
  import { rankAll } from "./task-ranker.js";
5
+ import { getChangedFiles } from "./git-helpers.js";
6
6
  function gitDiffFiles(cwd, opts) {
7
7
  if (opts.files && opts.files.length > 0)
8
8
  return [...opts.files];
9
- const args = ['diff', '--name-only'];
10
9
  if (opts.staged)
11
- args.push('--cached');
10
+ return getChangedFiles(cwd, { staged: true });
12
11
  if (opts.since)
13
- args.push(opts.since);
14
- const res = spawnSync('git', args, { cwd, encoding: 'utf8' });
15
- if (res.status !== 0)
16
- return [];
17
- return (res.stdout ?? '')
18
- .toString()
19
- .split('\n')
20
- .map((s) => s.trim())
21
- .filter((s) => s.length > 0);
12
+ return getChangedFiles(cwd, { since: opts.since });
13
+ // Default working-tree view: include non-ignored untracked files so a
14
+ // just-generated, never-staged source file is visible to review (and to the
15
+ // "is each new src/ file tested?" heuristic below). Mirrors `shrk changes
16
+ // summary` and every other working-tree caller; `.gitignore` stays honored.
17
+ // `{ untracked: false }` restores the legacy tracked-only `git diff` view.
18
+ if (opts.untracked === false)
19
+ return getChangedFiles(cwd, {});
20
+ return getChangedFiles(cwd, { includeWorktree: true });
22
21
  }
23
22
  function buildPseudoTask(files) {
24
23
  // Use the top-level dir of each file as a pseudo-task description so the
@@ -1 +1 @@
1
- {"version":3,"file":"sharkcraft-inspector.d.ts","sourceRoot":"","sources":["../src/sharkcraft-inspector.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sharkcraft-inspector.d.ts","sourceRoot":"","sources":["../src/sharkcraft-inspector.ts"],"names":[],"mappings":"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"}
@@ -14,6 +14,7 @@ import { BoundaryRegistry, loadBoundaryRulesFromFile } from '@shrkcrft/boundarie
14
14
  import { DoctorSeverity } from "./doctor-result.js";
15
15
  import { diagnoseActionHints } from "./action-hint-diagnostics.js";
16
16
  import { buildCodeIntelligenceChecks } from "./code-intelligence-doctor.js";
17
+ import { loadSearchTuning } from "./search-tuning-registry.js";
17
18
  import { buildDelegateRecipeChecks } from "./delegate-doctor.js";
18
19
  import { computeFileFingerprint, createInspectorCache, } from "./inspector-cache.js";
19
20
  import { DEFAULT_SLOW_LOADER_THRESHOLD_MS, LARGE_FILE_THRESHOLD_BYTES, } from "./loader-diagnostics.js";
@@ -422,8 +423,23 @@ export async function inspectSharkcraft(options = {}) {
422
423
  await loadFile(rel, 'knowledge');
423
424
  for (const rel of c.ruleFiles ?? [])
424
425
  await loadFile(rel, 'rules');
425
- for (const rel of c.pathFiles ?? [])
426
+ const loadedPathRels = new Set();
427
+ for (const rel of c.pathFiles ?? []) {
428
+ loadedPathRels.add(rel);
426
429
  await loadFile(rel, 'paths');
430
+ }
431
+ // `pathConventionFiles` is a manifest slot distinct from `pathFiles` (see the
432
+ // plugin-api comment "separate from pathFiles"), but it still feeds the path
433
+ // domain. Previously NO loader consumed it, so a pack shipping conventions
434
+ // here loaded nothing. Load it through the same path loader, skipping any rel
435
+ // already handled by `pathFiles` so a file listed in both slots doesn't
436
+ // double-load (entry ids are also deduped by `loadFile`).
437
+ for (const rel of c.pathConventionFiles ?? []) {
438
+ if (loadedPathRels.has(rel))
439
+ continue;
440
+ loadedPathRels.add(rel);
441
+ await loadFile(rel, 'paths');
442
+ }
427
443
  for (const rel of c.docsFiles ?? [])
428
444
  await loadFile(rel, 'docs');
429
445
  for (const rel of c.templateFiles ?? [])
@@ -550,7 +566,7 @@ export async function inspectSharkcraft(options = {}) {
550
566
  if (tracked.skipped)
551
567
  warnings.push(...tracked.warnings);
552
568
  }
553
- return {
569
+ const inspection = {
554
570
  projectRoot: workspace.projectRoot,
555
571
  workspace,
556
572
  hasSharkcraftFolder: workspace.hasSharkcraftFolder,
@@ -581,6 +597,16 @@ export async function inspectSharkcraft(options = {}) {
581
597
  cacheEnabled: cache.enabled,
582
598
  cacheDir: cache.dir,
583
599
  };
600
+ // Warm the pack search-tuning cache (best-effort; existsSync-gated + cached
601
+ // per projectRoot) so the synchronous buildTaskPacket → rankAll path can read
602
+ // listSearchTuning and apply pack boostIds — the same tuning `shrk search` uses.
603
+ try {
604
+ await loadSearchTuning(inspection);
605
+ }
606
+ catch {
607
+ /* tuning is best-effort */
608
+ }
609
+ return inspection;
584
610
  }
585
611
  export function runDoctor(inspection) {
586
612
  const checks = [];
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=spec-evidence.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-evidence.test.d.ts","sourceRoot":"","sources":["../../../src/spec/__tests__/spec-evidence.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { mapChecklistToEvidence, } from "../spec-evidence.js";
3
+ describe('mapChecklistToEvidence', () => {
4
+ it('marks exactly the unimplemented criterion UNMET', () => {
5
+ const criteria = [
6
+ { id: 'ac-1', text: 'Add a coverage flag to spec verify' },
7
+ { id: 'ac-2', text: 'Export a telemetry dashboard widget' },
8
+ ];
9
+ // The changeset implements ac-1 only (coverage), nothing for ac-2.
10
+ const changedFiles = [
11
+ 'packages/cli/src/commands/spec.command.ts',
12
+ 'packages/inspector/src/spec/__tests__/spec-coverage.test.ts',
13
+ ];
14
+ const fileContents = {
15
+ 'packages/cli/src/commands/spec.command.ts': [
16
+ 'export function specVerifyCoverageReport() {',
17
+ ' return mapChecklistToEvidence();',
18
+ '}',
19
+ ].join('\n'),
20
+ 'packages/inspector/src/spec/__tests__/spec-coverage.test.ts': [
21
+ "import { specVerifyCoverageReport } from '../x.ts';",
22
+ "it('reports coverage', () => {});",
23
+ ].join('\n'),
24
+ };
25
+ const report = mapChecklistToEvidence({ criteria, changedFiles, fileContents });
26
+ expect(report.criteria).toHaveLength(2);
27
+ const covered = report.criteria.find((c) => c.id === 'ac-1');
28
+ const unmet = report.criteria.find((c) => c.id === 'ac-2');
29
+ expect(covered.covered).toBe(true);
30
+ expect(covered.evidence.length).toBeGreaterThan(0);
31
+ // The exported symbol naming the feature is concrete backing evidence.
32
+ expect(covered.evidence.some((e) => e.kind === 'symbol')).toBe(true);
33
+ expect(unmet.covered).toBe(false);
34
+ expect(unmet.evidence).toHaveLength(0);
35
+ expect(report.coveredCount).toBe(1);
36
+ expect(report.unmetCount).toBe(1);
37
+ // Exactly the unimplemented criterion is UNMET.
38
+ expect(report.criteria.filter((c) => !c.covered).map((c) => c.id)).toEqual(['ac-2']);
39
+ });
40
+ it('recognises a new test file as backing evidence', () => {
41
+ const report = mapChecklistToEvidence({
42
+ criteria: [{ id: 'ac-1', text: 'Wire a deterministic compaction pass' }],
43
+ changedFiles: ['packages/compress/src/__tests__/compaction.test.ts'],
44
+ fileContents: {
45
+ 'packages/compress/src/__tests__/compaction.test.ts': "it('compacts', () => {});",
46
+ },
47
+ });
48
+ const c = report.criteria[0];
49
+ expect(c.covered).toBe(true);
50
+ expect(c.evidence.every((e) => e.kind === 'test')).toBe(true);
51
+ expect(report.unmetCount).toBe(0);
52
+ });
53
+ it('flags a registration / array membership as evidence', () => {
54
+ const report = mapChecklistToEvidence({
55
+ criteria: [{ id: 'ac-1', text: 'Register the telemetry handler' }],
56
+ changedFiles: ['packages/cli/src/registry.ts'],
57
+ fileContents: {
58
+ 'packages/cli/src/registry.ts': [
59
+ 'const handlerMap = {',
60
+ ' telemetry: telemetryHandler,',
61
+ '};',
62
+ ].join('\n'),
63
+ },
64
+ });
65
+ const c = report.criteria[0];
66
+ expect(c.covered).toBe(true);
67
+ expect(c.evidence.some((e) => e.kind === 'registration' || e.kind === 'route')).toBe(true);
68
+ });
69
+ it('reports every criterion UNMET when the changeset is empty', () => {
70
+ const report = mapChecklistToEvidence({
71
+ criteria: [
72
+ { id: 'ac-1', text: 'Add coverage mapping' },
73
+ { id: 'ac-2', text: 'Add telemetry dashboard' },
74
+ ],
75
+ changedFiles: [],
76
+ fileContents: {},
77
+ });
78
+ expect(report.coveredCount).toBe(0);
79
+ expect(report.unmetCount).toBe(2);
80
+ });
81
+ });
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Deterministic feature-checklist → changeset-evidence mapper.
3
+ *
4
+ * For each acceptance criterion of a spec, scan the changeset (the set of
5
+ * tracked + untracked changed files plus their current contents) for
6
+ * backing evidence:
7
+ *
8
+ * - a new exported symbol whose name relates to the criterion,
9
+ * - a new registration / array membership,
10
+ * - a new route / command registration,
11
+ * - a touched companion file whose path relates to the criterion,
12
+ * - a new test file.
13
+ *
14
+ * A criterion with zero footprint across the whole changeset is `UNMET`
15
+ * (claimed-but-unimplemented). There is NO model in the loop: matching is a
16
+ * pure keyword/identifier intersection between the criterion text and the
17
+ * identifiers/paths added in the changeset.
18
+ *
19
+ * This module is read-only and side-effect free; the caller is responsible
20
+ * for collecting `changedFiles` + `fileContents` (see the CLI
21
+ * `collectChangedPaths` helper and `spec verify --coverage`).
22
+ */
23
+ /** The shape of evidence backing (or claiming to back) a criterion. */
24
+ export type SpecEvidenceKind = 'symbol' | 'registration' | 'route' | 'companion' | 'test';
25
+ export interface ISpecEvidenceItem {
26
+ readonly kind: SpecEvidenceKind;
27
+ /** Changeset-relative path the evidence was found in. */
28
+ readonly file: string;
29
+ /** The matched identifier / path token / line fragment. */
30
+ readonly detail: string;
31
+ /** The criterion keyword that produced the match. */
32
+ readonly matched: string;
33
+ }
34
+ export interface ICriterionCoverage {
35
+ readonly id: string;
36
+ readonly text: string;
37
+ /** True iff at least one piece of evidence was found in the changeset. */
38
+ readonly covered: boolean;
39
+ readonly evidence: readonly ISpecEvidenceItem[];
40
+ }
41
+ export interface IChecklistCriterionInput {
42
+ readonly id: string;
43
+ readonly text: string;
44
+ }
45
+ export interface IMapChecklistToEvidenceInput {
46
+ readonly criteria: readonly IChecklistCriterionInput[];
47
+ readonly changedFiles: readonly string[];
48
+ /** Map of changed-file path → current file contents. Missing = treated as empty. */
49
+ readonly fileContents: Readonly<Record<string, string>>;
50
+ }
51
+ export interface IChecklistEvidenceReport {
52
+ readonly criteria: readonly ICriterionCoverage[];
53
+ readonly coveredCount: number;
54
+ /** Criteria with zero footprint in the changeset (claimed-but-unimplemented). */
55
+ readonly unmetCount: number;
56
+ }
57
+ /**
58
+ * Map a spec checklist to changeset evidence. Pure; deterministic for a
59
+ * given input (stable iteration over `changedFiles` and sorted keywords).
60
+ */
61
+ export declare function mapChecklistToEvidence(input: IMapChecklistToEvidenceInput): IChecklistEvidenceReport;
62
+ //# sourceMappingURL=spec-evidence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spec-evidence.d.ts","sourceRoot":"","sources":["../../src/spec/spec-evidence.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,uEAAuE;AACvE,MAAM,MAAM,gBAAgB,GAAG,QAAQ,GAAG,cAAc,GAAG,OAAO,GAAG,WAAW,GAAG,MAAM,CAAC;AAE1F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,SAAS,iBAAiB,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,QAAQ,EAAE,SAAS,wBAAwB,EAAE,CAAC;IACvD,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,oFAAoF;IACpF,QAAQ,CAAC,YAAY,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACzD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACjD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iFAAiF;IACjF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAgCD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,4BAA4B,GAClC,wBAAwB,CAQ1B"}