@beignet/cli 0.0.49 → 0.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +54 -5
  3. package/dist/analysis/workspace.d.ts +1 -0
  4. package/dist/analysis/workspace.d.ts.map +1 -1
  5. package/dist/analysis/workspace.js +20 -10
  6. package/dist/analysis/workspace.js.map +1 -1
  7. package/dist/app-map-changes.d.ts +103 -0
  8. package/dist/app-map-changes.d.ts.map +1 -0
  9. package/dist/app-map-changes.js +949 -0
  10. package/dist/app-map-changes.js.map +1 -0
  11. package/dist/git-changes.d.ts +30 -0
  12. package/dist/git-changes.d.ts.map +1 -0
  13. package/dist/git-changes.js +367 -0
  14. package/dist/git-changes.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +58 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/inspect.js +413 -43
  20. package/dist/inspect.js.map +1 -1
  21. package/dist/lib.d.ts +3 -0
  22. package/dist/lib.d.ts.map +1 -1
  23. package/dist/lib.js +1 -0
  24. package/dist/lib.js.map +1 -1
  25. package/dist/make/shared.d.ts.map +1 -1
  26. package/dist/make/shared.js +31 -27
  27. package/dist/make/shared.js.map +1 -1
  28. package/dist/make.d.ts.map +1 -1
  29. package/dist/make.js +0 -2
  30. package/dist/make.js.map +1 -1
  31. package/dist/mcp.d.ts.map +1 -1
  32. package/dist/mcp.js +46 -6
  33. package/dist/mcp.js.map +1 -1
  34. package/dist/operational-process.d.ts +1 -0
  35. package/dist/operational-process.d.ts.map +1 -1
  36. package/dist/operational-process.js.map +1 -1
  37. package/dist/operational-runner.js +1 -0
  38. package/dist/operational-runner.js.map +1 -1
  39. package/dist/outbox.d.ts +1 -0
  40. package/dist/outbox.d.ts.map +1 -1
  41. package/dist/outbox.js +58 -12
  42. package/dist/outbox.js.map +1 -1
  43. package/dist/templates/agents.d.ts.map +1 -1
  44. package/dist/templates/agents.js +28 -3
  45. package/dist/templates/agents.js.map +1 -1
  46. package/dist/templates/base.d.ts.map +1 -1
  47. package/dist/templates/base.js +4 -1
  48. package/dist/templates/base.js.map +1 -1
  49. package/package.json +2 -2
  50. package/skills/app-structure/SKILL.md +30 -5
  51. package/src/analysis/workspace.ts +25 -9
  52. package/src/app-map-changes.ts +1462 -0
  53. package/src/git-changes.ts +511 -0
  54. package/src/index.ts +84 -4
  55. package/src/inspect.ts +586 -51
  56. package/src/lib.ts +21 -0
  57. package/src/make/shared.ts +31 -27
  58. package/src/make.ts +0 -2
  59. package/src/mcp.ts +65 -12
  60. package/src/operational-process.ts +1 -0
  61. package/src/operational-runner.ts +1 -0
  62. package/src/outbox.ts +63 -12
  63. package/src/templates/agents.ts +28 -3
  64. package/src/templates/base.ts +4 -1
@@ -0,0 +1,949 @@
1
+ import { readFile, realpath } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { localImportReferences, } from "./analysis/source-index.js";
4
+ import { createAnalysisWorkspace, listProjectFiles, resolveLocalModulePath, } from "./analysis/workspace.js";
5
+ import { mapApp, } from "./app-map.js";
6
+ import { hashGitChanges, readGitChangeSet, } from "./git-changes.js";
7
+ const changedFileLimit = 200;
8
+ const impactedNodeLimit = 250;
9
+ const relationshipLimit = 400;
10
+ const gapLimit = 100;
11
+ const reasonLimitPerNode = 3;
12
+ const edgeImpactBehavior = {
13
+ owns: "context",
14
+ binds: "consumer",
15
+ executes: "consumer",
16
+ authorizes: "consumer",
17
+ emits: "consumer",
18
+ dispatches: "consumer",
19
+ sends: "consumer",
20
+ publishes: "consumer",
21
+ provides: "consumer",
22
+ registers: "consumer",
23
+ "listens-to": "consumer",
24
+ exposes: "consumer",
25
+ "depends-on": "consumer",
26
+ "contains-test": "context",
27
+ };
28
+ const containerExpansionKinds = {
29
+ feature: new Set(["owns"]),
30
+ entrypoint: new Set(["exposes", "registers"]),
31
+ openapi: new Set(["exposes"]),
32
+ registry: new Set(["registers"]),
33
+ "route-group": new Set(["registers"]),
34
+ };
35
+ /** Map the current Git change set to bounded, source-backed Beignet concepts. */
36
+ export async function mapAppChanges(options = {}) {
37
+ const targetDir = path.resolve(options.cwd ?? process.cwd());
38
+ const git = await readGitChangeSet({ cwd: targetDir, base: options.base });
39
+ const canonicalTargetDir = await realpath(targetDir);
40
+ assertInsideRepository(canonicalTargetDir, git.repositoryRoot);
41
+ const appRepositoryPrefix = normalizePath(path.relative(git.repositoryRoot, canonicalTargetDir));
42
+ const [appMap, workspace, workspaceScope] = await Promise.all([
43
+ mapApp({ cwd: targetDir, strict: true }),
44
+ createAnalysisWorkspace(targetDir),
45
+ resolveWorkspaceScope(git.repositoryRoot, canonicalTargetDir),
46
+ ]);
47
+ const governingFiles = new Set(workspace.compilerConfigFiles
48
+ .map((file) => path.resolve(canonicalTargetDir, path.relative(workspace.targetDir, file)))
49
+ .filter((file) => isAbsoluteWithin(file, git.repositoryRoot))
50
+ .map((file) => normalizePath(path.relative(git.repositoryRoot, file))));
51
+ const changedFiles = classifyChangedFiles({
52
+ files: git.files,
53
+ governingFiles,
54
+ repositoryRoot: git.repositoryRoot,
55
+ targetDir: canonicalTargetDir,
56
+ workspaceScope,
57
+ });
58
+ const documentationRoots = [
59
+ normalizePath(path.relative(git.repositoryRoot, canonicalTargetDir)),
60
+ ...workspaceScope.dependencyDirectories,
61
+ ];
62
+ const isDocumentationChangePath = (file) => isDocumentationPath(file, documentationRoots);
63
+ const scopedFiles = changedFiles.filter((file) => file.scope !== "outside");
64
+ const appWideFiles = scopedFiles.filter((file) => (file.scope === "governing" || file.scope === "workspace-dependency") &&
65
+ !changedFilePaths(file).every(isDocumentationChangePath));
66
+ const docsOnly = scopedFiles.length > 0 &&
67
+ scopedFiles.every((file) => changedFilePaths(file).every(isDocumentationChangePath));
68
+ const appWide = appWideFiles.length > 0 && !docsOnly;
69
+ const impacts = new Map();
70
+ const relationships = [];
71
+ const workspaceRelationshipRelevant = changedFiles.some((file) => file.scope === "outside") ||
72
+ changedFiles.some((file) => file.scope === "governing" &&
73
+ path.posix.basename(file.path) === "package.json");
74
+ const gaps = workspaceScope.gaps.filter(isAppDependencyGap);
75
+ if (workspaceRelationshipRelevant) {
76
+ gaps.push(...workspaceScope.gaps.filter((gap) => !isAppDependencyGap(gap)));
77
+ }
78
+ const nodesById = new Map(appMap.nodes.map((node) => [node.id, node]));
79
+ const nodesByFile = groupNodesByFile(appMap.nodes);
80
+ const directNodeIds = new Set();
81
+ if (appWide) {
82
+ const reasons = appWideChangeReasons(appWideFiles, isDocumentationChangePath);
83
+ for (const node of appMap.nodes) {
84
+ for (const reason of reasons) {
85
+ addImpact(impacts, node, "app-wide", reason);
86
+ }
87
+ }
88
+ }
89
+ const appSourceChanges = appRelativeSourceChanges(changedFiles, git.repositoryRoot, canonicalTargetDir);
90
+ for (const change of appSourceChanges) {
91
+ const directNodes = nodesByFile.get(change.file) ?? [];
92
+ for (const node of directNodes) {
93
+ directNodeIds.add(node.id);
94
+ addImpact(impacts, node, "direct", {
95
+ kind: "source",
96
+ changedFile: change.repositoryPath,
97
+ message: `Source ${change.repositoryPath} changed directly.`,
98
+ evidence: {
99
+ file: node.source.file,
100
+ ...(node.source.line ? { line: node.source.line } : {}),
101
+ ...(node.source.column ? { column: node.source.column } : {}),
102
+ confidence: "exact",
103
+ },
104
+ });
105
+ }
106
+ }
107
+ await addImportConsumers({
108
+ workspace,
109
+ changes: appSourceChanges,
110
+ nodesByFile,
111
+ impacts,
112
+ });
113
+ addSemanticConsumers({
114
+ directNodeIds,
115
+ appMapEdges: appMap.edges,
116
+ nodesById,
117
+ impacts,
118
+ relationships,
119
+ });
120
+ addFeatureContext({
121
+ changes: appSourceChanges,
122
+ appMapEdges: appMap.edges,
123
+ nodes: appMap.nodes,
124
+ nodesById,
125
+ impacts,
126
+ });
127
+ addUnmappedChangeGaps({
128
+ changes: appSourceChanges,
129
+ impacts,
130
+ gaps,
131
+ });
132
+ addAppMapGaps(appMap.unresolved, appSourceChanges, impacts, gaps, appRepositoryPrefix);
133
+ for (const file of changedFiles) {
134
+ if (file.status === "unmerged" || file.status === "unknown") {
135
+ gaps.push({
136
+ code: "git_change_status_unresolved",
137
+ file: file.path,
138
+ message: `Git reported ${file.status} status for ${file.path}; Beignet cannot classify its impact completely.`,
139
+ });
140
+ }
141
+ }
142
+ const sortedFiles = [...changedFiles].sort(compareAppChangedFiles);
143
+ const sortedImpacts = finalizeImpacts(impacts);
144
+ const sortedRelationships = dedupeRelationships(relationships).sort(compareRelationships);
145
+ const sortedGaps = dedupeGaps(gaps).sort(compareGaps);
146
+ const impactedFeatures = new Set(sortedImpacts
147
+ .map((item) => item.feature ?? (item.kind === "feature" ? item.name : undefined))
148
+ .filter((feature) => Boolean(feature)));
149
+ const repositoryChangeHash = await hashGitChanges(git);
150
+ const scopedRawFiles = git.files.filter((file) => scopedFiles.some((scoped) => scoped.path === file.path && scoped.oldPath === file.oldPath));
151
+ const scopedChangeHash = await hashGitChanges(git, scopedRawFiles);
152
+ return {
153
+ schemaVersion: 1,
154
+ targetDir,
155
+ repository: {
156
+ root: git.repositoryRoot,
157
+ ...(workspaceScope.workspaceRoot
158
+ ? { workspaceRoot: workspaceScope.workspaceRoot }
159
+ : {}),
160
+ comparison: git.comparison,
161
+ repositoryChangeHash,
162
+ scopedChangeHash,
163
+ },
164
+ classification: { appWide, docsOnly },
165
+ summary: {
166
+ changedFiles: sortedFiles.length,
167
+ scopedChangedFiles: scopedFiles.length,
168
+ outsideChangedFiles: sortedFiles.length - scopedFiles.length,
169
+ impactedFeatures: impactedFeatures.size,
170
+ impactedNodes: sortedImpacts.length,
171
+ direct: countImpact(sortedImpacts, "direct"),
172
+ consumers: countImpact(sortedImpacts, "consumer"),
173
+ related: countImpact(sortedImpacts, "related"),
174
+ contexts: countImpact(sortedImpacts, "context"),
175
+ appWide: countImpact(sortedImpacts, "app-wide"),
176
+ relationships: sortedRelationships.length,
177
+ gaps: sortedGaps.length,
178
+ },
179
+ changedFiles: bounded(sortedFiles, changedFileLimit),
180
+ impactedNodes: bounded(sortedImpacts, impactedNodeLimit),
181
+ relationships: bounded(sortedRelationships, relationshipLimit),
182
+ gaps: boundedGaps(sortedGaps),
183
+ };
184
+ }
185
+ /** Format the report-only changed app map for a terminal. */
186
+ export function formatAppChangeImpact(result) {
187
+ const comparison = result.repository.comparison.mode === "base"
188
+ ? `${terminalText(result.repository.comparison.base)} at ${shortId(result.repository.comparison.baseCommit)} (merge base ${shortId(result.repository.comparison.mergeBase)}) → HEAD/index/worktree`
189
+ : result.repository.comparison.head
190
+ ? `HEAD at ${shortId(result.repository.comparison.head)} → index/worktree`
191
+ : "empty Git tree → index/worktree";
192
+ const lines = [
193
+ `Beignet change map for ${terminalText(result.targetDir)}`,
194
+ `Comparison: ${comparison}`,
195
+ `Changes: ${result.summary.scopedChangedFiles} scoped, ${result.summary.outsideChangedFiles} outside app`,
196
+ `Impact: ${result.summary.direct} direct, ${result.summary.consumers} consumers, ${result.summary.related} related, ${result.summary.contexts} context, ${result.summary.appWide} app-wide`,
197
+ ];
198
+ if (result.classification.appWide)
199
+ lines.push("Scope: app-wide");
200
+ if (result.classification.docsOnly)
201
+ lines.push("Scope: documentation only");
202
+ const scopedChanges = result.changedFiles.items.filter((file) => file.scope !== "outside");
203
+ lines.push("", "Changed files");
204
+ if (scopedChanges.length === 0) {
205
+ lines.push(" No scoped changes found.");
206
+ }
207
+ else {
208
+ for (const file of scopedChanges.slice(0, 30)) {
209
+ const rename = file.oldPath ? `${terminalText(file.oldPath)} → ` : "";
210
+ lines.push(` ${statusMark(file.status)} ${rename}${terminalText(file.path)} (${file.scope})`);
211
+ }
212
+ if (scopedChanges.length > 30 ||
213
+ result.summary.scopedChangedFiles > scopedChanges.length) {
214
+ lines.push(` … showing ${Math.min(scopedChanges.length, 30)} of ${result.summary.scopedChangedFiles} scoped changes`);
215
+ }
216
+ }
217
+ lines.push("", "Potential impact");
218
+ if (result.impactedNodes.items.length === 0) {
219
+ lines.push(" No mapped application concepts were connected to this change set.");
220
+ }
221
+ else {
222
+ for (const item of result.impactedNodes.items.slice(0, 30)) {
223
+ lines.push(` ${item.kind.padEnd(18)} ${terminalText(item.id)} [${item.impact}]`);
224
+ const reason = item.reasons[0];
225
+ if (reason)
226
+ lines.push(` ${terminalText(reason.message)}`);
227
+ }
228
+ if (result.impactedNodes.total > 30) {
229
+ lines.push(` … showing 30 of ${result.impactedNodes.total} impacted concepts`);
230
+ }
231
+ }
232
+ lines.push("", "Unresolved evidence");
233
+ if (result.gaps.total === 0) {
234
+ lines.push(" None");
235
+ }
236
+ else {
237
+ for (const gap of result.gaps.items.slice(0, 20)) {
238
+ lines.push(` ${gap.code}${gap.file ? ` (${terminalText(gap.file)})` : ""}`);
239
+ lines.push(` ${terminalText(gap.message)}`);
240
+ }
241
+ if (result.gaps.total > 20) {
242
+ lines.push(` … showing 20 of ${result.gaps.total} gaps`);
243
+ }
244
+ }
245
+ lines.push("", `Scoped hash: ${shortHash(result.repository.scopedChangeHash)}`, "Run beignet map --changed --json for the bounded source-backed report.");
246
+ return lines.join("\n");
247
+ }
248
+ async function resolveWorkspaceScope(repositoryRoot, targetDir) {
249
+ const repositoryFiles = await listProjectFiles(repositoryRoot);
250
+ const packageFiles = repositoryFiles.filter((file) => path.posix.basename(file) === "package.json");
251
+ const packageFileSet = new Set(packageFiles);
252
+ const gaps = [];
253
+ const manifests = new Map();
254
+ const attemptedManifests = new Set();
255
+ const readManifest = async (file) => {
256
+ const existing = manifests.get(file);
257
+ if (existing)
258
+ return existing;
259
+ if (attemptedManifests.has(file))
260
+ return undefined;
261
+ attemptedManifests.add(file);
262
+ try {
263
+ const manifest = await readPackageJson(path.join(repositoryRoot, file));
264
+ if (manifest)
265
+ manifests.set(file, manifest);
266
+ return manifest;
267
+ }
268
+ catch (error) {
269
+ gaps.push({
270
+ code: "workspace_manifest_unresolved",
271
+ file,
272
+ message: `Could not inspect workspace manifest ${file}: ${error instanceof Error ? error.message : String(error)}`,
273
+ });
274
+ return undefined;
275
+ }
276
+ };
277
+ const ancestorFiles = ancestorPackageFiles(repositoryRoot, targetDir).filter((file) => packageFileSet.has(file));
278
+ await Promise.all(ancestorFiles.map(readManifest));
279
+ const appPackageFile = normalizePath(path.relative(repositoryRoot, path.join(targetDir, "package.json")));
280
+ const appPackage = manifests.get(appPackageFile);
281
+ if (!appPackage)
282
+ return { dependencyDirectories: new Set(), gaps };
283
+ const workspaceManifest = ancestorFiles
284
+ .map((file) => ({ file, manifest: manifests.get(file) }))
285
+ .find(({ manifest }) => workspacePatterns(manifest).length > 0);
286
+ const workspaceRoot = workspaceManifest
287
+ ? path.join(repositoryRoot, path.posix.dirname(workspaceManifest.file))
288
+ : undefined;
289
+ const patterns = workspacePatterns(workspaceManifest?.manifest);
290
+ for (const pattern of patterns.filter(hasUnsupportedWorkspacePattern)) {
291
+ gaps.push({
292
+ code: "workspace_pattern_unresolved",
293
+ message: `Workspace pattern ${JSON.stringify(pattern)} uses syntax changed mapping cannot resolve statically.`,
294
+ });
295
+ }
296
+ const workspaceRelativeRoot = workspaceRoot
297
+ ? normalizePath(path.relative(repositoryRoot, workspaceRoot))
298
+ : "";
299
+ const workspacePackageFiles = workspaceRoot
300
+ ? packageFiles.filter((file) => {
301
+ const directory = normalizePath(path.posix.dirname(file));
302
+ const relative = normalizePath(path.posix.relative(workspaceRelativeRoot, directory));
303
+ return patterns.some((pattern) => matchesWorkspacePattern(relative, pattern));
304
+ })
305
+ : [];
306
+ await Promise.all(workspacePackageFiles.map(readManifest));
307
+ const packages = new Map();
308
+ const duplicatePackageNames = new Set();
309
+ for (const file of workspacePackageFiles) {
310
+ const manifest = manifests.get(file);
311
+ if (!manifest)
312
+ continue;
313
+ if (manifest.name) {
314
+ if (packages.has(manifest.name)) {
315
+ duplicatePackageNames.add(manifest.name);
316
+ packages.delete(manifest.name);
317
+ continue;
318
+ }
319
+ if (duplicatePackageNames.has(manifest.name))
320
+ continue;
321
+ packages.set(manifest.name, {
322
+ directory: normalizePath(path.posix.dirname(file)),
323
+ file,
324
+ manifest,
325
+ });
326
+ }
327
+ }
328
+ for (const name of [...duplicatePackageNames].sort()) {
329
+ gaps.push({
330
+ code: "workspace_package_ambiguous",
331
+ message: `Multiple repository packages declare the name ${JSON.stringify(name)}; direct dependency impact cannot be scoped uniquely.`,
332
+ });
333
+ }
334
+ const workspacePackages = new Map([...packages.entries()].filter(([, pkg]) => {
335
+ if (!workspaceRoot)
336
+ return false;
337
+ const relative = normalizePath(path.posix.relative(workspaceRelativeRoot, pkg.directory));
338
+ return patterns.some((pattern) => matchesWorkspacePattern(relative, pattern));
339
+ }));
340
+ const dependencyDirectories = new Set();
341
+ for (const [name, version] of Object.entries(packageDependencies(appPackage))) {
342
+ const local = workspacePackages.get(name);
343
+ if (local) {
344
+ dependencyDirectories.add(local.directory);
345
+ continue;
346
+ }
347
+ if (version.startsWith("file:") || version.startsWith("link:")) {
348
+ const requested = version.slice(version.indexOf(":") + 1);
349
+ const absolute = path.resolve(targetDir, requested);
350
+ const relative = normalizePath(path.relative(repositoryRoot, absolute));
351
+ if (relative === ".." || relative.startsWith("../")) {
352
+ gaps.push({
353
+ code: "local_dependency_outside_repository",
354
+ message: `Local dependency ${name} resolves outside the Git repository and cannot be scoped.`,
355
+ });
356
+ }
357
+ else {
358
+ try {
359
+ const manifest = await readPackageJson(path.join(absolute, "package.json"));
360
+ if (manifest)
361
+ dependencyDirectories.add(relative);
362
+ else {
363
+ gaps.push({
364
+ code: "local_dependency_unresolved",
365
+ file: relative,
366
+ message: `Local dependency ${name} points to ${relative}, but no package.json was found there.`,
367
+ });
368
+ }
369
+ }
370
+ catch (error) {
371
+ dependencyDirectories.add(relative);
372
+ gaps.push({
373
+ code: "local_dependency_manifest_unresolved",
374
+ file: normalizePath(path.posix.join(relative, "package.json")),
375
+ message: `Could not inspect local dependency ${name} at ${relative}: ${error instanceof Error ? error.message : String(error)}`,
376
+ });
377
+ }
378
+ }
379
+ }
380
+ else if (version.startsWith("workspace:")) {
381
+ gaps.push({
382
+ code: "workspace_dependency_unresolved",
383
+ message: `Workspace dependency ${name} is declared but no matching local workspace package was found.`,
384
+ });
385
+ }
386
+ }
387
+ return {
388
+ ...(workspaceRoot ? { workspaceRoot } : {}),
389
+ dependencyDirectories,
390
+ gaps,
391
+ };
392
+ }
393
+ function classifyChangedFiles(args) {
394
+ const targetPrefix = normalizePath(path.relative(args.repositoryRoot, args.targetDir));
395
+ return args.files.map((file) => ({
396
+ ...file,
397
+ scope: highestPriorityScope(changedFilePaths(file).map((value) => classifyChangedFile(value, args.governingFiles, targetPrefix, args.workspaceScope, args.repositoryRoot))),
398
+ }));
399
+ }
400
+ function classifyChangedFile(file, governingFiles, targetPrefix, workspaceScope, repositoryRoot) {
401
+ if (governingFiles.has(file))
402
+ return "governing";
403
+ const workspacePrefix = workspaceScope.workspaceRoot
404
+ ? normalizePath(path.relative(repositoryRoot, workspaceScope.workspaceRoot))
405
+ : undefined;
406
+ if (isGoverningFile(file, targetPrefix, workspacePrefix)) {
407
+ return "governing";
408
+ }
409
+ if (isWithin(file, targetPrefix))
410
+ return "app";
411
+ if ([...workspaceScope.dependencyDirectories].some((directory) => isWithin(file, directory))) {
412
+ return "workspace-dependency";
413
+ }
414
+ return "outside";
415
+ }
416
+ function isGoverningFile(file, targetPrefix, workspacePrefix) {
417
+ const basename = path.posix.basename(file);
418
+ const directory = normalizePath(path.posix.dirname(file));
419
+ const targetAncestors = pathAncestors(targetPrefix);
420
+ const isTargetOrAncestor = isWithin(file, targetPrefix) || targetAncestors.includes(directory);
421
+ const isWorkspaceRoot = workspacePrefix !== undefined && directory === workspacePrefix;
422
+ if (!isTargetOrAncestor && !isWorkspaceRoot) {
423
+ return false;
424
+ }
425
+ if ([
426
+ "bun.lock",
427
+ "bun.lockb",
428
+ "package-lock.json",
429
+ "pnpm-lock.yaml",
430
+ "yarn.lock",
431
+ "package.json",
432
+ "turbo.json",
433
+ "bunfig.toml",
434
+ "biome.json",
435
+ "biome.jsonc",
436
+ "beignet.config.json",
437
+ ".npmrc",
438
+ ].includes(basename)) {
439
+ return true;
440
+ }
441
+ return (/^(?:beignet|drizzle|eslint|next|postcss|tailwind|vite)\.config\.(?:cjs|cts|js|mjs|mts|ts)$/.test(basename) || /^tsconfig(?:\.[^.]+)?\.json$/.test(basename));
442
+ }
443
+ function appRelativeSourceChanges(files, repositoryRoot, canonicalTargetDir) {
444
+ const targetPrefix = normalizePath(path.relative(repositoryRoot, canonicalTargetDir));
445
+ const changes = [];
446
+ for (const changed of files) {
447
+ if (changed.scope !== "app")
448
+ continue;
449
+ for (const repositoryPath of changedFilePaths(changed)) {
450
+ if (!isWithin(repositoryPath, targetPrefix))
451
+ continue;
452
+ const file = targetPrefix
453
+ ? normalizePath(path.posix.relative(targetPrefix, repositoryPath))
454
+ : repositoryPath;
455
+ if (isDocumentationPath(file, [""]))
456
+ continue;
457
+ changes.push({
458
+ file,
459
+ repositoryPath,
460
+ status: changed.status,
461
+ });
462
+ }
463
+ }
464
+ return uniqueBy(changes, (change) => `${change.repositoryPath}:${change.status}`);
465
+ }
466
+ async function addImportConsumers(args) {
467
+ const reverseImports = new Map();
468
+ const sourceFiles = args.workspace.files.filter(isSourceFile);
469
+ await Promise.all(sourceFiles.map(async (file) => {
470
+ for (const reference of await localImportReferences(args.workspace, file)) {
471
+ const target = reference.resolvedFile ??
472
+ resolveLocalModulePath(args.workspace, file, reference.importPath);
473
+ if (!target)
474
+ continue;
475
+ const confidence = reference.resolvedFile ? "exact" : "partial";
476
+ const keys = reference.resolvedFile
477
+ ? [resolvedModuleKey(target)]
478
+ : unresolvedModuleKeys(target);
479
+ for (const key of keys) {
480
+ const references = reverseImports.get(key) ?? [];
481
+ references.push({ confidence, importer: file, reference });
482
+ reverseImports.set(key, references);
483
+ }
484
+ }
485
+ }));
486
+ for (const references of reverseImports.values()) {
487
+ references.sort((left, right) => `${left.importer}:${left.reference.line}:${left.reference.column}`.localeCompare(`${right.importer}:${right.reference.line}:${right.reference.column}`));
488
+ }
489
+ for (const change of args.changes.filter((item) => isSourceFile(item.file))) {
490
+ const queue = [{ confidence: "exact", file: change.file, depth: 0 }];
491
+ const visited = new Set([resolvedModuleKey(change.file)]);
492
+ for (let index = 0; index < queue.length; index += 1) {
493
+ const current = queue[index];
494
+ for (const key of moduleLookupKeys(current.file)) {
495
+ for (const { confidence: edgeConfidence, importer, reference, } of reverseImports.get(key) ?? []) {
496
+ const importerKey = resolvedModuleKey(importer);
497
+ if (visited.has(importerKey))
498
+ continue;
499
+ visited.add(importerKey);
500
+ const depth = current.depth + 1;
501
+ const confidence = current.confidence === "partial" || edgeConfidence === "partial"
502
+ ? "partial"
503
+ : "exact";
504
+ for (const node of args.nodesByFile.get(importer) ?? []) {
505
+ addImpact(args.impacts, node, "consumer", {
506
+ kind: "import",
507
+ changedFile: change.repositoryPath,
508
+ message: importReasonMessage({
509
+ changedFile: change.file,
510
+ confidence,
511
+ depth,
512
+ importer,
513
+ }),
514
+ evidence: {
515
+ file: importer,
516
+ line: reference.line,
517
+ column: reference.column,
518
+ confidence,
519
+ },
520
+ });
521
+ }
522
+ queue.push({ confidence, file: importer, depth });
523
+ }
524
+ }
525
+ }
526
+ }
527
+ }
528
+ function importReasonMessage(args) {
529
+ const dependency = args.confidence === "exact" ? "depends on" : "may depend on";
530
+ const depth = args.depth > 1 ? ` through ${args.depth} local imports` : "";
531
+ const uncertainty = args.confidence === "partial"
532
+ ? "; at least one import could not be resolved exactly"
533
+ : "";
534
+ return `${args.importer} ${dependency} changed source ${args.changedFile}${depth}${uncertainty}.`;
535
+ }
536
+ function addSemanticConsumers(args) {
537
+ for (const edge of args.appMapEdges) {
538
+ if (args.directNodeIds.has(edge.to) &&
539
+ edgeImpactBehavior[edge.kind] === "consumer") {
540
+ const consumer = args.nodesById.get(edge.from);
541
+ const changed = args.nodesById.get(edge.to);
542
+ const changedFile = changed
543
+ ? primaryChangeReason(args.impacts.get(changed.id))?.changedFile
544
+ : undefined;
545
+ if (consumer && changed && changedFile) {
546
+ addImpact(args.impacts, consumer, "consumer", {
547
+ kind: "relationship",
548
+ changedFile,
549
+ message: `${consumer.id} ${edge.kind} changed ${changed.id}.`,
550
+ evidence: edge.evidence,
551
+ relationship: { kind: edge.kind, from: edge.from, to: edge.to },
552
+ });
553
+ args.relationships.push({ ...edge, impact: "consumer" });
554
+ }
555
+ }
556
+ if (!args.directNodeIds.has(edge.from))
557
+ continue;
558
+ const container = args.nodesById.get(edge.from);
559
+ const expansionKinds = container
560
+ ? containerExpansionKinds[container.kind]
561
+ : undefined;
562
+ if (!container || !expansionKinds?.has(edge.kind))
563
+ continue;
564
+ const related = args.nodesById.get(edge.to);
565
+ if (!related)
566
+ continue;
567
+ addImpact(args.impacts, related, "related", {
568
+ kind: "relationship",
569
+ changedFile: primaryChangeReason(args.impacts.get(container.id))?.changedFile ??
570
+ container.source.file,
571
+ message: `${related.id} is connected to the directly changed ${container.id} container.`,
572
+ evidence: edge.evidence,
573
+ relationship: { kind: edge.kind, from: edge.from, to: edge.to },
574
+ });
575
+ args.relationships.push({ ...edge, impact: "related" });
576
+ }
577
+ }
578
+ function addFeatureContext(args) {
579
+ for (const change of args.changes) {
580
+ for (const feature of args.nodes.filter((node) => node.kind === "feature")) {
581
+ if (!isWithin(change.file, feature.source.file))
582
+ continue;
583
+ addImpact(args.impacts, feature, "context", {
584
+ kind: "ownership",
585
+ changedFile: change.repositoryPath,
586
+ message: `${change.file} belongs to feature ${feature.name}.`,
587
+ evidence: { file: change.file, confidence: "exact" },
588
+ });
589
+ }
590
+ }
591
+ const impactedIds = new Set(args.impacts.keys());
592
+ for (const edge of args.appMapEdges) {
593
+ if (!impactedIds.has(edge.to) ||
594
+ (edge.kind !== "owns" && edge.kind !== "contains-test")) {
595
+ continue;
596
+ }
597
+ const feature = args.nodesById.get(edge.from);
598
+ const member = args.nodesById.get(edge.to);
599
+ const memberImpact = member ? args.impacts.get(member.id) : undefined;
600
+ if (!feature || !member || memberImpact?.impact === "app-wide")
601
+ continue;
602
+ addImpact(args.impacts, feature, "context", {
603
+ kind: "ownership",
604
+ changedFile: primaryChangeReason(memberImpact)?.changedFile ?? member.source.file,
605
+ message: `${feature.id} owns impacted ${member.id}.`,
606
+ evidence: edge.evidence,
607
+ relationship: { kind: edge.kind, from: edge.from, to: edge.to },
608
+ });
609
+ }
610
+ }
611
+ function addUnmappedChangeGaps(args) {
612
+ const reasons = [...args.impacts.values()].flatMap((impact) => impact.reasons);
613
+ for (const change of args.changes) {
614
+ const represented = reasons.some((reason) => reason.changedFile === change.repositoryPath);
615
+ const sourceFile = isSourceFile(change.file);
616
+ if (sourceFile && change.status === "deleted") {
617
+ args.gaps.push({
618
+ code: "deleted_source_unresolved",
619
+ file: change.repositoryPath,
620
+ message: "The declaration is absent from the current app map, so Beignet can report surviving importers and feature context but cannot reconstruct every previous semantic relationship.",
621
+ });
622
+ }
623
+ else if (!represented) {
624
+ args.gaps.push({
625
+ code: sourceFile ? "changed_source_unmapped" : "changed_file_unmapped",
626
+ file: change.repositoryPath,
627
+ message: sourceFile
628
+ ? "No mapped concept, feature, or surviving local importer could be connected to this changed source file."
629
+ : "No mapped concept or feature context could be connected to this changed file.",
630
+ });
631
+ }
632
+ }
633
+ }
634
+ function addAppMapGaps(unresolved, changes, impacts, gaps, appRepositoryPrefix) {
635
+ const relevantFiles = new Set([
636
+ ...changes.map((change) => change.file),
637
+ ...[...impacts.values()].flatMap((impact) => [
638
+ impact.node.source.file,
639
+ ...impact.reasons
640
+ .map((reason) => reason.evidence?.file)
641
+ .filter(Boolean),
642
+ ]),
643
+ ].filter((file) => Boolean(file)));
644
+ for (const item of unresolved) {
645
+ if (!relevantFiles.has(item.source.file))
646
+ continue;
647
+ gaps.push({
648
+ code: "app_map_unresolved_reference",
649
+ file: normalizePath(path.posix.join(appRepositoryPrefix, item.source.file)),
650
+ message: `${item.code}: ${item.message}`,
651
+ });
652
+ }
653
+ }
654
+ function addImpact(impacts, node, impact, reason) {
655
+ const existing = impacts.get(node.id);
656
+ if (!existing) {
657
+ impacts.set(node.id, { node, impact, reasons: [reason] });
658
+ return;
659
+ }
660
+ if (impactRank(impact) > impactRank(existing.impact))
661
+ existing.impact = impact;
662
+ if (!existing.reasons.some((item) => reasonKey(item) === reasonKey(reason))) {
663
+ existing.reasons.push(reason);
664
+ }
665
+ }
666
+ function finalizeImpacts(impacts) {
667
+ return [...impacts.values()]
668
+ .map(({ node, impact, reasons }) => {
669
+ const sortedReasons = [...reasons].sort((left, right) => reasonKindSort(left.kind) - reasonKindSort(right.kind) ||
670
+ reasonKey(left).localeCompare(reasonKey(right)));
671
+ return {
672
+ ...node,
673
+ impact,
674
+ reasons: sortedReasons.slice(0, reasonLimitPerNode),
675
+ reasonCount: sortedReasons.length,
676
+ reasonsTruncated: sortedReasons.length > reasonLimitPerNode,
677
+ };
678
+ })
679
+ .sort((left, right) => `${impactSort(left.impact)}:${left.kind}:${left.id}`.localeCompare(`${impactSort(right.impact)}:${right.kind}:${right.id}`));
680
+ }
681
+ function groupNodesByFile(nodes) {
682
+ const grouped = new Map();
683
+ for (const node of nodes) {
684
+ const values = grouped.get(node.source.file) ?? [];
685
+ values.push(node);
686
+ grouped.set(node.source.file, values);
687
+ }
688
+ return grouped;
689
+ }
690
+ function packageDependencies(manifest) {
691
+ return {
692
+ ...manifest.dependencies,
693
+ ...manifest.devDependencies,
694
+ ...manifest.optionalDependencies,
695
+ ...manifest.peerDependencies,
696
+ };
697
+ }
698
+ function workspacePatterns(manifest) {
699
+ if (!manifest?.workspaces)
700
+ return [];
701
+ return Array.isArray(manifest.workspaces)
702
+ ? manifest.workspaces
703
+ : (manifest.workspaces.packages ?? []);
704
+ }
705
+ function matchesWorkspacePattern(file, pattern) {
706
+ const source = normalizePath(pattern).replace(/^\.\//, "").replace(/\/$/, "");
707
+ let expression = "";
708
+ for (let index = 0; index < source.length; index += 1) {
709
+ const character = source[index];
710
+ if (character === "*" && source[index + 1] === "*") {
711
+ expression += ".*";
712
+ index += 1;
713
+ }
714
+ else if (character === "*")
715
+ expression += "[^/]*";
716
+ else if (character === "?")
717
+ expression += "[^/]";
718
+ else
719
+ expression += character.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
720
+ }
721
+ return new RegExp(`^${expression}$`).test(file);
722
+ }
723
+ function hasUnsupportedWorkspacePattern(pattern) {
724
+ return [...pattern].some((character) => "!{}[]".includes(character));
725
+ }
726
+ function ancestorPackageFiles(repositoryRoot, targetDir) {
727
+ const files = [];
728
+ let directory = targetDir;
729
+ while (true) {
730
+ files.push(normalizePath(path.relative(repositoryRoot, path.join(directory, "package.json"))));
731
+ if (directory === repositoryRoot)
732
+ break;
733
+ const parent = path.dirname(directory);
734
+ if (parent === directory || !isAbsoluteWithin(parent, repositoryRoot))
735
+ break;
736
+ directory = parent;
737
+ }
738
+ return files;
739
+ }
740
+ async function readPackageJson(file) {
741
+ try {
742
+ return JSON.parse(await readFile(file, "utf8"));
743
+ }
744
+ catch (error) {
745
+ if (error.code === "ENOENT")
746
+ return undefined;
747
+ throw new Error(`Could not read workspace manifest ${file}: ${String(error)}`);
748
+ }
749
+ }
750
+ function bounded(items, limit) {
751
+ return {
752
+ items: items.slice(0, limit),
753
+ returned: Math.min(items.length, limit),
754
+ total: items.length,
755
+ truncated: items.length > limit,
756
+ };
757
+ }
758
+ function boundedGaps(gaps) {
759
+ const section = bounded(gaps, gapLimit);
760
+ const countsByCode = {};
761
+ for (const gap of gaps) {
762
+ countsByCode[gap.code] = (countsByCode[gap.code] ?? 0) + 1;
763
+ }
764
+ return { ...section, countsByCode };
765
+ }
766
+ function dedupeRelationships(relationships) {
767
+ return uniqueBy(relationships, (edge) => `${edge.impact}:${edge.kind}:${edge.from}:${edge.to}`);
768
+ }
769
+ function dedupeGaps(gaps) {
770
+ return uniqueBy(gaps, (gap) => `${gap.code}:${gap.file ?? ""}:${gap.message}`);
771
+ }
772
+ function uniqueBy(items, key) {
773
+ const seen = new Set();
774
+ return items.filter((item) => {
775
+ const value = key(item);
776
+ if (seen.has(value))
777
+ return false;
778
+ seen.add(value);
779
+ return true;
780
+ });
781
+ }
782
+ function compareAppChangedFiles(left, right) {
783
+ return `${scopeSort(left.scope)}:${left.path}:${left.oldPath ?? ""}`.localeCompare(`${scopeSort(right.scope)}:${right.path}:${right.oldPath ?? ""}`);
784
+ }
785
+ function compareRelationships(left, right) {
786
+ return `${left.impact}:${left.kind}:${left.from}:${left.to}`.localeCompare(`${right.impact}:${right.kind}:${right.from}:${right.to}`);
787
+ }
788
+ function compareGaps(left, right) {
789
+ return `${left.code}:${left.file ?? ""}:${left.message}`.localeCompare(`${right.code}:${right.file ?? ""}:${right.message}`);
790
+ }
791
+ function isAppDependencyGap(gap) {
792
+ return (gap.code.startsWith("local_dependency_") ||
793
+ gap.code === "workspace_dependency_unresolved");
794
+ }
795
+ function countImpact(items, impact) {
796
+ return items.filter((item) => item.impact === impact).length;
797
+ }
798
+ function reasonKey(reason) {
799
+ return `${reason.kind}:${reason.changedFile}:${reason.relationship?.kind ?? ""}:${reason.relationship?.from ?? ""}:${reason.relationship?.to ?? ""}:${reason.evidence?.file ?? ""}:${reason.evidence?.line ?? 0}:${reason.evidence?.column ?? 0}`;
800
+ }
801
+ function reasonKindSort(kind) {
802
+ return {
803
+ source: 0,
804
+ relationship: 1,
805
+ import: 2,
806
+ ownership: 3,
807
+ "app-wide": 4,
808
+ }[kind];
809
+ }
810
+ function impactRank(impact) {
811
+ return { "app-wide": 0, context: 1, related: 2, consumer: 3, direct: 4 }[impact];
812
+ }
813
+ function impactSort(impact) {
814
+ return { direct: 0, consumer: 1, related: 2, context: 3, "app-wide": 4 }[impact];
815
+ }
816
+ function scopeSort(scope) {
817
+ return { governing: 0, app: 1, "workspace-dependency": 2, outside: 3 }[scope];
818
+ }
819
+ function moduleStem(file) {
820
+ return normalizePath(file).replace(/\.(?:c|m)?[jt]sx?$/, "");
821
+ }
822
+ function resolvedModuleKey(file) {
823
+ return `resolved:${normalizePath(file)}`;
824
+ }
825
+ function unresolvedModuleKeys(file) {
826
+ const stem = moduleStem(file);
827
+ return stem.endsWith("/index")
828
+ ? [`unresolved:${stem}`]
829
+ : [`unresolved:${stem}`, `unresolved:${stem}/index`];
830
+ }
831
+ function moduleLookupKeys(file) {
832
+ return [resolvedModuleKey(file), ...unresolvedModuleKeys(file)];
833
+ }
834
+ function changedFilePaths(file) {
835
+ return file.status === "renamed" && file.oldPath
836
+ ? [file.path, file.oldPath]
837
+ : [file.path];
838
+ }
839
+ function primaryChangeReason(impact) {
840
+ return (impact?.reasons.find((reason) => reason.kind === "source") ??
841
+ impact?.reasons[0]);
842
+ }
843
+ function highestPriorityScope(scopes) {
844
+ return ([...scopes].sort((left, right) => scopeSort(left) - scopeSort(right))[0] ??
845
+ "outside");
846
+ }
847
+ function isSourceFile(file) {
848
+ return /\.(?:c|m)?[jt]sx?$/.test(file) && !file.endsWith(".d.ts");
849
+ }
850
+ function isDocumentationPath(file, roots) {
851
+ const normalized = normalizePath(file);
852
+ const basename = path.posix.basename(normalized);
853
+ if (/^(?:AGENTS|CLAUDE|README|CHANGELOG|CONTRIBUTING|SECURITY|CODE_OF_CONDUCT|LICENSE|NOTICE)$/i.test(basename) ||
854
+ /^(?:AGENTS|CLAUDE|README|CHANGELOG|CONTRIBUTING|SECURITY|CODE_OF_CONDUCT|LICENSE|NOTICE)(?:\.[a-z0-9_-]+)?\.(?:md|mdx|txt)$/i.test(basename)) {
855
+ return true;
856
+ }
857
+ return (/\.(?:md|mdx|txt)$/i.test(normalized) &&
858
+ roots.some((root) => {
859
+ if (!isWithin(normalized, root))
860
+ return false;
861
+ const relative = root
862
+ ? normalizePath(path.posix.relative(root, normalized))
863
+ : normalized;
864
+ return /^(?:docs|documentation)\//i.test(relative);
865
+ }));
866
+ }
867
+ function appWideChangeReasons(files, isDocumentation) {
868
+ return ["governing", "workspace-dependency"].flatMap((scope) => {
869
+ const scoped = files.filter((file) => file.scope === scope);
870
+ if (scoped.length === 0)
871
+ return [];
872
+ const first = scoped[0];
873
+ const changedFile = changedFilePaths(first).find((file) => !isDocumentation(file)) ??
874
+ first.path;
875
+ const message = scope === "workspace-dependency"
876
+ ? scoped.length === 1
877
+ ? `Direct local dependency ${changedFile} changed.`
878
+ : `${scoped.length} direct local dependency files changed, including ${changedFile}.`
879
+ : scoped.length === 1
880
+ ? `Governing file ${changedFile} changed.`
881
+ : `${scoped.length} governing files changed, including ${changedFile}.`;
882
+ return [{ kind: "app-wide", changedFile, message }];
883
+ });
884
+ }
885
+ function isWithin(file, directory) {
886
+ if (!directory || directory === ".")
887
+ return true;
888
+ return file === directory || file.startsWith(`${directory}/`);
889
+ }
890
+ function isAbsoluteWithin(file, directory) {
891
+ const relative = path.relative(directory, file);
892
+ return (relative === "" ||
893
+ (relative !== ".." && !relative.startsWith(`..${path.sep}`)));
894
+ }
895
+ function assertInsideRepository(targetDir, repositoryRoot) {
896
+ if (!isAbsoluteWithin(targetDir, repositoryRoot)) {
897
+ throw new Error(`App directory ${targetDir} is outside Git repository ${repositoryRoot}.`);
898
+ }
899
+ }
900
+ function pathAncestors(relativePath) {
901
+ const values = [];
902
+ let current = relativePath;
903
+ while (true) {
904
+ values.push(current || ".");
905
+ if (!current || current === ".")
906
+ break;
907
+ const parent = path.posix.dirname(current);
908
+ if (parent === current)
909
+ break;
910
+ current = parent === "." ? "" : parent;
911
+ }
912
+ return values;
913
+ }
914
+ function normalizePath(file) {
915
+ const normalized = file.replaceAll("\\", "/");
916
+ return normalized === "." ? "" : normalized;
917
+ }
918
+ function statusMark(status) {
919
+ return {
920
+ added: "A",
921
+ copied: "C",
922
+ deleted: "D",
923
+ modified: "M",
924
+ renamed: "R",
925
+ "type-changed": "T",
926
+ unmerged: "U",
927
+ unknown: "?",
928
+ untracked: "?",
929
+ }[status];
930
+ }
931
+ function shortId(value) {
932
+ return value.slice(0, 8);
933
+ }
934
+ function shortHash(value) {
935
+ return `${value.slice(0, 15)}…`;
936
+ }
937
+ function terminalText(value) {
938
+ return JSON.stringify(value)
939
+ .slice(1, -1)
940
+ .replace(/[\u007f-\u009f\u2028\u2029\p{Cf}]/gu, (character) => {
941
+ const codePoint = character.codePointAt(0);
942
+ if (codePoint === undefined)
943
+ return "";
944
+ return codePoint <= 0xffff
945
+ ? `\\u${codePoint.toString(16).padStart(4, "0")}`
946
+ : `\\u{${codePoint.toString(16)}}`;
947
+ });
948
+ }
949
+ //# sourceMappingURL=app-map-changes.js.map