@happyvertical/smrt-dev-mcp 0.40.37 → 0.40.39
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 +89 -2
- package/README.md +70 -3
- package/dist/index.d.ts +61 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +190 -24
- package/dist/index.js.map +1 -1
- package/dist/knowledge/index.d.ts +78 -1
- package/dist/knowledge/index.d.ts.map +1 -1
- package/dist/{knowledge-BPbgCtVJ.js → knowledge-DUUhTM5u.js} +796 -61
- package/dist/knowledge-DUUhTM5u.js.map +1 -0
- package/dist/knowledge.js +2 -2
- package/dist/tools/introspect-project.d.ts +17 -0
- package/dist/tools/introspect-project.d.ts.map +1 -1
- package/package.json +4 -4
- package/dist/knowledge-BPbgCtVJ.js.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync, lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
1
|
+
import { existsSync, lstatSync, opendirSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { execFileSync } from "node:child_process";
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { MODULE_DOC_HASH_PREFIX, readAgentModuleDocs } from "@happyvertical/smrt-core/knowledge";
|
|
6
|
+
import { ManifestAdapter, OxcScanner } from "@happyvertical/smrt-scanner";
|
|
6
7
|
//#region src/knowledge/index.ts
|
|
7
8
|
var SDK_PACKAGE_NAMES = /* @__PURE__ */ new Set([
|
|
8
9
|
"@happyvertical/ai",
|
|
@@ -40,6 +41,39 @@ var WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
40
41
|
"dist",
|
|
41
42
|
"node_modules"
|
|
42
43
|
]);
|
|
44
|
+
/** Used only when neither pnpm-workspace.yaml nor package.json#workspaces exists. */
|
|
45
|
+
var WORKSPACE_GLOB_FALLBACK = ["packages/*"];
|
|
46
|
+
/**
|
|
47
|
+
* Bounds total directory-entry work across every workspace glob. This is a
|
|
48
|
+
* cardinality budget rather than a depth cap: valid deeply nested workspaces
|
|
49
|
+
* remain discoverable, while broad or repeated globstars fail loudly before
|
|
50
|
+
* they can amplify into unbounded filesystem work.
|
|
51
|
+
*/
|
|
52
|
+
var MAX_WORKSPACE_GLOB_TRAVERSAL_ENTRIES = 1e4;
|
|
53
|
+
/** Prevents a broad but sub-budget glob from fanning out package reads. */
|
|
54
|
+
var MAX_DISCOVERED_WORKSPACE_PACKAGES = 512;
|
|
55
|
+
/** Bounds simultaneous OxcScanner instances and their filesystem handles. */
|
|
56
|
+
var MAX_SCANNER_CONCURRENCY = 8;
|
|
57
|
+
/** Caps the downstream package fallback so a large product stays in budget. */
|
|
58
|
+
var MAX_FALLBACK_PACKAGES = 8;
|
|
59
|
+
/** Kept in sync with `tools/introspect-project.ts` so both paths see one corpus. */
|
|
60
|
+
var SCAN_INCLUDE = [
|
|
61
|
+
"**/*.ts",
|
|
62
|
+
"**/*.tsx",
|
|
63
|
+
"**/*.js",
|
|
64
|
+
"**/*.jsx"
|
|
65
|
+
];
|
|
66
|
+
var SCAN_EXCLUDE = [
|
|
67
|
+
"**/node_modules/**",
|
|
68
|
+
"**/dist/**",
|
|
69
|
+
"**/build/**",
|
|
70
|
+
"**/.git/**",
|
|
71
|
+
"**/.smrt/**",
|
|
72
|
+
"**/*.d.ts",
|
|
73
|
+
"**/*.test.ts",
|
|
74
|
+
"**/*.spec.ts",
|
|
75
|
+
"**/__tests__/**"
|
|
76
|
+
];
|
|
43
77
|
var STALE_PATTERNS = [
|
|
44
78
|
{
|
|
45
79
|
code: "stale-have-namespace",
|
|
@@ -65,45 +99,185 @@ var STALE_PATTERNS = [
|
|
|
65
99
|
async function buildKnowledgeIndex(options = {}) {
|
|
66
100
|
const rootDir = findProjectRoot(options.rootDir ?? process.cwd());
|
|
67
101
|
const includeDocs = options.includeDocs ?? true;
|
|
68
|
-
const
|
|
69
|
-
const
|
|
102
|
+
const { globs, globSource, dirs: discoveredPackageDirs, diagnostics: discoveryDiagnostics } = discoverProjectPackageDirs(rootDir);
|
|
103
|
+
const resolvedRoot = realpathSync(rootDir);
|
|
104
|
+
const packageDirs = [];
|
|
105
|
+
const packages = [];
|
|
106
|
+
for (const dir of discoveredPackageDirs) {
|
|
107
|
+
if (confinedRealPath(resolvedRoot, dir) === void 0) {
|
|
108
|
+
discoveryDiagnostics.push({
|
|
109
|
+
severity: "error",
|
|
110
|
+
code: "workspace-package-root-escape",
|
|
111
|
+
message: "Rejected a workspace package whose real path changed or escaped the workspace root before it could be read.",
|
|
112
|
+
remedy: "Keep workspace package paths stable and inside the workspace root throughout knowledge discovery."
|
|
113
|
+
});
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
packageDirs.push(dir);
|
|
117
|
+
packages.push(readKnowledgePackage(rootDir, dir, includeDocs));
|
|
118
|
+
}
|
|
119
|
+
await applyScannerFallbacks(packages, packageDirs.filter((dir) => resolve(dir) !== resolve(rootDir)).map((dir) => `${relative(rootDir, dir).replaceAll("\\", "/")}/**`));
|
|
70
120
|
packages.push(...discoverInstalledSdkPackages(rootDir, packageDirs, includeDocs));
|
|
71
|
-
const
|
|
121
|
+
const uniquePackages = dedupePackages(packages);
|
|
122
|
+
const scopedPackages = filterKnowledgePackages(uniquePackages, options);
|
|
72
123
|
const smrtPackages = scopedPackages.filter((pkg) => pkg.kind === "smrt");
|
|
73
124
|
const sdkPackages = scopedPackages.filter((pkg) => pkg.kind === "sdk");
|
|
125
|
+
const coverage = buildCoverage({
|
|
126
|
+
rootDir,
|
|
127
|
+
globs,
|
|
128
|
+
globSource,
|
|
129
|
+
packageDirs,
|
|
130
|
+
packages: uniquePackages
|
|
131
|
+
});
|
|
74
132
|
return {
|
|
75
|
-
schemaVersion:
|
|
133
|
+
schemaVersion: 2,
|
|
76
134
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
77
135
|
rootDir,
|
|
78
136
|
packages: scopedPackages,
|
|
79
137
|
smrtPackages,
|
|
80
138
|
sdkPackages,
|
|
81
|
-
relationshipsV2: summarizeRelationshipsV2(scopedPackages)
|
|
139
|
+
relationshipsV2: summarizeRelationshipsV2(scopedPackages),
|
|
140
|
+
coverage,
|
|
141
|
+
diagnostics: buildIndexDiagnostics(rootDir, uniquePackages, coverage, discoveryDiagnostics)
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function buildCoverage(options) {
|
|
145
|
+
return {
|
|
146
|
+
workspaceGlobs: options.globs,
|
|
147
|
+
workspaceGlobSource: options.globSource,
|
|
148
|
+
packageDirs: options.packageDirs.map((dir) => relative(options.rootDir, dir) || "."),
|
|
149
|
+
packagesWithObjects: options.packages.filter((pkg) => pkg.objects.length > 0).map((pkg) => `${pkg.name} (${pkg.objects.length}, ${pkg.objectSource})`),
|
|
150
|
+
packagesWithoutObjects: options.packages.filter((pkg) => pkg.objects.length === 0).map((pkg) => ({
|
|
151
|
+
name: pkg.name,
|
|
152
|
+
reason: pkg.objectSourceReason ?? pkg.objectSource,
|
|
153
|
+
checkedPaths: pkg.checkedObjectPaths,
|
|
154
|
+
remedy: remedyForReason(pkg)
|
|
155
|
+
}))
|
|
82
156
|
};
|
|
83
157
|
}
|
|
158
|
+
function remedyForReason(pkg) {
|
|
159
|
+
const reason = pkg.objectSourceReason ?? "";
|
|
160
|
+
if (reason.startsWith("manifest-objects-owned-by-other-packages")) return `${pkg.relativeDirectory || "."} has an aggregate or stale manifest owned by other packages. Regenerate it (pnpm build in that package) so it declares this package's own objects.`;
|
|
161
|
+
if (reason.startsWith("scanner-failed")) return `Source scanning failed for ${pkg.relativeDirectory || "."}; fix the parse error or generate a manifest with pnpm build.`;
|
|
162
|
+
if (reason === "no-smrt-objects-in-sources") return "No @smrt() classes were found in this package. Expected if it is a UI, contract, or tooling package.";
|
|
163
|
+
return `Add @smrt() classes, or run pnpm build in ${pkg.relativeDirectory || "."} to emit .smrt/manifest.json.`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Turns a discovery failure into an explicit signal.
|
|
167
|
+
*
|
|
168
|
+
* A zero-object index used to be indistinguishable from a project with no
|
|
169
|
+
* relationships, so callers acted on empty context as if it were an answer
|
|
170
|
+
* (#2143). The zero case is therefore error-grade and names what was checked.
|
|
171
|
+
*/
|
|
172
|
+
function buildIndexDiagnostics(rootDir, packages, coverage, discoveryDiagnostics = []) {
|
|
173
|
+
const diagnostics = [...discoveryDiagnostics];
|
|
174
|
+
if (packages.reduce((total, pkg) => total + pkg.objects.length, 0) === 0) diagnostics.push({
|
|
175
|
+
severity: "error",
|
|
176
|
+
code: "no-smrt-objects-discovered",
|
|
177
|
+
message: [
|
|
178
|
+
`No SMRT objects were discovered under ${rootDir}.`,
|
|
179
|
+
`Workspace globs (${coverage.workspaceGlobSource}): ${coverage.workspaceGlobs.join(", ") || "(none)"}.`,
|
|
180
|
+
`Package directories checked (${coverage.packageDirs.length}): ${coverage.packageDirs.join(", ") || "(none)"}.`,
|
|
181
|
+
"Treat this as a discovery failure, not as evidence that the project has no SMRT model."
|
|
182
|
+
].join(" "),
|
|
183
|
+
checkedPaths: [...new Set(packages.flatMap((pkg) => pkg.checkedObjectPaths))],
|
|
184
|
+
remedy: [
|
|
185
|
+
"Confirm rootDir is the workspace root;",
|
|
186
|
+
"confirm pnpm-workspace.yaml `packages:` covers the directories that hold @smrt() classes (for example apps/*);",
|
|
187
|
+
"run `pnpm build` in the owning package to emit .smrt/manifest.json;",
|
|
188
|
+
"then re-run. Cross-check with introspect-project on the same root."
|
|
189
|
+
].join(" ")
|
|
190
|
+
});
|
|
191
|
+
for (const pkg of packages) {
|
|
192
|
+
const reason = pkg.objectSourceReason ?? "";
|
|
193
|
+
if (reason.startsWith("manifest-objects-owned-by-other-packages")) {
|
|
194
|
+
diagnostics.push({
|
|
195
|
+
severity: "warning",
|
|
196
|
+
code: "foreign-manifest-objects",
|
|
197
|
+
message: `${pkg.name}: every object in the discovered manifest is owned by another package (${reason}); it was rejected instead of being counted as this package's.`,
|
|
198
|
+
packageName: pkg.name,
|
|
199
|
+
checkedPaths: pkg.checkedObjectPaths,
|
|
200
|
+
remedy: remedyForReason(pkg)
|
|
201
|
+
});
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (reason.startsWith("rejected ")) {
|
|
205
|
+
diagnostics.push({
|
|
206
|
+
severity: "warning",
|
|
207
|
+
code: "partial-foreign-manifest-objects",
|
|
208
|
+
message: `${pkg.name}: ${reason}.`,
|
|
209
|
+
packageName: pkg.name,
|
|
210
|
+
checkedPaths: pkg.checkedObjectPaths,
|
|
211
|
+
remedy: remedyForReason(pkg)
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (reason.startsWith("scanner-failed")) diagnostics.push({
|
|
216
|
+
severity: "warning",
|
|
217
|
+
code: "scanner-fallback-failed",
|
|
218
|
+
message: `${pkg.name}: ${reason}`,
|
|
219
|
+
packageName: pkg.name,
|
|
220
|
+
checkedPaths: pkg.checkedObjectPaths,
|
|
221
|
+
remedy: remedyForReason(pkg)
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
diagnostics.push(...buildDuplicateIdentityDiagnostics(packages));
|
|
225
|
+
return diagnostics;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Reports one table claimed by more than one package. Relationships-v2 already
|
|
229
|
+
* counts it once, but the duplication itself usually means a stale or
|
|
230
|
+
* re-qualified generated artifact, which the caller should know about (#2143).
|
|
231
|
+
*/
|
|
232
|
+
function buildDuplicateIdentityDiagnostics(packages) {
|
|
233
|
+
const duplicates = [];
|
|
234
|
+
for (const [identity, entries] of groupObjectsByIdentity(packages)) {
|
|
235
|
+
if (collapseIdentityGroup(entries).length >= entries.length) continue;
|
|
236
|
+
duplicates.push([identity, [...new Set(entries.map((entry) => entry.pkg.name))]]);
|
|
237
|
+
}
|
|
238
|
+
if (duplicates.length === 0) return [];
|
|
239
|
+
const sample = duplicates.slice(0, 5).map(([identity, names]) => `${identity} (${names.join(" + ")})`).join("; ");
|
|
240
|
+
return [{
|
|
241
|
+
severity: "warning",
|
|
242
|
+
code: "duplicate-object-identity",
|
|
243
|
+
message: `${duplicates.length} object identit${duplicates.length === 1 ? "y is" : "ies are"} reported by more than one package: ${sample}${duplicates.length > 5 ? "; …" : ""}. Relationships-v2 counts each once.`,
|
|
244
|
+
remedy: "Usually a stale or aggregate generated artifact in a consuming package that re-qualifies its dependencies' objects. Regenerate it (pnpm build) so each package reports only the objects it declares."
|
|
245
|
+
}];
|
|
246
|
+
}
|
|
84
247
|
async function checkKnowledgeFreshness(options = {}) {
|
|
85
248
|
return checkKnowledgeFreshnessFromIndex(await buildKnowledgeIndex(options), options);
|
|
86
249
|
}
|
|
87
250
|
async function checkKnowledgeFreshnessFromIndex(index, options = {}) {
|
|
88
251
|
const issues = [];
|
|
89
252
|
const changedFiles = options.changed ? getChangedFiles(index.rootDir) : void 0;
|
|
90
|
-
|
|
253
|
+
const hasMemberPackages = index.packages.some((item) => item.kind !== "sdk" && !item.isWorkspaceRoot);
|
|
254
|
+
const memberDirectories = index.packages.filter((item) => item.kind !== "sdk" && !item.isWorkspaceRoot && item.relativeDirectory).map((item) => item.relativeDirectory);
|
|
255
|
+
const isNestedMember = (pkg) => Boolean(pkg.relativeDirectory) && memberDirectories.some((directory) => directory !== pkg.relativeDirectory && pkg.relativeDirectory.startsWith(`${directory}/`));
|
|
256
|
+
for (const pkg of index.packages.filter((item) => item.kind !== "sdk" && !(item.isWorkspaceRoot && hasMemberPackages))) {
|
|
91
257
|
const packageJsonPath = join(pkg.directory, "package.json");
|
|
92
|
-
|
|
258
|
+
const nested = isNestedMember(pkg);
|
|
259
|
+
if (!pkg.hasAgentsMd && !nested) issues.push({
|
|
93
260
|
severity: "error",
|
|
94
261
|
code: "missing-agents-md",
|
|
95
262
|
message: "Workspace package is missing canonical AGENTS.md",
|
|
96
263
|
file: relative(index.rootDir, join(pkg.directory, "AGENTS.md")),
|
|
97
264
|
packageName: pkg.name
|
|
98
265
|
});
|
|
99
|
-
if (
|
|
266
|
+
if (nested && pkg.hasAgentsMd) issues.push({
|
|
267
|
+
severity: "error",
|
|
268
|
+
code: "nested-agents-md",
|
|
269
|
+
message: "Nested workspace package must not define AGENTS.md; move it to the parent package as a linked agents/<module>.md",
|
|
270
|
+
file: relative(index.rootDir, join(pkg.directory, "AGENTS.md")),
|
|
271
|
+
packageName: pkg.name
|
|
272
|
+
});
|
|
273
|
+
if (!pkg.hasClaudeMd && !nested) issues.push({
|
|
100
274
|
severity: "error",
|
|
101
275
|
code: "missing-claude-shim",
|
|
102
276
|
message: "Workspace package is missing CLAUDE.md compatibility shim",
|
|
103
277
|
file: relative(index.rootDir, join(pkg.directory, "CLAUDE.md")),
|
|
104
278
|
packageName: pkg.name
|
|
105
279
|
});
|
|
106
|
-
else if (!pkg.hasClaudeShim) issues.push({
|
|
280
|
+
else if (pkg.hasClaudeMd && !pkg.hasClaudeShim) issues.push({
|
|
107
281
|
severity: "error",
|
|
108
282
|
code: "claude-not-shim",
|
|
109
283
|
message: "CLAUDE.md must contain only @AGENTS.md",
|
|
@@ -120,6 +294,7 @@ async function checkKnowledgeFreshnessFromIndex(index, options = {}) {
|
|
|
120
294
|
packageName: pkg.name
|
|
121
295
|
});
|
|
122
296
|
}
|
|
297
|
+
if (pkg.isPrivate) continue;
|
|
123
298
|
if (!pkg.files.includes("AGENTS.md")) issues.push({
|
|
124
299
|
severity: "error",
|
|
125
300
|
code: "package-files-missing-agents",
|
|
@@ -262,11 +437,14 @@ async function buildReviewContext(options = {}) {
|
|
|
262
437
|
sdkPackages: selectedSdkPackages,
|
|
263
438
|
sourceFiles: changedFiles,
|
|
264
439
|
extraContext: options.focus,
|
|
440
|
+
detail: options.detail,
|
|
265
441
|
moduleDocHints: {
|
|
266
442
|
changedFiles,
|
|
267
443
|
text: [options.focus, options.documentation].filter(Boolean).join("\n")
|
|
268
444
|
}
|
|
269
|
-
})
|
|
445
|
+
}),
|
|
446
|
+
coverage: index.coverage,
|
|
447
|
+
diagnostics: index.diagnostics
|
|
270
448
|
};
|
|
271
449
|
}
|
|
272
450
|
async function smrtReview(options = {}) {
|
|
@@ -277,7 +455,9 @@ async function smrtReview(options = {}) {
|
|
|
277
455
|
selectedPackages: context.selectedPackages,
|
|
278
456
|
selectedSdkPackages: context.selectedSdkPackages,
|
|
279
457
|
...mode !== "prompt-bundle" ? { deterministicFindings: context.deterministicFindings } : {},
|
|
280
|
-
...mode !== "findings" ? { promptBundle: context.promptBundle } : {}
|
|
458
|
+
...mode !== "findings" ? { promptBundle: context.promptBundle } : {},
|
|
459
|
+
coverage: context.coverage,
|
|
460
|
+
diagnostics: context.diagnostics
|
|
281
461
|
};
|
|
282
462
|
}
|
|
283
463
|
async function buildArchitectureContext(options = {}) {
|
|
@@ -307,8 +487,11 @@ async function buildArchitectureContext(options = {}) {
|
|
|
307
487
|
sdkPackages: selectedSdkPackages,
|
|
308
488
|
sourceFiles: [],
|
|
309
489
|
extraContext: text,
|
|
490
|
+
detail: options.detail,
|
|
310
491
|
moduleDocHints: { text }
|
|
311
|
-
})
|
|
492
|
+
}),
|
|
493
|
+
coverage: index.coverage,
|
|
494
|
+
diagnostics: index.diagnostics
|
|
312
495
|
};
|
|
313
496
|
}
|
|
314
497
|
async function smrtArchitecture(options = {}) {
|
|
@@ -341,6 +524,7 @@ function renderKnowledgeIndexMarkdown(index) {
|
|
|
341
524
|
`- polymorphic associations: ${index.relationshipsV2.polymorphicAssociations}`,
|
|
342
525
|
`- UUID columns: ${index.relationshipsV2.uuidColumns}`,
|
|
343
526
|
"",
|
|
527
|
+
...renderDiagnosticsSection(index.diagnostics),
|
|
344
528
|
"## Packages",
|
|
345
529
|
""
|
|
346
530
|
];
|
|
@@ -377,34 +561,261 @@ function renderFreshnessResult(result) {
|
|
|
377
561
|
}
|
|
378
562
|
return lines.join("\n");
|
|
379
563
|
}
|
|
564
|
+
/**
|
|
565
|
+
* Nearest ancestor that declares a workspace (#2143).
|
|
566
|
+
*
|
|
567
|
+
* The old rule also required a literal `packages/` directory, which silently
|
|
568
|
+
* mis-rooted every `apps/*`-shaped product. A workspace declaration is the
|
|
569
|
+
* actual signal; `startDir` is the fallback so single-package repos still work.
|
|
570
|
+
*/
|
|
380
571
|
function findProjectRoot(startDir) {
|
|
381
572
|
let current = resolve(startDir);
|
|
382
573
|
for (;;) {
|
|
383
|
-
if (existsSync(join(current, "pnpm-workspace.yaml"))
|
|
574
|
+
if (existsSync(join(current, "pnpm-workspace.yaml"))) return current;
|
|
575
|
+
if (readPackageJsonWorkspaceGlobs(current).length > 0) return current;
|
|
384
576
|
const parent = dirname(current);
|
|
385
577
|
if (parent === current) return resolve(startDir);
|
|
386
578
|
current = parent;
|
|
387
579
|
}
|
|
388
580
|
}
|
|
389
|
-
function
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
581
|
+
function readPackageJsonWorkspaceGlobs(dir) {
|
|
582
|
+
const workspaces = objectRecord(readJson(join(dir, "package.json"))).workspaces;
|
|
583
|
+
return Array.isArray(workspaces) ? stringArray(workspaces) : stringArray(objectRecord(workspaces).packages);
|
|
584
|
+
}
|
|
585
|
+
function readWorkspaceGlobs(rootDir) {
|
|
586
|
+
const pnpmWorkspacePath = join(rootDir, "pnpm-workspace.yaml");
|
|
587
|
+
if (existsSync(pnpmWorkspacePath)) {
|
|
588
|
+
const globs = parseYamlStringList(readFileSync(pnpmWorkspacePath, "utf8"), "packages");
|
|
589
|
+
if (globs.length > 0) return {
|
|
590
|
+
globs,
|
|
591
|
+
source: "pnpm-workspace.yaml"
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
const packageJsonGlobs = readPackageJsonWorkspaceGlobs(rootDir);
|
|
595
|
+
if (packageJsonGlobs.length > 0) return {
|
|
596
|
+
globs: packageJsonGlobs,
|
|
597
|
+
source: "package.json#workspaces"
|
|
598
|
+
};
|
|
599
|
+
return {
|
|
600
|
+
globs: [...WORKSPACE_GLOB_FALLBACK],
|
|
601
|
+
source: "fallback"
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/**
|
|
605
|
+
* Reads a top-level `key:` string list out of `pnpm-workspace.yaml`.
|
|
606
|
+
*
|
|
607
|
+
* Deliberately dependency-free: the shape consumed here is a fixed, tiny list
|
|
608
|
+
* of globs, and a read-only dev server should not pull in a YAML parser for it.
|
|
609
|
+
* Handles both block sequences and a single-line flow sequence.
|
|
610
|
+
*/
|
|
611
|
+
function parseYamlStringList(content, key) {
|
|
612
|
+
const keyPattern = new RegExp(`^${escapeRegExp(key)}\\s*:\\s*(.*)$`);
|
|
613
|
+
const values = [];
|
|
614
|
+
let inBlock = false;
|
|
615
|
+
for (const line of content.split(/\r?\n/)) {
|
|
616
|
+
if (!inBlock) {
|
|
617
|
+
const match = line.match(keyPattern);
|
|
618
|
+
if (!match) continue;
|
|
619
|
+
const rest = (match[1] ?? "").trim();
|
|
620
|
+
if (rest.startsWith("[")) return rest.replace(/^\[/, "").replace(/\]\s*$/, "").split(",").map((entry) => unquoteYamlScalar(entry)).filter(Boolean);
|
|
621
|
+
if (rest !== "" && !rest.startsWith("#")) continue;
|
|
622
|
+
inBlock = true;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (line.trim() === "" || line.trimStart().startsWith("#")) continue;
|
|
626
|
+
const item = line.match(/^\s+-\s*(.+?)\s*$/);
|
|
627
|
+
if (!item) break;
|
|
628
|
+
const value = unquoteYamlScalar(item[1] ?? "");
|
|
629
|
+
if (value) values.push(value);
|
|
630
|
+
}
|
|
631
|
+
return values;
|
|
632
|
+
}
|
|
633
|
+
function unquoteYamlScalar(value) {
|
|
634
|
+
const trimmed = value.trim();
|
|
635
|
+
const quoted = trimmed.match(/^(['"])([\s\S]*?)\1/);
|
|
636
|
+
if (quoted) return (quoted[2] ?? "").trim();
|
|
637
|
+
return trimmed.replace(/\s+#.*$/, "").trim();
|
|
638
|
+
}
|
|
639
|
+
var WorkspaceGlobTraversalLimitError = class extends Error {
|
|
640
|
+
glob;
|
|
641
|
+
constructor(glob) {
|
|
642
|
+
super(`Workspace glob traversal exceeded ${MAX_WORKSPACE_GLOB_TRAVERSAL_ENTRIES} directory entries while expanding ${glob}`);
|
|
643
|
+
this.glob = glob;
|
|
644
|
+
this.name = "WorkspaceGlobTraversalLimitError";
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
function expandWorkspaceGlobs(rootDir, globs) {
|
|
648
|
+
const diagnostics = [];
|
|
649
|
+
const positiveGlobs = [];
|
|
650
|
+
const negations = [];
|
|
651
|
+
for (const rawGlob of globs) {
|
|
652
|
+
const negated = rawGlob.trim().startsWith("!");
|
|
653
|
+
const glob = normalizeGlob(negated ? rawGlob.trim().slice(1) : rawGlob.trim());
|
|
654
|
+
const unsafeReason = unsafeWorkspaceGlobReason(glob);
|
|
655
|
+
if (unsafeReason) {
|
|
656
|
+
diagnostics.push({
|
|
657
|
+
severity: "error",
|
|
658
|
+
code: "unsafe-workspace-glob",
|
|
659
|
+
message: `Rejected workspace glob ${JSON.stringify(rawGlob)}: ${unsafeReason}.`,
|
|
660
|
+
remedy: "Keep every workspace glob relative to the declared workspace root; absolute paths and parent-directory (`..`) segments are not supported."
|
|
661
|
+
});
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
if (!glob) continue;
|
|
665
|
+
if (negated) negations.push(globToRegExp(glob));
|
|
666
|
+
else positiveGlobs.push(glob);
|
|
667
|
+
}
|
|
668
|
+
const matched = /* @__PURE__ */ new Set();
|
|
669
|
+
const budget = { entries: 0 };
|
|
670
|
+
try {
|
|
671
|
+
for (const glob of positiveGlobs) for (const dir of expandGlob(rootDir, glob, budget)) matched.add(dir);
|
|
672
|
+
} catch (error) {
|
|
673
|
+
if (!(error instanceof WorkspaceGlobTraversalLimitError)) throw error;
|
|
674
|
+
diagnostics.push({
|
|
675
|
+
severity: "error",
|
|
676
|
+
code: "workspace-glob-expansion-limit",
|
|
677
|
+
message: error.message,
|
|
678
|
+
remedy: "Narrow the workspace package globs or remove repeated broad globstars. Discovery stopped without reading any partially matched package set."
|
|
679
|
+
});
|
|
680
|
+
return {
|
|
681
|
+
dirs: [],
|
|
682
|
+
diagnostics,
|
|
683
|
+
fatal: true
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const resolvedRoot = realpathSync(rootDir);
|
|
687
|
+
const confined = [...matched].map((dir) => confinedRealPath(resolvedRoot, dir)).filter((dir) => dir !== void 0);
|
|
688
|
+
if (confined.length !== matched.size) diagnostics.push({
|
|
689
|
+
severity: "error",
|
|
690
|
+
code: "workspace-glob-root-escape",
|
|
691
|
+
message: "Rejected a workspace package directory whose real path resolves outside the workspace root.",
|
|
692
|
+
remedy: "Keep workspace packages inside the workspace root and do not route workspace globs through symlinks to sibling directories."
|
|
693
|
+
});
|
|
694
|
+
return {
|
|
695
|
+
dirs: [...new Set(confined)].filter((dir) => existsSync(join(dir, "package.json"))).filter((dir) => {
|
|
696
|
+
const rel = relative(rootDir, dir).replaceAll("\\", "/");
|
|
697
|
+
return rel !== "" && !negations.some((pattern) => pattern.test(rel));
|
|
698
|
+
}).sort(),
|
|
699
|
+
diagnostics,
|
|
700
|
+
fatal: false
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function normalizeGlob(glob) {
|
|
704
|
+
return glob.trim().replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
393
705
|
}
|
|
394
|
-
function
|
|
395
|
-
|
|
396
|
-
if (
|
|
397
|
-
return
|
|
706
|
+
function unsafeWorkspaceGlobReason(glob) {
|
|
707
|
+
if (glob.includes("\0")) return "NUL bytes are not valid path content";
|
|
708
|
+
if (isAbsolute(glob) || /^[A-Za-z]:\//.test(glob)) return "absolute paths are outside the workspace trust boundary";
|
|
709
|
+
if (glob.split("/").includes("..")) return "parent-directory segments can escape the workspace root";
|
|
398
710
|
}
|
|
399
|
-
function
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
711
|
+
function confinedRealPath(resolvedRoot, candidate) {
|
|
712
|
+
try {
|
|
713
|
+
const rel = relative(resolvedRoot, realpathSync(candidate));
|
|
714
|
+
if (rel === "" || !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`)) return candidate;
|
|
715
|
+
} catch {}
|
|
716
|
+
}
|
|
717
|
+
function expandGlob(rootDir, glob, budget) {
|
|
718
|
+
const segments = glob.split("/").filter((segment) => segment !== "");
|
|
719
|
+
if (segments.length === 0) return [];
|
|
720
|
+
let current = [rootDir];
|
|
721
|
+
for (const [segmentIndex, segment] of segments.entries()) {
|
|
722
|
+
const next = [];
|
|
723
|
+
for (const dir of current) {
|
|
724
|
+
if (segment === "**") {
|
|
725
|
+
next.push(dir, ...descendantDirs(dir, budget, glob));
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
if (segment.includes("*")) {
|
|
729
|
+
const pattern = globToRegExp(segment);
|
|
730
|
+
next.push(...childDirs(dir, budget, glob).filter((child) => pattern.test(basename(child))));
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const literal = join(dir, segment);
|
|
734
|
+
const isFinalSegment = segmentIndex === segments.length - 1;
|
|
735
|
+
if (isDirectory(literal) || isFinalSegment && isSymbolicLink(literal)) next.push(literal);
|
|
736
|
+
}
|
|
737
|
+
current = [...new Set(next)];
|
|
738
|
+
if (current.length === 0) break;
|
|
739
|
+
}
|
|
740
|
+
return current;
|
|
741
|
+
}
|
|
742
|
+
function childDirs(dir, budget, glob) {
|
|
743
|
+
const children = [];
|
|
744
|
+
try {
|
|
745
|
+
const directory = opendirSync(dir);
|
|
746
|
+
try {
|
|
747
|
+
let entry = directory.readSync();
|
|
748
|
+
while (entry !== null) {
|
|
749
|
+
budget.entries += 1;
|
|
750
|
+
if (budget.entries > MAX_WORKSPACE_GLOB_TRAVERSAL_ENTRIES) throw new WorkspaceGlobTraversalLimitError(glob);
|
|
751
|
+
if (entry.isDirectory() && !WALK_SKIP_DIRS.has(entry.name)) children.push(join(dir, entry.name));
|
|
752
|
+
entry = directory.readSync();
|
|
753
|
+
}
|
|
754
|
+
} finally {
|
|
755
|
+
directory.closeSync();
|
|
756
|
+
}
|
|
757
|
+
return children;
|
|
758
|
+
} catch (error) {
|
|
759
|
+
if (error instanceof WorkspaceGlobTraversalLimitError) throw error;
|
|
760
|
+
return children;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
function descendantDirs(dir, budget, glob) {
|
|
764
|
+
const descendants = [];
|
|
765
|
+
const pending = [dir];
|
|
766
|
+
for (let index = 0; index < pending.length; index += 1) {
|
|
767
|
+
const children = childDirs(pending[index], budget, glob);
|
|
768
|
+
descendants.push(...children);
|
|
769
|
+
pending.push(...children);
|
|
770
|
+
}
|
|
771
|
+
return descendants;
|
|
772
|
+
}
|
|
773
|
+
function isDirectory(path) {
|
|
774
|
+
try {
|
|
775
|
+
return lstatSync(path).isDirectory();
|
|
776
|
+
} catch {
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function isSymbolicLink(path) {
|
|
781
|
+
try {
|
|
782
|
+
return lstatSync(path).isSymbolicLink();
|
|
783
|
+
} catch {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Translates a workspace glob into an anchored regular expression. `*` stays
|
|
789
|
+
* within one path segment; `**` spans any depth (and collapses so `a/**` also
|
|
790
|
+
* matches `a`).
|
|
791
|
+
*/
|
|
792
|
+
function globToRegExp(glob) {
|
|
793
|
+
const ANY_DEPTH = "\0";
|
|
794
|
+
const ANY_SEGMENT = "";
|
|
795
|
+
const source = glob.replace(/\*\*/g, ANY_DEPTH).replace(/\*/g, ANY_SEGMENT).replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replaceAll(`/${ANY_DEPTH}`, "(?:/.*)?").replaceAll(`${ANY_DEPTH}/`, "(?:.*/)?").replaceAll(ANY_DEPTH, ".*").replaceAll(ANY_SEGMENT, "[^/]*");
|
|
796
|
+
return new RegExp(`^${source}$`);
|
|
797
|
+
}
|
|
798
|
+
function discoverProjectPackageDirs(rootDir) {
|
|
799
|
+
const { globs, source } = readWorkspaceGlobs(rootDir);
|
|
800
|
+
const expansion = expandWorkspaceGlobs(rootDir, globs);
|
|
801
|
+
const dirs = expansion.fatal ? [] : existsSync(join(rootDir, "package.json")) ? [rootDir, ...expansion.dirs.filter((dir) => resolve(dir) !== resolve(rootDir))] : expansion.dirs;
|
|
802
|
+
if (dirs.length > MAX_DISCOVERED_WORKSPACE_PACKAGES) return {
|
|
803
|
+
globs,
|
|
804
|
+
globSource: source,
|
|
805
|
+
dirs: [],
|
|
806
|
+
diagnostics: [...expansion.diagnostics, {
|
|
807
|
+
severity: "error",
|
|
808
|
+
code: "workspace-package-limit",
|
|
809
|
+
message: `Workspace discovery found ${dirs.length} packages, exceeding the ${MAX_DISCOVERED_WORKSPACE_PACKAGES}-package limit.`,
|
|
810
|
+
remedy: "Narrow the workspace package globs. Discovery stopped without reading any partially matched package set."
|
|
811
|
+
}]
|
|
812
|
+
};
|
|
813
|
+
return {
|
|
814
|
+
globs,
|
|
815
|
+
globSource: source,
|
|
816
|
+
dirs,
|
|
817
|
+
diagnostics: expansion.diagnostics
|
|
818
|
+
};
|
|
408
819
|
}
|
|
409
820
|
function discoverInstalledSdkPackages(rootDir, packageDirs, includeDocs) {
|
|
410
821
|
return [join(rootDir, "node_modules", "@happyvertical"), ...packageDirs.map((dir) => join(dir, "node_modules", "@happyvertical"))].filter((scopeDir, index, all) => existsSync(scopeDir) && all.indexOf(scopeDir) === index).flatMap((scopeDir) => readdirSync(scopeDir, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isSymbolicLink()).map((entry) => {
|
|
@@ -454,7 +865,14 @@ function readKnowledgePackage(rootDir, directory, includeDocs) {
|
|
|
454
865
|
const domainKnowledge = readDomainKnowledge(directory);
|
|
455
866
|
const docSource = hasAgentsMd ? "AGENTS.md" : fallbackClaudeDoc ? "CLAUDE.md" : null;
|
|
456
867
|
const manifest = readManifest(directory);
|
|
457
|
-
const
|
|
868
|
+
const resolvedObjects = resolvePackageObjects({
|
|
869
|
+
packageName: name,
|
|
870
|
+
directory,
|
|
871
|
+
rootDir,
|
|
872
|
+
domainKnowledge,
|
|
873
|
+
manifest
|
|
874
|
+
});
|
|
875
|
+
const objects = resolvedObjects.objects;
|
|
458
876
|
const prompts = domainKnowledge ? readDomainKnowledgePrompts(domainKnowledge.content, directory, rootDir) : readPrompts(directory, rootDir);
|
|
459
877
|
const smrtDependencies = Object.keys(allDeps).filter((dep) => dep.startsWith("@happyvertical/smrt-")).sort();
|
|
460
878
|
const sdkDependencies = Object.keys(allDeps).filter((dep) => SDK_PACKAGE_NAMES.has(dep)).sort();
|
|
@@ -485,8 +903,131 @@ function readKnowledgePackage(rootDir, directory, includeDocs) {
|
|
|
485
903
|
objects,
|
|
486
904
|
prompts,
|
|
487
905
|
mcpTools: domainKnowledge ? domainMcpTools(domainKnowledge.content) : mcpTools(objects),
|
|
488
|
-
relationshipFeatures: relationshipFeatures(objects)
|
|
906
|
+
relationshipFeatures: relationshipFeatures(objects),
|
|
907
|
+
isWorkspaceRoot: resolve(directory) === resolve(rootDir),
|
|
908
|
+
isPrivate: packageJson.private === true,
|
|
909
|
+
objectSource: resolvedObjects.source,
|
|
910
|
+
objectSourceReason: resolvedObjects.reason,
|
|
911
|
+
checkedObjectPaths: resolvedObjects.checkedPaths
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
/** Artifact paths consulted for a package's objects, in precedence order. */
|
|
915
|
+
function objectArtifactCandidates(directory) {
|
|
916
|
+
return [
|
|
917
|
+
join(directory, ".smrt", "smrt-knowledge.json"),
|
|
918
|
+
join(directory, "dist", "smrt-knowledge.json"),
|
|
919
|
+
join(directory, "src", "manifest", "smrt-knowledge.json"),
|
|
920
|
+
join(directory, "src", "manifest", "manifest.json"),
|
|
921
|
+
join(directory, ".smrt", "manifest.json"),
|
|
922
|
+
join(directory, "dist", "manifest.json")
|
|
923
|
+
];
|
|
924
|
+
}
|
|
925
|
+
function resolvePackageObjects(options) {
|
|
926
|
+
const checkedPaths = objectArtifactCandidates(options.directory).map((path) => relativeOrAbsolute(options.rootDir, path));
|
|
927
|
+
if (options.domainKnowledge) return {
|
|
928
|
+
objects: readDomainKnowledgeObjects(options.domainKnowledge.content),
|
|
929
|
+
source: "domain-artifact",
|
|
930
|
+
checkedPaths
|
|
489
931
|
};
|
|
932
|
+
if (!options.manifest) return {
|
|
933
|
+
objects: [],
|
|
934
|
+
source: "none",
|
|
935
|
+
reason: "no-artifact",
|
|
936
|
+
checkedPaths
|
|
937
|
+
};
|
|
938
|
+
const { owned, foreignCount } = partitionOwnedObjects(options.manifest.content, options.packageName);
|
|
939
|
+
const objects = readManifestObjects({ objects: owned });
|
|
940
|
+
if (objects.length === 0) return {
|
|
941
|
+
objects: [],
|
|
942
|
+
source: "none",
|
|
943
|
+
reason: foreignCount > 0 ? `manifest-objects-owned-by-other-packages (${foreignCount} rejected)` : "manifest-has-no-objects",
|
|
944
|
+
checkedPaths
|
|
945
|
+
};
|
|
946
|
+
return {
|
|
947
|
+
objects,
|
|
948
|
+
source: "manifest",
|
|
949
|
+
reason: foreignCount > 0 ? `rejected ${foreignCount} manifest object(s) owned by other packages` : void 0,
|
|
950
|
+
checkedPaths
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function relativeOrAbsolute(rootDir, path) {
|
|
954
|
+
const rel = relative(rootDir, path);
|
|
955
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
956
|
+
}
|
|
957
|
+
/**
|
|
958
|
+
* Resolves objects from source when no artifact could supply them (#2143).
|
|
959
|
+
*
|
|
960
|
+
* `introspect-project` already reports `manifestSource: "scanner"` and works on
|
|
961
|
+
* an unbuilt checkout. Without the same fallback here, an unbuilt package
|
|
962
|
+
* silently contributed nothing and the two tools disagreed about one root —
|
|
963
|
+
* which was the defect underneath every symptom in #2143.
|
|
964
|
+
*
|
|
965
|
+
* Mutates in place because `mcpTools` and `relationshipFeatures` are derived
|
|
966
|
+
* from `objects` and have to be recomputed together.
|
|
967
|
+
*/
|
|
968
|
+
async function applyScannerFallback(pkg, extraExcludes = []) {
|
|
969
|
+
if (pkg.objectSource !== "none") return;
|
|
970
|
+
if (!hasScannableSources(pkg.directory)) {
|
|
971
|
+
pkg.objectSourceReason = `${pkg.objectSourceReason ?? "no-artifact"}; no-typescript-sources`;
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
try {
|
|
975
|
+
const objects = await scanPackageObjects(pkg.directory, pkg.name, extraExcludes);
|
|
976
|
+
if (objects.length === 0) {
|
|
977
|
+
pkg.objectSourceReason = "no-smrt-objects-in-sources";
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
980
|
+
pkg.objects = objects;
|
|
981
|
+
pkg.objectSource = "scanner";
|
|
982
|
+
pkg.objectSourceReason = void 0;
|
|
983
|
+
pkg.mcpTools = mcpTools(objects);
|
|
984
|
+
pkg.relationshipFeatures = relationshipFeatures(objects);
|
|
985
|
+
} catch (error) {
|
|
986
|
+
pkg.objectSourceReason = `scanner-failed: ${messageFromError(error)}`;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
async function applyScannerFallbacks(packages, memberExcludes) {
|
|
990
|
+
for (let offset = 0; offset < packages.length; offset += MAX_SCANNER_CONCURRENCY) await Promise.all(packages.slice(offset, offset + MAX_SCANNER_CONCURRENCY).map((pkg) => applyScannerFallback(pkg, pkg.isWorkspaceRoot ? memberExcludes : [])));
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Stops at the first candidate instead of walking a whole tree.
|
|
994
|
+
*
|
|
995
|
+
* Deliberately unbounded in depth: a package may keep its models at
|
|
996
|
+
* `src/features/billing/models/internal/Invoice.ts`, and a shallow probe would
|
|
997
|
+
* report "no TypeScript sources" for a package `OxcScanner` can actually read.
|
|
998
|
+
*/
|
|
999
|
+
function hasScannableSources(directory) {
|
|
1000
|
+
let entries;
|
|
1001
|
+
try {
|
|
1002
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
1003
|
+
} catch {
|
|
1004
|
+
return false;
|
|
1005
|
+
}
|
|
1006
|
+
const subdirectories = [];
|
|
1007
|
+
for (const entry of entries) {
|
|
1008
|
+
if (entry.isDirectory()) {
|
|
1009
|
+
if (!WALK_SKIP_DIRS.has(entry.name)) subdirectories.push(join(directory, entry.name));
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name) && !entry.name.endsWith(".d.ts") && !entry.name.endsWith(".test.ts") && !entry.name.endsWith(".spec.ts")) return true;
|
|
1013
|
+
}
|
|
1014
|
+
return subdirectories.some((child) => hasScannableSources(child));
|
|
1015
|
+
}
|
|
1016
|
+
async function scanPackageObjects(directory, packageName, extraExcludes = []) {
|
|
1017
|
+
const { results, resolved } = await new OxcScanner({
|
|
1018
|
+
cwd: directory,
|
|
1019
|
+
include: SCAN_INCLUDE,
|
|
1020
|
+
exclude: [...SCAN_EXCLUDE, ...extraExcludes]
|
|
1021
|
+
}).scanAndResolve();
|
|
1022
|
+
const decorated = resolved.filter((classDef) => classDef.hasSmartDecorator);
|
|
1023
|
+
if (decorated.length === 0) return [];
|
|
1024
|
+
return readManifestObjects(new ManifestAdapter().toManifest(decorated, {
|
|
1025
|
+
packageName,
|
|
1026
|
+
typeAliases: results.typeAliases
|
|
1027
|
+
}));
|
|
1028
|
+
}
|
|
1029
|
+
function messageFromError(error) {
|
|
1030
|
+
return error instanceof Error ? error.message : String(error);
|
|
490
1031
|
}
|
|
491
1032
|
function readDomainKnowledge(directory) {
|
|
492
1033
|
for (const path of [
|
|
@@ -521,6 +1062,40 @@ function readManifest(directory) {
|
|
|
521
1062
|
}
|
|
522
1063
|
return null;
|
|
523
1064
|
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Rejects objects a manifest does not own (#2143).
|
|
1067
|
+
*
|
|
1068
|
+
* A runtime `.smrt/manifest.json` is frequently an *aggregate*: it registers
|
|
1069
|
+
* every object reachable from the app, including its dependencies'. Counting
|
|
1070
|
+
* those as the package's own doubles Relationships-v2 (a stale 759KB
|
|
1071
|
+
* `packages/cli/.smrt/manifest.json` reported 200 foreignKey / 413 UUID against
|
|
1072
|
+
* a real 89 / 206). Ownership is decided per object, so an aggregate still
|
|
1073
|
+
* contributes the objects it genuinely owns.
|
|
1074
|
+
*/
|
|
1075
|
+
function partitionOwnedObjects(manifest, packageName) {
|
|
1076
|
+
const objects = objectRecord(manifest.objects);
|
|
1077
|
+
const owned = {};
|
|
1078
|
+
let foreignCount = 0;
|
|
1079
|
+
for (const [key, raw] of Object.entries(objects)) {
|
|
1080
|
+
if (manifestObjectIsOwned(objectRecord(raw), packageName)) {
|
|
1081
|
+
owned[key] = raw;
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
foreignCount += 1;
|
|
1085
|
+
}
|
|
1086
|
+
return {
|
|
1087
|
+
owned,
|
|
1088
|
+
foreignCount
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function manifestObjectIsOwned(object, packageName) {
|
|
1092
|
+
const declared = typeof object.packageName === "string" ? object.packageName : void 0;
|
|
1093
|
+
if (declared) return declared === packageName;
|
|
1094
|
+
const qualifiedName = typeof object.qualifiedName === "string" ? object.qualifiedName : void 0;
|
|
1095
|
+
const qualifier = qualifiedName?.includes(":") ? qualifiedName.slice(0, qualifiedName.lastIndexOf(":")) : void 0;
|
|
1096
|
+
if (qualifier) return qualifier === packageName;
|
|
1097
|
+
return true;
|
|
1098
|
+
}
|
|
524
1099
|
function readManifestObjects(manifest) {
|
|
525
1100
|
const objects = objectRecord(manifest.objects);
|
|
526
1101
|
return Object.values(objects).map((raw) => {
|
|
@@ -656,8 +1231,105 @@ function domainMcpTools(manifest) {
|
|
|
656
1231
|
operation: surface.operation
|
|
657
1232
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
658
1233
|
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Identity of the table an object maps to, used to avoid counting one table
|
|
1236
|
+
* twice (#2143).
|
|
1237
|
+
*
|
|
1238
|
+
* A consuming app's generated artifact can re-qualify a dependency's objects
|
|
1239
|
+
* under the app's own package name (observed: 18 of `apps/work-web`'s 21
|
|
1240
|
+
* artifact objects are classes declared in `packages/work`, same tables). Name
|
|
1241
|
+
* prefixes cannot detect that, so corpus-level facts are keyed by class plus
|
|
1242
|
+
* table instead.
|
|
1243
|
+
*/
|
|
1244
|
+
function objectIdentity(object) {
|
|
1245
|
+
return `${object.className}::${object.tableName ?? object.collection ?? ""}`;
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* True when either package declares the other as a dependency.
|
|
1249
|
+
*
|
|
1250
|
+
* This is what separates a re-qualified copy from a coincidence. A consuming
|
|
1251
|
+
* package can only restate its *dependencies'* objects, so a shared identity
|
|
1252
|
+
* across a dependency edge is one table reported twice. Two unrelated packages
|
|
1253
|
+
* that happen to share a class and table name (observed: `Account::accounts` in
|
|
1254
|
+
* both smrt-messages and smrt-ledgers) are genuinely distinct objects and must
|
|
1255
|
+
* both keep contributing their fields.
|
|
1256
|
+
*/
|
|
1257
|
+
function hasDependencyEdge(a, b) {
|
|
1258
|
+
return dependsOn(a, b.name) || dependsOn(b, a.name);
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Picks the copy that owns the class within one connected group.
|
|
1262
|
+
*
|
|
1263
|
+
* Ownership follows the dependency direction: a consumer can restate its
|
|
1264
|
+
* *dependency's* objects, never the reverse, so the entry that the most other
|
|
1265
|
+
* members of the group depend on is the declaring package. Provenance alone is
|
|
1266
|
+
* not enough — preferring a scanner copy kept `smrt-support`'s compatibility
|
|
1267
|
+
* subtype over the canonical `smrt-projects` model and changed the facts.
|
|
1268
|
+
*/
|
|
1269
|
+
function pickOwningEntry(entries) {
|
|
1270
|
+
let best = entries[0];
|
|
1271
|
+
let bestScore = -1;
|
|
1272
|
+
for (const entry of entries) {
|
|
1273
|
+
const score = entries.filter((other) => other !== entry && dependsOn(other.pkg, entry.pkg.name)).length;
|
|
1274
|
+
if (score > bestScore) {
|
|
1275
|
+
best = entry;
|
|
1276
|
+
bestScore = score;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return best;
|
|
1280
|
+
}
|
|
1281
|
+
function dependsOn(pkg, name) {
|
|
1282
|
+
return name in pkg.dependencies || name in pkg.devDependencies || name in pkg.peerDependencies;
|
|
1283
|
+
}
|
|
1284
|
+
function dedupeObjectsByIdentity(packages) {
|
|
1285
|
+
return [...groupObjectsByIdentity(packages).values()].flatMap((entries) => collapseIdentityGroup(entries).map((entry) => entry.object));
|
|
1286
|
+
}
|
|
1287
|
+
function groupObjectsByIdentity(packages) {
|
|
1288
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1289
|
+
for (const pkg of packages) for (const object of pkg.objects) {
|
|
1290
|
+
const identity = objectIdentity(object);
|
|
1291
|
+
const entries = groups.get(identity) ?? [];
|
|
1292
|
+
entries.push({
|
|
1293
|
+
pkg,
|
|
1294
|
+
object
|
|
1295
|
+
});
|
|
1296
|
+
groups.set(identity, entries);
|
|
1297
|
+
}
|
|
1298
|
+
return groups;
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Collapses entries linked by dependency edges, leaving unrelated same-named
|
|
1302
|
+
* objects intact.
|
|
1303
|
+
*
|
|
1304
|
+
* Grouping is by connected component, not a greedy first match: when two
|
|
1305
|
+
* independent consumers both restate one shared dependency's object, neither
|
|
1306
|
+
* consumer has an edge to the other, so a greedy pass would keep both and
|
|
1307
|
+
* double-count the very table this exists to collapse. Packages arrive
|
|
1308
|
+
* name-sorted, so the result is stable.
|
|
1309
|
+
*/
|
|
1310
|
+
function collapseIdentityGroup(entries) {
|
|
1311
|
+
if (entries.length === 1) return entries;
|
|
1312
|
+
const parent = entries.map((_, index) => index);
|
|
1313
|
+
const find = (index) => {
|
|
1314
|
+
let root = index;
|
|
1315
|
+
while (parent[root] !== root) root = parent[root];
|
|
1316
|
+
return root;
|
|
1317
|
+
};
|
|
1318
|
+
for (let a = 0; a < entries.length; a += 1) for (let b = a + 1; b < entries.length; b += 1) {
|
|
1319
|
+
if (!hasDependencyEdge(entries[a].pkg, entries[b].pkg)) continue;
|
|
1320
|
+
const rootA = find(a);
|
|
1321
|
+
const rootB = find(b);
|
|
1322
|
+
if (rootA !== rootB) parent[rootB] = rootA;
|
|
1323
|
+
}
|
|
1324
|
+
const components = /* @__PURE__ */ new Map();
|
|
1325
|
+
for (const [index, entry] of entries.entries()) {
|
|
1326
|
+
const root = find(index);
|
|
1327
|
+
components.set(root, [...components.get(root) ?? [], entry]);
|
|
1328
|
+
}
|
|
1329
|
+
return [...components.values()].map((component) => component.length === 1 ? component[0] : pickOwningEntry(component));
|
|
1330
|
+
}
|
|
659
1331
|
function summarizeRelationshipsV2(packages) {
|
|
660
|
-
const objects = packages
|
|
1332
|
+
const objects = dedupeObjectsByIdentity(packages);
|
|
661
1333
|
const fields = objects.flatMap((object) => object.fields);
|
|
662
1334
|
return {
|
|
663
1335
|
foreignKeyFields: fields.filter((field) => field.type === "foreignKey").length,
|
|
@@ -710,7 +1382,7 @@ function shouldScanStalePatternFile(rootDir, filePath) {
|
|
|
710
1382
|
function buildReviewFindings(index, changedFiles, selectedPackages) {
|
|
711
1383
|
const issues = [];
|
|
712
1384
|
for (const pkg of selectedPackages) {
|
|
713
|
-
const changedPackageFiles = changedFiles.filter((file) =>
|
|
1385
|
+
const changedPackageFiles = changedFiles.filter((file) => packageOwnsFile(pkg, file, index.packages));
|
|
714
1386
|
if (!pkg.agentDoc && pkg.kind === "smrt") issues.push({
|
|
715
1387
|
severity: "warning",
|
|
716
1388
|
code: "missing-package-expertise",
|
|
@@ -769,14 +1441,47 @@ function buildReviewFindings(index, changedFiles, selectedPackages) {
|
|
|
769
1441
|
}
|
|
770
1442
|
return issues;
|
|
771
1443
|
}
|
|
1444
|
+
/**
|
|
1445
|
+
* Joins a package-relative path onto a package's workspace-relative directory.
|
|
1446
|
+
*
|
|
1447
|
+
* The workspace root can itself be an indexed package — including the
|
|
1448
|
+
* single-package layout #2143 added — and its `relativeDirectory` is the empty
|
|
1449
|
+
* string. Naive interpolation emits `/AGENTS.md`, an absolute filesystem path,
|
|
1450
|
+
* instead of the project-relative path callers are told to read.
|
|
1451
|
+
*/
|
|
1452
|
+
function packageRelativePath(pkg, path) {
|
|
1453
|
+
return pkg.relativeDirectory ? `${pkg.relativeDirectory}/${path}` : path;
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Workspace-relative paths of a package's authored documentation.
|
|
1457
|
+
*
|
|
1458
|
+
* Shared with the MCP transport layer so the summary projection and the
|
|
1459
|
+
* Markdown bundle can never disagree about where a doc lives.
|
|
1460
|
+
*/
|
|
1461
|
+
function packageDocPaths(pkg) {
|
|
1462
|
+
return [...pkg.docSource ? [packageRelativePath(pkg, pkg.docSource)] : [], ...pkg.moduleDocs.map((doc) => packageRelativePath(pkg, doc.path))];
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* True when a workspace-relative changed file belongs to `pkg`.
|
|
1466
|
+
*
|
|
1467
|
+
* A workspace-root package has an empty `relativeDirectory`, so the nested-path
|
|
1468
|
+
* test would compare against a leading `/` and never match — which silently
|
|
1469
|
+
* selected no packages for exactly the single-package layout #2143 added. The
|
|
1470
|
+
* root instead owns any path no member package owns, mirroring the
|
|
1471
|
+
* member-excluded scan in `applyScannerFallbacks`.
|
|
1472
|
+
*/
|
|
1473
|
+
function packageOwnsFile(pkg, file, siblings) {
|
|
1474
|
+
if (pkg.relativeDirectory) return file === pkg.relativeDirectory || file.startsWith(`${pkg.relativeDirectory}/`);
|
|
1475
|
+
return !siblings.some((member) => member !== pkg && member.relativeDirectory && (file === member.relativeDirectory || file.startsWith(`${member.relativeDirectory}/`)));
|
|
1476
|
+
}
|
|
772
1477
|
function isPackageManifestFile(pkg, file) {
|
|
773
|
-
return file ===
|
|
1478
|
+
return file === packageRelativePath(pkg, "package.json");
|
|
774
1479
|
}
|
|
775
1480
|
function isPackageAgentDocFile(pkg, file) {
|
|
776
|
-
return file ===
|
|
1481
|
+
return file === packageRelativePath(pkg, "AGENTS.md") || pkg.moduleDocs.some((doc) => file === packageRelativePath(pkg, doc.path));
|
|
777
1482
|
}
|
|
778
1483
|
function isPublicEntrypointFile(pkg, file) {
|
|
779
|
-
const sourcePrefix =
|
|
1484
|
+
const sourcePrefix = packageRelativePath(pkg, "src/");
|
|
780
1485
|
if (!file.startsWith(sourcePrefix)) return false;
|
|
781
1486
|
const sourcePath = file.slice(sourcePrefix.length);
|
|
782
1487
|
return sourcePath === "index.ts" || sourcePath === "index.tsx" || sourcePath === "index.js" || sourcePath.startsWith("api/") || sourcePath.startsWith("cli/") || sourcePath.startsWith("mcp/") || sourcePath.startsWith("tools/");
|
|
@@ -785,7 +1490,7 @@ function buildArchitectureRecommendations(context, ideaText) {
|
|
|
785
1490
|
return {
|
|
786
1491
|
smrtPackages: context.selectedPackages.map((pkg) => pkg.name),
|
|
787
1492
|
sdkPackages: context.selectedSdkPackages.map((pkg) => pkg.name),
|
|
788
|
-
objectModelSketch: buildObjectModelSketch(context.selectedPackages),
|
|
1493
|
+
objectModelSketch: buildObjectModelSketch(context.selectedPackages, context.diagnostics),
|
|
789
1494
|
risks: buildArchitectureRisks(context.selectedPackages, ideaText),
|
|
790
1495
|
questions: buildArchitectureQuestions(context.selectedPackages, ideaText),
|
|
791
1496
|
notes: [
|
|
@@ -795,10 +1500,12 @@ function buildArchitectureRecommendations(context, ideaText) {
|
|
|
795
1500
|
]
|
|
796
1501
|
};
|
|
797
1502
|
}
|
|
798
|
-
function buildObjectModelSketch(packages) {
|
|
1503
|
+
function buildObjectModelSketch(packages, diagnostics = []) {
|
|
1504
|
+
const blocking = diagnostics.find((diagnostic) => diagnostic.code === "no-smrt-objects-discovered");
|
|
1505
|
+
if (blocking) return [`No object model could be derived: ${blocking.message}`, `Remediation: ${blocking.remedy ?? "verify workspace discovery."}`];
|
|
799
1506
|
const lines = packages.flatMap((pkg) => {
|
|
800
1507
|
const objects = pkg.objects.filter((object) => object.extends !== "SmrtCollection").slice(0, 6).map((object) => object.className);
|
|
801
|
-
if (objects.length === 0) return [`${pkg.name}: use package services or templates; no manifest objects indexed.`];
|
|
1508
|
+
if (objects.length === 0) return [`${pkg.name}: use package services or templates; no manifest objects indexed (${pkg.objectSourceReason ?? pkg.objectSource}).`];
|
|
802
1509
|
return [`${pkg.name}: start from ${objects.join(", ")}.`];
|
|
803
1510
|
});
|
|
804
1511
|
return lines.length > 0 ? lines : ["Define the core domain as SmrtObject classes with explicit relationships and generated surfaces."];
|
|
@@ -832,7 +1539,8 @@ function buildArchitectureQuestions(packages, ideaText) {
|
|
|
832
1539
|
}
|
|
833
1540
|
function selectPackagesForFiles(index, changedFiles) {
|
|
834
1541
|
const selected = /* @__PURE__ */ new Set();
|
|
835
|
-
|
|
1542
|
+
const candidates = domainPackages(index);
|
|
1543
|
+
for (const file of changedFiles) for (const pkg of candidates) if (packageOwnsFile(pkg, file, index.packages)) selected.add(pkg);
|
|
836
1544
|
return [...selected].sort((a, b) => a.name.localeCompare(b.name));
|
|
837
1545
|
}
|
|
838
1546
|
function selectPackages(index, options) {
|
|
@@ -848,15 +1556,21 @@ function selectPackages(index, options) {
|
|
|
848
1556
|
if (includesToken(text, pkg.name.replace("@happyvertical/smrt-", "")) || text.includes(pkg.name.toLowerCase()) || pkg.objects.some((object) => includesToken(text, object.className.toLowerCase()))) selected.add(pkg);
|
|
849
1557
|
}
|
|
850
1558
|
if (selected.size === 0 && options.scope === "local") for (const pkg of domainPackages(index).filter((item) => scopeAllowsPackage(item, "local"))) selected.add(pkg);
|
|
851
|
-
if (selected.size === 0 && options.scope !== "sdk")
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1559
|
+
if (selected.size === 0 && options.scope !== "sdk") {
|
|
1560
|
+
for (const name of [
|
|
1561
|
+
"@happyvertical/smrt-core",
|
|
1562
|
+
"@happyvertical/smrt-config",
|
|
1563
|
+
"@happyvertical/smrt-cli",
|
|
1564
|
+
"@happyvertical/smrt-scanner",
|
|
1565
|
+
"@happyvertical/smrt-dev-mcp"
|
|
1566
|
+
]) {
|
|
1567
|
+
const pkg = index.smrtPackages.find((item) => item.name === name);
|
|
1568
|
+
if (pkg) selected.add(pkg);
|
|
1569
|
+
}
|
|
1570
|
+
if (selected.size === 0) {
|
|
1571
|
+
const contributing = domainPackages(index).filter((pkg) => pkg.objects.length > 0 && scopeAllowsPackage(pkg, options.scope) && !pkg.relativeDirectory.includes("node_modules")).sort((a, b) => b.objects.length - a.objects.length).slice(0, MAX_FALLBACK_PACKAGES);
|
|
1572
|
+
for (const pkg of contributing) selected.add(pkg);
|
|
1573
|
+
}
|
|
860
1574
|
}
|
|
861
1575
|
return [...selected].sort((a, b) => a.name.localeCompare(b.name));
|
|
862
1576
|
}
|
|
@@ -898,12 +1612,14 @@ function packageMatches(pkg, query) {
|
|
|
898
1612
|
return pkg.name.toLowerCase() === normalized || pkg.name.toLowerCase().includes(normalized) || shortName.toLowerCase() === normalized || pkg.relativeDirectory.toLowerCase().endsWith(`/${normalized}`);
|
|
899
1613
|
}
|
|
900
1614
|
function buildPromptBundle(options) {
|
|
901
|
-
const
|
|
1615
|
+
const detail = options.detail ?? "summary";
|
|
1616
|
+
const render = (pkg) => renderPackageContext(pkg, options.moduleDocHints, detail);
|
|
902
1617
|
const contextMarkdown = [
|
|
903
1618
|
`# ${options.title}`,
|
|
904
1619
|
"",
|
|
905
1620
|
`Baseline root: ${options.index.rootDir}`,
|
|
906
1621
|
"",
|
|
1622
|
+
...renderDiagnosticsSection(options.index.diagnostics),
|
|
907
1623
|
"## Task",
|
|
908
1624
|
"",
|
|
909
1625
|
options.task,
|
|
@@ -924,13 +1640,24 @@ function buildPromptBundle(options) {
|
|
|
924
1640
|
];
|
|
925
1641
|
return {
|
|
926
1642
|
title: options.title,
|
|
927
|
-
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.",
|
|
1643
|
+
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.", detail === "summary" ? "Authored package docs are listed by path rather than embedded; read the ones you need, or re-request with detail: \"full\"." : ""].filter(Boolean).join(" "),
|
|
928
1644
|
contextMarkdown: contextMarkdown.join("\n"),
|
|
929
1645
|
selectedPackages: options.packages.map((pkg) => pkg.name),
|
|
930
1646
|
selectedSdkPackages: options.sdkPackages.map((pkg) => pkg.name),
|
|
931
1647
|
sourceFiles: options.sourceFiles
|
|
932
1648
|
};
|
|
933
1649
|
}
|
|
1650
|
+
function renderDiagnosticsSection(diagnostics) {
|
|
1651
|
+
if (diagnostics.length === 0) return [];
|
|
1652
|
+
const lines = ["## Diagnostics", ""];
|
|
1653
|
+
for (const diagnostic of diagnostics) {
|
|
1654
|
+
lines.push(`- [${diagnostic.severity.toUpperCase()}] ${diagnostic.code}: ${diagnostic.message}`);
|
|
1655
|
+
if (diagnostic.checkedPaths && diagnostic.checkedPaths.length > 0) lines.push(` - checked: ${diagnostic.checkedPaths.join(", ")}`);
|
|
1656
|
+
if (diagnostic.remedy) lines.push(` - remedy: ${diagnostic.remedy}`);
|
|
1657
|
+
}
|
|
1658
|
+
lines.push("");
|
|
1659
|
+
return lines;
|
|
1660
|
+
}
|
|
934
1661
|
/**
|
|
935
1662
|
* Which of a package's module docs to embed for this request (#2108).
|
|
936
1663
|
*
|
|
@@ -946,7 +1673,8 @@ function selectModuleDocs(pkg, hints) {
|
|
|
946
1673
|
embedded: [],
|
|
947
1674
|
scoped: false
|
|
948
1675
|
};
|
|
949
|
-
const
|
|
1676
|
+
const prefix = pkg.relativeDirectory ? `${pkg.relativeDirectory}/` : "";
|
|
1677
|
+
const packageFiles = (hints?.changedFiles ?? []).filter((file) => file === pkg.relativeDirectory || file.startsWith(prefix));
|
|
950
1678
|
const text = hints?.text ?? "";
|
|
951
1679
|
if (packageFiles.length === 0 && text.trim() === "") return {
|
|
952
1680
|
embedded: all,
|
|
@@ -954,7 +1682,7 @@ function selectModuleDocs(pkg, hints) {
|
|
|
954
1682
|
};
|
|
955
1683
|
const matched = all.filter((doc) => {
|
|
956
1684
|
const segment = new RegExp(`(^|[/.])${escapeRegExp(doc.module)}([/.]|$)`);
|
|
957
|
-
return packageFiles.some((file) => segment.test(file.slice(
|
|
1685
|
+
return packageFiles.some((file) => segment.test(file.slice(prefix.length)) || file === packageRelativePath(pkg, doc.path)) || includesToken(text, doc.module);
|
|
958
1686
|
});
|
|
959
1687
|
return matched.length > 0 ? {
|
|
960
1688
|
embedded: matched,
|
|
@@ -967,7 +1695,7 @@ function selectModuleDocs(pkg, hints) {
|
|
|
967
1695
|
function escapeRegExp(value) {
|
|
968
1696
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
969
1697
|
}
|
|
970
|
-
function renderPackageContext(pkg, hints) {
|
|
1698
|
+
function renderPackageContext(pkg, hints, detail = "full") {
|
|
971
1699
|
const { embedded, scoped } = selectModuleDocs(pkg, hints);
|
|
972
1700
|
const lines = [
|
|
973
1701
|
`### ${pkg.name}`,
|
|
@@ -976,6 +1704,7 @@ function renderPackageContext(pkg, hints) {
|
|
|
976
1704
|
`- kind: ${pkg.kind}`,
|
|
977
1705
|
`- directory: ${pkg.relativeDirectory}`,
|
|
978
1706
|
`- domain knowledge: ${pkg.domainKnowledgePath ?? "(manifest fallback)"}`,
|
|
1707
|
+
`- object source: ${pkg.objectSource}${pkg.objectSourceReason ? ` (${pkg.objectSourceReason})` : ""}`,
|
|
979
1708
|
`- docs: ${[pkg.docSource ?? "(none)", ...pkg.moduleDocs.map((doc) => doc.path)].join(", ")}`,
|
|
980
1709
|
`- relationship features: ${pkg.relationshipFeatures.join(", ") || "(none)"}`,
|
|
981
1710
|
`- SDK deps: ${pkg.sdkDependencies.join(", ") || "(none)"}`,
|
|
@@ -984,14 +1713,20 @@ function renderPackageContext(pkg, hints) {
|
|
|
984
1713
|
`- objects: ${pkg.objects.slice(0, 20).map((object) => object.qualifiedName ?? object.className).join(", ")}`
|
|
985
1714
|
];
|
|
986
1715
|
if (pkg.objects.length > 20) lines.push(`- object count: ${pkg.objects.length}`);
|
|
1716
|
+
if (detail === "summary") {
|
|
1717
|
+
const docPaths = packageDocPaths(pkg);
|
|
1718
|
+
if (docPaths.length > 0) lines.push("", `> Authored docs not embedded (read on demand, or re-request with detail: "full"): ${docPaths.join(", ")}`);
|
|
1719
|
+
lines.push("");
|
|
1720
|
+
return lines.join("\n");
|
|
1721
|
+
}
|
|
987
1722
|
if (pkg.agentDoc) lines.push("", pkg.agentDoc.trim());
|
|
988
1723
|
for (const doc of embedded) {
|
|
989
|
-
lines.push("", `#### ${pkg
|
|
1724
|
+
lines.push("", `#### ${packageRelativePath(pkg, doc.path)}`, "");
|
|
990
1725
|
lines.push(doc.content.trim());
|
|
991
1726
|
}
|
|
992
1727
|
if (scoped) {
|
|
993
1728
|
const omitted = pkg.moduleDocs.filter((doc) => !embedded.includes(doc));
|
|
994
|
-
if (omitted.length > 0) lines.push("", `> Module docs not loaded for this request (read on demand): ${omitted.map((doc) =>
|
|
1729
|
+
if (omitted.length > 0) lines.push("", `> Module docs not loaded for this request (read on demand): ${omitted.map((doc) => packageRelativePath(pkg, doc.path)).join(", ")}`);
|
|
995
1730
|
}
|
|
996
1731
|
lines.push("");
|
|
997
1732
|
return lines.join("\n");
|
|
@@ -1099,6 +1834,6 @@ function sortJson(value) {
|
|
|
1099
1834
|
return value;
|
|
1100
1835
|
}
|
|
1101
1836
|
//#endregion
|
|
1102
|
-
export { checkKnowledgeFreshnessFromIndex as a,
|
|
1837
|
+
export { checkKnowledgeFreshnessFromIndex as a, renderFreshnessResult as c, smrtReview as d, checkKnowledgeFreshness as i, renderKnowledgeIndexMarkdown as l, buildKnowledgeIndex as n, diffKnowledgeIndex as o, buildReviewContext as r, packageDocPaths as s, buildArchitectureContext as t, smrtArchitecture as u };
|
|
1103
1838
|
|
|
1104
|
-
//# sourceMappingURL=knowledge-
|
|
1839
|
+
//# sourceMappingURL=knowledge-DUUhTM5u.js.map
|