@shrkcrft/inspector 0.1.0-alpha.24 → 0.1.0-alpha.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,6 +23,14 @@ export declare enum ChangeArea {
23
23
  Tests = "tests",
24
24
  Examples = "examples",
25
25
  Scripts = "scripts",
26
+ /**
27
+ * Build / CI / tooling directories (`.github/`, `.husky/`, `ci/`, `tooling/`,
28
+ * `build/`). A generic, project-agnostic bucket so tooling paths stop
29
+ * falling through to {@link ChangeArea.Unknown}. Distinct from
30
+ * {@link ChangeArea.Config} (which is for manifest/config *files*) and from
31
+ * {@link ChangeArea.Scripts} (the dedicated `scripts/` area).
32
+ */
33
+ Tooling = "tooling",
26
34
  E2E = "e2e",
27
35
  Sharkcraft = "sharkcraft-self-config",
28
36
  Reports = "reports",
@@ -30,7 +38,13 @@ export declare enum ChangeArea {
30
38
  }
31
39
  export interface IChangedFileSummary {
32
40
  path: string;
33
- area: ChangeArea;
41
+ /**
42
+ * Resolved area. One of the built-in {@link ChangeArea} slugs, OR a
43
+ * project-declared area (a boundary-rule tag/id or a monorepo package name)
44
+ * when the built-in taxonomy doesn't cover the file. `'unknown'` only when no
45
+ * declared taxonomy matches either — a high unknown rate is itself a signal.
46
+ */
47
+ area: string;
34
48
  isTest: boolean;
35
49
  isDoc: boolean;
36
50
  isMcpTool: boolean;
@@ -48,6 +62,14 @@ export interface IChangesSummaryReport {
48
62
  roundLabel?: string;
49
63
  totalFiles: number;
50
64
  filesByArea: Record<string, readonly string[]>;
65
+ /**
66
+ * Fraction (0..1) of changed files that resolved to no declared area. A high
67
+ * rate is a classifier self-diagnostic — the declared taxonomy is missing
68
+ * globs, not that the diff is low-risk.
69
+ */
70
+ unknownRate: number;
71
+ /** Count of files bucketed as `unknown` (the numerator behind `unknownRate`). */
72
+ unknownFiles: number;
51
73
  files: readonly IChangedFileSummary[];
52
74
  /** Touched CLI command files (relative). */
53
75
  touchedCommands: readonly string[];
@@ -1 +1 @@
1
- {"version":3,"file":"changes-summary.d.ts","sourceRoot":"","sources":["../src/changes-summary.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"changes-summary.d.ts","sourceRoot":"","sources":["../src/changes-summary.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,7 @@
11
11
  *
12
12
  * Read-only; no AI; deterministic. Schema: sharkcraft.changes-summary/v1.
13
13
  */
14
+ import { globToRegex } from '@shrkcrft/boundaries';
14
15
  import { getChangedFiles } from "./git-helpers.js";
15
16
  import { resolveVerificationCommands } from "./resolve-verification-commands.js";
16
17
  export const CHANGES_SUMMARY_SCHEMA = 'sharkcraft.changes-summary/v1';
@@ -38,6 +39,14 @@ export var ChangeArea;
38
39
  ChangeArea["Tests"] = "tests";
39
40
  ChangeArea["Examples"] = "examples";
40
41
  ChangeArea["Scripts"] = "scripts";
42
+ /**
43
+ * Build / CI / tooling directories (`.github/`, `.husky/`, `ci/`, `tooling/`,
44
+ * `build/`). A generic, project-agnostic bucket so tooling paths stop
45
+ * falling through to {@link ChangeArea.Unknown}. Distinct from
46
+ * {@link ChangeArea.Config} (which is for manifest/config *files*) and from
47
+ * {@link ChangeArea.Scripts} (the dedicated `scripts/` area).
48
+ */
49
+ ChangeArea["Tooling"] = "tooling";
41
50
  ChangeArea["E2E"] = "e2e";
42
51
  ChangeArea["Sharkcraft"] = "sharkcraft-self-config";
43
52
  ChangeArea["Reports"] = "reports";
@@ -96,10 +105,112 @@ function classifyArea(file) {
96
105
  // doc as `unknown`.
97
106
  if (/\.mdx?$/i.test(file))
98
107
  return ChangeArea.Docs;
108
+ // NOTE: generic config / tooling / manifest paths are NOT resolved here. They
109
+ // are a FALLBACK layer applied by `buildAreaResolver` *after* the declared
110
+ // taxonomy (see `classifyConfigTooling`), so a project's own area declaration
111
+ // still wins for config paths it explicitly claims.
99
112
  return ChangeArea.Unknown;
100
113
  }
101
- function classifyFile(file) {
102
- const area = classifyArea(file);
114
+ /**
115
+ * Built-in, project-AGNOSTIC config / tooling / manifest classifier.
116
+ *
117
+ * This is a FALLBACK layer: `buildAreaResolver` consults it only *after* the
118
+ * declared taxonomy (boundary `from` globs + monorepo package roots), so a
119
+ * project that explicitly claims a config path keeps its own area. It exists so
120
+ * the large slice of a typical changeset that is build / CI / tooling /
121
+ * manifest / config stops falling through to {@link ChangeArea.Unknown} — which
122
+ * (with its loud `unknown = taxonomy gap` diagnostic) is now reserved for
123
+ * *genuinely* unclassifiable paths.
124
+ *
125
+ * Ordering is deterministic and most-specific-wins: a recognized manifest /
126
+ * config *filename* (→ {@link ChangeArea.Config}) is matched before a generic
127
+ * tooling *directory* (→ {@link ChangeArea.Tooling}). Returns `undefined` when
128
+ * nothing matches so the caller can fall through to `unknown`.
129
+ */
130
+ function classifyConfigTooling(file) {
131
+ const base = file.slice(file.lastIndexOf('/') + 1).toLowerCase();
132
+ // Manifests / config files (matched by basename, at repo root or anywhere) →
133
+ // config. Covers: package.json, tsconfig*.json, *.config.{ts,js,mjs,cjs,json},
134
+ // nx.json, biome.json, .eslintrc*, .prettierrc*, bunfig.toml, lockfiles,
135
+ // .npmrc, .editorconfig.
136
+ if (base === 'package.json' ||
137
+ base === 'nx.json' ||
138
+ base === 'biome.json' ||
139
+ base === 'bunfig.toml' ||
140
+ base === '.npmrc' ||
141
+ base === '.editorconfig' ||
142
+ base === 'package-lock.json' ||
143
+ /^tsconfig(?:\..+)?\.json$/.test(base) ||
144
+ /\.config\.(?:ts|js|mjs|cjs|json)$/.test(base) ||
145
+ /^\.eslintrc(?:\..+)?$/.test(base) ||
146
+ /^\.prettierrc(?:\..+)?$/.test(base) ||
147
+ // Lockfiles: yarn.lock, pnpm-lock.yaml, bun.lock(b), *.lock / *.lockb.
148
+ base === 'pnpm-lock.yaml' ||
149
+ /\.lockb?$/.test(base)) {
150
+ return ChangeArea.Config;
151
+ }
152
+ // CI / build / tooling directories → tooling. (`scripts/` is intentionally
153
+ // handled earlier by `classifyArea` as its own dedicated area.) `build/` is
154
+ // treated as build *scripts*; genuine build *output* dirs (e.g. `dist/`) are
155
+ // not claimed here.
156
+ if (/(?:^|\/)(?:\.github|\.husky|ci|tooling|build)\//.test(file)) {
157
+ return ChangeArea.Tooling;
158
+ }
159
+ return undefined;
160
+ }
161
+ /** Generic monorepo package/app root → area name. */
162
+ const PACKAGE_ROOT_RE = /^(?:packages|apps|libs|modules|services)\/([^/]+)\//;
163
+ /**
164
+ * Build a resolver that maps a file to a declared area, falling back to the
165
+ * built-in SharkCraft prefixes. Sources, in priority order:
166
+ * 1. the built-in {@link classifyArea} slugs (nice names for this repo);
167
+ * 2. the boundary gate's own `from` globs — the declared layer/area taxonomy;
168
+ * 3. a generic monorepo package/app root (`packages/<name>/…` → `<name>`), so
169
+ * a foreign monorepo's packages resolve instead of bucketing to `unknown`;
170
+ * 4. built-in, project-agnostic config / tooling / manifest patterns
171
+ * ({@link classifyConfigTooling}) — the FALLBACK layer that runs after the
172
+ * declared taxonomy so config paths (manifests, lockfiles, CI/build dirs)
173
+ * resolve to `config` / `tooling` instead of `unknown`.
174
+ * A file only stays `unknown` when NONE of these match — and a high unknown rate
175
+ * is surfaced as a self-diagnostic (`unknownRate`), not a low-risk diff.
176
+ */
177
+ function buildAreaResolver(inspection) {
178
+ const boundaryMatchers = [];
179
+ try {
180
+ for (const rule of inspection.boundaryRegistry?.list() ?? []) {
181
+ const from = rule.from ?? [];
182
+ if (from.length === 0)
183
+ continue;
184
+ boundaryMatchers.push({
185
+ area: rule.tags?.[0] ?? rule.id,
186
+ matchers: from.map((g) => globToRegex(g)),
187
+ });
188
+ }
189
+ }
190
+ catch {
191
+ // Boundary registry unavailable — fall through to built-ins + package roots.
192
+ }
193
+ return (file) => {
194
+ const builtin = classifyArea(file);
195
+ if (builtin !== ChangeArea.Unknown)
196
+ return builtin;
197
+ for (const b of boundaryMatchers) {
198
+ if (b.matchers.some((re) => re.test(file)))
199
+ return b.area;
200
+ }
201
+ const pkg = PACKAGE_ROOT_RE.exec(file);
202
+ if (pkg?.[1])
203
+ return pkg[1];
204
+ // Built-in config / tooling fallback — runs last, before `unknown`, so the
205
+ // declared taxonomy (steps 2–3) still wins where it applies.
206
+ const configTooling = classifyConfigTooling(file);
207
+ if (configTooling)
208
+ return configTooling;
209
+ return ChangeArea.Unknown;
210
+ };
211
+ }
212
+ function classifyFile(file, resolveArea) {
213
+ const area = resolveArea(file);
103
214
  const lower = file.toLowerCase();
104
215
  const isTest = lower.includes('__tests__/') ||
105
216
  lower.endsWith('.test.ts') ||
@@ -246,8 +357,11 @@ export async function buildChangesSummary(inspection, options = {}) {
246
357
  changedFiles = getChangedFiles(cwd, { includeWorktree: true });
247
358
  source = 'working-tree';
248
359
  }
249
- const classified = changedFiles.map(classifyFile);
360
+ const resolveArea = buildAreaResolver(inspection);
361
+ const classified = changedFiles.map((f) => classifyFile(f, resolveArea));
250
362
  const filesByArea = groupByArea(classified);
363
+ const unknownFiles = classified.filter((c) => c.area === ChangeArea.Unknown).length;
364
+ const unknownRate = classified.length > 0 ? unknownFiles / classified.length : 0;
251
365
  const touchedCommands = classified.filter((c) => c.isCliCommand).map((c) => c.path);
252
366
  const touchedMcpTools = classified.filter((c) => c.isMcpTool).map((c) => c.path);
253
367
  const touchedSchemas = classified.filter((c) => c.isSchema).map((c) => c.path);
@@ -264,6 +378,8 @@ export async function buildChangesSummary(inspection, options = {}) {
264
378
  ...(options.roundLabel ? { roundLabel: options.roundLabel } : {}),
265
379
  totalFiles: classified.length,
266
380
  filesByArea,
381
+ unknownRate,
382
+ unknownFiles,
267
383
  files: classified,
268
384
  touchedCommands,
269
385
  touchedMcpTools,
@@ -296,6 +412,9 @@ export function renderChangesSummaryMarkdown(report) {
296
412
  for (const f of files)
297
413
  lines.push(`- ${f}`);
298
414
  }
415
+ if (report.unknownFiles > 0) {
416
+ lines.push(`> ⚠ ${report.unknownFiles}/${report.totalFiles} file(s) (${Math.round(report.unknownRate * 100)}%) matched no declared area — the layer/area taxonomy may be missing globs (classifier diagnostic).`);
417
+ }
299
418
  if (report.touchedCommands.length > 0) {
300
419
  lines.push('');
301
420
  lines.push('## Touched CLI commands');
@@ -1 +1 @@
1
- {"version":3,"file":"plan-simulation.d.ts","sourceRoot":"","sources":["../src/plan-simulation.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plan-simulation.d.ts","sourceRoot":"","sources":["../src/plan-simulation.ts"],"names":[],"mappings":"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"}
@@ -522,6 +522,15 @@ export async function simulatePlan(inspection, planPath, options = {}) {
522
522
  if (potentialBoundaryConcerns.length > 0 || planIntroducedBoundaryConcerns.length > 0) {
523
523
  requiredValidations.add('shrk architecture violations');
524
524
  }
525
+ // Scale the required-validation wall to BLAST RADIUS: a one-file scaffold and a
526
+ // multi-file / multi-package construct shouldn't get the same wall. A broad
527
+ // change earns the composite changed-only "safe to finish?" verdict (project-
528
+ // agnostic — always a valid shrk command) on top of the per-file checks.
529
+ const touchedPackages = new Set(sourceList.map((c) => /^((?:packages|apps|libs|modules|services)\/[^/]+)\//.exec(c.relativePath)?.[1] ?? '(root)'));
530
+ const blastRadius = { files: sourceList.length, packages: touchedPackages.size };
531
+ if (blastRadius.files >= 5 || blastRadius.packages >= 2) {
532
+ requiredValidations.add('shrk finish');
533
+ }
525
534
  // Affected constructs / playbooks (lightweight).
526
535
  await loadConstructs(inspection);
527
536
  const constructList = listConstructs(inspection) ?? [];
@@ -37,6 +37,10 @@ export interface IRegistryLifecycleReport {
37
37
  totalFiles: number;
38
38
  /** True when the scan hit the file cap and did NOT see every candidate file. */
39
39
  truncated: boolean;
40
+ /** True when the scan hit its wall-clock budget and flushed partial results. */
41
+ timedOut: boolean;
42
+ /** True when the scan was scoped to the changed file set (`--changed-only`). */
43
+ changedOnly: boolean;
40
44
  /** Project-relative subtree the scan was scoped to, when `--scope` was used. */
41
45
  scope?: string;
42
46
  registersFound: number;
@@ -51,11 +55,33 @@ export interface IRegistryLifecycleReport {
51
55
  ignored: ReadonlyArray<IRegistryIgnored>;
52
56
  recommendations: ReadonlyArray<string>;
53
57
  }
58
+ /**
59
+ * Default directories the walk skips — build artefacts + non-source trees that
60
+ * inflate the scan without owning runtime lifecycle. This is the DEFAULT only;
61
+ * a repo that genuinely registers code under `tools/`, a non-standard root, etc.
62
+ * can override the set via config (`registryLifecycle.skipDirs`) / the engine's
63
+ * `skipDirs` input, so a hardcoded exclusion never silently blinds the check.
64
+ */
65
+ export declare const DEFAULT_REGISTRY_LIFECYCLE_SKIP_DIRS: readonly string[];
54
66
  export declare function buildRegistryLifecycleReport(input: {
55
67
  projectRoot: string;
56
68
  limit?: number;
57
69
  /** Project-relative subtree to scope the scan to (sub-second on a subtree). */
58
70
  scope?: string;
71
+ /**
72
+ * Changed-only scope: scan JUST these files (project-relative or absolute)
73
+ * instead of walking the tree. Non-`.ts/.tsx` entries are ignored. When set,
74
+ * an empty relevant set is reported as a loud skip, never a green pass.
75
+ */
76
+ files?: readonly string[];
77
+ /** Hard wall-clock budget (ms). The scan flushes partial results on timeout. */
78
+ budgetMs?: number;
79
+ /**
80
+ * Directory names the walk skips. Overrides {@link DEFAULT_REGISTRY_LIFECYCLE_SKIP_DIRS}
81
+ * so a repo that registers code under `tools/` / a non-standard root isn't
82
+ * silently blinded by a baked-in exclusion. Ignored on the changed-only path.
83
+ */
84
+ skipDirs?: readonly string[];
59
85
  }): IRegistryLifecycleReport;
60
86
  export declare function renderRegistryLifecycleReportText(report: IRegistryLifecycleReport): string;
61
87
  //# sourceMappingURL=registry-lifecycle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"registry-lifecycle.d.ts","sourceRoot":"","sources":["../src/registry-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,kCAAkC,CAAC;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,SAAS,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAC3C,eAAe,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAC;IACxD;;;;OAIG;IACH,gBAAgB,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAClD,OAAO,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACzC,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACxC;AAiMD,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,wBAAwB,CA2F3B;AAED,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAkC1F"}
1
+ {"version":3,"file":"registry-lifecycle.d.ts","sourceRoot":"","sources":["../src/registry-lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAKH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,oBAAoB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,kCAAkC,CAAC;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,UAAU,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,SAAS,EAAE,OAAO,CAAC;IACnB,gFAAgF;IAChF,QAAQ,EAAE,OAAO,CAAC;IAClB,gFAAgF;IAChF,WAAW,EAAE,OAAO,CAAC;IACrB,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,aAAa,CAAC,aAAa,CAAC,CAAC;IAC3C,eAAe,EAAE,aAAa,CAAC,uBAAuB,CAAC,CAAC;IACxD;;;;OAIG;IACH,gBAAgB,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IAClD,OAAO,EAAE,aAAa,CAAC,gBAAgB,CAAC,CAAC;IACzC,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACxC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oCAAoC,EAAE,SAAS,MAAM,EAiBhE,CAAC;AAuOH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE;IAClD,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B,GAAG,wBAAwB,CA6H3B;AAED,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAwD1F"}
@@ -11,7 +11,14 @@
11
11
  */
12
12
  import { readdirSync, readFileSync } from 'node:fs';
13
13
  import { extname, join, relative } from 'node:path';
14
- const SKIP_DIRS = new Set([
14
+ /**
15
+ * Default directories the walk skips — build artefacts + non-source trees that
16
+ * inflate the scan without owning runtime lifecycle. This is the DEFAULT only;
17
+ * a repo that genuinely registers code under `tools/`, a non-standard root, etc.
18
+ * can override the set via config (`registryLifecycle.skipDirs`) / the engine's
19
+ * `skipDirs` input, so a hardcoded exclusion never silently blinds the check.
20
+ */
21
+ export const DEFAULT_REGISTRY_LIFECYCLE_SKIP_DIRS = Object.freeze([
15
22
  'node_modules',
16
23
  'dist',
17
24
  '.git',
@@ -20,8 +27,20 @@ const SKIP_DIRS = new Set([
20
27
  '.nx',
21
28
  'build',
22
29
  'out',
30
+ 'examples',
31
+ 'e2e',
32
+ 'fixtures',
33
+ '__fixtures__',
34
+ 'scripts',
35
+ 'tools',
36
+ '.agents',
37
+ '.github',
23
38
  ]);
24
39
  const SCAN_EXTENSIONS = new Set(['.ts', '.tsx']);
40
+ /** Skip pathological single files — a 200 KB command file has no registry to pair. */
41
+ const MAX_SCAN_FILE_BYTES = 256 * 1024;
42
+ /** Hard wall-clock budget so the scan fails loud (partial flush) instead of hanging. */
43
+ const DEFAULT_BUDGET_MS = 15_000;
25
44
  function isGeneratedFile(file, content) {
26
45
  if (file.endsWith('.d.ts'))
27
46
  return true;
@@ -31,7 +50,13 @@ function isGeneratedFile(file, content) {
31
50
  return true;
32
51
  return false;
33
52
  }
34
- function walk(dir, projectRoot, out) {
53
+ function walk(dir, projectRoot, out, skipDirs, deadline) {
54
+ // The wall-clock budget must bound the TREE WALK too, not only the per-file
55
+ // scan loop — a pathological tree could otherwise eat the whole budget before
56
+ // the loop's deadline check ever runs (a25 §2.5). Bail the walk on expiry;
57
+ // partial candidates flow through and the caller flags timedOut.
58
+ if (deadline !== undefined && Date.now() > deadline)
59
+ return;
35
60
  let entries;
36
61
  try {
37
62
  entries = readdirSync(dir, { withFileTypes: true });
@@ -40,7 +65,9 @@ function walk(dir, projectRoot, out) {
40
65
  return;
41
66
  }
42
67
  for (const e of entries) {
43
- if (SKIP_DIRS.has(e.name))
68
+ if (deadline !== undefined && Date.now() > deadline)
69
+ return;
70
+ if (skipDirs.has(e.name))
44
71
  continue;
45
72
  if (e.name.startsWith('.') && e.name !== '.sharkcraft') {
46
73
  // skip hidden dotfiles
@@ -49,7 +76,7 @@ function walk(dir, projectRoot, out) {
49
76
  }
50
77
  const abs = join(dir, e.name);
51
78
  if (e.isDirectory()) {
52
- walk(abs, projectRoot, out);
79
+ walk(abs, projectRoot, out, skipDirs, deadline);
53
80
  }
54
81
  else if (e.isFile() && SCAN_EXTENSIONS.has(extname(e.name))) {
55
82
  out.push(abs);
@@ -174,7 +201,38 @@ function stripCommentsAndLiterals(content) {
174
201
  }
175
202
  return out.join('');
176
203
  }
177
- function findRegistersInFile(content) {
204
+ /**
205
+ * Precompute the byte offset of each line start, so an offset → line lookup is
206
+ * O(log n) instead of the O(fileLen) `content.slice(0, i).split('\n')` done PER
207
+ * MATCH (the original quadratic hotspot — a 57 KB file with 200 `register*`
208
+ * tokens paid ~200 × 57 KB of slicing). Built once per file.
209
+ */
210
+ function buildLineStarts(content) {
211
+ const starts = [0];
212
+ for (let i = 0; i < content.length; i += 1) {
213
+ if (content.charCodeAt(i) === 10 /* \n */)
214
+ starts.push(i + 1);
215
+ }
216
+ return starts;
217
+ }
218
+ /** 1-based line number for a byte offset, via binary search over line starts. */
219
+ function lineAtOffset(lineStarts, offset) {
220
+ let lo = 0;
221
+ let hi = lineStarts.length - 1;
222
+ let ans = 0;
223
+ while (lo <= hi) {
224
+ const mid = (lo + hi) >> 1;
225
+ if (lineStarts[mid] <= offset) {
226
+ ans = mid;
227
+ lo = mid + 1;
228
+ }
229
+ else {
230
+ hi = mid - 1;
231
+ }
232
+ }
233
+ return ans + 1;
234
+ }
235
+ function findRegistersInFile(content, lineStarts) {
178
236
  const out = [];
179
237
  const seen = new Set();
180
238
  for (const re of REGISTER_PATTERNS) {
@@ -185,8 +243,7 @@ function findRegistersInFile(content) {
185
243
  if (seen.has(`${name}@${m.index}`))
186
244
  continue;
187
245
  seen.add(`${name}@${m.index}`);
188
- const line = content.slice(0, m.index).split('\n').length;
189
- out.push({ name, line });
246
+ out.push({ name, line: lineAtOffset(lineStarts, m.index) });
190
247
  }
191
248
  }
192
249
  return out;
@@ -217,13 +274,12 @@ function expectedRemoverNames(registerName) {
217
274
  `${stem}Off`,
218
275
  ];
219
276
  }
220
- function findRemoverInContent(content, candidates) {
277
+ function findRemoverInContent(content, candidates, lineStarts) {
221
278
  for (const name of candidates) {
222
279
  const re = new RegExp(`(?:export\\s+)?(?:async\\s+)?function\\s+${name}\\s*\\(|\\b${name}\\s*=\\s*\\(|(?:public|private|protected)?\\s*(?:async\\s+)?${name}\\s*\\(`);
223
280
  const m = content.match(re);
224
281
  if (m && typeof m.index === 'number') {
225
- const line = content.slice(0, m.index).split('\n').length;
226
- return { name, line };
282
+ return { name, line: lineAtOffset(lineStarts, m.index) };
227
283
  }
228
284
  }
229
285
  return null;
@@ -231,17 +287,43 @@ function findRemoverInContent(content, candidates) {
231
287
  export function buildRegistryLifecycleReport(input) {
232
288
  const { projectRoot } = input;
233
289
  const scope = input.scope && input.scope.length > 0 ? input.scope : undefined;
234
- const walkRoot = scope ? join(projectRoot, scope) : projectRoot;
235
- const files = [];
236
- walk(walkRoot, projectRoot, files);
290
+ const changedOnly = input.files !== undefined;
291
+ const deadline = Date.now() + (input.budgetMs ?? DEFAULT_BUDGET_MS);
237
292
  const limit = input.limit ?? 2000;
293
+ const skipDirs = new Set(input.skipDirs ?? DEFAULT_REGISTRY_LIFECYCLE_SKIP_DIRS);
294
+ // Candidate files: either the explicit changed set (bypassing the walk) or a
295
+ // bounded tree walk. The changed set resolves relative paths under the root
296
+ // and keeps only scannable extensions.
297
+ let files;
298
+ if (input.files !== undefined) {
299
+ files = input.files
300
+ .map((f) => (f.startsWith('/') ? f : join(projectRoot, f)))
301
+ .filter((f) => SCAN_EXTENSIONS.has(extname(f)));
302
+ }
303
+ else {
304
+ const walkRoot = scope ? join(projectRoot, scope) : projectRoot;
305
+ files = [];
306
+ walk(walkRoot, projectRoot, files, skipDirs, deadline);
307
+ }
238
308
  const scanFiles = files.slice(0, limit);
239
309
  const matchedPairs = [];
240
310
  const missingRemovers = [];
241
311
  const oneShotBootstrap = [];
242
312
  const ignored = [];
243
313
  let registersFound = 0;
314
+ // The tree walk itself is now deadline-bounded (a25 §2.5), so an exhausted
315
+ // budget can leave `scanFiles` empty and the per-file loop below would never
316
+ // run to set the flag. Seed it from the deadline so a walk-level timeout is
317
+ // reported (NOT-verified `2`) rather than read as a clean zero-registration pass.
318
+ let timedOut = Date.now() > deadline;
319
+ let filesScanned = 0;
244
320
  for (const file of scanFiles) {
321
+ // Hard wall-clock budget checked between files — the scan is synchronous, so
322
+ // a Date.now() deadline is the only interruption point. Flush partial results.
323
+ if (Date.now() > deadline) {
324
+ timedOut = true;
325
+ break;
326
+ }
245
327
  let content;
246
328
  try {
247
329
  content = readFileSync(file, 'utf8');
@@ -249,12 +331,19 @@ export function buildRegistryLifecycleReport(input) {
249
331
  catch {
250
332
  continue;
251
333
  }
334
+ // Per-file size cap: a huge file (a big command/aggregate module) is a
335
+ // catastrophic-backtracking / quadratic hazard and never holds a registry
336
+ // that owes a remover. Skip it rather than let one file wedge the scan.
337
+ if (content.length > MAX_SCAN_FILE_BYTES)
338
+ continue;
252
339
  if (isGeneratedFile(file, content))
253
340
  continue;
341
+ filesScanned += 1;
254
342
  // Scan declarations on code with comments/strings blanked out; keep the raw
255
343
  // content only for the comment-based @shrkcrft annotations.
256
344
  const code = stripCommentsAndLiterals(content);
257
- const registers = findRegistersInFile(code);
345
+ const lineStarts = buildLineStarts(code);
346
+ const registers = findRegistersInFile(code, lineStarts);
258
347
  // Per-file lifecycle evidence: only a file that BOTH accumulates removable
259
348
  // state AND has a teardown-shaped API plausibly owes a per-register remover.
260
349
  const hasAccumulation = ACCUMULATION_RE.test(code);
@@ -274,7 +363,7 @@ export function buildRegistryLifecycleReport(input) {
274
363
  continue;
275
364
  }
276
365
  const candidates = expectedRemoverNames(reg.name);
277
- const remover = findRemoverInContent(code, candidates);
366
+ const remover = findRemoverInContent(code, candidates, lineStarts);
278
367
  if (remover) {
279
368
  matchedPairs.push({
280
369
  registerName: reg.name,
@@ -310,9 +399,11 @@ export function buildRegistryLifecycleReport(input) {
310
399
  }
311
400
  return {
312
401
  schema: 'sharkcraft.registry-lifecycle/v1',
313
- filesScanned: scanFiles.length,
402
+ filesScanned,
314
403
  totalFiles: files.length,
315
- truncated: files.length > scanFiles.length,
404
+ truncated: files.length > scanFiles.length || timedOut,
405
+ timedOut,
406
+ changedOnly,
316
407
  ...(scope ? { scope } : {}),
317
408
  registersFound,
318
409
  matchedPairs,
@@ -327,13 +418,34 @@ export function renderRegistryLifecycleReportText(report) {
327
418
  lines.push('=== Registry lifecycle ===');
328
419
  if (report.scope)
329
420
  lines.push(` scope ${report.scope}`);
421
+ if (report.changedOnly)
422
+ lines.push(' scope changed-only');
423
+ // Loud skip: a changed diff that touches no scannable file was NOT verified —
424
+ // never let "scanned nothing" read as a clean pass.
425
+ if (report.changedOnly && report.filesScanned === 0) {
426
+ lines.push('');
427
+ lines.push(' ! 0 files in the changed scope — lifecycle NOT verified (this is not a pass).');
428
+ return lines.join('\n') + '\n';
429
+ }
430
+ if (report.timedOut) {
431
+ lines.push(` ! wall-clock budget hit — scanned ${report.filesScanned}/${report.totalFiles} before flushing partial results (re-run with --scope <dir> to finish).`);
432
+ }
330
433
  lines.push(` files scanned ${report.filesScanned}` +
331
- (report.truncated ? ` ! capped (${report.totalFiles} candidates — re-run with --scope <dir> for the rest)` : ''));
434
+ (report.truncated && !report.timedOut
435
+ ? ` ! capped (${report.totalFiles} candidates — re-run with --scope <dir> for the rest)`
436
+ : ''));
332
437
  lines.push(` registers found ${report.registersFound}`);
333
438
  lines.push(` matched pairs ${report.matchedPairs.length}`);
334
439
  lines.push(` missing removers ${report.missingRemovers.length}`);
335
440
  lines.push(` one-shot bootstrap ${report.oneShotBootstrap.length}`);
336
441
  lines.push(` ignored ${report.ignored.length}`);
442
+ // Zero registrations in scope means nothing was actually checked — say so
443
+ // loudly so it never reads as the green "all removers present" pass (the exit
444
+ // code is NOT-verified `2` to match; a25 §1 / §2.5).
445
+ if (report.registersFound === 0 && !report.timedOut) {
446
+ lines.push('');
447
+ lines.push(' ! 0 registrations found in scope — lifecycle NOT verified (this is not a pass).');
448
+ }
337
449
  lines.push('');
338
450
  if (report.missingRemovers.length > 0) {
339
451
  lines.push('Missing removers:');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/inspector",
3
- "version": "0.1.0-alpha.24",
3
+ "version": "0.1.0-alpha.26",
4
4
  "description": "SharkCraft inspector: project overview, doctor checks, AI-agent instructions.",
5
5
  "license": "MIT",
6
6
  "author": "SharkCraft contributors",
@@ -42,22 +42,22 @@
42
42
  "typecheck": "tsc --noEmit -p tsconfig.json"
43
43
  },
44
44
  "dependencies": {
45
- "@shrkcrft/core": "^0.1.0-alpha.24",
46
- "@shrkcrft/config": "^0.1.0-alpha.24",
47
- "@shrkcrft/workspace": "^0.1.0-alpha.24",
48
- "@shrkcrft/knowledge": "^0.1.0-alpha.24",
49
- "@shrkcrft/rules": "^0.1.0-alpha.24",
50
- "@shrkcrft/paths": "^0.1.0-alpha.24",
51
- "@shrkcrft/templates": "^0.1.0-alpha.24",
52
- "@shrkcrft/context": "^0.1.0-alpha.24",
53
- "@shrkcrft/pipelines": "^0.1.0-alpha.24",
54
- "@shrkcrft/packs": "^0.1.0-alpha.24",
55
- "@shrkcrft/presets": "^0.1.0-alpha.24",
56
- "@shrkcrft/boundaries": "^0.1.0-alpha.24",
57
- "@shrkcrft/generator": "^0.1.0-alpha.24",
58
- "@shrkcrft/importer": "^0.1.0-alpha.24",
59
- "@shrkcrft/plugin-api": "^0.1.0-alpha.24",
60
- "@shrkcrft/dashboard-api": "^0.1.0-alpha.24",
45
+ "@shrkcrft/core": "^0.1.0-alpha.26",
46
+ "@shrkcrft/config": "^0.1.0-alpha.26",
47
+ "@shrkcrft/workspace": "^0.1.0-alpha.26",
48
+ "@shrkcrft/knowledge": "^0.1.0-alpha.26",
49
+ "@shrkcrft/rules": "^0.1.0-alpha.26",
50
+ "@shrkcrft/paths": "^0.1.0-alpha.26",
51
+ "@shrkcrft/templates": "^0.1.0-alpha.26",
52
+ "@shrkcrft/context": "^0.1.0-alpha.26",
53
+ "@shrkcrft/pipelines": "^0.1.0-alpha.26",
54
+ "@shrkcrft/packs": "^0.1.0-alpha.26",
55
+ "@shrkcrft/presets": "^0.1.0-alpha.26",
56
+ "@shrkcrft/boundaries": "^0.1.0-alpha.26",
57
+ "@shrkcrft/generator": "^0.1.0-alpha.26",
58
+ "@shrkcrft/importer": "^0.1.0-alpha.26",
59
+ "@shrkcrft/plugin-api": "^0.1.0-alpha.26",
60
+ "@shrkcrft/dashboard-api": "^0.1.0-alpha.26",
61
61
  "typescript": "^5.6.0"
62
62
  },
63
63
  "publishConfig": {