@happyvertical/smrt-dev-mcp 0.60.8 → 0.60.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/AGENTS.md +27 -1
  2. package/README.md +17 -1
  3. package/agents/intent-duplicates.md +26 -0
  4. package/dist/dev-plane.js +2 -2
  5. package/dist/{http-CCiO7qQW.js → http-BqKiyqgN.js} +2 -2
  6. package/dist/{http-CCiO7qQW.js.map → http-BqKiyqgN.js.map} +1 -1
  7. package/dist/index.js +12 -409
  8. package/dist/index.js.map +1 -1
  9. package/dist/knowledge/index.d.ts +1 -1
  10. package/dist/knowledge/index.d.ts.map +1 -1
  11. package/dist/knowledge/intents/audit-trail.md +63 -0
  12. package/dist/knowledge/intents/auth-and-users.md +67 -0
  13. package/dist/knowledge/intents/background-work.md +72 -0
  14. package/dist/knowledge/intents/feature-flags-and-entitlements.md +60 -0
  15. package/dist/knowledge/intents/file-and-asset-storage.md +71 -0
  16. package/dist/knowledge/intents/human-approvals.md +56 -0
  17. package/dist/knowledge/intents/index.md +41 -0
  18. package/dist/knowledge/intents/money-and-rates.md +65 -0
  19. package/dist/knowledge/intents/outbound-messages.md +67 -0
  20. package/dist/knowledge/intents/prompts-and-ai-operations.md +72 -0
  21. package/dist/knowledge/intents/relationships-and-trees.md +56 -0
  22. package/dist/knowledge/intents/scheduled-runs.md +61 -0
  23. package/dist/knowledge/intents/search-facts-knowledge.md +68 -0
  24. package/dist/knowledge/intents/secrets-and-config.md +66 -0
  25. package/dist/knowledge/intents/tenant-scoping.md +63 -0
  26. package/dist/knowledge/intents/webhooks-and-events.md +71 -0
  27. package/dist/knowledge/okf-bundle.d.ts +61 -0
  28. package/dist/knowledge/okf-bundle.d.ts.map +1 -0
  29. package/dist/{knowledge-DqoDX-lZ.js → knowledge-DUiMUjEg.js} +218 -3
  30. package/dist/knowledge-DUiMUjEg.js.map +1 -0
  31. package/dist/knowledge.js +2 -2
  32. package/dist/review-cli-DJIWUlFp.js +75 -0
  33. package/dist/review-cli-DJIWUlFp.js.map +1 -0
  34. package/dist/review-smrt-project-ByEwPcor.js +774 -0
  35. package/dist/review-smrt-project-ByEwPcor.js.map +1 -0
  36. package/dist/smrt-knowledge.okf.md +60 -0
  37. package/dist/{tool-catalog-RHq7nyCm.js → tool-catalog-BhnSCF8g.js} +11 -2
  38. package/dist/tool-catalog-BhnSCF8g.js.map +1 -0
  39. package/dist/tool-catalog.d.ts.map +1 -1
  40. package/dist/tools/intent-detector.d.ts +57 -0
  41. package/dist/tools/intent-detector.d.ts.map +1 -0
  42. package/dist/tools/intent-index.d.ts +40 -0
  43. package/dist/tools/intent-index.d.ts.map +1 -0
  44. package/dist/tools/review-cli.d.ts +40 -0
  45. package/dist/tools/review-cli.d.ts.map +1 -0
  46. package/dist/tools/review-smrt-project.d.ts +25 -0
  47. package/dist/tools/review-smrt-project.d.ts.map +1 -1
  48. package/package.json +6 -6
  49. package/dist/knowledge-DqoDX-lZ.js.map +0 -1
  50. package/dist/tool-catalog-RHq7nyCm.js.map +0 -1
@@ -0,0 +1,774 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { access, readFile, readdir } from "node:fs/promises";
5
+ //#region src/tools/intent-detector.ts
6
+ var COMMENT_LINE = /^\s*(\/\/|\/\*|\*|<!--)/;
7
+ function normalizeSpecifier(value) {
8
+ if (value.startsWith("@")) {
9
+ const [scope, name] = value.split("/");
10
+ return scope && name ? `${scope}/${name}` : value;
11
+ }
12
+ return value.split("/")[0];
13
+ }
14
+ function escapeRegExp(value) {
15
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16
+ }
17
+ /** `*_cents`-style glob (`*` = any run) -> case-insensitive anchored regex. */
18
+ function globToRegExp(glob) {
19
+ const pattern = glob.split("*").map((part) => escapeRegExp(part)).join(".*");
20
+ return new RegExp(`^${pattern}$`, "i");
21
+ }
22
+ function camelToSnake(name) {
23
+ return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
24
+ }
25
+ var DECIMAL_PROPERTY = /^\s*(?:(?:public|private|protected|readonly|static)\s+)*(\w+)\s*(?::\s*number\s*)?=\s*-?\d+\.\d+/;
26
+ var DECIMAL_TYPED_FIELD = /@field\(\s*\{[^}]*type\s*:\s*['"]decimal['"][^}]*\}\s*\)\s*(?:(?:public|private|protected|readonly)\s+)*(\w+)/;
27
+ /**
28
+ * Signal grammar. Add a kind here only when an index row cannot be expressed
29
+ * with the existing ones; intents themselves never appear in this file.
30
+ *
31
+ * - `import:<module>` the module (or a subpath of it) is imported.
32
+ * - `call:<ident>` `ident(` appears in code.
33
+ * - `regex:<pattern>` the JavaScript regular expression matches a line.
34
+ * - `decimal-field:<glob>` a property whose name matches the glob (compared
35
+ * as written and as snake_case) is declared with a decimal literal
36
+ * (`= 0.0`) or `@field({ type: 'decimal' })`.
37
+ */
38
+ var COMPILERS = {
39
+ import: (signal) => ({ matchImport: normalizeSpecifier(signal.value) }),
40
+ call: (signal) => {
41
+ if (!/^[\w$.]+$/.test(signal.value)) return { error: `invalid identifier "${signal.value}"` };
42
+ const pattern = new RegExp(`(?<![\\w$])${escapeRegExp(signal.value)}\\s*\\(`);
43
+ return { matchLine: (line) => pattern.test(line) };
44
+ },
45
+ regex: (signal) => {
46
+ try {
47
+ const pattern = new RegExp(signal.value);
48
+ return { matchLine: (line) => pattern.test(line) };
49
+ } catch (error) {
50
+ return { error: `invalid regular expression (${error instanceof Error ? error.message : String(error)})` };
51
+ }
52
+ },
53
+ "decimal-field": (signal) => {
54
+ const glob = globToRegExp(signal.value);
55
+ const matches = (name) => !!name && (glob.test(name) || glob.test(camelToSnake(name)));
56
+ return { matchLine: (line) => matches(DECIMAL_PROPERTY.exec(line)?.[1]) || matches(DECIMAL_TYPED_FIELD.exec(line)?.[1]) };
57
+ }
58
+ };
59
+ Object.keys(COMPILERS);
60
+ function compileRow(row, noted, onNote) {
61
+ const compiled = [];
62
+ for (const signal of row.signals) {
63
+ const compiler = COMPILERS[signal.kind];
64
+ const skip = (reason) => {
65
+ const key = `${row.intentPage}\0${signal.raw}`;
66
+ if (noted.has(key)) return;
67
+ noted.add(key);
68
+ onNote(`intent index: skipping signal \`${signal.raw}\` for ${row.intentPage} (${reason})`);
69
+ };
70
+ if (!compiler) {
71
+ skip(`unknown signal kind "${signal.kind}"`);
72
+ continue;
73
+ }
74
+ const result = compiler(signal);
75
+ if ("error" in result) skip(result.error);
76
+ else compiled.push({
77
+ raw: signal.raw,
78
+ ...result
79
+ });
80
+ }
81
+ return compiled;
82
+ }
83
+ function importLine(file, specifier) {
84
+ const lines = file.content.split("\n");
85
+ const pattern = new RegExp(`['"]${escapeRegExp(specifier)}(?:/[^'"]*)?['"]`);
86
+ const index = lines.findIndex((line) => !COMMENT_LINE.test(line) && pattern.test(line));
87
+ return index < 0 ? 1 : index + 1;
88
+ }
89
+ /**
90
+ * For every (intent row, package) where a signal is present and the package
91
+ * imports none of the row's home packages, emit one finding.
92
+ */
93
+ function detectHandRolled(rows, packages, options = {}) {
94
+ const onNote = options.onNote ?? (() => void 0);
95
+ const maxEvidence = options.maxEvidence ?? 6;
96
+ const noted = /* @__PURE__ */ new Set();
97
+ const findings = [];
98
+ for (const row of rows) {
99
+ const signals = compileRow(row, noted, onNote);
100
+ if (signals.length === 0 || row.homePackages.length === 0) continue;
101
+ const homeNames = row.homePackages.map((home) => home.packageName);
102
+ for (const pkg of packages) {
103
+ if (pkg.name && homeNames.includes(pkg.name)) continue;
104
+ if (pkg.files.some((file) => file.imports.some((specifier) => homeNames.includes(specifier)))) continue;
105
+ const evidence = [];
106
+ const matched = /* @__PURE__ */ new Set();
107
+ for (const file of pkg.files) for (const signal of signals) if (signal.matchImport !== void 0) {
108
+ if (!file.imports.includes(signal.matchImport)) continue;
109
+ matched.add(signal.raw);
110
+ evidence.push({
111
+ filePath: file.relativePath,
112
+ line: importLine(file, signal.matchImport),
113
+ signal: signal.raw,
114
+ detail: `Imports ${signal.matchImport}`
115
+ });
116
+ } else if (signal.matchLine) {
117
+ const lines = file.content.split("\n");
118
+ for (let index = 0; index < lines.length; index += 1) {
119
+ if (COMMENT_LINE.test(lines[index])) continue;
120
+ if (!signal.matchLine(lines[index])) continue;
121
+ matched.add(signal.raw);
122
+ evidence.push({
123
+ filePath: file.relativePath,
124
+ line: index + 1,
125
+ signal: signal.raw,
126
+ detail: `Matches ${signal.raw}`
127
+ });
128
+ break;
129
+ }
130
+ }
131
+ if (evidence.length === 0) continue;
132
+ evidence.sort((left, right) => left.filePath.localeCompare(right.filePath) || left.line - right.line);
133
+ findings.push({
134
+ code: `hand-rolled-${row.intentPage.replace(/^\/intents\//, "").replace(/\.md$/, "")}`,
135
+ intent: row.intent,
136
+ intentPage: row.intentPage,
137
+ componentPages: row.homePackages.map((home) => home.componentPage),
138
+ entryPoint: row.entryPoint,
139
+ packageName: pkg.name ?? pkg.relativePath,
140
+ packagePath: pkg.relativePath,
141
+ installedHomePackages: homeNames.filter((name) => pkg.installed.has(name)),
142
+ homePackages: homeNames,
143
+ matchedSignals: Array.from(matched),
144
+ evidence: evidence.slice(0, maxEvidence)
145
+ });
146
+ }
147
+ }
148
+ return findings;
149
+ }
150
+ //#endregion
151
+ //#region src/tools/intent-index.ts
152
+ var PACKAGE_SCOPE = "@happyvertical/";
153
+ /** Split a table row on unescaped pipes, unescaping `\|` inside cells. */
154
+ function splitRow(line) {
155
+ const cells = [];
156
+ let current = "";
157
+ for (let index = 0; index < line.length; index += 1) {
158
+ const char = line[index];
159
+ if (char === "\\" && line[index + 1] === "|") {
160
+ current += "|";
161
+ index += 1;
162
+ } else if (char === "|") {
163
+ cells.push(current);
164
+ current = "";
165
+ } else current += char;
166
+ }
167
+ cells.push(current);
168
+ if (cells[0]?.trim() === "") cells.shift();
169
+ if (cells.length > 0 && cells[cells.length - 1].trim() === "") cells.pop();
170
+ return cells.map((cell) => cell.trim());
171
+ }
172
+ function linkTargets(cell) {
173
+ return Array.from(cell.matchAll(/\[([^\]]*)\]\(([^)\s]+)\)/g)).map((match) => ({
174
+ text: match[1],
175
+ href: match[2]
176
+ }));
177
+ }
178
+ /** `/intents/x.md`, `./x.md` or `x.md` -> `/<kind>/x.md`. */
179
+ function conceptId(href, kind) {
180
+ return `/${kind}/${href.split("#")[0].split("/").pop() ?? href}`;
181
+ }
182
+ function parseSignals(cell) {
183
+ const signals = [];
184
+ for (const match of cell.matchAll(/`([^`]+)`/g)) {
185
+ const raw = match[1].trim();
186
+ const colon = raw.indexOf(":");
187
+ if (colon <= 0) {
188
+ signals.push({
189
+ kind: "",
190
+ value: raw,
191
+ raw
192
+ });
193
+ continue;
194
+ }
195
+ signals.push({
196
+ kind: raw.slice(0, colon).trim(),
197
+ value: raw.slice(colon + 1),
198
+ raw
199
+ });
200
+ }
201
+ return signals;
202
+ }
203
+ /**
204
+ * Parse the first intent table found in `markdown`. Columns are located by
205
+ * header name (`Intent`, `Page`, `Home package(s)`, `Entry point`,
206
+ * `Hand-rolled signals`) so column order may change. Rows without a page link
207
+ * are ignored.
208
+ */
209
+ function parseIntentIndex(markdown) {
210
+ const lines = markdown.split(/\r?\n/);
211
+ const headerIndex = lines.findIndex((line) => /^\s*\|/.test(line) && /\bintent\b/i.test(line));
212
+ if (headerIndex < 0) return [];
213
+ const headers = splitRow(lines[headerIndex]).map((cell) => cell.toLowerCase());
214
+ const column = (needle, fallback) => {
215
+ const found = headers.findIndex((header) => header.includes(needle));
216
+ return found >= 0 ? found : fallback;
217
+ };
218
+ const columns = {
219
+ intent: column("intent", 0),
220
+ page: column("page", 1),
221
+ home: column("home", 2),
222
+ entry: column("entry", 3),
223
+ signals: column("signal", 4)
224
+ };
225
+ const rows = [];
226
+ for (const line of lines.slice(headerIndex + 1)) {
227
+ if (!/^\s*\|/.test(line)) {
228
+ if (rows.length > 0 || line.trim() !== "") break;
229
+ continue;
230
+ }
231
+ if (/^\s*\|[\s:|-]+\|\s*$/.test(line)) continue;
232
+ const cells = splitRow(line);
233
+ const pageLink = linkTargets(cells[columns.page] ?? "")[0];
234
+ if (!pageLink) continue;
235
+ const homePackages = linkTargets(cells[columns.home] ?? "").map((link) => {
236
+ const componentPage = conceptId(link.href, "components");
237
+ const name = componentPage.replace(/^\/components\//, "").replace(/\.md$/, "");
238
+ return {
239
+ name,
240
+ packageName: `${PACKAGE_SCOPE}${name}`,
241
+ componentPage
242
+ };
243
+ });
244
+ rows.push({
245
+ intent: cells[columns.intent] ?? "",
246
+ intentPage: conceptId(pageLink.href, "intents"),
247
+ homePackages,
248
+ entryPoint: cells[columns.entry] ?? "",
249
+ signals: parseSignals(cells[columns.signals] ?? "")
250
+ });
251
+ }
252
+ return rows;
253
+ }
254
+ //#endregion
255
+ //#region src/tools/review-smrt-project.ts
256
+ /**
257
+ * Ecosystem-aware downstream project review.
258
+ * Advisory only: scans manifests and source files, then reports alignment risks.
259
+ */
260
+ /** Relative location of the intent index inside this package's `dist/`. */
261
+ var PACKAGED_INTENT_INDEX = join("dist", "knowledge", "intents", "index.md");
262
+ function findOwnPackageRoot() {
263
+ let current = dirname(fileURLToPath(import.meta.url));
264
+ for (;;) {
265
+ const manifest = join(current, "package.json");
266
+ if (existsSync(manifest)) try {
267
+ if (JSON.parse(readFileSync(manifest, "utf-8")).name === "@happyvertical/smrt-dev-mcp") return current;
268
+ } catch {}
269
+ const parent = dirname(current);
270
+ if (parent === current) return null;
271
+ current = parent;
272
+ }
273
+ }
274
+ /**
275
+ * Locations of the trusted default intent index, in resolution order (#4690):
276
+ * the packaged full index, then the monorepo's committed index when this
277
+ * package sits in `<repo>/packages/` of the smrt checkout. The reviewed
278
+ * project's own tree is never consulted.
279
+ */
280
+ function defaultIntentIndexCandidates(packageRoot) {
281
+ const root = packageRoot ?? findOwnPackageRoot();
282
+ if (!root) return [];
283
+ const candidates = [join(root, PACKAGED_INTENT_INDEX)];
284
+ const packagesDir = dirname(root);
285
+ if (basename(packagesDir) === "packages") {
286
+ const repoRoot = dirname(packagesDir);
287
+ if (existsSync(join(repoRoot, "pnpm-workspace.yaml"))) candidates.push(join(repoRoot, "docs", "knowledge", "intents", "index.md"));
288
+ }
289
+ return candidates;
290
+ }
291
+ var SOURCE_EXTENSIONS = /\.(tsx?|jsx?|svelte)$/;
292
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
293
+ "node_modules",
294
+ "dist",
295
+ "build",
296
+ ".git",
297
+ ".smrt",
298
+ ".svelte-kit",
299
+ "coverage"
300
+ ]);
301
+ var DEPENDENCY_SECTIONS = [
302
+ "dependencies",
303
+ "devDependencies",
304
+ "peerDependencies",
305
+ "optionalDependencies"
306
+ ];
307
+ var SMRT_PACKAGE_PREFIX = "@happyvertical/smrt-";
308
+ var HAPPYVERTICAL_PACKAGE_PREFIX = "@happyvertical/";
309
+ async function reviewSmrtProject(args) {
310
+ const projectPath = resolve(args.directory ?? args.rootDir ?? process.cwd());
311
+ if (!await pathExists(projectPath)) return JSON.stringify({
312
+ projectPath,
313
+ packageCount: 0,
314
+ packages: [],
315
+ findings: [],
316
+ summary: {
317
+ high: 0,
318
+ medium: 0,
319
+ low: 0
320
+ },
321
+ diagnostics: [{
322
+ severity: "warning",
323
+ message: `Project directory does not exist: ${projectPath}`
324
+ }]
325
+ }, null, 2);
326
+ const packageContexts = await buildPackageContexts(projectPath);
327
+ const sourceFiles = await listSourceFiles(projectPath, packageContexts);
328
+ await attachSourceFiles(sourceFiles, packageContexts, projectPath);
329
+ const duplicates = await runDuplicateDetection(args, projectPath, packageContexts);
330
+ const findings = limitFindings([
331
+ ...duplicates.findings,
332
+ ...findMissingHappyVerticalDependencies(packageContexts),
333
+ ...findCustomManifestGeneration(sourceFiles, projectPath),
334
+ ...findDirectStorageBypasses(sourceFiles, packageContexts, projectPath),
335
+ ...findCustomHttpShells(sourceFiles, packageContexts, projectPath),
336
+ ...findLocalAuthTenancy(sourceFiles, packageContexts, projectPath),
337
+ ...findUiShellDrift(packageContexts, projectPath),
338
+ ...findMissingManifestArtifacts(sourceFiles, packageContexts, projectPath)
339
+ ], args.maxFindings);
340
+ const packages = packageContexts.map(packageInventory).sort((left, right) => left.path.localeCompare(right.path));
341
+ const summary = summarizeFindings(findings);
342
+ return JSON.stringify({
343
+ projectPath,
344
+ packageCount: packages.length,
345
+ packages,
346
+ findings: args.includeSourceEvidence === false ? findings.map(({ evidence: _evidence, ...finding }) => finding) : findings,
347
+ summary,
348
+ duplicateDetection: duplicates.status,
349
+ referenceChecks: [
350
+ "Prefer SMRT scanner/runtime manifests over custom manifest builders.",
351
+ "Prefer SvelteKit plus @happyvertical/smrt-svelte for app shells.",
352
+ "Prefer @happyvertical/sql and @happyvertical/files/assets/content over direct durable node:fs storage.",
353
+ "Prefer smrt-users, smrt-tenancy, and profile/audit packages over local static auth seams."
354
+ ],
355
+ suggestedFollowUpIssues: findings.map((finding) => ({
356
+ title: finding.suggestedIssueTitle,
357
+ severity: finding.severity,
358
+ area: finding.area
359
+ }))
360
+ }, null, 2);
361
+ }
362
+ async function buildPackageContexts(projectPath) {
363
+ const packageJsonPaths = await listPackageJsonFiles(projectPath);
364
+ const contexts = await Promise.all(packageJsonPaths.map(async (packageJsonPath) => {
365
+ const json = JSON.parse(await readFile(packageJsonPath, "utf-8"));
366
+ const directory = dirname(packageJsonPath);
367
+ return {
368
+ directory,
369
+ relativePath: relative(projectPath, directory) || ".",
370
+ packageJsonPath,
371
+ json,
372
+ dependencies: collectDependencies(json),
373
+ scripts: isRecord(json.scripts) ? json.scripts : {},
374
+ imports: /* @__PURE__ */ new Map(),
375
+ sourceFiles: []
376
+ };
377
+ }));
378
+ if (contexts.length === 0) contexts.push({
379
+ directory: projectPath,
380
+ relativePath: ".",
381
+ packageJsonPath: join(projectPath, "package.json"),
382
+ json: {},
383
+ dependencies: /* @__PURE__ */ new Set(),
384
+ scripts: {},
385
+ imports: /* @__PURE__ */ new Map(),
386
+ sourceFiles: []
387
+ });
388
+ return contexts.sort((left, right) => left.directory.length - right.directory.length);
389
+ }
390
+ async function listPackageJsonFiles(projectPath) {
391
+ const files = [];
392
+ await visit(projectPath, async (filePath, entryName) => {
393
+ if (entryName === "package.json") files.push(filePath);
394
+ });
395
+ return files;
396
+ }
397
+ async function listSourceFiles(projectPath, packages) {
398
+ const files = [];
399
+ await visit(projectPath, async (filePath, entryName) => {
400
+ if (!SOURCE_EXTENSIONS.test(entryName)) return;
401
+ if (entryName.endsWith(".d.ts") || entryName.endsWith(".test.ts") || entryName.endsWith(".spec.ts")) return;
402
+ const packageDir = findOwningPackage(filePath, packages).directory;
403
+ const content = await readFile(filePath, "utf-8");
404
+ files.push({
405
+ path: filePath,
406
+ packageDir,
407
+ content
408
+ });
409
+ });
410
+ return files;
411
+ }
412
+ async function attachSourceFiles(sourceFiles, packages, projectPath) {
413
+ for (const sourceFile of sourceFiles) {
414
+ const owner = packages.find((pkg) => pkg.directory === sourceFile.packageDir);
415
+ if (!owner) continue;
416
+ owner.sourceFiles.push(sourceFile);
417
+ for (const importedPackage of extractImports(sourceFile.content)) {
418
+ if (!importedPackage.startsWith(HAPPYVERTICAL_PACKAGE_PREFIX)) continue;
419
+ const evidence = {
420
+ filePath: relative(projectPath, sourceFile.path),
421
+ line: lineNumber(sourceFile.content, importedPackage),
422
+ detail: `Imports ${importedPackage}`
423
+ };
424
+ const existing = owner.imports.get(importedPackage) ?? [];
425
+ existing.push(evidence);
426
+ owner.imports.set(importedPackage, existing);
427
+ }
428
+ }
429
+ }
430
+ function findMissingHappyVerticalDependencies(packages) {
431
+ return packages.flatMap((pkg) => {
432
+ const ownName = typeof pkg.json.name === "string" ? pkg.json.name : void 0;
433
+ const missing = Array.from(pkg.imports.keys()).filter((importedPackage) => importedPackage !== ownName && !pkg.dependencies.has(importedPackage));
434
+ if (missing.length === 0) return [];
435
+ return [{
436
+ severity: missing.some((name) => name.startsWith(SMRT_PACKAGE_PREFIX)) ? "high" : "medium",
437
+ area: "dependencies",
438
+ code: "missing-happyvertical-dependencies",
439
+ title: `${packageLabel(pkg)} imports HappyVertical packages that package.json does not declare`,
440
+ evidence: missing.flatMap((name) => pkg.imports.get(name) ?? []),
441
+ recommendation: "Declare every imported @happyvertical package in the owning package manifest so downstream installs and generated knowledge stay reproducible.",
442
+ suggestedIssueTitle: `Declare missing HappyVertical dependencies in ${packageLabel(pkg)}`
443
+ }];
444
+ });
445
+ }
446
+ function findCustomManifestGeneration(sourceFiles, projectPath) {
447
+ return sourceFiles.filter((file) => /manifest\.json/.test(file.content) && /objects\s*:/.test(file.content) && /\b(writeFile|writeFileSync)\b/.test(file.content) && !/@happyvertical\/smrt-scanner/.test(file.content)).map((file) => ({
448
+ severity: "high",
449
+ area: "manifest",
450
+ code: "custom-object-manifest-generation",
451
+ title: "Custom SMRT object manifest generation detected",
452
+ evidence: [{
453
+ filePath: relative(projectPath, file.path),
454
+ line: lineNumber(file.content, "manifest.json"),
455
+ detail: "Writes a manifest.json object inventory outside the SMRT scanner/runtime path."
456
+ }],
457
+ recommendation: "Use the SMRT scanner/runtime manifest path so defaults, relationships, schemas, tenant fields, and cross-package references match framework behavior.",
458
+ suggestedIssueTitle: "Replace custom object manifest generation with SMRT scanner/runtime manifest generation"
459
+ }));
460
+ }
461
+ function findDirectStorageBypasses(sourceFiles, packages, projectPath) {
462
+ return sourceFiles.flatMap((file) => {
463
+ const owner = findOwningPackage(file.path, packages);
464
+ const usesFs = /from\s+['"](?:node:fs|node:fs\/promises|fs|fs\/promises)['"]|require\(['"](?:node:fs|node:fs\/promises|fs|fs\/promises)['"]\)/.test(file.content);
465
+ const writesDurableData = /\b(writeFile|appendFile|mkdir|rm|rename)\b/.test(file.content) && /(\.json|data|storage|persist|cache|db)/i.test(file.content);
466
+ const usesDirectSql = /from\s+['"](?:better-sqlite3|sqlite3|pg|mysql2?|knex|drizzle-orm)['"]/.test(file.content);
467
+ if ((!usesFs || !writesDurableData) && !usesDirectSql) return [];
468
+ if (owner.dependencies.has("@happyvertical/sql") || owner.dependencies.has("@happyvertical/files") || owner.dependencies.has("@happyvertical/smrt-assets") || owner.dependencies.has("@happyvertical/smrt-content")) return [];
469
+ return [{
470
+ severity: "medium",
471
+ area: "storage",
472
+ code: "direct-storage-bypass",
473
+ title: `${packageLabel(owner)} appears to bypass HappyVertical storage packages`,
474
+ evidence: [{
475
+ filePath: relative(projectPath, file.path),
476
+ line: lineNumber(file.content, usesDirectSql ? "sqlite" : "writeFile"),
477
+ detail: usesDirectSql ? "Imports a direct SQL/storage library without @happyvertical/sql." : "Uses node:fs-style durable writes without @happyvertical/files/assets/content."
478
+ }],
479
+ recommendation: "Route durable data through @happyvertical/sql, @happyvertical/files, SMRT assets, or SMRT content unless this is explicitly build-only tooling.",
480
+ suggestedIssueTitle: `Review direct storage usage in ${packageLabel(owner)}`
481
+ }];
482
+ });
483
+ }
484
+ function findCustomHttpShells(sourceFiles, packages, projectPath) {
485
+ const evidenceByPackage = /* @__PURE__ */ new Map();
486
+ for (const pkg of packages) {
487
+ if (pkg.dependencies.has("@sveltejs/kit")) continue;
488
+ const routerDependencies = [
489
+ "express",
490
+ "fastify",
491
+ "hono",
492
+ "koa"
493
+ ].filter((dep) => pkg.dependencies.has(dep));
494
+ if (routerDependencies.length === 0) continue;
495
+ appendEvidence(evidenceByPackage, pkg, {
496
+ filePath: relative(projectPath, pkg.packageJsonPath),
497
+ detail: `Declares custom router package(s): ${routerDependencies.join(", ")}.`
498
+ });
499
+ }
500
+ for (const file of sourceFiles) {
501
+ const owner = findOwningPackage(file.path, packages);
502
+ if (owner.dependencies.has("@sveltejs/kit")) continue;
503
+ if (!/from\s+['"](?:node:http|http|node:https|https)['"]|createServer\s*\(/.test(file.content)) continue;
504
+ appendEvidence(evidenceByPackage, owner, {
505
+ filePath: relative(projectPath, file.path),
506
+ line: lineNumber(file.content, "createServer"),
507
+ detail: "Custom HTTP routing found without the SvelteKit/SMRT app-shell dependency pattern."
508
+ });
509
+ }
510
+ return Array.from(evidenceByPackage.entries()).map(([owner, evidence]) => ({
511
+ severity: "medium",
512
+ area: "api-shell",
513
+ code: "custom-http-shell",
514
+ title: `${packageLabel(owner)} uses a custom HTTP shell`,
515
+ evidence,
516
+ recommendation: "Compare the app shell against the Anytown/Ergot SvelteKit + SMRT shell pattern before adding custom HTTP infrastructure.",
517
+ suggestedIssueTitle: `Align ${packageLabel(owner)} app shell with SMRT/SvelteKit conventions`
518
+ }));
519
+ }
520
+ function appendEvidence(evidenceByPackage, pkg, evidence) {
521
+ const existing = evidenceByPackage.get(pkg);
522
+ if (existing) {
523
+ existing.push(evidence);
524
+ return;
525
+ }
526
+ evidenceByPackage.set(pkg, [evidence]);
527
+ }
528
+ function findLocalAuthTenancy(sourceFiles, packages, projectPath) {
529
+ return sourceFiles.flatMap((file) => {
530
+ const owner = findOwningPackage(file.path, packages);
531
+ const pathSignal = /(auth|tenant|audit|session|rbac|user)/i.test(file.path);
532
+ const codeSignal = /\b(tenantId|tenant|role|permission|session|auditLog|apiKey)\b/.test(file.content);
533
+ if (!pathSignal || !codeSignal) return [];
534
+ if (owner.dependencies.has("@happyvertical/smrt-users") || owner.dependencies.has("@happyvertical/smrt-tenancy") || owner.dependencies.has("@happyvertical/smrt-profiles")) return [];
535
+ return [{
536
+ severity: "medium",
537
+ area: "auth-tenancy",
538
+ code: "local-auth-tenancy",
539
+ title: `${packageLabel(owner)} contains local auth/tenancy/audit logic`,
540
+ evidence: [{
541
+ filePath: relative(projectPath, file.path),
542
+ line: lineNumber(file.content, "tenant"),
543
+ detail: "Auth, tenancy, session, role, or audit terminology appears without smrt-users/smrt-tenancy/smrt-profiles dependencies."
544
+ }],
545
+ recommendation: "Add explicit adapters to smrt-users, smrt-tenancy, and profile/audit models or document why this local implementation is intentionally isolated.",
546
+ suggestedIssueTitle: `Review auth and tenancy adapters in ${packageLabel(owner)}`
547
+ }];
548
+ });
549
+ }
550
+ function findUiShellDrift(packages, projectPath) {
551
+ return packages.flatMap((pkg) => {
552
+ if (!(pkg.sourceFiles.some((file) => file.path.endsWith(".svelte")) || /(?:web|ui|app|site|frontend)/i.test(packageLabel(pkg)) || pkg.dependencies.has("svelte") || pkg.dependencies.has("vite"))) return [];
553
+ if (pkg.dependencies.has("@happyvertical/smrt-svelte")) return [];
554
+ return [{
555
+ severity: "low",
556
+ area: "ui-shell",
557
+ code: "missing-smrt-svelte-shell",
558
+ title: `${packageLabel(pkg)} looks like UI work without @happyvertical/smrt-svelte`,
559
+ evidence: [{
560
+ filePath: relative(projectPath, pkg.packageJsonPath),
561
+ detail: "UI-facing package does not declare @happyvertical/smrt-svelte."
562
+ }],
563
+ recommendation: "Use @happyvertical/smrt-svelte and the SvelteKit shell pattern for downstream app UI unless this package is intentionally framework-agnostic.",
564
+ suggestedIssueTitle: `Check SMRT Svelte shell alignment for ${packageLabel(pkg)}`
565
+ }];
566
+ });
567
+ }
568
+ function findMissingManifestArtifacts(sourceFiles, packages, projectPath) {
569
+ return packages.flatMap((pkg) => {
570
+ if (!pkg.sourceFiles.some((file) => /@smrt\s*\(/.test(file.content))) return [];
571
+ if (sourceFiles.some((file) => file.packageDir === pkg.directory && /@happyvertical\/smrt-scanner/.test(file.content)) || pathExistsSyncHint(join(pkg.directory, ".smrt", "manifest.json")) || pathExistsSyncHint(join(pkg.directory, "dist", "manifest.json"))) return [];
572
+ return [{
573
+ severity: "low",
574
+ area: "manifest",
575
+ code: "missing-generated-manifest-artifact",
576
+ title: `${packageLabel(pkg)} has @smrt objects but no generated manifest artifact was found`,
577
+ evidence: [{
578
+ filePath: relative(projectPath, pkg.packageJsonPath),
579
+ detail: "No .smrt/manifest.json or dist/manifest.json was visible during review."
580
+ }],
581
+ recommendation: "Run the package build/test manifest generation path and verify it uses the SMRT scanner/runtime manifest pipeline.",
582
+ suggestedIssueTitle: `Verify SMRT manifest generation for ${packageLabel(pkg)}`
583
+ }];
584
+ });
585
+ }
586
+ async function visit(dir, onFile) {
587
+ let entries;
588
+ try {
589
+ entries = await readdir(dir, { withFileTypes: true });
590
+ } catch {
591
+ return;
592
+ }
593
+ for (const entry of entries) {
594
+ const fullPath = join(dir, entry.name);
595
+ if (entry.isDirectory()) {
596
+ if (SKIP_DIRS.has(entry.name)) continue;
597
+ await visit(fullPath, onFile);
598
+ continue;
599
+ }
600
+ if (entry.isFile()) await onFile(fullPath, entry.name);
601
+ }
602
+ }
603
+ function collectDependencies(packageJson) {
604
+ const dependencies = /* @__PURE__ */ new Set();
605
+ for (const sectionName of DEPENDENCY_SECTIONS) {
606
+ const section = packageJson[sectionName];
607
+ if (!isRecord(section)) continue;
608
+ for (const dependencyName of Object.keys(section)) dependencies.add(dependencyName);
609
+ }
610
+ return dependencies;
611
+ }
612
+ function extractImports(content) {
613
+ const imports = /* @__PURE__ */ new Set();
614
+ for (const pattern of [
615
+ /(?:import|export)\s+(?:type\s+)?(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/g,
616
+ /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
617
+ /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g
618
+ ]) for (const match of content.matchAll(pattern)) {
619
+ const imported = normalizePackageImport(match[1]);
620
+ if (imported) imports.add(imported);
621
+ }
622
+ return Array.from(imports);
623
+ }
624
+ function normalizePackageImport(value) {
625
+ if (!value || value.startsWith(".") || value.startsWith("/")) return void 0;
626
+ if (value.startsWith("@")) {
627
+ const [scope, name] = value.split("/");
628
+ return scope && name ? `${scope}/${name}` : value;
629
+ }
630
+ return value.split("/")[0];
631
+ }
632
+ function findOwningPackage(filePath, packages) {
633
+ const normalized = resolve(filePath);
634
+ return packages.filter((pkg) => normalized === pkg.directory || normalized.startsWith(`${pkg.directory}${sep}`)).sort((left, right) => right.directory.length - left.directory.length)[0] ?? packages[0];
635
+ }
636
+ function packageInventory(pkg) {
637
+ const declaredHappyVerticalDependencies = Array.from(pkg.dependencies).filter((dependency) => dependency.startsWith(HAPPYVERTICAL_PACKAGE_PREFIX)).sort();
638
+ const importedHappyVerticalPackages = Array.from(pkg.imports.keys()).sort();
639
+ const ownName = typeof pkg.json.name === "string" ? pkg.json.name : void 0;
640
+ return {
641
+ name: packageLabel(pkg),
642
+ path: pkg.relativePath,
643
+ private: typeof pkg.json.private === "boolean" ? pkg.json.private : void 0,
644
+ scripts: Object.keys(pkg.scripts).sort(),
645
+ declaredHappyVerticalDependencies,
646
+ importedHappyVerticalPackages,
647
+ missingHappyVerticalDependencies: importedHappyVerticalPackages.filter((name) => name !== ownName && !pkg.dependencies.has(name)),
648
+ hasSvelteKit: pkg.dependencies.has("@sveltejs/kit"),
649
+ hasSmrtSvelte: pkg.dependencies.has("@happyvertical/smrt-svelte")
650
+ };
651
+ }
652
+ function summarizeFindings(findings) {
653
+ return findings.reduce((summary, finding) => {
654
+ summary[finding.severity]++;
655
+ return summary;
656
+ }, {
657
+ high: 0,
658
+ medium: 0,
659
+ low: 0
660
+ });
661
+ }
662
+ function limitFindings(findings, maxFindings) {
663
+ const sorted = findings.sort((left, right) => severityRank(left.severity) - severityRank(right.severity) || left.area.localeCompare(right.area) || left.title.localeCompare(right.title));
664
+ return maxFindings && maxFindings > 0 ? sorted.slice(0, maxFindings) : sorted;
665
+ }
666
+ function severityRank(severity) {
667
+ return severity === "high" ? 0 : severity === "medium" ? 1 : 2;
668
+ }
669
+ function packageLabel(pkg) {
670
+ return typeof pkg.json.name === "string" ? pkg.json.name : pkg.relativePath;
671
+ }
672
+ function lineNumber(content, needle) {
673
+ const index = content.indexOf(needle);
674
+ if (index < 0) return void 0;
675
+ return content.slice(0, index).split("\n").length;
676
+ }
677
+ function isRecord(value) {
678
+ return !!value && typeof value === "object" && !Array.isArray(value);
679
+ }
680
+ async function pathExists(path) {
681
+ try {
682
+ await access(path);
683
+ return true;
684
+ } catch {
685
+ return false;
686
+ }
687
+ }
688
+ function pathExistsSyncHint(path) {
689
+ return existsSync(path);
690
+ }
691
+ async function runDuplicateDetection(args, projectPath, packageContexts) {
692
+ const status = {
693
+ enabled: args.detectDuplicates !== false,
694
+ indexPath: null,
695
+ intentCount: 0,
696
+ notes: []
697
+ };
698
+ if (!status.enabled) return {
699
+ findings: [],
700
+ status
701
+ };
702
+ const explicit = args.intentIndexPath ? isAbsolute(args.intentIndexPath) ? args.intentIndexPath : resolve(projectPath, args.intentIndexPath) : void 0;
703
+ const candidates = explicit ? [explicit] : defaultIntentIndexCandidates(args.packageRoot);
704
+ const indexPath = candidates.find((candidate) => existsSync(candidate));
705
+ if (!indexPath) {
706
+ status.notes.push(`No intent index found (tried ${candidates.join(", ") || "none"}); duplicate detection skipped. Reinstall @happyvertical/smrt-dev-mcp or pass intentIndexPath.`);
707
+ return {
708
+ findings: [],
709
+ status
710
+ };
711
+ }
712
+ const rows = parseIntentIndex(await readFile(indexPath, "utf-8"));
713
+ status.indexPath = indexPath;
714
+ status.intentCount = rows.length;
715
+ if (rows.length === 0) {
716
+ status.notes.push(`Intent index ${indexPath} has no parsable rows.`);
717
+ return {
718
+ findings: [],
719
+ status
720
+ };
721
+ }
722
+ if (!packageContexts.some((pkg) => Array.from(pkg.dependencies).some((name) => name.startsWith(SMRT_PACKAGE_PREFIX)) || Array.from(pkg.imports.keys()).some((name) => name.startsWith(SMRT_PACKAGE_PREFIX)))) {
723
+ status.notes.push("No @happyvertical/smrt-* dependency or import found; duplicate detection skipped.");
724
+ return {
725
+ findings: [],
726
+ status
727
+ };
728
+ }
729
+ const homeNames = new Set(rows.flatMap((row) => row.homePackages.map((home) => home.packageName)));
730
+ return {
731
+ findings: detectHandRolled(rows, packageContexts.map((pkg) => ({
732
+ name: typeof pkg.json.name === "string" ? pkg.json.name : void 0,
733
+ relativePath: pkg.relativePath,
734
+ installed: new Set(Array.from(homeNames).filter((name) => pkg.dependencies.has(name) || existsSync(join(pkg.directory, "node_modules", name)) || existsSync(join(projectPath, "node_modules", name)))),
735
+ files: pkg.sourceFiles.map((file) => ({
736
+ relativePath: relative(projectPath, file.path),
737
+ content: file.content,
738
+ imports: extractImports(file.content)
739
+ }))
740
+ })), { onNote: (note) => {
741
+ status.notes.push(note);
742
+ process.stderr.write(`[smrt-dev-mcp] ${note}\n`);
743
+ } }).map(duplicateFinding),
744
+ status
745
+ };
746
+ }
747
+ function duplicateFinding(found) {
748
+ const first = found.evidence[0];
749
+ const installedUnused = found.installedHomePackages.length > 0;
750
+ const homeList = found.homePackages.join(", ");
751
+ const title = `${found.packageName} appears to hand-roll "${found.intent}"`;
752
+ return {
753
+ severity: installedUnused ? "medium" : "low",
754
+ area: "duplicate-detection",
755
+ code: found.code,
756
+ title,
757
+ message: `${title} (signals: ${found.matchedSignals.join(", ")}); ${installedUnused ? `${found.installedHomePackages.join(", ")} is installed but never imported` : `none of ${homeList} is imported`}. Intent page ${found.intentPage}; component pages ${found.componentPages.join(", ")}.`,
758
+ file: `${first.filePath}:${first.line}`,
759
+ packageName: found.packageName,
760
+ intentPage: found.intentPage,
761
+ componentPages: found.componentPages,
762
+ evidence: found.evidence.map((item) => ({
763
+ filePath: item.filePath,
764
+ line: item.line,
765
+ detail: item.detail
766
+ })),
767
+ recommendation: `Read ${found.intentPage} before keeping this implementation; the framework entry point is ${found.entryPoint} (${found.componentPages.join(", ")}). Advisory only: keep the hand-rolled code if it covers a case the package does not.`,
768
+ suggestedIssueTitle: `Evaluate ${homeList} instead of hand-rolled ${found.intent.toLowerCase()} in ${found.packageName}`
769
+ };
770
+ }
771
+ //#endregion
772
+ export { reviewSmrtProject as t };
773
+
774
+ //# sourceMappingURL=review-smrt-project-ByEwPcor.js.map