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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -30,7 +30,13 @@ export declare enum ChangeArea {
30
30
  }
31
31
  export interface IChangedFileSummary {
32
32
  path: string;
33
- area: ChangeArea;
33
+ /**
34
+ * Resolved area. One of the built-in {@link ChangeArea} slugs, OR a
35
+ * project-declared area (a boundary-rule tag/id or a monorepo package name)
36
+ * when the built-in taxonomy doesn't cover the file. `'unknown'` only when no
37
+ * declared taxonomy matches either — a high unknown rate is itself a signal.
38
+ */
39
+ area: string;
34
40
  isTest: boolean;
35
41
  isDoc: boolean;
36
42
  isMcpTool: boolean;
@@ -48,6 +54,14 @@ export interface IChangesSummaryReport {
48
54
  roundLabel?: string;
49
55
  totalFiles: number;
50
56
  filesByArea: Record<string, readonly string[]>;
57
+ /**
58
+ * Fraction (0..1) of changed files that resolved to no declared area. A high
59
+ * rate is a classifier self-diagnostic — the declared taxonomy is missing
60
+ * globs, not that the diff is low-risk.
61
+ */
62
+ unknownRate: number;
63
+ /** Count of files bucketed as `unknown` (the numerator behind `unknownRate`). */
64
+ unknownFiles: number;
51
65
  files: readonly IChangedFileSummary[];
52
66
  /** Touched CLI command files (relative). */
53
67
  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":"AAgBA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAEvE,eAAO,MAAM,sBAAsB,kCAAkC,CAAC;AAEtE,oBAAY,UAAU;IACpB,GAAG,QAAQ;IACX,SAAS,eAAe;IACxB,SAAS,cAAc;IACvB,SAAS,cAAc;IACvB,SAAS,cAAc;IACvB,KAAK,UAAU;IACf,SAAS,cAAc;IACvB,OAAO,YAAY;IACnB,UAAU,eAAe;IACzB,KAAK,UAAU;IACf,IAAI,SAAS;IACb,SAAS,cAAc;IACvB,MAAM,WAAW;IACjB,SAAS,eAAe;IACxB,QAAQ,aAAa;IACrB,SAAS,cAAc;IACvB,WAAW,uBAAuB;IAClC,YAAY,kBAAkB;IAC9B,IAAI,SAAS;IACb,KAAK,UAAU;IACf,QAAQ,aAAa;IACrB,OAAO,YAAY;IACnB,GAAG,QAAQ;IACX,UAAU,2BAA2B;IACrC,OAAO,YAAY;IACnB,OAAO,YAAY;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,OAAO,CAAC;IACnB,YAAY,EAAE,OAAO,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,gBAAgB,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,OAAO,sBAAsB,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,cAAc,CAAC;IACtD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IAC/C;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACtC,4CAA4C;IAC5C,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,8BAA8B;IAC9B,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,gDAAgD;IAChD,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,0BAA0B;IAC1B,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,0BAA0B;IAC1B,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,2CAA2C;IAC3C,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,qCAAqC;IACrC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,0CAA0C;IAC1C,IAAI,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAChC,wCAAwC;IACxC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,qCAAqC;IACrC,2BAA2B,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/C,qCAAqC;IACrC,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,sBAAsB;IACrC,qCAAqC;IACrC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6BAA6B;IAC7B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAmND,wBAAsB,mBAAmB,CACvC,UAAU,EAAE,qBAAqB,EACjC,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC,CA2DhC;AAED,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAsDlF"}
@@ -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';
@@ -98,8 +99,50 @@ function classifyArea(file) {
98
99
  return ChangeArea.Docs;
99
100
  return ChangeArea.Unknown;
100
101
  }
101
- function classifyFile(file) {
102
- const area = classifyArea(file);
102
+ /** Generic monorepo package/app root → area name. */
103
+ const PACKAGE_ROOT_RE = /^(?:packages|apps|libs|modules|services)\/([^/]+)\//;
104
+ /**
105
+ * Build a resolver that maps a file to a declared area, falling back to the
106
+ * built-in SharkCraft prefixes. Sources, in priority order:
107
+ * 1. the built-in {@link classifyArea} slugs (nice names for this repo);
108
+ * 2. the boundary gate's own `from` globs — the declared layer/area taxonomy;
109
+ * 3. a generic monorepo package/app root (`packages/<name>/…` → `<name>`), so
110
+ * a foreign monorepo's packages resolve instead of bucketing to `unknown`.
111
+ * A file only stays `unknown` when NONE of these match — and a high unknown rate
112
+ * is surfaced as a self-diagnostic (`unknownRate`), not a low-risk diff.
113
+ */
114
+ function buildAreaResolver(inspection) {
115
+ const boundaryMatchers = [];
116
+ try {
117
+ for (const rule of inspection.boundaryRegistry?.list() ?? []) {
118
+ const from = rule.from ?? [];
119
+ if (from.length === 0)
120
+ continue;
121
+ boundaryMatchers.push({
122
+ area: rule.tags?.[0] ?? rule.id,
123
+ matchers: from.map((g) => globToRegex(g)),
124
+ });
125
+ }
126
+ }
127
+ catch {
128
+ // Boundary registry unavailable — fall through to built-ins + package roots.
129
+ }
130
+ return (file) => {
131
+ const builtin = classifyArea(file);
132
+ if (builtin !== ChangeArea.Unknown)
133
+ return builtin;
134
+ for (const b of boundaryMatchers) {
135
+ if (b.matchers.some((re) => re.test(file)))
136
+ return b.area;
137
+ }
138
+ const pkg = PACKAGE_ROOT_RE.exec(file);
139
+ if (pkg?.[1])
140
+ return pkg[1];
141
+ return ChangeArea.Unknown;
142
+ };
143
+ }
144
+ function classifyFile(file, resolveArea) {
145
+ const area = resolveArea(file);
103
146
  const lower = file.toLowerCase();
104
147
  const isTest = lower.includes('__tests__/') ||
105
148
  lower.endsWith('.test.ts') ||
@@ -246,8 +289,11 @@ export async function buildChangesSummary(inspection, options = {}) {
246
289
  changedFiles = getChangedFiles(cwd, { includeWorktree: true });
247
290
  source = 'working-tree';
248
291
  }
249
- const classified = changedFiles.map(classifyFile);
292
+ const resolveArea = buildAreaResolver(inspection);
293
+ const classified = changedFiles.map((f) => classifyFile(f, resolveArea));
250
294
  const filesByArea = groupByArea(classified);
295
+ const unknownFiles = classified.filter((c) => c.area === ChangeArea.Unknown).length;
296
+ const unknownRate = classified.length > 0 ? unknownFiles / classified.length : 0;
251
297
  const touchedCommands = classified.filter((c) => c.isCliCommand).map((c) => c.path);
252
298
  const touchedMcpTools = classified.filter((c) => c.isMcpTool).map((c) => c.path);
253
299
  const touchedSchemas = classified.filter((c) => c.isSchema).map((c) => c.path);
@@ -264,6 +310,8 @@ export async function buildChangesSummary(inspection, options = {}) {
264
310
  ...(options.roundLabel ? { roundLabel: options.roundLabel } : {}),
265
311
  totalFiles: classified.length,
266
312
  filesByArea,
313
+ unknownRate,
314
+ unknownFiles,
267
315
  files: classified,
268
316
  touchedCommands,
269
317
  touchedMcpTools,
@@ -296,6 +344,9 @@ export function renderChangesSummaryMarkdown(report) {
296
344
  for (const f of files)
297
345
  lines.push(`- ${f}`);
298
346
  }
347
+ if (report.unknownFiles > 0) {
348
+ 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).`);
349
+ }
299
350
  if (report.touchedCommands.length > 0) {
300
351
  lines.push('');
301
352
  lines.push('## Touched CLI commands');
@@ -1 +1 @@
1
- {"version":3,"file":"plan-simulation.d.ts","sourceRoot":"","sources":["../src/plan-simulation.ts"],"names":[],"mappings":"AAwBA,OAAO,EAAmB,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAK3E,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAEvE,eAAO,MAAM,sBAAsB,kCAAkC,CAAC;AAEtE,oBAAY,8BAA8B;IACxC,KAAK,UAAU;IACf,cAAc,oBAAoB;IAClC,QAAQ,aAAa;IACrB,gBAAgB,sBAAsB;IACtC,UAAU,gBAAgB;CAC3B;AAED,oBAAY,kBAAkB;IAC5B,KAAK,UAAU;IACf,eAAe,sBAAsB;IACrC,gBAAgB,sBAAsB;IACtC,aAAa,mBAAmB;IAChC,eAAe,qBAAqB;IACpC,gBAAgB,sBAAsB;IACtC,oBAAoB,2BAA2B;CAChD;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,8BAA8B,CAAC;IACxC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB;;qCAEiC;IACjC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;gCAC4B;IAC5B,gBAAgB,EAAE,OAAO,CAAC;IAC1B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,cAAc,EAAE,OAAO,CAAC;IACxB,8CAA8C;IAC9C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yCAAyC;IACzC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,MAAM,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,OAAO,sBAAsB,CAAC;IACtC,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;IAC5C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACtC,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,uBAAuB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,gBAAgB,EAAE,OAAO,CAAC;IAC1B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,sBAAsB,EAAE,OAAO,CAAC;IAChC,sBAAsB,EAAE,OAAO,CAAC;IAChC,uBAAuB,EAAE,OAAO,CAAC;IACjC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;IACvC,kBAAkB,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,yBAAyB,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACjE,8BAA8B,EAAE,SAAS,0BAA0B,EAAE,CAAC;IACtE,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,cAAc,EAAE,kBAAkB,CAAC;IACnC,qBAAqB,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,YAAY,CAAC,EAAE,iBAAiB,CAAC;IACjC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,sBAAsB;IACrC,iEAAiE;IACjE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,mDAAmD;IACnD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAYD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAoG5K;AA+GD,wBAAsB,YAAY,CAChC,UAAU,EAAE,qBAAqB,EACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,qBAAqB,CAAC,CA6UhC;AAOD,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAwDzE;AAOD,wBAAgB,4BAA4B,CAAC,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAkD7E;AAWD,wBAAgB,wBAAwB,CAAC,CAAC,EAAE,qBAAqB,GAAG,MAAM,CAWzE"}
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;AA2NH,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,CAyH3B;AAED,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,wBAAwB,GAAG,MAAM,CAiD1F"}
@@ -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,7 @@ 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) {
35
54
  let entries;
36
55
  try {
37
56
  entries = readdirSync(dir, { withFileTypes: true });
@@ -40,7 +59,7 @@ function walk(dir, projectRoot, out) {
40
59
  return;
41
60
  }
42
61
  for (const e of entries) {
43
- if (SKIP_DIRS.has(e.name))
62
+ if (skipDirs.has(e.name))
44
63
  continue;
45
64
  if (e.name.startsWith('.') && e.name !== '.sharkcraft') {
46
65
  // skip hidden dotfiles
@@ -49,7 +68,7 @@ function walk(dir, projectRoot, out) {
49
68
  }
50
69
  const abs = join(dir, e.name);
51
70
  if (e.isDirectory()) {
52
- walk(abs, projectRoot, out);
71
+ walk(abs, projectRoot, out, skipDirs);
53
72
  }
54
73
  else if (e.isFile() && SCAN_EXTENSIONS.has(extname(e.name))) {
55
74
  out.push(abs);
@@ -174,7 +193,38 @@ function stripCommentsAndLiterals(content) {
174
193
  }
175
194
  return out.join('');
176
195
  }
177
- function findRegistersInFile(content) {
196
+ /**
197
+ * Precompute the byte offset of each line start, so an offset → line lookup is
198
+ * O(log n) instead of the O(fileLen) `content.slice(0, i).split('\n')` done PER
199
+ * MATCH (the original quadratic hotspot — a 57 KB file with 200 `register*`
200
+ * tokens paid ~200 × 57 KB of slicing). Built once per file.
201
+ */
202
+ function buildLineStarts(content) {
203
+ const starts = [0];
204
+ for (let i = 0; i < content.length; i += 1) {
205
+ if (content.charCodeAt(i) === 10 /* \n */)
206
+ starts.push(i + 1);
207
+ }
208
+ return starts;
209
+ }
210
+ /** 1-based line number for a byte offset, via binary search over line starts. */
211
+ function lineAtOffset(lineStarts, offset) {
212
+ let lo = 0;
213
+ let hi = lineStarts.length - 1;
214
+ let ans = 0;
215
+ while (lo <= hi) {
216
+ const mid = (lo + hi) >> 1;
217
+ if (lineStarts[mid] <= offset) {
218
+ ans = mid;
219
+ lo = mid + 1;
220
+ }
221
+ else {
222
+ hi = mid - 1;
223
+ }
224
+ }
225
+ return ans + 1;
226
+ }
227
+ function findRegistersInFile(content, lineStarts) {
178
228
  const out = [];
179
229
  const seen = new Set();
180
230
  for (const re of REGISTER_PATTERNS) {
@@ -185,8 +235,7 @@ function findRegistersInFile(content) {
185
235
  if (seen.has(`${name}@${m.index}`))
186
236
  continue;
187
237
  seen.add(`${name}@${m.index}`);
188
- const line = content.slice(0, m.index).split('\n').length;
189
- out.push({ name, line });
238
+ out.push({ name, line: lineAtOffset(lineStarts, m.index) });
190
239
  }
191
240
  }
192
241
  return out;
@@ -217,13 +266,12 @@ function expectedRemoverNames(registerName) {
217
266
  `${stem}Off`,
218
267
  ];
219
268
  }
220
- function findRemoverInContent(content, candidates) {
269
+ function findRemoverInContent(content, candidates, lineStarts) {
221
270
  for (const name of candidates) {
222
271
  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
272
  const m = content.match(re);
224
273
  if (m && typeof m.index === 'number') {
225
- const line = content.slice(0, m.index).split('\n').length;
226
- return { name, line };
274
+ return { name, line: lineAtOffset(lineStarts, m.index) };
227
275
  }
228
276
  }
229
277
  return null;
@@ -231,17 +279,39 @@ function findRemoverInContent(content, candidates) {
231
279
  export function buildRegistryLifecycleReport(input) {
232
280
  const { projectRoot } = input;
233
281
  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);
282
+ const changedOnly = input.files !== undefined;
283
+ const deadline = Date.now() + (input.budgetMs ?? DEFAULT_BUDGET_MS);
237
284
  const limit = input.limit ?? 2000;
285
+ const skipDirs = new Set(input.skipDirs ?? DEFAULT_REGISTRY_LIFECYCLE_SKIP_DIRS);
286
+ // Candidate files: either the explicit changed set (bypassing the walk) or a
287
+ // bounded tree walk. The changed set resolves relative paths under the root
288
+ // and keeps only scannable extensions.
289
+ let files;
290
+ if (input.files !== undefined) {
291
+ files = input.files
292
+ .map((f) => (f.startsWith('/') ? f : join(projectRoot, f)))
293
+ .filter((f) => SCAN_EXTENSIONS.has(extname(f)));
294
+ }
295
+ else {
296
+ const walkRoot = scope ? join(projectRoot, scope) : projectRoot;
297
+ files = [];
298
+ walk(walkRoot, projectRoot, files, skipDirs);
299
+ }
238
300
  const scanFiles = files.slice(0, limit);
239
301
  const matchedPairs = [];
240
302
  const missingRemovers = [];
241
303
  const oneShotBootstrap = [];
242
304
  const ignored = [];
243
305
  let registersFound = 0;
306
+ let timedOut = false;
307
+ let filesScanned = 0;
244
308
  for (const file of scanFiles) {
309
+ // Hard wall-clock budget checked between files — the scan is synchronous, so
310
+ // a Date.now() deadline is the only interruption point. Flush partial results.
311
+ if (Date.now() > deadline) {
312
+ timedOut = true;
313
+ break;
314
+ }
245
315
  let content;
246
316
  try {
247
317
  content = readFileSync(file, 'utf8');
@@ -249,12 +319,19 @@ export function buildRegistryLifecycleReport(input) {
249
319
  catch {
250
320
  continue;
251
321
  }
322
+ // Per-file size cap: a huge file (a big command/aggregate module) is a
323
+ // catastrophic-backtracking / quadratic hazard and never holds a registry
324
+ // that owes a remover. Skip it rather than let one file wedge the scan.
325
+ if (content.length > MAX_SCAN_FILE_BYTES)
326
+ continue;
252
327
  if (isGeneratedFile(file, content))
253
328
  continue;
329
+ filesScanned += 1;
254
330
  // Scan declarations on code with comments/strings blanked out; keep the raw
255
331
  // content only for the comment-based @shrkcrft annotations.
256
332
  const code = stripCommentsAndLiterals(content);
257
- const registers = findRegistersInFile(code);
333
+ const lineStarts = buildLineStarts(code);
334
+ const registers = findRegistersInFile(code, lineStarts);
258
335
  // Per-file lifecycle evidence: only a file that BOTH accumulates removable
259
336
  // state AND has a teardown-shaped API plausibly owes a per-register remover.
260
337
  const hasAccumulation = ACCUMULATION_RE.test(code);
@@ -274,7 +351,7 @@ export function buildRegistryLifecycleReport(input) {
274
351
  continue;
275
352
  }
276
353
  const candidates = expectedRemoverNames(reg.name);
277
- const remover = findRemoverInContent(code, candidates);
354
+ const remover = findRemoverInContent(code, candidates, lineStarts);
278
355
  if (remover) {
279
356
  matchedPairs.push({
280
357
  registerName: reg.name,
@@ -310,9 +387,11 @@ export function buildRegistryLifecycleReport(input) {
310
387
  }
311
388
  return {
312
389
  schema: 'sharkcraft.registry-lifecycle/v1',
313
- filesScanned: scanFiles.length,
390
+ filesScanned,
314
391
  totalFiles: files.length,
315
- truncated: files.length > scanFiles.length,
392
+ truncated: files.length > scanFiles.length || timedOut,
393
+ timedOut,
394
+ changedOnly,
316
395
  ...(scope ? { scope } : {}),
317
396
  registersFound,
318
397
  matchedPairs,
@@ -327,8 +406,22 @@ export function renderRegistryLifecycleReportText(report) {
327
406
  lines.push('=== Registry lifecycle ===');
328
407
  if (report.scope)
329
408
  lines.push(` scope ${report.scope}`);
409
+ if (report.changedOnly)
410
+ lines.push(' scope changed-only');
411
+ // Loud skip: a changed diff that touches no scannable file was NOT verified —
412
+ // never let "scanned nothing" read as a clean pass.
413
+ if (report.changedOnly && report.filesScanned === 0) {
414
+ lines.push('');
415
+ lines.push(' ! 0 files in the changed scope — lifecycle NOT verified (this is not a pass).');
416
+ return lines.join('\n') + '\n';
417
+ }
418
+ if (report.timedOut) {
419
+ lines.push(` ! wall-clock budget hit — scanned ${report.filesScanned}/${report.totalFiles} before flushing partial results (re-run with --scope <dir> to finish).`);
420
+ }
330
421
  lines.push(` files scanned ${report.filesScanned}` +
331
- (report.truncated ? ` ! capped (${report.totalFiles} candidates — re-run with --scope <dir> for the rest)` : ''));
422
+ (report.truncated && !report.timedOut
423
+ ? ` ! capped (${report.totalFiles} candidates — re-run with --scope <dir> for the rest)`
424
+ : ''));
332
425
  lines.push(` registers found ${report.registersFound}`);
333
426
  lines.push(` matched pairs ${report.matchedPairs.length}`);
334
427
  lines.push(` missing removers ${report.missingRemovers.length}`);
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.25",
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.25",
46
+ "@shrkcrft/config": "^0.1.0-alpha.25",
47
+ "@shrkcrft/workspace": "^0.1.0-alpha.25",
48
+ "@shrkcrft/knowledge": "^0.1.0-alpha.25",
49
+ "@shrkcrft/rules": "^0.1.0-alpha.25",
50
+ "@shrkcrft/paths": "^0.1.0-alpha.25",
51
+ "@shrkcrft/templates": "^0.1.0-alpha.25",
52
+ "@shrkcrft/context": "^0.1.0-alpha.25",
53
+ "@shrkcrft/pipelines": "^0.1.0-alpha.25",
54
+ "@shrkcrft/packs": "^0.1.0-alpha.25",
55
+ "@shrkcrft/presets": "^0.1.0-alpha.25",
56
+ "@shrkcrft/boundaries": "^0.1.0-alpha.25",
57
+ "@shrkcrft/generator": "^0.1.0-alpha.25",
58
+ "@shrkcrft/importer": "^0.1.0-alpha.25",
59
+ "@shrkcrft/plugin-api": "^0.1.0-alpha.25",
60
+ "@shrkcrft/dashboard-api": "^0.1.0-alpha.25",
61
61
  "typescript": "^5.6.0"
62
62
  },
63
63
  "publishConfig": {