@happyvertical/smrt-dev-mcp 0.30.0
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/AGENTS.md +75 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +323 -0
- package/agent-skills/smrt-code-review/SKILL.md +68 -0
- package/agent-skills/smrt-code-review/references/review-output.md +31 -0
- package/dist/agent-skills.d.ts +19 -0
- package/dist/agent-skills.d.ts.map +1 -0
- package/dist/index-DF0HSB_8.js +1274 -0
- package/dist/index-DF0HSB_8.js.map +1 -0
- package/dist/index.d.ts +853 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2039 -0
- package/dist/index.js.map +1 -0
- package/dist/knowledge/index.d.ts +175 -0
- package/dist/knowledge/index.d.ts.map +1 -0
- package/dist/knowledge.d.ts +2 -0
- package/dist/knowledge.d.ts.map +1 -0
- package/dist/knowledge.js +2 -0
- package/dist/knowledge.js.map +1 -0
- package/dist/tools/generate-smrt-class.d.ts +54 -0
- package/dist/tools/generate-smrt-class.d.ts.map +1 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/introspect-project.d.ts +14 -0
- package/dist/tools/introspect-project.d.ts.map +1 -0
- package/dist/tools/review-smrt-project.d.ts +13 -0
- package/dist/tools/review-smrt-project.d.ts.map +1 -0
- package/package.json +66 -0
|
@@ -0,0 +1,1274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
|
|
5
|
+
import { join, relative, resolve, dirname } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const SDK_PACKAGE_NAMES = /* @__PURE__ */ new Set([
|
|
8
|
+
"@happyvertical/ai",
|
|
9
|
+
"@happyvertical/cache",
|
|
10
|
+
"@happyvertical/documents",
|
|
11
|
+
"@happyvertical/email",
|
|
12
|
+
"@happyvertical/encryption",
|
|
13
|
+
"@happyvertical/files",
|
|
14
|
+
"@happyvertical/geo",
|
|
15
|
+
"@happyvertical/images",
|
|
16
|
+
"@happyvertical/jobs",
|
|
17
|
+
"@happyvertical/json",
|
|
18
|
+
"@happyvertical/logger",
|
|
19
|
+
"@happyvertical/messages",
|
|
20
|
+
"@happyvertical/ocr",
|
|
21
|
+
"@happyvertical/pdf",
|
|
22
|
+
"@happyvertical/projects",
|
|
23
|
+
"@happyvertical/repos",
|
|
24
|
+
"@happyvertical/secrets",
|
|
25
|
+
"@happyvertical/spider",
|
|
26
|
+
"@happyvertical/sql",
|
|
27
|
+
"@happyvertical/utils"
|
|
28
|
+
]);
|
|
29
|
+
const RELATIONSHIP_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
30
|
+
"foreignKey",
|
|
31
|
+
"crossPackageRef",
|
|
32
|
+
"oneToMany",
|
|
33
|
+
"manyToMany"
|
|
34
|
+
]);
|
|
35
|
+
const WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
36
|
+
".git",
|
|
37
|
+
".svelte-kit",
|
|
38
|
+
".turbo",
|
|
39
|
+
"coverage",
|
|
40
|
+
"dist",
|
|
41
|
+
"node_modules"
|
|
42
|
+
]);
|
|
43
|
+
const STALE_PATTERNS = [
|
|
44
|
+
{
|
|
45
|
+
code: "stale-have-namespace",
|
|
46
|
+
pattern: new RegExp(`@${"have"}/`),
|
|
47
|
+
message: "Stale HappyVertical legacy namespace reference found"
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
code: "stale-smrt-core-namespace",
|
|
51
|
+
pattern: new RegExp(`@${"smrt"}/core`),
|
|
52
|
+
message: "Stale SMRT core namespace reference found"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
code: "stale-field-helper-import",
|
|
56
|
+
pattern: /@happyvertical\/smrt-core\/fields/,
|
|
57
|
+
message: "Stale field-helper import path found"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
code: "stale-docs-codex-command",
|
|
61
|
+
pattern: new RegExp(
|
|
62
|
+
`docs:${"codex"}|docs-${"codex"}|\\.${"codex"}|${"codex"}-command`,
|
|
63
|
+
"i"
|
|
64
|
+
),
|
|
65
|
+
message: "Stale Codex-specific downstream-doc reference found"
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
async function buildKnowledgeIndex(options = {}) {
|
|
69
|
+
const rootDir = findProjectRoot(options.rootDir ?? process.cwd());
|
|
70
|
+
const includeDocs = options.includeDocs ?? true;
|
|
71
|
+
const packageDirs = discoverProjectPackageDirs(rootDir);
|
|
72
|
+
const packages = packageDirs.map(
|
|
73
|
+
(dir) => readKnowledgePackage(rootDir, dir, includeDocs)
|
|
74
|
+
);
|
|
75
|
+
packages.push(
|
|
76
|
+
...discoverInstalledSdkPackages(rootDir, packageDirs, includeDocs)
|
|
77
|
+
);
|
|
78
|
+
const uniquePackages = dedupePackages(packages);
|
|
79
|
+
const scopedPackages = filterKnowledgePackages(uniquePackages, options);
|
|
80
|
+
const smrtPackages = scopedPackages.filter((pkg) => pkg.kind === "smrt");
|
|
81
|
+
const sdkPackages = scopedPackages.filter((pkg) => pkg.kind === "sdk");
|
|
82
|
+
return {
|
|
83
|
+
schemaVersion: 1,
|
|
84
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
85
|
+
rootDir,
|
|
86
|
+
packages: scopedPackages,
|
|
87
|
+
smrtPackages,
|
|
88
|
+
sdkPackages,
|
|
89
|
+
relationshipsV2: summarizeRelationshipsV2(scopedPackages)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function checkKnowledgeFreshness(options = {}) {
|
|
93
|
+
const index = await buildKnowledgeIndex(options);
|
|
94
|
+
return checkKnowledgeFreshnessFromIndex(index, options);
|
|
95
|
+
}
|
|
96
|
+
async function checkKnowledgeFreshnessFromIndex(index, options = {}) {
|
|
97
|
+
const issues = [];
|
|
98
|
+
const changedFiles = options.changed ? getChangedFiles(index.rootDir) : void 0;
|
|
99
|
+
for (const pkg of index.packages.filter((item) => item.kind !== "sdk")) {
|
|
100
|
+
const packageJsonPath = join(pkg.directory, "package.json");
|
|
101
|
+
if (!pkg.hasAgentsMd) {
|
|
102
|
+
issues.push({
|
|
103
|
+
severity: "error",
|
|
104
|
+
code: "missing-agents-md",
|
|
105
|
+
message: "Workspace package is missing canonical AGENTS.md",
|
|
106
|
+
file: relative(index.rootDir, join(pkg.directory, "AGENTS.md")),
|
|
107
|
+
packageName: pkg.name
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (!pkg.hasClaudeMd) {
|
|
111
|
+
issues.push({
|
|
112
|
+
severity: "error",
|
|
113
|
+
code: "missing-claude-shim",
|
|
114
|
+
message: "Workspace package is missing CLAUDE.md compatibility shim",
|
|
115
|
+
file: relative(index.rootDir, join(pkg.directory, "CLAUDE.md")),
|
|
116
|
+
packageName: pkg.name
|
|
117
|
+
});
|
|
118
|
+
} else if (!pkg.hasClaudeShim) {
|
|
119
|
+
issues.push({
|
|
120
|
+
severity: "error",
|
|
121
|
+
code: "claude-not-shim",
|
|
122
|
+
message: "CLAUDE.md must contain only @AGENTS.md",
|
|
123
|
+
file: relative(index.rootDir, join(pkg.directory, "CLAUDE.md")),
|
|
124
|
+
packageName: pkg.name
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
for (const entry of pkg.files) {
|
|
128
|
+
if (entry === "dist" || entry.startsWith("dist/") || entry.includes("*")) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const entryPath = join(pkg.directory, entry);
|
|
132
|
+
if (!existsSync(entryPath)) {
|
|
133
|
+
issues.push({
|
|
134
|
+
severity: "error",
|
|
135
|
+
code: "package-files-entry-missing",
|
|
136
|
+
message: `package.json files entry "${entry}" does not exist`,
|
|
137
|
+
file: relative(index.rootDir, packageJsonPath),
|
|
138
|
+
packageName: pkg.name
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!pkg.files.includes("AGENTS.md")) {
|
|
143
|
+
issues.push({
|
|
144
|
+
severity: "error",
|
|
145
|
+
code: "package-files-missing-agents",
|
|
146
|
+
message: "package.json files allowlist must include AGENTS.md",
|
|
147
|
+
file: relative(index.rootDir, packageJsonPath),
|
|
148
|
+
packageName: pkg.name
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (!pkg.files.includes("CLAUDE.md")) {
|
|
152
|
+
issues.push({
|
|
153
|
+
severity: "error",
|
|
154
|
+
code: "package-files-missing-claude-shim",
|
|
155
|
+
message: "package.json files allowlist must include CLAUDE.md shim",
|
|
156
|
+
file: relative(index.rootDir, packageJsonPath),
|
|
157
|
+
packageName: pkg.name
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const pkg of index.packages) {
|
|
162
|
+
issues.push(...checkDomainKnowledgeArtifact(index.rootDir, pkg));
|
|
163
|
+
}
|
|
164
|
+
issues.push(...findStalePatternIssues(index.rootDir, changedFiles));
|
|
165
|
+
const effectiveIssues = issues.map(
|
|
166
|
+
(issue) => issue.code.startsWith("stale-") ? {
|
|
167
|
+
...issue,
|
|
168
|
+
severity: options.strict ? "error" : "warning"
|
|
169
|
+
} : issue
|
|
170
|
+
);
|
|
171
|
+
const errorCount = effectiveIssues.filter(
|
|
172
|
+
(i) => i.severity === "error"
|
|
173
|
+
).length;
|
|
174
|
+
const warningCount = effectiveIssues.filter(
|
|
175
|
+
(i) => i.severity === "warning"
|
|
176
|
+
).length;
|
|
177
|
+
return {
|
|
178
|
+
ok: errorCount === 0,
|
|
179
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
180
|
+
rootDir: index.rootDir,
|
|
181
|
+
issueCount: effectiveIssues.length,
|
|
182
|
+
errorCount,
|
|
183
|
+
warningCount,
|
|
184
|
+
issues: effectiveIssues
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function checkDomainKnowledgeArtifact(rootDir, pkg) {
|
|
188
|
+
const issues = [];
|
|
189
|
+
if (pkg.exportKeys.includes("./smrt-knowledge.json") && !pkg.hasDomainKnowledge) {
|
|
190
|
+
issues.push({
|
|
191
|
+
severity: "error",
|
|
192
|
+
code: "missing-domain-knowledge",
|
|
193
|
+
message: "package exports ./smrt-knowledge.json but no domain knowledge artifact was found",
|
|
194
|
+
file: relative(rootDir, join(pkg.directory, "package.json")),
|
|
195
|
+
packageName: pkg.name
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (!pkg.domainKnowledge || !pkg.domainKnowledgePath) {
|
|
199
|
+
return issues;
|
|
200
|
+
}
|
|
201
|
+
const hashes = pkg.domainKnowledge.sourceHashes ?? {};
|
|
202
|
+
const checks = [
|
|
203
|
+
{
|
|
204
|
+
key: "packageJson",
|
|
205
|
+
filePath: join(pkg.directory, "package.json"),
|
|
206
|
+
kind: "raw",
|
|
207
|
+
label: "package.json"
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
key: "agents",
|
|
211
|
+
filePath: existsSync(join(pkg.directory, "AGENTS.md")) ? join(pkg.directory, "AGENTS.md") : void 0,
|
|
212
|
+
kind: "raw",
|
|
213
|
+
label: "AGENTS.md"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
key: "manifest",
|
|
217
|
+
filePath: domainSourceManifestPath(rootDir, pkg),
|
|
218
|
+
kind: "json",
|
|
219
|
+
label: "manifest"
|
|
220
|
+
}
|
|
221
|
+
];
|
|
222
|
+
for (const check of checks) {
|
|
223
|
+
const expected = hashes[check.key];
|
|
224
|
+
if (!expected) continue;
|
|
225
|
+
if (!check.filePath || !existsSync(check.filePath)) {
|
|
226
|
+
issues.push({
|
|
227
|
+
severity: "error",
|
|
228
|
+
code: "domain-knowledge-source-missing",
|
|
229
|
+
message: `${check.label} source for smrt-knowledge.json is missing`,
|
|
230
|
+
file: pkg.domainKnowledgePath,
|
|
231
|
+
packageName: pkg.name
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const actual = check.kind === "json" ? hashJsonFile(check.filePath) : hashFile(check.filePath);
|
|
236
|
+
if (actual !== expected) {
|
|
237
|
+
issues.push({
|
|
238
|
+
severity: "error",
|
|
239
|
+
code: "stale-domain-knowledge",
|
|
240
|
+
message: `${check.label} changed since smrt-knowledge.json was generated`,
|
|
241
|
+
file: pkg.domainKnowledgePath,
|
|
242
|
+
packageName: pkg.name
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return issues;
|
|
247
|
+
}
|
|
248
|
+
function domainSourceManifestPath(rootDir, pkg) {
|
|
249
|
+
if (pkg.domainKnowledge?.sourceManifestPath) {
|
|
250
|
+
return join(pkg.directory, pkg.domainKnowledge.sourceManifestPath);
|
|
251
|
+
}
|
|
252
|
+
if (pkg.manifestPath) {
|
|
253
|
+
return join(rootDir, pkg.manifestPath);
|
|
254
|
+
}
|
|
255
|
+
return void 0;
|
|
256
|
+
}
|
|
257
|
+
async function diffKnowledgeIndex(options = {}) {
|
|
258
|
+
const index = await buildKnowledgeIndex({ rootDir: options.rootDir });
|
|
259
|
+
const base = options.base ?? "HEAD";
|
|
260
|
+
const changedFiles = getChangedFiles(index.rootDir, base);
|
|
261
|
+
const packageQuery = options.packageName ?? options.package;
|
|
262
|
+
const selected = selectPackagesForFiles(index, changedFiles).filter((pkg) => scopeAllowsPackage(pkg, options.scope)).filter(
|
|
263
|
+
(pkg) => packageQuery ? packageMatches(pkg, packageQuery.toLowerCase()) : true
|
|
264
|
+
);
|
|
265
|
+
const changedPackages = selected.map((pkg) => pkg.name);
|
|
266
|
+
return { base, changedFiles, changedPackages, index };
|
|
267
|
+
}
|
|
268
|
+
async function buildReviewContext(options = {}) {
|
|
269
|
+
const index = await buildKnowledgeIndex({ rootDir: options.rootDir });
|
|
270
|
+
const changedFiles = options.changedFiles ?? getChangedFiles(index.rootDir);
|
|
271
|
+
const selectedPackages = selectPackages(index, {
|
|
272
|
+
changedFiles,
|
|
273
|
+
text: [options.focus, options.documentation].filter(Boolean).join("\n"),
|
|
274
|
+
scope: options.scope,
|
|
275
|
+
packageName: options.packageName ?? options.package
|
|
276
|
+
});
|
|
277
|
+
const selectedSdkPackages = selectSdkPackages(
|
|
278
|
+
index,
|
|
279
|
+
selectedPackages,
|
|
280
|
+
[
|
|
281
|
+
options.focus,
|
|
282
|
+
options.documentation,
|
|
283
|
+
options.packageName ?? options.package
|
|
284
|
+
],
|
|
285
|
+
{
|
|
286
|
+
scope: options.scope,
|
|
287
|
+
packageName: options.packageName ?? options.package
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
const deterministicFindings = findStalePatternIssues(
|
|
291
|
+
index.rootDir,
|
|
292
|
+
changedFiles.length > 0 ? changedFiles : void 0
|
|
293
|
+
).concat(buildReviewFindings(index, changedFiles, selectedPackages));
|
|
294
|
+
return {
|
|
295
|
+
selectedPackages,
|
|
296
|
+
selectedSdkPackages,
|
|
297
|
+
deterministicFindings,
|
|
298
|
+
promptBundle: buildPromptBundle({
|
|
299
|
+
title: "SMRT code review",
|
|
300
|
+
task: "Review the changed SMRT code. Prioritize correctness, relationships-v2 invariants, tenancy, SDK usage, prompt/data safety, and stale documentation.",
|
|
301
|
+
index,
|
|
302
|
+
packages: selectedPackages,
|
|
303
|
+
sdkPackages: selectedSdkPackages,
|
|
304
|
+
sourceFiles: changedFiles,
|
|
305
|
+
extraContext: options.focus
|
|
306
|
+
})
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
async function smrtReview(options = {}) {
|
|
310
|
+
const context = await buildReviewContext(options);
|
|
311
|
+
const mode = options.mode ?? "both";
|
|
312
|
+
return {
|
|
313
|
+
mode,
|
|
314
|
+
selectedPackages: context.selectedPackages,
|
|
315
|
+
selectedSdkPackages: context.selectedSdkPackages,
|
|
316
|
+
...mode !== "prompt-bundle" ? { deterministicFindings: context.deterministicFindings } : {},
|
|
317
|
+
...mode !== "findings" ? { promptBundle: context.promptBundle } : {}
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async function buildArchitectureContext(options = {}) {
|
|
321
|
+
const index = await buildKnowledgeIndex({ rootDir: options.rootDir });
|
|
322
|
+
const text = [options.idea, options.documentation, options.focus].filter(Boolean).join("\n");
|
|
323
|
+
const selectedPackages = selectPackages(index, {
|
|
324
|
+
text,
|
|
325
|
+
scope: options.scope,
|
|
326
|
+
packageName: options.packageName ?? options.package
|
|
327
|
+
});
|
|
328
|
+
const selectedSdkPackages = selectSdkPackages(
|
|
329
|
+
index,
|
|
330
|
+
selectedPackages,
|
|
331
|
+
[text, options.packageName ?? options.package],
|
|
332
|
+
{
|
|
333
|
+
scope: options.scope,
|
|
334
|
+
packageName: options.packageName ?? options.package
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
return {
|
|
338
|
+
selectedPackages,
|
|
339
|
+
selectedSdkPackages,
|
|
340
|
+
promptBundle: buildPromptBundle({
|
|
341
|
+
title: "SMRT architecture planning",
|
|
342
|
+
task: "Suggest the SMRT packages, HappyVertical SDK packages, object model, integration points, risks, and implementation slices for this project idea.",
|
|
343
|
+
index,
|
|
344
|
+
packages: selectedPackages,
|
|
345
|
+
sdkPackages: selectedSdkPackages,
|
|
346
|
+
sourceFiles: [],
|
|
347
|
+
extraContext: text
|
|
348
|
+
})
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
async function smrtArchitecture(options = {}) {
|
|
352
|
+
const context = await buildArchitectureContext(options);
|
|
353
|
+
const ideaText = [options.idea, options.documentation, options.focus].filter(Boolean).join("\n");
|
|
354
|
+
return {
|
|
355
|
+
...context,
|
|
356
|
+
recommendations: buildArchitectureRecommendations(context, ideaText)
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
function renderKnowledgeIndexMarkdown(index) {
|
|
360
|
+
const lines = [
|
|
361
|
+
"# SMRT Knowledge Index",
|
|
362
|
+
"",
|
|
363
|
+
`Generated: ${index.generatedAt}`,
|
|
364
|
+
`Root: ${index.rootDir}`,
|
|
365
|
+
"",
|
|
366
|
+
"## Summary",
|
|
367
|
+
"",
|
|
368
|
+
`- SMRT packages: ${index.smrtPackages.length}`,
|
|
369
|
+
`- SDK packages: ${index.sdkPackages.length}`,
|
|
370
|
+
`- foreignKey fields: ${index.relationshipsV2.foreignKeyFields}`,
|
|
371
|
+
`- crossPackageRef fields: ${index.relationshipsV2.crossPackageRefFields}`,
|
|
372
|
+
`- junction collections: ${index.relationshipsV2.junctionCollections}`,
|
|
373
|
+
`- hierarchical objects: ${index.relationshipsV2.hierarchicalObjects}`,
|
|
374
|
+
`- polymorphic associations: ${index.relationshipsV2.polymorphicAssociations}`,
|
|
375
|
+
`- UUID columns: ${index.relationshipsV2.uuidColumns}`,
|
|
376
|
+
"",
|
|
377
|
+
"## Packages",
|
|
378
|
+
""
|
|
379
|
+
];
|
|
380
|
+
for (const pkg of index.packages) {
|
|
381
|
+
lines.push(`### ${pkg.name}`);
|
|
382
|
+
lines.push("");
|
|
383
|
+
lines.push(`- kind: ${pkg.kind}`);
|
|
384
|
+
lines.push(`- version: ${pkg.version}`);
|
|
385
|
+
lines.push(`- objects: ${pkg.objects.length}`);
|
|
386
|
+
lines.push(`- SMRT deps: ${pkg.smrtDependencies.join(", ") || "(none)"}`);
|
|
387
|
+
lines.push(`- SDK deps: ${pkg.sdkDependencies.join(", ") || "(none)"}`);
|
|
388
|
+
lines.push(`- exports: ${pkg.exportKeys.join(", ") || "(none)"}`);
|
|
389
|
+
lines.push(`- MCP tools: ${pkg.mcpTools.length}`);
|
|
390
|
+
lines.push(
|
|
391
|
+
`- domain knowledge: ${pkg.domainKnowledgePath ?? "(manifest fallback)"}`
|
|
392
|
+
);
|
|
393
|
+
lines.push(
|
|
394
|
+
`- docs: ${pkg.docSource ?? "(none)"}${pkg.hasClaudeShim ? " + CLAUDE.md shim" : ""}`
|
|
395
|
+
);
|
|
396
|
+
if (pkg.relationshipFeatures.length > 0) {
|
|
397
|
+
lines.push(`- relationships-v2: ${pkg.relationshipFeatures.join(", ")}`);
|
|
398
|
+
}
|
|
399
|
+
lines.push("");
|
|
400
|
+
}
|
|
401
|
+
return lines.join("\n");
|
|
402
|
+
}
|
|
403
|
+
function renderFreshnessResult(result) {
|
|
404
|
+
const lines = [
|
|
405
|
+
result.ok ? "✓ SMRT knowledge is fresh" : "✗ SMRT knowledge has issues",
|
|
406
|
+
"",
|
|
407
|
+
`Root: ${result.rootDir}`,
|
|
408
|
+
`Errors: ${result.errorCount}`,
|
|
409
|
+
`Warnings: ${result.warningCount}`,
|
|
410
|
+
""
|
|
411
|
+
];
|
|
412
|
+
for (const issue of result.issues) {
|
|
413
|
+
const location = issue.file ? ` (${issue.file})` : "";
|
|
414
|
+
lines.push(
|
|
415
|
+
`- [${issue.severity.toUpperCase()}] ${issue.code}: ${issue.message}${location}`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
return lines.join("\n");
|
|
419
|
+
}
|
|
420
|
+
function findProjectRoot(startDir) {
|
|
421
|
+
let current = resolve(startDir);
|
|
422
|
+
for (; ; ) {
|
|
423
|
+
if (existsSync(join(current, "pnpm-workspace.yaml")) && existsSync(join(current, "packages"))) {
|
|
424
|
+
return current;
|
|
425
|
+
}
|
|
426
|
+
const parent = dirname(current);
|
|
427
|
+
if (parent === current) return resolve(startDir);
|
|
428
|
+
current = parent;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function discoverWorkspacePackageDirs(rootDir) {
|
|
432
|
+
const packagesDir = join(rootDir, "packages");
|
|
433
|
+
if (!existsSync(packagesDir)) return [];
|
|
434
|
+
return readdirSync(packagesDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(packagesDir, entry.name)).filter((dir) => existsSync(join(dir, "package.json"))).sort();
|
|
435
|
+
}
|
|
436
|
+
function discoverProjectPackageDirs(rootDir) {
|
|
437
|
+
const workspaceDirs = discoverWorkspacePackageDirs(rootDir);
|
|
438
|
+
if (existsSync(join(rootDir, "package.json")) && hasLocalDomainArtifact(rootDir)) {
|
|
439
|
+
return [rootDir, ...workspaceDirs.filter((dir) => dir !== rootDir)];
|
|
440
|
+
}
|
|
441
|
+
return workspaceDirs;
|
|
442
|
+
}
|
|
443
|
+
function hasLocalDomainArtifact(rootDir) {
|
|
444
|
+
return [
|
|
445
|
+
join(rootDir, ".smrt", "smrt-knowledge.json"),
|
|
446
|
+
join(rootDir, ".smrt", "manifest.json"),
|
|
447
|
+
join(rootDir, "dist", "smrt-knowledge.json"),
|
|
448
|
+
join(rootDir, "dist", "manifest.json"),
|
|
449
|
+
join(rootDir, "src", "manifest", "smrt-knowledge.json"),
|
|
450
|
+
join(rootDir, "src", "manifest", "manifest.json")
|
|
451
|
+
].some((path) => existsSync(path));
|
|
452
|
+
}
|
|
453
|
+
function discoverInstalledSdkPackages(rootDir, packageDirs, includeDocs) {
|
|
454
|
+
const scopeDirs = [
|
|
455
|
+
join(rootDir, "node_modules", "@happyvertical"),
|
|
456
|
+
...packageDirs.map((dir) => join(dir, "node_modules", "@happyvertical"))
|
|
457
|
+
];
|
|
458
|
+
return scopeDirs.filter(
|
|
459
|
+
(scopeDir, index, all) => existsSync(scopeDir) && all.indexOf(scopeDir) === index
|
|
460
|
+
).flatMap(
|
|
461
|
+
(scopeDir) => readdirSync(scopeDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map((entry) => {
|
|
462
|
+
const entryPath = join(scopeDir, entry.name);
|
|
463
|
+
try {
|
|
464
|
+
return lstatSync(entryPath).isSymbolicLink() ? realpathSync(entryPath) : entryPath;
|
|
465
|
+
} catch {
|
|
466
|
+
return entryPath;
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
).filter((dir) => {
|
|
470
|
+
const pkg = readJson(join(dir, "package.json"));
|
|
471
|
+
return typeof pkg?.name === "string" && SDK_PACKAGE_NAMES.has(pkg.name) && !pkg.name.startsWith("@happyvertical/smrt-");
|
|
472
|
+
}).map((dir) => readKnowledgePackage(rootDir, dir, includeDocs));
|
|
473
|
+
}
|
|
474
|
+
function filterKnowledgePackages(packages, options) {
|
|
475
|
+
const packageQuery = options.packageName ?? options.package;
|
|
476
|
+
return packages.filter((pkg) => {
|
|
477
|
+
if (packageQuery && !packageMatches(pkg, packageQuery.toLowerCase())) {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
switch (options.scope) {
|
|
481
|
+
case "local":
|
|
482
|
+
return pkg.kind !== "sdk" && !pkg.relativeDirectory.includes("node_modules");
|
|
483
|
+
case "package":
|
|
484
|
+
return pkg.kind !== "sdk";
|
|
485
|
+
case "sdk":
|
|
486
|
+
return pkg.kind === "sdk";
|
|
487
|
+
case "project":
|
|
488
|
+
case void 0:
|
|
489
|
+
return true;
|
|
490
|
+
default:
|
|
491
|
+
return true;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
function readKnowledgePackage(rootDir, directory, includeDocs) {
|
|
496
|
+
const packageJson = readJson(join(directory, "package.json")) ?? {};
|
|
497
|
+
const dependencies = objectRecord(packageJson.dependencies);
|
|
498
|
+
const devDependencies = objectRecord(packageJson.devDependencies);
|
|
499
|
+
const peerDependencies = objectRecord(packageJson.peerDependencies);
|
|
500
|
+
const allDeps = { ...dependencies, ...devDependencies, ...peerDependencies };
|
|
501
|
+
const name = String(packageJson.name ?? directory);
|
|
502
|
+
const agentsPath = join(directory, "AGENTS.md");
|
|
503
|
+
const claudePath = join(directory, "CLAUDE.md");
|
|
504
|
+
const hasAgentsMd = existsSync(agentsPath);
|
|
505
|
+
const hasClaudeMd = existsSync(claudePath);
|
|
506
|
+
const claudeContent = hasClaudeMd ? readFileSync(claudePath, "utf8") : "";
|
|
507
|
+
const agentsContent = hasAgentsMd ? readFileSync(agentsPath, "utf8") : "";
|
|
508
|
+
const fallbackClaudeDoc = hasClaudeMd && claudeContent.trim() !== "@AGENTS.md" ? claudeContent : "";
|
|
509
|
+
const domainKnowledge = readDomainKnowledge(directory);
|
|
510
|
+
const docSource = hasAgentsMd ? "AGENTS.md" : fallbackClaudeDoc ? "CLAUDE.md" : null;
|
|
511
|
+
const manifest = readManifest(directory);
|
|
512
|
+
const objects = domainKnowledge ? readDomainKnowledgeObjects(domainKnowledge.content) : manifest ? readManifestObjects(manifest.content) : [];
|
|
513
|
+
const prompts = domainKnowledge ? readDomainKnowledgePrompts(domainKnowledge.content, directory, rootDir) : readPrompts(directory, rootDir);
|
|
514
|
+
const smrtDependencies = Object.keys(allDeps).filter((dep) => dep.startsWith("@happyvertical/smrt-")).sort();
|
|
515
|
+
const sdkDependencies = Object.keys(allDeps).filter((dep) => SDK_PACKAGE_NAMES.has(dep)).sort();
|
|
516
|
+
return {
|
|
517
|
+
name,
|
|
518
|
+
version: String(packageJson.version ?? "0.0.0"),
|
|
519
|
+
kind: packageKind(name),
|
|
520
|
+
directory,
|
|
521
|
+
relativeDirectory: relative(rootDir, directory),
|
|
522
|
+
files: Array.isArray(packageJson.files) ? packageJson.files : [],
|
|
523
|
+
exportKeys: exportKeys(packageJson.exports),
|
|
524
|
+
dependencies,
|
|
525
|
+
devDependencies,
|
|
526
|
+
peerDependencies,
|
|
527
|
+
smrtDependencies,
|
|
528
|
+
sdkDependencies,
|
|
529
|
+
hasAgentsMd,
|
|
530
|
+
hasClaudeMd,
|
|
531
|
+
hasClaudeShim: claudeContent.trim() === "@AGENTS.md",
|
|
532
|
+
docSource,
|
|
533
|
+
agentDoc: includeDocs ? domainKnowledge?.content.agentDoc || (hasAgentsMd ? agentsContent : fallbackClaudeDoc || void 0) : void 0,
|
|
534
|
+
hasDomainKnowledge: Boolean(domainKnowledge),
|
|
535
|
+
domainKnowledgePath: domainKnowledge?.path ? relative(rootDir, domainKnowledge.path) : void 0,
|
|
536
|
+
domainKnowledge: domainKnowledge?.content,
|
|
537
|
+
manifestPath: manifest?.path ? relative(rootDir, manifest.path) : void 0,
|
|
538
|
+
manifestVersion: typeof manifest?.content.version === "string" ? manifest.content.version : void 0,
|
|
539
|
+
objects,
|
|
540
|
+
prompts,
|
|
541
|
+
mcpTools: domainKnowledge ? domainMcpTools(domainKnowledge.content) : mcpTools(objects),
|
|
542
|
+
relationshipFeatures: relationshipFeatures(objects)
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function readDomainKnowledge(directory) {
|
|
546
|
+
for (const path of [
|
|
547
|
+
join(directory, ".smrt", "smrt-knowledge.json"),
|
|
548
|
+
join(directory, "dist", "smrt-knowledge.json"),
|
|
549
|
+
join(directory, "src", "manifest", "smrt-knowledge.json")
|
|
550
|
+
]) {
|
|
551
|
+
const content = readJson(path);
|
|
552
|
+
if (isDomainKnowledgeManifest(content)) {
|
|
553
|
+
return { path, content };
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
function isDomainKnowledgeManifest(value) {
|
|
559
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
const record = value;
|
|
563
|
+
return record.schemaVersion === 1 && Array.isArray(record.objects);
|
|
564
|
+
}
|
|
565
|
+
function readManifest(directory) {
|
|
566
|
+
for (const path of [
|
|
567
|
+
join(directory, "src", "manifest", "manifest.json"),
|
|
568
|
+
join(directory, ".smrt", "manifest.json"),
|
|
569
|
+
join(directory, "dist", "manifest.json")
|
|
570
|
+
]) {
|
|
571
|
+
const content = readJson(path);
|
|
572
|
+
if (content) return { path, content };
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
function readManifestObjects(manifest) {
|
|
577
|
+
const objects = objectRecord(manifest.objects);
|
|
578
|
+
return Object.values(objects).map((raw) => {
|
|
579
|
+
const item = raw;
|
|
580
|
+
const fields = objectRecord(item.fields);
|
|
581
|
+
const schemaColumns = objectRecord(item.schema?.columns);
|
|
582
|
+
const decoratorConfig = item.decoratorConfig;
|
|
583
|
+
const knowledgeFields = Object.entries(fields).map(([fieldName, field]) => {
|
|
584
|
+
const fieldInfo = field;
|
|
585
|
+
const columnName = camelToSnake(fieldName);
|
|
586
|
+
const column = schemaColumns[columnName];
|
|
587
|
+
return {
|
|
588
|
+
name: fieldName,
|
|
589
|
+
type: String(fieldInfo.type ?? "unknown"),
|
|
590
|
+
required: typeof fieldInfo.required === "boolean" ? fieldInfo.required : void 0,
|
|
591
|
+
related: typeof fieldInfo.related === "string" ? fieldInfo.related : void 0,
|
|
592
|
+
columnType: typeof column?.type === "string" ? column.type : void 0
|
|
593
|
+
};
|
|
594
|
+
});
|
|
595
|
+
return {
|
|
596
|
+
className: String(item.className ?? item.name ?? "Unknown"),
|
|
597
|
+
qualifiedName: typeof item.qualifiedName === "string" ? item.qualifiedName : void 0,
|
|
598
|
+
filePath: typeof item.filePath === "string" ? item.filePath : void 0,
|
|
599
|
+
extends: typeof item.extends === "string" ? item.extends : void 0,
|
|
600
|
+
collection: typeof item.collection === "string" ? item.collection : void 0,
|
|
601
|
+
mcpOperations: mcpOperations(item.decoratorConfig?.mcp),
|
|
602
|
+
tableName: typeof decoratorConfig?.tableName === "string" ? decoratorConfig.tableName : typeof item.schema?.tableName === "string" ? item.schema.tableName : void 0,
|
|
603
|
+
idColumnType: typeof schemaColumns.id?.type === "string" ? schemaColumns.id.type : void 0,
|
|
604
|
+
fields: knowledgeFields,
|
|
605
|
+
relationships: knowledgeFields.filter(
|
|
606
|
+
(field) => RELATIONSHIP_FIELD_TYPES.has(field.type)
|
|
607
|
+
),
|
|
608
|
+
methods: Object.keys(objectRecord(item.methods)).sort()
|
|
609
|
+
};
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
function readDomainKnowledgeObjects(manifest) {
|
|
613
|
+
return manifest.objects.map((object) => ({
|
|
614
|
+
className: object.name,
|
|
615
|
+
qualifiedName: object.qualifiedName,
|
|
616
|
+
extends: object.extends,
|
|
617
|
+
collection: object.collection,
|
|
618
|
+
mcpOperations: object.surfaces.filter((surface) => surface.kind === "mcp").map((surface) => surface.operation).sort(),
|
|
619
|
+
tableName: object.tableName,
|
|
620
|
+
idColumnType: object.fields.find((field) => field.name === "id")?.columnType,
|
|
621
|
+
fields: object.fields.map((field) => ({
|
|
622
|
+
name: field.name,
|
|
623
|
+
type: field.type,
|
|
624
|
+
required: field.required,
|
|
625
|
+
related: field.related,
|
|
626
|
+
columnType: field.columnType
|
|
627
|
+
})),
|
|
628
|
+
relationships: object.relationships.map((field) => ({
|
|
629
|
+
name: field.name,
|
|
630
|
+
type: field.type,
|
|
631
|
+
required: field.required,
|
|
632
|
+
related: field.related,
|
|
633
|
+
columnType: field.columnType
|
|
634
|
+
})),
|
|
635
|
+
methods: object.methods
|
|
636
|
+
}));
|
|
637
|
+
}
|
|
638
|
+
function readDomainKnowledgePrompts(manifest, directory, rootDir) {
|
|
639
|
+
return manifest.prompts.map((prompt) => ({
|
|
640
|
+
filePath: relative(rootDir, join(directory, prompt.filePath)),
|
|
641
|
+
key: prompt.key
|
|
642
|
+
}));
|
|
643
|
+
}
|
|
644
|
+
function readPrompts(directory, rootDir) {
|
|
645
|
+
const srcDir = join(directory, "src");
|
|
646
|
+
if (!existsSync(srcDir)) return [];
|
|
647
|
+
const prompts = [];
|
|
648
|
+
for (const filePath of walkFiles(srcDir)) {
|
|
649
|
+
if (!filePath.endsWith(".ts")) continue;
|
|
650
|
+
const content = readFileSync(filePath, "utf8");
|
|
651
|
+
if (!content.includes("definePrompt")) continue;
|
|
652
|
+
const keyMatch = content.match(/definePrompt\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
653
|
+
prompts.push({
|
|
654
|
+
filePath: relative(rootDir, filePath),
|
|
655
|
+
key: keyMatch?.[1]
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
return prompts;
|
|
659
|
+
}
|
|
660
|
+
function walkFiles(dir) {
|
|
661
|
+
const files = [];
|
|
662
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
663
|
+
if (WALK_SKIP_DIRS.has(entry.name)) continue;
|
|
664
|
+
const fullPath = join(dir, entry.name);
|
|
665
|
+
if (entry.isDirectory()) {
|
|
666
|
+
files.push(...walkFiles(fullPath));
|
|
667
|
+
} else if (entry.isFile()) {
|
|
668
|
+
files.push(fullPath);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return files;
|
|
672
|
+
}
|
|
673
|
+
function packageKind(name) {
|
|
674
|
+
if (name.startsWith("@happyvertical/smrt-")) return "smrt";
|
|
675
|
+
if (SDK_PACKAGE_NAMES.has(name)) return "sdk";
|
|
676
|
+
return "workspace";
|
|
677
|
+
}
|
|
678
|
+
function exportKeys(exportsField) {
|
|
679
|
+
if (typeof exportsField === "string") return ["."];
|
|
680
|
+
if (typeof exportsField !== "object" || exportsField === null || Array.isArray(exportsField)) {
|
|
681
|
+
return [];
|
|
682
|
+
}
|
|
683
|
+
return Object.keys(exportsField).sort();
|
|
684
|
+
}
|
|
685
|
+
function mcpOperations(config) {
|
|
686
|
+
if (config === false) return [];
|
|
687
|
+
const defaultOperations = ["list", "get", "create", "update", "delete"];
|
|
688
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
689
|
+
return defaultOperations;
|
|
690
|
+
}
|
|
691
|
+
const record = config;
|
|
692
|
+
const include = Array.isArray(record.include) ? record.include.filter((item) => typeof item === "string") : defaultOperations;
|
|
693
|
+
const exclude = new Set(
|
|
694
|
+
Array.isArray(record.exclude) ? record.exclude.filter(
|
|
695
|
+
(item) => typeof item === "string"
|
|
696
|
+
) : []
|
|
697
|
+
);
|
|
698
|
+
return include.filter((operation) => !exclude.has(operation));
|
|
699
|
+
}
|
|
700
|
+
function mcpTools(objects) {
|
|
701
|
+
return objects.flatMap((object) => {
|
|
702
|
+
const collection = object.collection ?? object.className.toLowerCase();
|
|
703
|
+
return object.mcpOperations.map((operation) => ({
|
|
704
|
+
name: `${operation}_${collection}`,
|
|
705
|
+
sourceObject: object.qualifiedName ?? object.className,
|
|
706
|
+
operation
|
|
707
|
+
}));
|
|
708
|
+
}).sort((a, b) => a.name.localeCompare(b.name));
|
|
709
|
+
}
|
|
710
|
+
function domainMcpTools(manifest) {
|
|
711
|
+
return manifest.surfaces.filter((surface) => surface.kind === "mcp").map((surface) => ({
|
|
712
|
+
name: surface.name,
|
|
713
|
+
sourceObject: surface.objectName ?? "",
|
|
714
|
+
operation: surface.operation
|
|
715
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
716
|
+
}
|
|
717
|
+
function summarizeRelationshipsV2(packages) {
|
|
718
|
+
const objects = packages.flatMap((pkg) => pkg.objects);
|
|
719
|
+
const fields = objects.flatMap((object) => object.fields);
|
|
720
|
+
return {
|
|
721
|
+
foreignKeyFields: fields.filter((field) => field.type === "foreignKey").length,
|
|
722
|
+
crossPackageRefFields: fields.filter(
|
|
723
|
+
(field) => field.type === "crossPackageRef"
|
|
724
|
+
).length,
|
|
725
|
+
junctionCollections: objects.filter(
|
|
726
|
+
(object) => object.extends === "SmrtJunction"
|
|
727
|
+
).length,
|
|
728
|
+
hierarchicalObjects: objects.filter(
|
|
729
|
+
(object) => object.extends === "SmrtHierarchical"
|
|
730
|
+
).length,
|
|
731
|
+
polymorphicAssociations: objects.filter(
|
|
732
|
+
(object) => object.fields.some(
|
|
733
|
+
(field) => field.name === "metaType" || field.name === "metaId"
|
|
734
|
+
)
|
|
735
|
+
).length,
|
|
736
|
+
uuidColumns: fields.filter((field) => field.columnType === "UUID").length
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
function relationshipFeatures(objects) {
|
|
740
|
+
const features = /* @__PURE__ */ new Set();
|
|
741
|
+
if (objects.some(
|
|
742
|
+
(object) => object.fields.some((field) => field.type === "foreignKey")
|
|
743
|
+
)) {
|
|
744
|
+
features.add("foreignKey");
|
|
745
|
+
}
|
|
746
|
+
if (objects.some(
|
|
747
|
+
(object) => object.fields.some((field) => field.type === "crossPackageRef")
|
|
748
|
+
)) {
|
|
749
|
+
features.add("crossPackageRef");
|
|
750
|
+
}
|
|
751
|
+
if (objects.some((object) => object.extends === "SmrtJunction")) {
|
|
752
|
+
features.add("SmrtJunction");
|
|
753
|
+
}
|
|
754
|
+
if (objects.some((object) => object.extends === "SmrtHierarchical")) {
|
|
755
|
+
features.add("SmrtHierarchical");
|
|
756
|
+
}
|
|
757
|
+
if (objects.some(
|
|
758
|
+
(object) => object.fields.some(
|
|
759
|
+
(field) => field.name === "metaType" || field.name === "metaId"
|
|
760
|
+
)
|
|
761
|
+
)) {
|
|
762
|
+
features.add("SmrtPolymorphicAssociation");
|
|
763
|
+
}
|
|
764
|
+
if (objects.some(
|
|
765
|
+
(object) => object.fields.some((field) => field.columnType === "UUID")
|
|
766
|
+
)) {
|
|
767
|
+
features.add("uuidColumns");
|
|
768
|
+
}
|
|
769
|
+
return [...features].sort();
|
|
770
|
+
}
|
|
771
|
+
function findStalePatternIssues(rootDir, changedFiles) {
|
|
772
|
+
const candidates = changedFiles && changedFiles.length > 0 ? changedFiles.map((file) => join(rootDir, file)) : walkFiles(rootDir);
|
|
773
|
+
const issues = [];
|
|
774
|
+
for (const file of candidates.filter(
|
|
775
|
+
(candidate) => shouldScanStalePatternFile(rootDir, candidate)
|
|
776
|
+
)) {
|
|
777
|
+
if (!existsSync(file) || lstatSync(file).isDirectory()) continue;
|
|
778
|
+
const rel = relative(rootDir, file);
|
|
779
|
+
const content = readFileSync(file, "utf8");
|
|
780
|
+
for (const stale of STALE_PATTERNS) {
|
|
781
|
+
if (!stale.pattern.test(content)) continue;
|
|
782
|
+
issues.push({
|
|
783
|
+
severity: "warning",
|
|
784
|
+
code: stale.code,
|
|
785
|
+
message: stale.message,
|
|
786
|
+
file: rel
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return issues;
|
|
791
|
+
}
|
|
792
|
+
function shouldScanStalePatternFile(rootDir, filePath) {
|
|
793
|
+
const rel = relative(rootDir, filePath).replaceAll("\\", "/");
|
|
794
|
+
if (!rel || rel.startsWith("..")) return false;
|
|
795
|
+
const parts = rel.split("/");
|
|
796
|
+
if (parts.includes("node_modules") || parts.includes("dist")) return false;
|
|
797
|
+
if (rel === "AGENTS.md" || rel === "README.md") return true;
|
|
798
|
+
if (rel.endsWith("/AGENTS.md") || rel.endsWith("/README.md")) return true;
|
|
799
|
+
if (!rel.startsWith("docs/content/") || !rel.endsWith(".md")) return false;
|
|
800
|
+
return !(rel.startsWith("docs/content/api/") || rel.startsWith("docs/content/rfcs/") || rel.startsWith("docs/content/architecture/"));
|
|
801
|
+
}
|
|
802
|
+
function buildReviewFindings(index, changedFiles, selectedPackages) {
|
|
803
|
+
const issues = [];
|
|
804
|
+
for (const pkg of selectedPackages) {
|
|
805
|
+
const changedPackageFiles = changedFiles.filter(
|
|
806
|
+
(file) => file === pkg.relativeDirectory || file.startsWith(`${pkg.relativeDirectory}/`)
|
|
807
|
+
);
|
|
808
|
+
if (!pkg.agentDoc && pkg.kind === "smrt") {
|
|
809
|
+
issues.push({
|
|
810
|
+
severity: "warning",
|
|
811
|
+
code: "missing-package-expertise",
|
|
812
|
+
message: "Selected SMRT package has no authored AGENTS.md expertise",
|
|
813
|
+
file: pkg.relativeDirectory,
|
|
814
|
+
packageName: pkg.name
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
if (changedPackageFiles.some((file) => file.endsWith(".ts")) && pkg.relationshipFeatures.length > 0) {
|
|
818
|
+
issues.push({
|
|
819
|
+
severity: "warning",
|
|
820
|
+
code: "relationship-sensitive-review",
|
|
821
|
+
message: `Package uses relationships-v2 features: ${pkg.relationshipFeatures.join(", ")}`,
|
|
822
|
+
file: changedPackageFiles.find((file) => file.endsWith(".ts")),
|
|
823
|
+
packageName: pkg.name
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
if (changedPackageFiles.some((file) => file.endsWith(".ts")) && pkg.mcpTools.length > 0) {
|
|
827
|
+
issues.push({
|
|
828
|
+
severity: "warning",
|
|
829
|
+
code: "mcp-surface-review",
|
|
830
|
+
message: `Package exposes ${pkg.mcpTools.length} generated MCP tool(s); check public tool compatibility`,
|
|
831
|
+
file: changedPackageFiles.find((file) => file.endsWith(".ts")),
|
|
832
|
+
packageName: pkg.name
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
const manifestFile = changedPackageFiles.find(
|
|
836
|
+
(file) => isPackageManifestFile(pkg, file)
|
|
837
|
+
);
|
|
838
|
+
if (manifestFile) {
|
|
839
|
+
issues.push({
|
|
840
|
+
severity: "warning",
|
|
841
|
+
code: "package-manifest-review",
|
|
842
|
+
message: "package.json changed; verify exports, files, dependencies, AGENTS.md, and CLAUDE.md shim packaging",
|
|
843
|
+
file: manifestFile,
|
|
844
|
+
packageName: pkg.name
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
const publicEntrypointFile = changedPackageFiles.find(
|
|
848
|
+
(file) => isPublicEntrypointFile(pkg, file)
|
|
849
|
+
);
|
|
850
|
+
if (publicEntrypointFile) {
|
|
851
|
+
issues.push({
|
|
852
|
+
severity: "warning",
|
|
853
|
+
code: "public-entrypoint-review",
|
|
854
|
+
message: "Public package entrypoint changed; check exports, generated surfaces, and downstream docs",
|
|
855
|
+
file: publicEntrypointFile,
|
|
856
|
+
packageName: pkg.name
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
const agentDocFile = changedPackageFiles.find(
|
|
860
|
+
(file) => isPackageAgentDocFile(pkg, file)
|
|
861
|
+
);
|
|
862
|
+
if (agentDocFile) {
|
|
863
|
+
issues.push({
|
|
864
|
+
severity: "warning",
|
|
865
|
+
code: "agent-expertise-review",
|
|
866
|
+
message: "AGENTS.md changed; validate authored expertise against generated objects, relationships, exports, and SDK dependencies",
|
|
867
|
+
file: agentDocFile,
|
|
868
|
+
packageName: pkg.name
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
for (const file of changedFiles) {
|
|
873
|
+
if (!file.startsWith("packages/")) continue;
|
|
874
|
+
if (selectPackagesForFiles(index, [file]).length > 0) continue;
|
|
875
|
+
issues.push({
|
|
876
|
+
severity: "warning",
|
|
877
|
+
code: "changed-file-without-package-expert",
|
|
878
|
+
message: "Changed file is under packages/ but did not map to a SMRT package",
|
|
879
|
+
file
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
return issues;
|
|
883
|
+
}
|
|
884
|
+
function isPackageManifestFile(pkg, file) {
|
|
885
|
+
return file === `${pkg.relativeDirectory}/package.json`;
|
|
886
|
+
}
|
|
887
|
+
function isPackageAgentDocFile(pkg, file) {
|
|
888
|
+
return file === `${pkg.relativeDirectory}/AGENTS.md`;
|
|
889
|
+
}
|
|
890
|
+
function isPublicEntrypointFile(pkg, file) {
|
|
891
|
+
const sourcePrefix = `${pkg.relativeDirectory}/src/`;
|
|
892
|
+
if (!file.startsWith(sourcePrefix)) return false;
|
|
893
|
+
const sourcePath = file.slice(sourcePrefix.length);
|
|
894
|
+
return sourcePath === "index.ts" || sourcePath === "index.tsx" || sourcePath === "index.js" || sourcePath.startsWith("api/") || sourcePath.startsWith("cli/") || sourcePath.startsWith("mcp/") || sourcePath.startsWith("tools/");
|
|
895
|
+
}
|
|
896
|
+
function buildArchitectureRecommendations(context, ideaText) {
|
|
897
|
+
const smrtPackages = context.selectedPackages.map((pkg) => pkg.name);
|
|
898
|
+
const sdkPackages = context.selectedSdkPackages.map((pkg) => pkg.name);
|
|
899
|
+
const objectModelSketch = buildObjectModelSketch(context.selectedPackages);
|
|
900
|
+
const risks = buildArchitectureRisks(context.selectedPackages, ideaText);
|
|
901
|
+
const questions = buildArchitectureQuestions(
|
|
902
|
+
context.selectedPackages,
|
|
903
|
+
ideaText
|
|
904
|
+
);
|
|
905
|
+
return {
|
|
906
|
+
smrtPackages,
|
|
907
|
+
sdkPackages,
|
|
908
|
+
objectModelSketch,
|
|
909
|
+
risks,
|
|
910
|
+
questions,
|
|
911
|
+
notes: [
|
|
912
|
+
"Use SMRT packages for domain/runtime models and generated REST, CLI, MCP, and AI-operation surfaces.",
|
|
913
|
+
"Use HappyVertical SDK packages for AI, SQL, files, logging, secrets, and external capability adapters.",
|
|
914
|
+
"Run smrt dev:knowledge-check after applying model-assisted architecture or review updates."
|
|
915
|
+
]
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function buildObjectModelSketch(packages) {
|
|
919
|
+
const lines = packages.flatMap((pkg) => {
|
|
920
|
+
const objects = pkg.objects.filter((object) => object.extends !== "SmrtCollection").slice(0, 6).map((object) => object.className);
|
|
921
|
+
if (objects.length === 0) {
|
|
922
|
+
return [
|
|
923
|
+
`${pkg.name}: use package services or templates; no manifest objects indexed.`
|
|
924
|
+
];
|
|
925
|
+
}
|
|
926
|
+
return [`${pkg.name}: start from ${objects.join(", ")}.`];
|
|
927
|
+
});
|
|
928
|
+
return lines.length > 0 ? lines : [
|
|
929
|
+
"Define the core domain as SmrtObject classes with explicit relationships and generated surfaces."
|
|
930
|
+
];
|
|
931
|
+
}
|
|
932
|
+
function buildArchitectureRisks(packages, ideaText) {
|
|
933
|
+
const risks = /* @__PURE__ */ new Set();
|
|
934
|
+
const names = new Set(packages.map((pkg) => pkg.name));
|
|
935
|
+
const relationshipFeatures2 = new Set(
|
|
936
|
+
packages.flatMap((pkg) => pkg.relationshipFeatures)
|
|
937
|
+
);
|
|
938
|
+
const lowerText = ideaText.toLowerCase();
|
|
939
|
+
if (relationshipFeatures2.has("crossPackageRef")) {
|
|
940
|
+
risks.add(
|
|
941
|
+
"Cross-package references should stay as plain string ids and validate target package ownership at workflow boundaries."
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
if (relationshipFeatures2.has("SmrtJunction")) {
|
|
945
|
+
risks.add(
|
|
946
|
+
"Junction models need explicit conflictColumns so generated upserts stay deterministic."
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
if (relationshipFeatures2.has("SmrtHierarchical")) {
|
|
950
|
+
risks.add(
|
|
951
|
+
"Hierarchical models need cycle prevention and tenant-aware child loading paths."
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
if (names.has("@happyvertical/smrt-tenancy") || lowerText.includes("tenant")) {
|
|
955
|
+
risks.add(
|
|
956
|
+
"Tenant-scoped models need nullable tenantId semantics and tenant-guarded loadRelated usage."
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
if (names.has("@happyvertical/smrt-assets")) {
|
|
960
|
+
risks.add(
|
|
961
|
+
"Asset ownership should use package-owned join tables; reserve asset_associations for generic/provenance links."
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
if (names.has("@happyvertical/smrt-secrets")) {
|
|
965
|
+
risks.add(
|
|
966
|
+
"Secrets must use envelope encryption and avoid exposing decrypted values through generated tools."
|
|
967
|
+
);
|
|
968
|
+
}
|
|
969
|
+
if (names.has("@happyvertical/smrt-jobs") || lowerText.includes("schedule")) {
|
|
970
|
+
risks.add(
|
|
971
|
+
"Background work should use jobs/schedules rather than request-time side effects."
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
risks.add(
|
|
975
|
+
"Generated architecture should be rechecked with smrt dev:knowledge-check after docs or expertise edits."
|
|
976
|
+
);
|
|
977
|
+
return [...risks];
|
|
978
|
+
}
|
|
979
|
+
function buildArchitectureQuestions(packages, ideaText) {
|
|
980
|
+
const questions = /* @__PURE__ */ new Set();
|
|
981
|
+
const names = new Set(packages.map((pkg) => pkg.name));
|
|
982
|
+
const lowerText = ideaText.toLowerCase();
|
|
983
|
+
questions.add(
|
|
984
|
+
"Which generated surfaces are required first: REST, CLI, MCP, AI operations, or Svelte UI?"
|
|
985
|
+
);
|
|
986
|
+
if (names.has("@happyvertical/smrt-tenancy") || lowerText.includes("tenant")) {
|
|
987
|
+
questions.add(
|
|
988
|
+
"Which objects are global catalogs and which are tenant-scoped records?"
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
if (names.has("@happyvertical/smrt-assets")) {
|
|
992
|
+
questions.add("Which package owns each asset relationship join table?");
|
|
993
|
+
}
|
|
994
|
+
if (names.has("@happyvertical/smrt-profiles")) {
|
|
995
|
+
questions.add("Which identities own or administer the primary records?");
|
|
996
|
+
}
|
|
997
|
+
if (names.has("@happyvertical/smrt-content")) {
|
|
998
|
+
questions.add(
|
|
999
|
+
"Which content snapshots need citation-time reference pinning?"
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
if (names.has("@happyvertical/smrt-social")) {
|
|
1003
|
+
questions.add(
|
|
1004
|
+
"Which social providers need OAuth, scheduling, and post-state reconciliation?"
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
return [...questions];
|
|
1008
|
+
}
|
|
1009
|
+
function selectPackagesForFiles(index, changedFiles) {
|
|
1010
|
+
const selected = /* @__PURE__ */ new Set();
|
|
1011
|
+
for (const file of changedFiles) {
|
|
1012
|
+
for (const pkg of domainPackages(index)) {
|
|
1013
|
+
if (file === pkg.relativeDirectory || file.startsWith(`${pkg.relativeDirectory}/`)) {
|
|
1014
|
+
selected.add(pkg);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
return [...selected].sort((a, b) => a.name.localeCompare(b.name));
|
|
1019
|
+
}
|
|
1020
|
+
function selectPackages(index, options) {
|
|
1021
|
+
const selected = /* @__PURE__ */ new Set();
|
|
1022
|
+
const packageName = options.packageName?.toLowerCase();
|
|
1023
|
+
if (packageName) {
|
|
1024
|
+
for (const pkg of domainPackages(index)) {
|
|
1025
|
+
if (packageMatches(pkg, packageName)) {
|
|
1026
|
+
selected.add(pkg);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
for (const pkg of selectPackagesForFiles(index, options.changedFiles ?? [])) {
|
|
1031
|
+
if (scopeAllowsPackage(pkg, options.scope)) selected.add(pkg);
|
|
1032
|
+
}
|
|
1033
|
+
const text = (options.text ?? "").toLowerCase();
|
|
1034
|
+
if (text) {
|
|
1035
|
+
for (const pkg of domainPackages(index)) {
|
|
1036
|
+
if (!scopeAllowsPackage(pkg, options.scope)) continue;
|
|
1037
|
+
const packageKey = pkg.name.replace("@happyvertical/smrt-", "");
|
|
1038
|
+
if (includesToken(text, packageKey) || text.includes(pkg.name.toLowerCase()) || pkg.objects.some(
|
|
1039
|
+
(object) => includesToken(text, object.className.toLowerCase())
|
|
1040
|
+
)) {
|
|
1041
|
+
selected.add(pkg);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (selected.size === 0 && options.scope === "local") {
|
|
1046
|
+
for (const pkg of domainPackages(index).filter(
|
|
1047
|
+
(item) => scopeAllowsPackage(item, "local")
|
|
1048
|
+
)) {
|
|
1049
|
+
selected.add(pkg);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
if (selected.size === 0 && options.scope !== "sdk") {
|
|
1053
|
+
for (const name of [
|
|
1054
|
+
"@happyvertical/smrt-core",
|
|
1055
|
+
"@happyvertical/smrt-config",
|
|
1056
|
+
"@happyvertical/smrt-cli",
|
|
1057
|
+
"@happyvertical/smrt-scanner",
|
|
1058
|
+
"@happyvertical/smrt-dev-mcp"
|
|
1059
|
+
]) {
|
|
1060
|
+
const pkg = index.smrtPackages.find((item) => item.name === name);
|
|
1061
|
+
if (pkg) selected.add(pkg);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
return [...selected].sort((a, b) => a.name.localeCompare(b.name));
|
|
1065
|
+
}
|
|
1066
|
+
function selectSdkPackages(index, selectedPackages, texts, options = {}) {
|
|
1067
|
+
const selected = /* @__PURE__ */ new Set();
|
|
1068
|
+
const sdkNames = new Set(
|
|
1069
|
+
selectedPackages.flatMap((pkg) => pkg.sdkDependencies)
|
|
1070
|
+
);
|
|
1071
|
+
const text = texts.filter(Boolean).join("\n").toLowerCase();
|
|
1072
|
+
const packageName = options.packageName?.toLowerCase();
|
|
1073
|
+
for (const sdk of index.sdkPackages) {
|
|
1074
|
+
const shortName = sdk.name.replace("@happyvertical/", "");
|
|
1075
|
+
if (options.scope === "sdk" || sdkNames.has(sdk.name) || packageName && packageMatches(sdk, packageName) || text.includes(sdk.name.toLowerCase()) || includesToken(text, shortName)) {
|
|
1076
|
+
selected.add(sdk);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (selected.size === 0) {
|
|
1080
|
+
for (const name of [
|
|
1081
|
+
"@happyvertical/ai",
|
|
1082
|
+
"@happyvertical/sql",
|
|
1083
|
+
"@happyvertical/files",
|
|
1084
|
+
"@happyvertical/utils"
|
|
1085
|
+
]) {
|
|
1086
|
+
const sdk = index.sdkPackages.find((item) => item.name === name);
|
|
1087
|
+
if (sdk) selected.add(sdk);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return [...selected].sort((a, b) => a.name.localeCompare(b.name));
|
|
1091
|
+
}
|
|
1092
|
+
function domainPackages(index) {
|
|
1093
|
+
return index.packages.filter((pkg) => pkg.kind !== "sdk");
|
|
1094
|
+
}
|
|
1095
|
+
function scopeAllowsPackage(pkg, scope) {
|
|
1096
|
+
switch (scope) {
|
|
1097
|
+
case "sdk":
|
|
1098
|
+
return false;
|
|
1099
|
+
case "local":
|
|
1100
|
+
return !pkg.relativeDirectory.includes("node_modules");
|
|
1101
|
+
case "package":
|
|
1102
|
+
case "project":
|
|
1103
|
+
case void 0:
|
|
1104
|
+
return true;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function packageMatches(pkg, query) {
|
|
1108
|
+
const normalized = query.toLowerCase();
|
|
1109
|
+
const shortName = pkg.name.replace("@happyvertical/smrt-", "").replace("@happyvertical/", "");
|
|
1110
|
+
return pkg.name.toLowerCase() === normalized || pkg.name.toLowerCase().includes(normalized) || shortName.toLowerCase() === normalized || pkg.relativeDirectory.toLowerCase().endsWith(`/${normalized}`);
|
|
1111
|
+
}
|
|
1112
|
+
function buildPromptBundle(options) {
|
|
1113
|
+
const contextMarkdown = [
|
|
1114
|
+
`# ${options.title}`,
|
|
1115
|
+
"",
|
|
1116
|
+
`Baseline root: ${options.index.rootDir}`,
|
|
1117
|
+
"",
|
|
1118
|
+
"## Task",
|
|
1119
|
+
"",
|
|
1120
|
+
options.task,
|
|
1121
|
+
"",
|
|
1122
|
+
"## Relationships-v2 Summary",
|
|
1123
|
+
"",
|
|
1124
|
+
JSON.stringify(options.index.relationshipsV2, null, 2),
|
|
1125
|
+
"",
|
|
1126
|
+
"## Selected SMRT Packages",
|
|
1127
|
+
"",
|
|
1128
|
+
...options.packages.map(renderPackageContext),
|
|
1129
|
+
"",
|
|
1130
|
+
"## Selected SDK Packages",
|
|
1131
|
+
"",
|
|
1132
|
+
...options.sdkPackages.map(renderPackageContext),
|
|
1133
|
+
"",
|
|
1134
|
+
options.extraContext ? `## Extra Context
|
|
1135
|
+
|
|
1136
|
+
${options.extraContext}
|
|
1137
|
+
` : ""
|
|
1138
|
+
];
|
|
1139
|
+
return {
|
|
1140
|
+
title: options.title,
|
|
1141
|
+
instructions: "Use the supplied SMRT knowledge context as source material. Return concrete findings or architecture guidance with package names and source references. Do not assume model-provider access.",
|
|
1142
|
+
contextMarkdown: contextMarkdown.join("\n"),
|
|
1143
|
+
selectedPackages: options.packages.map((pkg) => pkg.name),
|
|
1144
|
+
selectedSdkPackages: options.sdkPackages.map((pkg) => pkg.name),
|
|
1145
|
+
sourceFiles: options.sourceFiles
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
function renderPackageContext(pkg) {
|
|
1149
|
+
const lines = [
|
|
1150
|
+
`### ${pkg.name}`,
|
|
1151
|
+
"",
|
|
1152
|
+
`- version: ${pkg.version}`,
|
|
1153
|
+
`- kind: ${pkg.kind}`,
|
|
1154
|
+
`- directory: ${pkg.relativeDirectory}`,
|
|
1155
|
+
`- domain knowledge: ${pkg.domainKnowledgePath ?? "(manifest fallback)"}`,
|
|
1156
|
+
`- docs: ${pkg.docSource ?? "(none)"}`,
|
|
1157
|
+
`- relationship features: ${pkg.relationshipFeatures.join(", ") || "(none)"}`,
|
|
1158
|
+
`- SDK deps: ${pkg.sdkDependencies.join(", ") || "(none)"}`,
|
|
1159
|
+
`- exports: ${pkg.exportKeys.join(", ") || "(none)"}`,
|
|
1160
|
+
`- MCP tools: ${pkg.mcpTools.slice(0, 20).map((tool) => tool.name).join(", ") || "(none)"}`,
|
|
1161
|
+
`- objects: ${pkg.objects.slice(0, 20).map((object) => object.qualifiedName ?? object.className).join(", ")}`
|
|
1162
|
+
];
|
|
1163
|
+
if (pkg.objects.length > 20) {
|
|
1164
|
+
lines.push(`- object count: ${pkg.objects.length}`);
|
|
1165
|
+
}
|
|
1166
|
+
if (pkg.agentDoc) {
|
|
1167
|
+
lines.push("", pkg.agentDoc.trim());
|
|
1168
|
+
}
|
|
1169
|
+
lines.push("");
|
|
1170
|
+
return lines.join("\n");
|
|
1171
|
+
}
|
|
1172
|
+
function getChangedFiles(rootDir, base) {
|
|
1173
|
+
if (base) {
|
|
1174
|
+
try {
|
|
1175
|
+
const output = execFileSync(
|
|
1176
|
+
"git",
|
|
1177
|
+
["diff", "--name-only", `${base}...HEAD`],
|
|
1178
|
+
{
|
|
1179
|
+
cwd: rootDir,
|
|
1180
|
+
encoding: "utf8",
|
|
1181
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1182
|
+
}
|
|
1183
|
+
);
|
|
1184
|
+
const files = output.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1185
|
+
if (files.length > 0) return files;
|
|
1186
|
+
} catch {
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
try {
|
|
1190
|
+
const output = execFileSync("git", ["diff", "--name-only"], {
|
|
1191
|
+
cwd: rootDir,
|
|
1192
|
+
encoding: "utf8",
|
|
1193
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1194
|
+
});
|
|
1195
|
+
const cachedOutput = execFileSync(
|
|
1196
|
+
"git",
|
|
1197
|
+
["diff", "--cached", "--name-only"],
|
|
1198
|
+
{
|
|
1199
|
+
cwd: rootDir,
|
|
1200
|
+
encoding: "utf8",
|
|
1201
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
1202
|
+
}
|
|
1203
|
+
);
|
|
1204
|
+
return uniqueStrings(
|
|
1205
|
+
[output, cachedOutput].flatMap(
|
|
1206
|
+
(value) => value.split("\n").map((line) => line.trim()).filter(Boolean)
|
|
1207
|
+
)
|
|
1208
|
+
);
|
|
1209
|
+
} catch {
|
|
1210
|
+
return [];
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function dedupePackages(packages) {
|
|
1214
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1215
|
+
for (const pkg of packages) {
|
|
1216
|
+
const current = byName.get(pkg.name);
|
|
1217
|
+
if (!current || current.kind === "sdk") {
|
|
1218
|
+
byName.set(pkg.name, pkg);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
1222
|
+
}
|
|
1223
|
+
function includesToken(text, token) {
|
|
1224
|
+
const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1225
|
+
return new RegExp(`\\b${escaped}\\b`, "i").test(text);
|
|
1226
|
+
}
|
|
1227
|
+
function readJson(path) {
|
|
1228
|
+
if (!existsSync(path)) return null;
|
|
1229
|
+
try {
|
|
1230
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
1231
|
+
} catch {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
function hashFile(path) {
|
|
1236
|
+
return createHash("sha256").update(readFileSync(path, "utf8")).digest("hex");
|
|
1237
|
+
}
|
|
1238
|
+
function hashJsonFile(path) {
|
|
1239
|
+
const content = readJson(path);
|
|
1240
|
+
const hashContent = content ? stableJson(normalizeJsonForHash(content)) : readFileSync(path, "utf8");
|
|
1241
|
+
return createHash("sha256").update(hashContent).digest("hex");
|
|
1242
|
+
}
|
|
1243
|
+
function objectRecord(value) {
|
|
1244
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
1245
|
+
}
|
|
1246
|
+
function camelToSnake(value) {
|
|
1247
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
1248
|
+
}
|
|
1249
|
+
function uniqueStrings(values) {
|
|
1250
|
+
return [...new Set(values)].sort();
|
|
1251
|
+
}
|
|
1252
|
+
function normalizeJsonForHash(value) {
|
|
1253
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1254
|
+
return value;
|
|
1255
|
+
}
|
|
1256
|
+
const normalized = { ...value };
|
|
1257
|
+
delete normalized.timestamp;
|
|
1258
|
+
return normalized;
|
|
1259
|
+
}
|
|
1260
|
+
function stableJson(value) {
|
|
1261
|
+
return JSON.stringify(sortJson(value), null, 2);
|
|
1262
|
+
}
|
|
1263
|
+
function sortJson(value) {
|
|
1264
|
+
if (Array.isArray(value)) return value.map(sortJson);
|
|
1265
|
+
if (value && typeof value === "object") {
|
|
1266
|
+
return Object.fromEntries(
|
|
1267
|
+
Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortJson(entry)])
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
return value;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
export { buildKnowledgeIndex as a, buildArchitectureContext as b, buildReviewContext as c, checkKnowledgeFreshness as d, checkKnowledgeFreshnessFromIndex as e, diffKnowledgeIndex as f, renderKnowledgeIndexMarkdown as g, smrtReview as h, renderFreshnessResult as r, smrtArchitecture as s };
|
|
1274
|
+
//# sourceMappingURL=index-DF0HSB_8.js.map
|