@beignet/cli 0.0.40 → 0.0.41

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 (93) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +76 -10
  3. package/dist/analysis/source-index.d.ts +38 -0
  4. package/dist/analysis/source-index.d.ts.map +1 -0
  5. package/dist/analysis/source-index.js +271 -0
  6. package/dist/analysis/source-index.js.map +1 -0
  7. package/dist/analysis/workspace.d.ts +16 -0
  8. package/dist/analysis/workspace.d.ts.map +1 -0
  9. package/dist/analysis/workspace.js +134 -0
  10. package/dist/analysis/workspace.js.map +1 -0
  11. package/dist/app-map-schema.d.ts +5 -0
  12. package/dist/app-map-schema.d.ts.map +1 -0
  13. package/dist/app-map-schema.js +26 -0
  14. package/dist/app-map-schema.js.map +1 -0
  15. package/dist/app-map.d.ts +96 -0
  16. package/dist/app-map.d.ts.map +1 -0
  17. package/dist/app-map.js +1141 -0
  18. package/dist/app-map.js.map +1 -0
  19. package/dist/check.d.ts +7 -0
  20. package/dist/check.d.ts.map +1 -1
  21. package/dist/check.js +20 -5
  22. package/dist/check.js.map +1 -1
  23. package/dist/db.d.ts +32 -7
  24. package/dist/db.d.ts.map +1 -1
  25. package/dist/db.js +182 -14
  26. package/dist/db.js.map +1 -1
  27. package/dist/explain.d.ts +98 -0
  28. package/dist/explain.d.ts.map +1 -0
  29. package/dist/explain.js +512 -0
  30. package/dist/explain.js.map +1 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +79 -2
  33. package/dist/index.js.map +1 -1
  34. package/dist/inspect.js +53 -3
  35. package/dist/inspect.js.map +1 -1
  36. package/dist/lib.d.ts +4 -0
  37. package/dist/lib.d.ts.map +1 -1
  38. package/dist/lib.js +2 -0
  39. package/dist/lib.js.map +1 -1
  40. package/dist/make.d.ts.map +1 -1
  41. package/dist/make.js +63 -2
  42. package/dist/make.js.map +1 -1
  43. package/dist/mcp.d.ts +1 -1
  44. package/dist/mcp.d.ts.map +1 -1
  45. package/dist/mcp.js +69 -1
  46. package/dist/mcp.js.map +1 -1
  47. package/dist/preflight.d.ts.map +1 -1
  48. package/dist/preflight.js +10 -0
  49. package/dist/preflight.js.map +1 -1
  50. package/dist/provider-add.d.ts.map +1 -1
  51. package/dist/provider-add.js +167 -13
  52. package/dist/provider-add.js.map +1 -1
  53. package/dist/provider-audit.d.ts +6 -0
  54. package/dist/provider-audit.d.ts.map +1 -1
  55. package/dist/provider-audit.js +63 -15
  56. package/dist/provider-audit.js.map +1 -1
  57. package/dist/templates/agents.d.ts.map +1 -1
  58. package/dist/templates/agents.js +17 -4
  59. package/dist/templates/agents.js.map +1 -1
  60. package/dist/templates/base.d.ts.map +1 -1
  61. package/dist/templates/base.js +10 -4
  62. package/dist/templates/base.js.map +1 -1
  63. package/dist/templates/index.d.ts +1 -1
  64. package/dist/templates/index.d.ts.map +1 -1
  65. package/dist/templates/index.js +8 -1
  66. package/dist/templates/index.js.map +1 -1
  67. package/dist/templates/server.d.ts +3 -0
  68. package/dist/templates/server.d.ts.map +1 -1
  69. package/dist/templates/server.js +55 -1
  70. package/dist/templates/server.js.map +1 -1
  71. package/dist/templates/shared.js +1 -1
  72. package/package.json +2 -2
  73. package/skills/app-structure/SKILL.md +22 -4
  74. package/src/analysis/source-index.ts +395 -0
  75. package/src/analysis/workspace.ts +180 -0
  76. package/src/app-map-schema.ts +28 -0
  77. package/src/app-map.ts +1705 -0
  78. package/src/check.ts +27 -4
  79. package/src/db.ts +232 -14
  80. package/src/explain.ts +786 -0
  81. package/src/index.ts +113 -2
  82. package/src/inspect.ts +70 -2
  83. package/src/lib.ts +36 -0
  84. package/src/make.ts +90 -2
  85. package/src/mcp.ts +107 -1
  86. package/src/preflight.ts +14 -0
  87. package/src/provider-add.ts +211 -12
  88. package/src/provider-audit.ts +127 -22
  89. package/src/templates/agents.ts +17 -4
  90. package/src/templates/base.ts +22 -4
  91. package/src/templates/index.ts +18 -1
  92. package/src/templates/server.ts +58 -1
  93. package/src/templates/shared.ts +1 -1
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
1
+ import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import {
4
4
  type ProviderPresetName,
@@ -10,6 +10,7 @@ import {
10
10
  loadBeignetConfig,
11
11
  type ResolvedBeignetConfig,
12
12
  } from "./config.js";
13
+ import { aliasModule } from "./make/shared.js";
13
14
  import {
14
15
  appendToArrayExpression,
15
16
  appendToNamedArray,
@@ -64,6 +65,7 @@ type ProviderPreset = {
64
65
  docs: string;
65
66
  nextSteps: string[];
66
67
  conflictingProviderEntries?: string[];
68
+ additionalImports?: string[];
67
69
  };
68
70
 
69
71
  type PlannedFile = {
@@ -169,7 +171,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
169
171
  displayName: "Inngest jobs",
170
172
  importName: "createInngestJobsProvider",
171
173
  importFrom: "@beignet/provider-jobs-inngest",
172
- providerEntry: "createInngestJobsProvider()",
174
+ providerEntry: "createInngestJobsProvider({ client: inngest })",
173
175
  dependencies: (packageJson) => ({
174
176
  "@beignet/provider-jobs-inngest": beignetDependencyVersion(packageJson),
175
177
  inngest: externalVersions.inngest,
@@ -180,11 +182,20 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
180
182
  importName: "JobDispatcherPort",
181
183
  importFrom: "@beignet/core/ports",
182
184
  },
183
- envVars: ["INNGEST_APP_NAME", "INNGEST_EVENT_KEY"],
185
+ envVars: [
186
+ "INNGEST_APP_NAME",
187
+ "INNGEST_DEV",
188
+ "INNGEST_EVENT_KEY",
189
+ "INNGEST_SIGNING_KEY",
190
+ "INNGEST_SIGNING_KEY_FALLBACK",
191
+ ],
184
192
  envExample: [
185
193
  "# Inngest jobs provider",
186
194
  "INNGEST_APP_NAME=beignet-app",
195
+ "INNGEST_DEV=1",
187
196
  "# INNGEST_EVENT_KEY=",
197
+ "# INNGEST_SIGNING_KEY=",
198
+ "# INNGEST_SIGNING_KEY_FALLBACK=",
188
199
  "",
189
200
  ].join("\n"),
190
201
  docs: [
@@ -192,16 +203,16 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
192
203
  "",
193
204
  "- Package: `@beignet/provider-jobs-inngest`",
194
205
  "- Peer dependency: `inngest`",
195
- "- The preset wires `createInngestJobsProvider()` in `server/providers.ts` and defers `jobs` in `infra/port-wiring.ts`.",
206
+ "- The preset creates one shared Inngest client, a central Beignet job registry, and wires that client into `server/providers.ts`.",
196
207
  "- Define jobs with `defineJob(...)` and dispatch them through `ctx.ports.jobs.dispatch(...)`.",
197
- "- Execution needs an Inngest serve route built from `createInngestJobFunction(...)` and `serve(...)`. See the jobs docs page.",
208
+ "- Next.js apps receive `app/api/inngest/route.ts`; other runtimes receive `createAppInngestFunctions(...)` so their host can bind an app-owned service context.",
198
209
  "- Use `ctx.ports.inngest` only as an escape hatch for Inngest-specific workflow APIs; prefer `ctx.ports.jobs.dispatch(...)` for Beignet jobs.",
199
210
  "",
200
211
  ].join("\n"),
201
212
  nextSteps: [
202
213
  "Run your package manager install command.",
203
- "Set INNGEST_APP_NAME and, for Inngest cloud, INNGEST_EVENT_KEY in .env.local or your deployment environment.",
204
- "Expose an Inngest serve route with createInngestJobFunction(...) so dispatched jobs execute. See the jobs docs page.",
214
+ "Use INNGEST_DEV=1 only with the local dev server. Set INNGEST_EVENT_KEY and INNGEST_SIGNING_KEY in production.",
215
+ "For non-Next runtimes, call createAppInngestFunctions(...) after creating the app server, then mount the result with Inngest's host adapter.",
205
216
  "Run beignet provider audit and beignet doctor --strict.",
206
217
  ],
207
218
  conflictingProviderEntries: ["createBullMQJobsProvider"],
@@ -764,7 +775,12 @@ export async function addProviderPreset(
764
775
  const config = await loadBeignetConfig(targetDir);
765
776
  const readSource: ProviderSourceReader = (filePath) =>
766
777
  readOptionalAppFile(targetDir, filePath);
767
- const filesToPlan = await providerPresetFiles(preset, config, readSource);
778
+ const filesToPlan = await providerPresetFiles(
779
+ preset,
780
+ config,
781
+ readSource,
782
+ targetDir,
783
+ );
768
784
  const plannedFiles = await planFiles(filesToPlan, readSource);
769
785
 
770
786
  if (!options.dryRun) {
@@ -802,7 +818,12 @@ export async function planProviderPresetsForCreate(options: {
802
818
 
803
819
  for (const name of options.names) {
804
820
  const preset = presets[name];
805
- const filesToPlan = await providerPresetFiles(preset, config, readSource);
821
+ const filesToPlan = await providerPresetFiles(
822
+ preset,
823
+ config,
824
+ readSource,
825
+ options.targetDir,
826
+ );
806
827
  const plannedFiles = await planFiles(filesToPlan, readSource);
807
828
 
808
829
  for (const file of plannedFiles) {
@@ -827,6 +848,7 @@ async function providerPresetFiles(
827
848
  preset: ProviderPreset,
828
849
  config: ResolvedBeignetConfig,
829
850
  readSource: ProviderSourceReader,
851
+ targetDir: string,
830
852
  ): Promise<ProviderPlanFile[]> {
831
853
  const packageJsonPath = "package.json";
832
854
  const providersPath = serverProvidersPath(config);
@@ -840,6 +862,20 @@ async function providerPresetFiles(
840
862
  await readSource(".env.example"),
841
863
  preset,
842
864
  );
865
+ const effectivePreset =
866
+ preset.name === "jobs-inngest"
867
+ ? {
868
+ ...preset,
869
+ additionalImports: [
870
+ `import { inngest } from "${aliasModule(
871
+ path.posix.join(
872
+ directoryPath(path.posix.dirname(config.paths.portWiring)),
873
+ "inngest.ts",
874
+ ),
875
+ )}";`,
876
+ ],
877
+ }
878
+ : preset;
843
879
  const files = [
844
880
  {
845
881
  path: packageJsonPath,
@@ -849,7 +885,7 @@ async function providerPresetFiles(
849
885
  path: providersPath,
850
886
  content: updateServerProviders(
851
887
  await readRequiredSource(readSource, providersPath),
852
- preset,
888
+ effectivePreset,
853
889
  ),
854
890
  },
855
891
  {
@@ -879,9 +915,167 @@ async function providerPresetFiles(
879
915
  });
880
916
  }
881
917
 
918
+ if (preset.name === "jobs-inngest") {
919
+ files.push(
920
+ ...(await inngestProviderFiles({ targetDir, config, readSource })),
921
+ );
922
+ }
923
+
924
+ return files;
925
+ }
926
+
927
+ async function inngestProviderFiles(options: {
928
+ targetDir: string;
929
+ config: ResolvedBeignetConfig;
930
+ readSource: ProviderSourceReader;
931
+ }): Promise<ProviderPlanFile[]> {
932
+ const infrastructurePath = directoryPath(
933
+ path.posix.dirname(options.config.paths.portWiring),
934
+ );
935
+ const serverPath = directoryPath(
936
+ path.posix.dirname(options.config.paths.server),
937
+ );
938
+ const clientPath = path.posix.join(infrastructurePath, "inngest.ts");
939
+ const registryPath = path.posix.join(serverPath, "inngest.ts");
940
+ const jobRegistries = await discoverJobRegistries(
941
+ options.targetDir,
942
+ options.config,
943
+ );
944
+ const imports = jobRegistries
945
+ .map(({ name, file }) => `import { ${name} } from "${aliasModule(file)}";`)
946
+ .join("\n");
947
+ const entries = jobRegistries.map(({ name }) => `\t...${name},`).join("\n");
948
+
949
+ const files: ProviderPlanFile[] = [];
950
+ if ((await options.readSource(clientPath)) === undefined) {
951
+ files.push({
952
+ path: clientPath,
953
+ content: `import { Inngest } from "inngest";
954
+
955
+ /** Shared by the Beignet jobs provider and the Inngest serve endpoint. */
956
+ export const inngest = new Inngest({
957
+ \tid: process.env.INNGEST_APP_NAME ?? "beignet-app",
958
+ });
959
+ `,
960
+ });
961
+ }
962
+ if ((await options.readSource(registryPath)) === undefined) {
963
+ files.push({
964
+ path: registryPath,
965
+ content:
966
+ options.config.framework === "next"
967
+ ? `import { createServiceActor } from "@beignet/core/ports";
968
+ import { createInngestJobFunctions } from "@beignet/provider-jobs-inngest";
969
+ import type { AppContext } from "${aliasModule(options.config.paths.appContext)}";
970
+ import { inngest } from "${aliasModule(clientPath)}";
971
+ ${imports ? `${imports}\n` : ""}import { getServer } from "${aliasModule(options.config.paths.server)}";
972
+
973
+ /** Central registry updated by \`beignet make job\`. */
974
+ export const inngestJobs = [
975
+ ${entries}
976
+ ] as const;
977
+
978
+ export const inngestFunctions = createInngestJobFunctions<AppContext>({
979
+ \tclient: inngest,
980
+ \tjobs: inngestJobs,
981
+ \tctx: async () => {
982
+ \t\tconst server = await getServer();
983
+ \t\treturn server.createServiceContext({
984
+ \t\t\tactor: createServiceActor("beignet-inngest"),
985
+ \t\t});
986
+ \t},
987
+ \tinstrumentation: async () => (await getServer()).ports,
988
+ \terrorReporter: async () => (await getServer()).ports.errorReporter,
989
+ });
990
+ `
991
+ : `import {
992
+ \tcreateInngestJobFunctions,
993
+ \ttype CreateInngestJobFunctionsOptions,
994
+ } from "@beignet/provider-jobs-inngest";
995
+ import type { AppContext } from "${aliasModule(options.config.paths.appContext)}";
996
+ import { inngest } from "${aliasModule(clientPath)}";
997
+ ${imports ? `${imports}\n` : ""}
998
+ /** Central registry updated by \`beignet make job\`. */
999
+ export const inngestJobs = [
1000
+ ${entries}
1001
+ ] as const;
1002
+
1003
+ export type AppInngestFunctionOptions = Omit<
1004
+ \tCreateInngestJobFunctionsOptions<AppContext>,
1005
+ \t"client" | "jobs" | "ctx"
1006
+ > & {
1007
+ \tctx: NonNullable<CreateInngestJobFunctionsOptions<AppContext>["ctx"]>;
1008
+ };
1009
+
1010
+ /** Bind the app-owned runtime context when mounting an Inngest host adapter. */
1011
+ export function createAppInngestFunctions(
1012
+ \toptions: AppInngestFunctionOptions,
1013
+ ) {
1014
+ \treturn createInngestJobFunctions<AppContext>({
1015
+ \t\t...options,
1016
+ \t\tclient: inngest,
1017
+ \t\tjobs: inngestJobs,
1018
+ \t});
1019
+ }
1020
+ `,
1021
+ });
1022
+ }
1023
+ if (options.config.framework === "next") {
1024
+ const routePath = path.posix.join(
1025
+ directoryPath(options.config.paths.routes),
1026
+ "inngest/route.ts",
1027
+ );
1028
+ if ((await options.readSource(routePath)) === undefined) {
1029
+ files.push({
1030
+ path: routePath,
1031
+ content: `import { serve } from "inngest/next";
1032
+ import { inngest } from "${aliasModule(clientPath)}";
1033
+ import { inngestFunctions } from "${aliasModule(registryPath)}";
1034
+
1035
+ export const { GET, POST, PUT } = serve({
1036
+ \tclient: inngest,
1037
+ \tfunctions: inngestFunctions,
1038
+ });
1039
+ `,
1040
+ });
1041
+ }
1042
+ }
1043
+
882
1044
  return files;
883
1045
  }
884
1046
 
1047
+ async function discoverJobRegistries(
1048
+ targetDir: string,
1049
+ config: ResolvedBeignetConfig,
1050
+ ): Promise<Array<{ name: string; file: string }>> {
1051
+ const featuresDir = path.join(
1052
+ targetDir,
1053
+ directoryPath(config.paths.features),
1054
+ );
1055
+ let entries: string[];
1056
+ try {
1057
+ entries = await readdir(featuresDir, { recursive: true, encoding: "utf8" });
1058
+ } catch {
1059
+ return [];
1060
+ }
1061
+
1062
+ const registries: Array<{ name: string; file: string }> = [];
1063
+ for (const entry of entries) {
1064
+ const normalized = entry.replaceAll(path.sep, "/");
1065
+ if (!normalized.endsWith("/jobs/index.ts")) continue;
1066
+ const absolutePath = path.join(featuresDir, entry);
1067
+ const source = await readFile(absolutePath, "utf8");
1068
+ const match = source.match(/export const\s+([A-Za-z_$][\w$]*Jobs)\s*=/);
1069
+ if (!match?.[1]) continue;
1070
+ registries.push({
1071
+ name: match[1],
1072
+ file: path.posix.join(directoryPath(config.paths.features), normalized),
1073
+ });
1074
+ }
1075
+
1076
+ return registries.sort((left, right) => left.file.localeCompare(right.file));
1077
+ }
1078
+
885
1079
  function providerPresetResult(
886
1080
  preset: ProviderPreset,
887
1081
  targetDir: string,
@@ -982,9 +1176,14 @@ function updatePackageJson(
982
1176
  function updateServerProviders(source: string, preset: ProviderPreset): string {
983
1177
  assertNoConflictingProviders(source, preset);
984
1178
 
1179
+ let next = source;
1180
+ for (const importLine of preset.additionalImports ?? []) {
1181
+ if (!next.includes(importLine)) next = insertAfterImports(next, importLine);
1182
+ }
1183
+
985
1184
  const importLine = `import { ${preset.importName} } from "${preset.importFrom}";`;
986
1185
  const result = appendToNamedArray(
987
- source,
1186
+ next,
988
1187
  "providers",
989
1188
  preset.providerEntry,
990
1189
  hasNamedImport(source, preset.importName, preset.importFrom)
@@ -998,7 +1197,7 @@ function updateServerProviders(source: string, preset: ProviderPreset): string {
998
1197
  );
999
1198
  }
1000
1199
 
1001
- return result.kind === "updated" ? result.source : source;
1200
+ return result.kind === "updated" ? result.source : next;
1002
1201
  }
1003
1202
 
1004
1203
  function assertNoConflictingProviders(
@@ -59,6 +59,12 @@ export type ProviderRegistrationStatus =
59
59
  | "optional-missing"
60
60
  | "not-required";
61
61
 
62
+ export type ProviderRegistrationSource = {
63
+ file: string;
64
+ line: number;
65
+ column: number;
66
+ };
67
+
62
68
  export type ProviderMetadataAudit =
63
69
  | {
64
70
  status: "valid";
@@ -102,6 +108,7 @@ export type ProviderAuditEntry = {
102
108
  declared: boolean;
103
109
  metadata: ProviderMetadataAudit;
104
110
  registration: ProviderRegistrationStatus;
111
+ registrationSources: ProviderRegistrationSource[];
105
112
  requiredEnv: ProviderRequirementAudit;
106
113
  requiredTables: ProviderRequirementAudit;
107
114
  appPorts: ProviderAppPortsAudit;
@@ -138,12 +145,15 @@ export async function auditProviders(
138
145
  const packageJson = await readPackageJson(targetDir, files);
139
146
  const packageNames = await providerAuditPackageNames(targetDir, packageJson);
140
147
  const sourceCache = new Map<string, string>();
141
- const providerEntries = await readProviderListEntries(
148
+ const providerEntryRecords = await readProviderListEntryRecords(
142
149
  targetDir,
143
150
  files,
144
151
  config,
145
152
  sourceCache,
146
153
  );
154
+ const providerEntries = providerEntryRecords.flatMap(
155
+ (entry) => entry.searchableText,
156
+ );
147
157
  const configFiles = providerOperationalConfigFiles(files, config);
148
158
  const portsSource = files.includes(config.paths.ports)
149
159
  ? await readCachedSource(targetDir, config.paths.ports, sourceCache)
@@ -191,6 +201,7 @@ export async function auditProviders(
191
201
  sourceCache,
192
202
  portsSource,
193
203
  providerEntries,
204
+ providerEntryRecords,
194
205
  sourceFile: source.file,
195
206
  packageJson,
196
207
  rule: providerDoctorRuleFromMetadata(packageName, parsed.metadata),
@@ -356,20 +367,47 @@ export async function readProviderListEntries(
356
367
  config: ResolvedBeignetConfig,
357
368
  sourceCache: Map<string, string>,
358
369
  ): Promise<string[]> {
370
+ const records = await readProviderListEntryRecords(
371
+ targetDir,
372
+ files,
373
+ config,
374
+ sourceCache,
375
+ );
376
+ return records.flatMap((entry) => entry.searchableText);
377
+ }
378
+
379
+ type ProviderListEntryRecord = {
380
+ searchableText: string[];
381
+ source: ProviderRegistrationSource;
382
+ };
383
+
384
+ async function readProviderListEntryRecords(
385
+ targetDir: string,
386
+ files: string[],
387
+ config: ResolvedBeignetConfig,
388
+ sourceCache: Map<string, string>,
389
+ ): Promise<ProviderListEntryRecord[]> {
359
390
  const providerFiles = serverProviderFiles(files, config);
360
- const providerSource = (
361
- await Promise.all(
362
- providerFiles.map((file) =>
363
- readCachedSource(targetDir, file, sourceCache),
364
- ),
365
- )
366
- ).join("\n");
367
- const providerListSource = extractProviderListSource(providerSource);
368
- const entries =
369
- providerListSource === undefined
370
- ? []
371
- : providerListEntries(providerListSource);
372
- return expandProviderEntryIdentifiers(entries, providerSource);
391
+ const records: ProviderListEntryRecord[] = [];
392
+ for (const file of providerFiles) {
393
+ const providerSource = await readCachedSource(targetDir, file, sourceCache);
394
+ for (const list of extractProviderLists(providerSource)) {
395
+ for (const entry of providerListEntries(list.source)) {
396
+ records.push({
397
+ searchableText: expandProviderEntryIdentifiers(
398
+ [entry.source],
399
+ providerSource,
400
+ ),
401
+ source: sourceLocation(
402
+ file,
403
+ providerSource,
404
+ list.start + entry.start,
405
+ ),
406
+ });
407
+ }
408
+ }
409
+ }
410
+ return records;
373
411
  }
374
412
 
375
413
  /**
@@ -1128,6 +1166,7 @@ async function providerAuditEntryForRule(options: {
1128
1166
  sourceCache: Map<string, string>;
1129
1167
  portsSource: string;
1130
1168
  providerEntries: string[];
1169
+ providerEntryRecords: ProviderListEntryRecord[];
1131
1170
  sourceFile: string;
1132
1171
  packageJson: ProviderPackageJson | undefined;
1133
1172
  rule: ProviderDoctorRule;
@@ -1201,6 +1240,9 @@ async function providerAuditEntryForRule(options: {
1201
1240
  requiredTables,
1202
1241
  appPorts,
1203
1242
  });
1243
+ const registrationTokens =
1244
+ options.rule.variants?.flatMap((variant) => variant.tokens) ??
1245
+ options.rule.tokens;
1204
1246
 
1205
1247
  return {
1206
1248
  packageName: options.rule.packageName,
@@ -1214,6 +1256,13 @@ async function providerAuditEntryForRule(options: {
1214
1256
  issues: [],
1215
1257
  },
1216
1258
  registration,
1259
+ registrationSources:
1260
+ registration === "registered"
1261
+ ? registrationSourcesForTokens(
1262
+ registrationTokens,
1263
+ options.providerEntryRecords,
1264
+ )
1265
+ : [],
1217
1266
  requiredEnv,
1218
1267
  requiredTables,
1219
1268
  appPorts,
@@ -1294,6 +1343,7 @@ function providerAuditEntryForMissingMetadata(
1294
1343
  declared: installedPackageNames(packageJson).has(packageName),
1295
1344
  metadata: { status: "missing", issues: [] },
1296
1345
  registration: "not-required",
1346
+ registrationSources: [],
1297
1347
  requiredEnv: emptyRequirement(),
1298
1348
  requiredTables: emptyRequirement(),
1299
1349
  appPorts: emptyAppPorts(),
@@ -1319,6 +1369,7 @@ function providerAuditEntryForInvalidMetadata(
1319
1369
  issues: [...issues],
1320
1370
  },
1321
1371
  registration: "not-required",
1372
+ registrationSources: [],
1322
1373
  requiredEnv: emptyRequirement(),
1323
1374
  requiredTables: emptyRequirement(),
1324
1375
  appPorts: emptyAppPorts(),
@@ -1345,6 +1396,28 @@ function registrationAudit(
1345
1396
  return "not-required";
1346
1397
  }
1347
1398
 
1399
+ function registrationSourcesForTokens(
1400
+ tokens: readonly string[],
1401
+ entries: readonly ProviderListEntryRecord[],
1402
+ ): ProviderRegistrationSource[] {
1403
+ const sources = entries
1404
+ .filter((entry) =>
1405
+ tokens.some((token) =>
1406
+ entry.searchableText.some((text) => text.includes(token)),
1407
+ ),
1408
+ )
1409
+ .map((entry) => entry.source);
1410
+ return sources.filter(
1411
+ (source, index) =>
1412
+ sources.findIndex(
1413
+ (candidate) =>
1414
+ candidate.file === source.file &&
1415
+ candidate.line === source.line &&
1416
+ candidate.column === source.column,
1417
+ ) === index,
1418
+ );
1419
+ }
1420
+
1348
1421
  function variantRegistrationAudit(
1349
1422
  variants: readonly ProviderDoctorVariantRule[],
1350
1423
  detectedVariants: readonly ProviderDoctorVariantRule[],
@@ -1572,18 +1645,25 @@ export function serverProviderFiles(
1572
1645
  return candidates.filter((file) => files.includes(file));
1573
1646
  }
1574
1647
 
1575
- function extractProviderListSource(source: string): string | undefined {
1648
+ function extractProviderLists(
1649
+ source: string,
1650
+ ): { source: string; start: number }[] {
1576
1651
  const listMatches = [
1577
1652
  ...source.matchAll(/\bproviders\s*=\s*\[([\s\S]*?)\]\s*(?:;|as const)/g),
1578
1653
  ...source.matchAll(/\bproviders\s*:\s*\[([\s\S]*?)\]/g),
1579
- ];
1580
- if (listMatches.length === 0) return undefined;
1581
- return listMatches.map((match) => match[1]).join("\n");
1654
+ ].sort((left, right) => (left.index ?? 0) - (right.index ?? 0));
1655
+ return listMatches.map((match) => ({
1656
+ source: match[1] ?? "",
1657
+ start: (match.index ?? 0) + match[0].indexOf("[") + 1,
1658
+ }));
1582
1659
  }
1583
1660
 
1584
- function providerListEntries(source: string): string[] {
1585
- const entries: string[] = [];
1661
+ function providerListEntries(
1662
+ source: string,
1663
+ ): { source: string; start: number }[] {
1664
+ const entries: { source: string; start: number }[] = [];
1586
1665
  let current = "";
1666
+ let currentStart: number | undefined;
1587
1667
  let depth = 0;
1588
1668
  let quote: '"' | "'" | "`" | undefined;
1589
1669
  let escaped = false;
@@ -1611,6 +1691,9 @@ function providerListEntries(source: string): string[] {
1611
1691
  }
1612
1692
 
1613
1693
  if (quote) {
1694
+ if (currentStart === undefined && !/\s/.test(char)) {
1695
+ currentStart = index;
1696
+ }
1614
1697
  current += char;
1615
1698
  if (escaped) {
1616
1699
  escaped = false;
@@ -1640,10 +1723,15 @@ function providerListEntries(source: string): string[] {
1640
1723
 
1641
1724
  if (char === '"' || char === "'" || char === "`") {
1642
1725
  quote = char;
1726
+ currentStart ??= index;
1643
1727
  current += char;
1644
1728
  continue;
1645
1729
  }
1646
1730
 
1731
+ if (currentStart === undefined && !/\s/.test(char)) {
1732
+ currentStart = index;
1733
+ }
1734
+
1647
1735
  if (char === "(" || char === "[" || char === "{") {
1648
1736
  depth += 1;
1649
1737
  } else if (char === ")" || char === "]" || char === "}") {
@@ -1652,8 +1740,9 @@ function providerListEntries(source: string): string[] {
1652
1740
 
1653
1741
  if (char === "," && depth === 0) {
1654
1742
  const entry = current.trim();
1655
- if (entry) entries.push(entry);
1743
+ if (entry) entries.push({ source: entry, start: currentStart ?? index });
1656
1744
  current = "";
1745
+ currentStart = undefined;
1657
1746
  continue;
1658
1747
  }
1659
1748
 
@@ -1661,10 +1750,26 @@ function providerListEntries(source: string): string[] {
1661
1750
  }
1662
1751
 
1663
1752
  const entry = current.trim();
1664
- if (entry) entries.push(entry);
1753
+ if (entry) {
1754
+ entries.push({ source: entry, start: currentStart ?? source.length });
1755
+ }
1665
1756
  return entries;
1666
1757
  }
1667
1758
 
1759
+ function sourceLocation(
1760
+ file: string,
1761
+ source: string,
1762
+ index: number,
1763
+ ): ProviderRegistrationSource {
1764
+ const prefix = source.slice(0, index);
1765
+ const lastNewline = prefix.lastIndexOf("\n");
1766
+ return {
1767
+ file,
1768
+ line: prefix.split("\n").length,
1769
+ column: index - lastNewline,
1770
+ };
1771
+ }
1772
+
1668
1773
  function metadataSourcePath(targetDir: string, file: string): string {
1669
1774
  const targetRelative = normalizePath(path.relative(targetDir, file));
1670
1775
  if (!targetRelative.startsWith("../")) return targetRelative;
@@ -107,6 +107,10 @@ formatting. The individual commands (\`${lint}\`, \`${cli} lint\`,
107
107
  \`${cli} doctor --strict\`, \`${test}\`, \`${typecheck}\`) still work when you
108
108
  need one check alone.
109
109
 
110
+ Before exploring unfamiliar code, run \`${cli} map\` for the human inventory
111
+ or \`${cli} map --json --feature <name>\` for an evidence-backed graph scoped
112
+ to one feature.
113
+
110
114
  ## Package skills
111
115
 
112
116
  This app trusts Beignet's package-shipped agent skills through
@@ -142,10 +146,19 @@ skill-loading block.
142
146
  ## MCP server
143
147
 
144
148
  \`.mcp.json\` registers the app-local \`@beignet/cli\` bin at
145
- \`./node_modules/.bin/beignet mcp\`, which exposes routes/doctor/lint/make/provider_add as
146
- structured tools named exactly: \`routes\`, \`doctor\`, \`doctor_fix\`,
147
- \`lint\`, \`make\`, \`provider_add\`. Clients that do not read \`.mcp.json\` can use the same
148
- command from the app root; use \`${cli} mcp\` only for terminal debugging.
149
+ \`./node_modules/.bin/beignet mcp\`, which exposes the app map, validation, and
150
+ generators as structured tools named exactly: \`app_map\`, \`explain\`,
151
+ \`check\`, \`routes\`, \`doctor\`, \`doctor_fix\`, \`lint\`, \`make\`,
152
+ \`provider_add\`. Use \`app_map\` before broad code search and pass a feature
153
+ or node kinds to keep context focused. Use \`explain\` for source-backed
154
+ relationships, conventions, findings, and suggested files for one feature,
155
+ route, provider, or diagnostic. Use \`check\` after edits to run the complete
156
+ validation loop; it returns the same versioned result as
157
+ \`${cli} check --json\`, bounds failure output, and cancels the active package
158
+ script when the MCP request is cancelled. It does not apply Beignet fixes, but
159
+ app-owned scripts retain their normal side effects. Clients that do not read
160
+ \`.mcp.json\` can use the same command from the app root; use \`${cli} mcp\`
161
+ only for terminal debugging.
149
162
  `;
150
163
  }
151
164