@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.
- package/dist/changes-summary.d.ts +23 -1
- package/dist/changes-summary.d.ts.map +1 -1
- package/dist/changes-summary.js +122 -3
- package/dist/plan-simulation.d.ts.map +1 -1
- package/dist/plan-simulation.js +9 -0
- package/dist/registry-lifecycle.d.ts +26 -0
- package/dist/registry-lifecycle.d.ts.map +1 -1
- package/dist/registry-lifecycle.js +130 -18
- package/package.json +17 -17
|
@@ -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
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"changes-summary.d.ts","sourceRoot":"","sources":["../src/changes-summary.ts"],"names":[],"mappings":"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"}
|
package/dist/changes-summary.js
CHANGED
|
@@ -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
|
-
|
|
102
|
-
|
|
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
|
|
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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"}
|
package/dist/plan-simulation.js
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
235
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
46
|
-
"@shrkcrft/config": "^0.1.0-alpha.
|
|
47
|
-
"@shrkcrft/workspace": "^0.1.0-alpha.
|
|
48
|
-
"@shrkcrft/knowledge": "^0.1.0-alpha.
|
|
49
|
-
"@shrkcrft/rules": "^0.1.0-alpha.
|
|
50
|
-
"@shrkcrft/paths": "^0.1.0-alpha.
|
|
51
|
-
"@shrkcrft/templates": "^0.1.0-alpha.
|
|
52
|
-
"@shrkcrft/context": "^0.1.0-alpha.
|
|
53
|
-
"@shrkcrft/pipelines": "^0.1.0-alpha.
|
|
54
|
-
"@shrkcrft/packs": "^0.1.0-alpha.
|
|
55
|
-
"@shrkcrft/presets": "^0.1.0-alpha.
|
|
56
|
-
"@shrkcrft/boundaries": "^0.1.0-alpha.
|
|
57
|
-
"@shrkcrft/generator": "^0.1.0-alpha.
|
|
58
|
-
"@shrkcrft/importer": "^0.1.0-alpha.
|
|
59
|
-
"@shrkcrft/plugin-api": "^0.1.0-alpha.
|
|
60
|
-
"@shrkcrft/dashboard-api": "^0.1.0-alpha.
|
|
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": {
|