@beignet/cli 0.0.50 → 0.0.52

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 (105) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +64 -22
  3. package/dist/analysis/layers.d.ts.map +1 -1
  4. package/dist/analysis/layers.js +4 -2
  5. package/dist/analysis/layers.js.map +1 -1
  6. package/dist/analysis/port-wiring.d.ts.map +1 -1
  7. package/dist/analysis/port-wiring.js +2 -1
  8. package/dist/analysis/port-wiring.js.map +1 -1
  9. package/dist/analysis/source-files.d.ts +15 -0
  10. package/dist/analysis/source-files.d.ts.map +1 -0
  11. package/dist/analysis/source-files.js +52 -0
  12. package/dist/analysis/source-files.js.map +1 -0
  13. package/dist/analysis/workspace.d.ts.map +1 -1
  14. package/dist/analysis/workspace.js +21 -25
  15. package/dist/analysis/workspace.js.map +1 -1
  16. package/dist/app-map-changes.d.ts.map +1 -1
  17. package/dist/app-map-changes.js +5 -7
  18. package/dist/app-map-changes.js.map +1 -1
  19. package/dist/app-map.d.ts.map +1 -1
  20. package/dist/app-map.js +33 -30
  21. package/dist/app-map.js.map +1 -1
  22. package/dist/check.d.ts +12 -0
  23. package/dist/check.d.ts.map +1 -1
  24. package/dist/check.js +92 -25
  25. package/dist/check.js.map +1 -1
  26. package/dist/git-changes.d.ts +17 -1
  27. package/dist/git-changes.d.ts.map +1 -1
  28. package/dist/git-changes.js +118 -84
  29. package/dist/git-changes.js.map +1 -1
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +35 -6
  33. package/dist/index.js.map +1 -1
  34. package/dist/inspect.d.ts.map +1 -1
  35. package/dist/inspect.js +436 -112
  36. package/dist/inspect.js.map +1 -1
  37. package/dist/lint.d.ts.map +1 -1
  38. package/dist/lint.js +3 -6
  39. package/dist/lint.js.map +1 -1
  40. package/dist/make.d.ts.map +1 -1
  41. package/dist/make.js +0 -2
  42. package/dist/make.js.map +1 -1
  43. package/dist/mcp.d.ts.map +1 -1
  44. package/dist/mcp.js +23 -5
  45. package/dist/mcp.js.map +1 -1
  46. package/dist/operational-process.d.ts +1 -0
  47. package/dist/operational-process.d.ts.map +1 -1
  48. package/dist/operational-process.js.map +1 -1
  49. package/dist/operational-runner.js +1 -0
  50. package/dist/operational-runner.js.map +1 -1
  51. package/dist/outbox.d.ts +1 -0
  52. package/dist/outbox.d.ts.map +1 -1
  53. package/dist/outbox.js +58 -12
  54. package/dist/outbox.js.map +1 -1
  55. package/dist/templates/agents.d.ts.map +1 -1
  56. package/dist/templates/agents.js +43 -15
  57. package/dist/templates/agents.js.map +1 -1
  58. package/dist/templates/base.js +5 -5
  59. package/dist/templates/db/mysql.d.ts.map +1 -1
  60. package/dist/templates/db/mysql.js +51 -22
  61. package/dist/templates/db/mysql.js.map +1 -1
  62. package/dist/templates/db/postgres.d.ts.map +1 -1
  63. package/dist/templates/db/postgres.js +60 -20
  64. package/dist/templates/db/postgres.js.map +1 -1
  65. package/dist/templates/db/sqlite.d.ts.map +1 -1
  66. package/dist/templates/db/sqlite.js +51 -23
  67. package/dist/templates/db/sqlite.js.map +1 -1
  68. package/dist/templates/shadcn.js +1 -1
  69. package/dist/templates/shadcn.js.map +1 -1
  70. package/dist/templates/shared.js +4 -4
  71. package/dist/templates/shared.js.map +1 -1
  72. package/dist/templates/testing.d.ts.map +1 -1
  73. package/dist/templates/testing.js +7 -19
  74. package/dist/templates/testing.js.map +1 -1
  75. package/dist/test-discovery.d.ts +9 -0
  76. package/dist/test-discovery.d.ts.map +1 -0
  77. package/dist/test-discovery.js +40 -0
  78. package/dist/test-discovery.js.map +1 -0
  79. package/package.json +7 -5
  80. package/skills/app-structure/SKILL.md +37 -10
  81. package/src/analysis/layers.ts +4 -2
  82. package/src/analysis/port-wiring.ts +2 -1
  83. package/src/analysis/source-files.ts +61 -0
  84. package/src/analysis/workspace.ts +27 -31
  85. package/src/app-map-changes.ts +10 -8
  86. package/src/app-map.ts +43 -32
  87. package/src/check.ts +115 -25
  88. package/src/git-changes.ts +218 -86
  89. package/src/index.ts +53 -8
  90. package/src/inspect.ts +627 -146
  91. package/src/lint.ts +6 -5
  92. package/src/make.ts +0 -2
  93. package/src/mcp.ts +34 -11
  94. package/src/operational-process.ts +1 -0
  95. package/src/operational-runner.ts +1 -0
  96. package/src/outbox.ts +63 -12
  97. package/src/templates/agents.ts +43 -15
  98. package/src/templates/base.ts +5 -5
  99. package/src/templates/db/mysql.ts +51 -22
  100. package/src/templates/db/postgres.ts +60 -20
  101. package/src/templates/db/sqlite.ts +51 -23
  102. package/src/templates/shadcn.ts +1 -1
  103. package/src/templates/shared.ts +4 -4
  104. package/src/templates/testing.ts +12 -19
  105. package/src/test-discovery.ts +53 -0
package/src/lint.ts CHANGED
@@ -6,6 +6,10 @@ import {
6
6
  classifySourcePath as classifyPath,
7
7
  type SourceLayer,
8
8
  } from "./analysis/layers.js";
9
+ import {
10
+ isAnalysisSourceFile,
11
+ stripSourceFileExtension,
12
+ } from "./analysis/source-files.js";
9
13
  import {
10
14
  type ModuleReference,
11
15
  moduleReferences,
@@ -202,10 +206,7 @@ export function formatLintGithub(result: LintAppResult): string {
202
206
  }
203
207
 
204
208
  function lintSourceFiles(files: string[]): string[] {
205
- return files.filter((file) => {
206
- if (/\.d\.(c|m)?ts$/.test(file)) return false;
207
- return /\.(c|m)?tsx?$/.test(file);
208
- });
209
+ return files.filter(isAnalysisSourceFile);
209
210
  }
210
211
 
211
212
  async function parseImports(
@@ -237,7 +238,7 @@ function packageName(importPath: string): string {
237
238
  }
238
239
 
239
240
  function stripKnownExtension(filePath: string): string {
240
- return filePath.replace(/\.(c|m)?tsx?$/, "");
241
+ return stripSourceFileExtension(filePath);
241
242
  }
242
243
 
243
244
  function lintImport(
package/src/make.ts CHANGED
@@ -2225,7 +2225,6 @@ async function updateOutboxDrainProvider(
2225
2225
  \t\t\tdefer: after,
2226
2226
  \t\t\tcreateContext: () => createServiceContext(undefined),
2227
2227
  \t\t\tregistry: async () => (await import("${outboxModule}")).outboxRegistry,
2228
- \t\t\tbatchSize: 100,
2229
2228
  \t\t});
2230
2229
 
2231
2230
  \t\treturn {
@@ -5921,7 +5920,6 @@ export const { GET, POST } = createOutboxDrainRoute({
5921
5920
  \tserver: getServer,
5922
5921
  \tregistry: outboxRegistry,
5923
5922
  \tsecret: env.CRON_SECRET,
5924
- \tbatchSize: 100,
5925
5923
  });
5926
5924
  `;
5927
5925
  }
package/src/mcp.ts CHANGED
@@ -285,6 +285,7 @@ const outboxRunInputSchema = z.discriminatedUnion("operation", [
285
285
  z.object({
286
286
  operation: z.literal("drain"),
287
287
  batchSize: z.number().int().positive().optional(),
288
+ concurrency: z.number().int().positive().optional(),
288
289
  module: operationalModuleSchema,
289
290
  timeoutMs: operationalTimeoutSchema,
290
291
  }),
@@ -349,17 +350,36 @@ function jsonResult(value: unknown): McpToolResult {
349
350
  };
350
351
  }
351
352
 
352
- function operationalJsonResult(value: unknown): McpToolResult {
353
+ function operationalJsonResult(
354
+ value: unknown,
355
+ options: { isError?: boolean } = {},
356
+ ): McpToolResult {
353
357
  const formatted = JSON.stringify(value, null, 2);
354
358
  const text =
355
359
  Buffer.byteLength(formatted) <= defaultOperationalResultMaxBytes
356
360
  ? formatted
357
361
  : JSON.stringify(value);
358
362
  return {
363
+ ...(options.isError ? { isError: true } : {}),
359
364
  content: [{ type: "text", text }],
360
365
  };
361
366
  }
362
367
 
368
+ function isUncertainOutboxDrainReport(value: unknown): boolean {
369
+ if (typeof value !== "object" || value === null || !("result" in value)) {
370
+ return false;
371
+ }
372
+ const result = value.result;
373
+ if (typeof result !== "object" || result === null) return false;
374
+ const settlementFailed =
375
+ "settlementFailed" in result ? result.settlementFailed : undefined;
376
+ const leaseLost = "leaseLost" in result ? result.leaseLost : undefined;
377
+ return (
378
+ (typeof settlementFailed === "number" && settlementFailed > 0) ||
379
+ (typeof leaseLost === "number" && leaseLost > 0)
380
+ );
381
+ }
382
+
363
383
  function errorResult(error: unknown): McpToolResult {
364
384
  return {
365
385
  isError: true,
@@ -667,7 +687,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
667
687
  "check",
668
688
  {
669
689
  description:
670
- "Run the full Beignet app validation loop: Beignet lint, strict doctor, optional disconnected or connected preflight, and the app's lint, typecheck, and test scripts. Returns the same versioned JSON as beignet check --json, including every step, bounded failure output, durations, and overall ok status. Does not apply Beignet fixes, but app-owned scripts run with their normal side effects.",
690
+ "Run the full Beignet app validation loop: Beignet lint, strict doctor, optional disconnected or connected preflight, and the app's lint, typecheck, and test scripts. Returns the same versioned JSON as beignet check --json, including every step, bounded failure output, durations, source-state provenance, and overall ok status. An ending fingerprint that differs from the baseline makes the result stale and unsuccessful. Does not apply Beignet fixes, but app-owned scripts run with their normal side effects.",
671
691
  inputSchema: {
672
692
  preflight: z
673
693
  .boolean()
@@ -972,7 +992,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
972
992
  "outbox_run",
973
993
  {
974
994
  description:
975
- "Run a state-changing outbox drain, requeue, purge, or prune operation and return exactly the matching versioned beignet outbox <operation> --json report. Purge and prune support dryRun. Runs in an isolated process with bounded structured output, cancellation, and a timeout.",
995
+ "Run a state-changing outbox drain, requeue, purge, or prune operation and return exactly the matching versioned beignet outbox <operation> --json report. Drain supports bounded unordered concurrency and returns an error-bearing report when settlement or lease state is uncertain. Purge and prune support dryRun. Runs in an isolated process with bounded structured output, cancellation, and a timeout.",
976
996
  inputSchema: outboxRunInputSchema,
977
997
  annotations: {
978
998
  readOnlyHint: false,
@@ -988,6 +1008,7 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
988
1008
  ? {
989
1009
  command: "outbox:drain" as const,
990
1010
  batchSize: input.batchSize,
1011
+ concurrency: input.concurrency,
991
1012
  modulePath: input.module,
992
1013
  }
993
1014
  : input.operation === "requeue"
@@ -1014,14 +1035,16 @@ export function buildBeignetMcpServer(options: McpServerOptions): McpServer {
1014
1035
  dryRun: input.dryRun,
1015
1036
  modulePath: input.module,
1016
1037
  };
1017
- return operationalJsonResult(
1018
- await runIsolatedOperationalCommand({
1019
- cwd,
1020
- request,
1021
- signal: ctx.mcpReq.signal,
1022
- timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
1023
- }),
1024
- );
1038
+ const result = await runIsolatedOperationalCommand({
1039
+ cwd,
1040
+ request,
1041
+ signal: ctx.mcpReq.signal,
1042
+ timeoutMs: input.timeoutMs ?? defaultOperationalCommandTimeoutMs,
1043
+ });
1044
+ return operationalJsonResult(result, {
1045
+ isError:
1046
+ input.operation === "drain" && isUncertainOutboxDrainReport(result),
1047
+ });
1025
1048
  }),
1026
1049
  );
1027
1050
 
@@ -44,6 +44,7 @@ export type OperationalCommandRequest =
44
44
  | {
45
45
  command: "outbox:drain";
46
46
  batchSize?: number;
47
+ concurrency?: number;
47
48
  modulePath?: string;
48
49
  }
49
50
  | {
@@ -47,6 +47,7 @@ async function runOperationalRequest(input: OperationalRunnerInput) {
47
47
  return runOutboxDrain({
48
48
  cwd,
49
49
  batchSize: request.batchSize,
50
+ concurrency: request.concurrency,
50
51
  modulePath: request.modulePath,
51
52
  });
52
53
  case "outbox:list":
package/src/outbox.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  OutboxMessageStatus,
11
11
  OutboxRegistry,
12
12
  } from "@beignet/core/outbox";
13
+ import type { TracingPort } from "@beignet/core/tracing";
13
14
  import { createJiti } from "jiti";
14
15
  import { loadBeignetConfig, normalizePath } from "./config.js";
15
16
  import {
@@ -26,6 +27,7 @@ export type RunOutboxDrainOptions = {
26
27
  cwd?: string;
27
28
  modulePath?: string;
28
29
  batchSize?: number;
30
+ concurrency?: number;
29
31
  };
30
32
 
31
33
  /**
@@ -175,6 +177,7 @@ type OutboxDrainPorts = {
175
177
  logger?: OutboxDrainLogger;
176
178
  devtools?: OutboxDrainInstrumentation;
177
179
  instrumentation?: OutboxDrainInstrumentation;
180
+ tracing?: TracingPort;
178
181
  errorReporter?: ErrorReporterPort;
179
182
  };
180
183
 
@@ -187,6 +190,7 @@ type OutboxDrainContext = {
187
190
  type OutboxDrainContextArgs = {
188
191
  registry: OutboxRegistry;
189
192
  batchSize?: number;
193
+ concurrency?: number;
190
194
  };
191
195
 
192
196
  type OutboxAdminOperation = "list" | "show" | "requeue" | "purge" | "prune";
@@ -219,7 +223,11 @@ export async function runOutboxDrain(
219
223
  const config = await loadBeignetConfig(cwd);
220
224
  const modulePath = normalizePath(options.modulePath ?? config.paths.outbox);
221
225
  const batchSize = options.batchSize;
226
+ const concurrency = options.concurrency;
222
227
  if (batchSize !== undefined) assertPositiveInteger("batchSize", batchSize);
228
+ if (concurrency !== undefined) {
229
+ assertPositiveInteger("concurrency", concurrency);
230
+ }
223
231
 
224
232
  const startedAt = performance.now();
225
233
  const outboxModule = await loadOutboxModule(cwd, modulePath);
@@ -227,7 +235,7 @@ export async function runOutboxDrain(
227
235
  outboxModule.outboxRegistry,
228
236
  modulePath,
229
237
  );
230
- const contextArgs = { registry, batchSize };
238
+ const contextArgs = { registry, batchSize, concurrency };
231
239
  const rawContext = outboxModule.createOutboxDrainContext
232
240
  ? await outboxModule.createOutboxDrainContext(contextArgs)
233
241
  : undefined;
@@ -247,7 +255,11 @@ export async function runOutboxDrain(
247
255
  eventBus: ctx.ports.eventBus,
248
256
  jobs: ctx.ports.jobs,
249
257
  batchSize,
250
- instrumentation: ctx.ports.instrumentation ?? ctx.ports.devtools,
258
+ concurrency,
259
+ instrumentation: {
260
+ instrumentation: ctx.ports.instrumentation ?? ctx.ports.devtools,
261
+ tracing: ctx.ports.tracing,
262
+ },
251
263
  instrumentationContext: {
252
264
  requestId: ctx.requestId,
253
265
  traceId: ctx.traceId,
@@ -288,27 +300,65 @@ export async function runOutboxDrain(
288
300
  },
289
301
  });
290
302
  },
291
- onSettlementError(settlementError, message) {
303
+ onLeaseError(failure) {
304
+ const outcome =
305
+ failure.state === "lost"
306
+ ? "leaseLost"
307
+ : failure.state === "recovered"
308
+ ? "leaseRecovered"
309
+ : "leaseDegraded";
292
310
  return reportOperationalFailure({
293
311
  ctx,
294
- error: settlementError,
312
+ error: failure.error,
313
+ reportOptions: {
314
+ level: failure.state === "lost" ? "error" : "warning",
315
+ mechanism: "beignet.outbox.cli",
316
+ handled: failure.state !== "lost",
317
+ tags: {
318
+ "beignet.kind": "outbox",
319
+ "beignet.outbox.kind": failure.message.kind,
320
+ "beignet.outbox.name": failure.message.name,
321
+ "beignet.outbox.outcome": outcome,
322
+ },
323
+ contexts: {
324
+ outbox: {
325
+ messageId: failure.message.id,
326
+ kind: failure.message.kind,
327
+ name: failure.message.name,
328
+ attempts: failure.message.attempts,
329
+ maxAttempts: failure.message.maxAttempts,
330
+ operation: failure.operation,
331
+ state: failure.state,
332
+ confirmedLost: failure.confirmedLost,
333
+ outcome,
334
+ },
335
+ },
336
+ },
337
+ });
338
+ },
339
+ onSettlementError(failure) {
340
+ return reportOperationalFailure({
341
+ ctx,
342
+ error: failure.error,
295
343
  reportOptions: {
296
344
  level: "error",
297
345
  mechanism: "beignet.outbox.cli",
298
346
  handled: false,
299
347
  tags: {
300
348
  "beignet.kind": "outbox",
301
- "beignet.outbox.kind": message.kind,
302
- "beignet.outbox.name": message.name,
349
+ "beignet.outbox.kind": failure.message.kind,
350
+ "beignet.outbox.name": failure.message.name,
303
351
  "beignet.outbox.outcome": "settlementFailed",
304
352
  },
305
353
  contexts: {
306
354
  outbox: {
307
- messageId: message.id,
308
- kind: message.kind,
309
- name: message.name,
310
- attempts: message.attempts,
311
- maxAttempts: message.maxAttempts,
355
+ messageId: failure.message.id,
356
+ kind: failure.message.kind,
357
+ name: failure.message.name,
358
+ attempts: failure.message.attempts,
359
+ maxAttempts: failure.message.maxAttempts,
360
+ operation: failure.operation,
361
+ deliverySucceeded: failure.deliverySucceeded,
312
362
  outcome: "settlementFailed",
313
363
  },
314
364
  },
@@ -620,6 +670,7 @@ function isOutboxPort(value: unknown): value is DrainOutboxOptions["outbox"] {
620
670
  isRecord(value) &&
621
671
  typeof value.enqueue === "function" &&
622
672
  typeof value.claimBatch === "function" &&
673
+ typeof value.renewClaim === "function" &&
623
674
  typeof value.markDelivered === "function" &&
624
675
  typeof value.markFailed === "function"
625
676
  );
@@ -677,7 +728,7 @@ async function recordOutboxDrain(
677
728
  watcher: "outbox",
678
729
  name: "outbox.drain",
679
730
  label: "Outbox drain",
680
- summary: `${result.delivered} delivered, ${result.retried} retried, ${result.deadLettered} dead-lettered`,
731
+ summary: `${result.delivered} delivered, ${result.retried} retried, ${result.deadLettered} dead-lettered, ${result.settlementFailed} settlement-uncertain, ${result.leaseLost} lease-lost`,
681
732
  requestId: ctx.requestId,
682
733
  traceId: ctx.traceId,
683
734
  details: result,
@@ -41,8 +41,8 @@ https://beignetjs.com (agent-friendly index at https://beignetjs.com/llms.txt).
41
41
  README.md covers setup and the app map; this file covers what is not
42
42
  discoverable from the code.
43
43
 
44
- This app requires Node.js 22.12 or newer. Bun 1.3.14 or newer is supported for
45
- Beignet commands.
44
+ This app requires Node.js 22.12 or newer. Bun is optional; npm, pnpm, Yarn, and
45
+ Bun can run Beignet commands.
46
46
 
47
47
  ## Registration is explicit
48
48
 
@@ -71,6 +71,12 @@ is a silent failure — the file exists but never runs:
71
71
  - Apps using runtime integrity must list workflow registries in
72
72
  \`defineRuntimeManifest({...})\`.
73
73
 
74
+ Event schemas must output canonical JSON transport data. Use null, strings,
75
+ finite numbers, booleans, arrays, and plain objects. Keep validation
76
+ deterministic and side-effect-free, keep transforms idempotent, and represent
77
+ timestamps as strings or numbers rather than \`Date\` objects.
78
+ Beignet rejects payloads that would change when decoded and validated again.
79
+
74
80
  The starter ships no workflow registries — generators create them on first
75
81
  use, so their absence is fine. \`${cli} make event\`, \`${cli} make job\`,
76
82
  \`${cli} make listener\`, \`${cli} make schedule\`, \`${cli} make task\`,
@@ -107,7 +113,9 @@ re-exports and \`db\` for \`generate\`, \`migrate\`, \`seed\`, or \`reset\`
107
113
  instead of falling back to a shell. Use the read-only \`db_status\` tool before
108
114
  deploying. Use \`task_run\`, \`schedule_run\`,
109
115
  \`outbox_inspect\`, and \`outbox_run\` for registered operational workflows
110
- and outbox recovery.
116
+ and outbox recovery. Treat a drain with nonzero \`settlementFailed\` or
117
+ \`leaseLost\` as an operational failure. Parallel delivery is opt-in and does
118
+ not preserve message order.
111
119
 
112
120
  ## The framework already solves these
113
121
 
@@ -144,20 +152,37 @@ formatting. The individual commands (\`${lint}\`, \`${cli} lint\`,
144
152
  \`${cli} doctor --strict\`, \`${test}\`, \`${typecheck}\`) still work when you
145
153
  need one check alone.
146
154
 
147
- Beignet's architecture lint parses TypeScript imports and re-exports,
148
- \`require()\`, import assignments, and literal dynamic imports, then follows
149
- local value imports through feature-root helpers. When lint cannot verify a
150
- computed module reference inside a constrained layer, use a string-literal
151
- specifier or move runtime loading behind an allowed boundary. Move near-miss
152
- feature layers to the canonical path named in the diagnostic.
155
+ Inside a Git worktree, \`${cli} check\` fingerprints HEAD, the index, the
156
+ worktree, and non-ignored untracked files before and after validation. If the
157
+ ending fingerprint differs from the baseline, the result is stale and
158
+ unsuccessful even when every step passed; rerun the check against the resulting
159
+ source. Outside Git, the same validation runs with source provenance marked
160
+ unavailable; report that limitation instead of describing the result as
161
+ source-current.
162
+
163
+ Beignet's architecture lint parses JavaScript and TypeScript imports and
164
+ re-exports, \`require()\`, import assignments, and literal dynamic imports, then
165
+ follows local value imports through feature-root helpers. This includes JSX
166
+ and the \`.mts\`, \`.cts\`, \`.mjs\`, and \`.cjs\` module variants. When lint
167
+ cannot verify a computed module reference inside a constrained layer, use a
168
+ string-literal specifier or move runtime loading behind an allowed boundary.
169
+ Move near-miss feature layers to the canonical path named in the diagnostic.
153
170
 
154
171
  Before exploring unfamiliar code, run \`${cli} map\` for the human inventory
155
172
  or \`${cli} map --json --feature <name>\` for an evidence-backed graph scoped
156
- to one feature. The graph honors the app's complete TypeScript \`baseUrl\` and
157
- \`paths\` configuration. Fix \`local_import_unresolved\` and
158
- \`contract_declaration_unresolved\` entries rather than treating their missing
159
- edges as proof that no relationship exists. Registration diagnostics expose
160
- the declaration as \`subject.file\` and \`subject.exportName\`.
173
+ to one feature. The graph analyzes JavaScript and TypeScript source and honors
174
+ the app's complete \`baseUrl\` and \`paths\` configuration from
175
+ \`tsconfig.json\`, or \`jsconfig.json\` when no TypeScript config exists. Fix
176
+ \`local_import_unresolved\` and \`contract_declaration_unresolved\` entries
177
+ rather than treating their missing edges as proof that no relationship exists.
178
+ Registration diagnostics expose the declaration as \`subject.file\` and
179
+ \`subject.exportName\`.
180
+
181
+ The graph includes JavaScript and TypeScript test/spec files under canonical
182
+ and configured app test roots. Feature tests retain feature ownership;
183
+ adjacent app, infra, server, and top-level tests remain app-level nodes and can
184
+ appear as reverse-import consumers in changed mode. Treat that as impact
185
+ evidence, not proof of behavioral coverage.
161
186
 
162
187
  After editing in a Git worktree, run \`${cli} map --changed\` before the full
163
188
  check. It reports direct changes, reverse imports, one-hop semantic consumers,
@@ -242,7 +267,10 @@ and reports the script without executing it; it does not simulate SQL or data
242
267
  changes. Use \`task_run\` and \`schedule_run\` for registered operational
243
268
  workflows, \`outbox_inspect\` for read-only \`list\` and \`show\`, and
244
269
  \`outbox_run\` for \`drain\`, \`requeue\`, \`purge\`, or \`prune\`; purge and
245
- prune support \`dryRun\`. Operational commands run in isolated process trees
270
+ prune support \`dryRun\`. A drain with nonzero \`settlementFailed\` or
271
+ \`leaseLost\` remains a complete JSON report but is an error result; inspect
272
+ storage health and clock synchronization before retrying. Operational commands
273
+ run in isolated process trees
246
274
  with bounded results, cancellation, and timeouts. Optional \`module\` overrides
247
275
  must remain inside the app root where the MCP server started. Cancellation and
248
276
  timeouts cannot roll back side effects that already completed. Inspect app
@@ -247,8 +247,8 @@ route groups such as \`app/(app)\` are not part of the URL.
247
247
 
248
248
  Beignet app scaffolded with \`@beignet/cli\`.
249
249
 
250
- Node.js 22.12 or newer is required. Bun 1.3.14 or newer is supported for
251
- Beignet commands.
250
+ Node.js 22.12 or newer is required. Bun is optional; npm, pnpm, Yarn, and Bun
251
+ can run Beignet commands.
252
252
 
253
253
  ## Getting started
254
254
 
@@ -288,9 +288,9 @@ use cases, workflows, ports, providers, and tests connect. \`check\` runs the
288
288
  whole validation loop in one pass: \`${cli} lint\` (dependency direction),
289
289
  \`${cli} doctor --strict\` (route, OpenAPI, and resource drift), and the
290
290
  \`lint\`, \`typecheck\`, and \`test\` package scripts. Use \`${format}\` to
291
- apply Biome formatting. Beignet lint uses the TypeScript import graph, follows
292
- feature-root helpers, and reports unverifiable computed module references in
293
- constrained layers.
291
+ apply Biome formatting. Beignet lint analyzes JavaScript and TypeScript import
292
+ graphs, follows feature-root helpers, and reports unverifiable computed module
293
+ references in constrained layers.
294
294
  ${testingNotes}
295
295
  ## Coding agents
296
296
 
@@ -15,7 +15,7 @@
15
15
  // lives outside the app schema, so `db generate` never diffs against it.
16
16
  //
17
17
  // The Better Auth tables were generated with
18
- // `bunx @better-auth/cli generate` against a scratch app configured with
18
+ // `bunx auth generate` against a scratch app configured with
19
19
  // drizzleAdapter(db, { provider: "mysql" }), then normalized to the starter's
20
20
  // conventions (tabs, no relations, no extra indexes, no column defaults).
21
21
  // MySQL cannot put UNIQUE constraints on bare text columns, so identity
@@ -60,6 +60,7 @@ export function closeDatabaseClient(): Promise<void> {
60
60
  mysqlTable,
61
61
  text,
62
62
  timestamp,
63
+ uniqueIndex,
63
64
  varchar,
64
65
  } from "drizzle-orm/mysql-core";
65
66
 
@@ -86,23 +87,33 @@ export const session = mysqlTable("session", {
86
87
  .references(() => user.id, { onDelete: "cascade" }),
87
88
  });
88
89
 
89
- export const account = mysqlTable("account", {
90
- id: varchar("id", { length: 36 }).primaryKey(),
91
- accountId: text("account_id").notNull(),
92
- providerId: text("provider_id").notNull(),
93
- userId: varchar("user_id", { length: 36 })
94
- .notNull()
95
- .references(() => user.id, { onDelete: "cascade" }),
96
- accessToken: text("access_token"),
97
- refreshToken: text("refresh_token"),
98
- idToken: text("id_token"),
99
- accessTokenExpiresAt: timestamp("access_token_expires_at", { fsp: 3 }),
100
- refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { fsp: 3 }),
101
- scope: text("scope"),
102
- password: text("password"),
103
- createdAt: timestamp("created_at", { fsp: 3 }).notNull(),
104
- updatedAt: timestamp("updated_at", { fsp: 3 }).notNull(),
105
- });
90
+ export const account = mysqlTable(
91
+ "account",
92
+ {
93
+ id: varchar("id", { length: 36 }).primaryKey(),
94
+ issuer: varchar("issuer", { length: 255 }).notNull(),
95
+ accountId: varchar("account_id", { length: 255 }).notNull(),
96
+ providerId: text("provider_id").notNull(),
97
+ userId: varchar("user_id", { length: 36 })
98
+ .notNull()
99
+ .references(() => user.id, { onDelete: "cascade" }),
100
+ accessToken: text("access_token"),
101
+ refreshToken: text("refresh_token"),
102
+ idToken: text("id_token"),
103
+ accessTokenExpiresAt: timestamp("access_token_expires_at", { fsp: 3 }),
104
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at", { fsp: 3 }),
105
+ scope: text("scope"),
106
+ password: text("password"),
107
+ createdAt: timestamp("created_at", { fsp: 3 }).notNull(),
108
+ updatedAt: timestamp("updated_at", { fsp: 3 }).notNull(),
109
+ },
110
+ (table) => [
111
+ uniqueIndex("account_issuer_accountId_uidx").on(
112
+ table.issuer,
113
+ table.accountId,
114
+ ),
115
+ ],
116
+ );
106
117
 
107
118
  export const verification = mysqlTable("verification", {
108
119
  id: varchar("id", { length: 36 }).primaryKey(),
@@ -476,7 +487,8 @@ const mysqlStarterMigration = {
476
487
  // above, with the provider idempotency setup statements appended.
477
488
  starterMigrationSql: `CREATE TABLE \`account\` (
478
489
  \`id\` varchar(36) NOT NULL,
479
- \`account_id\` text NOT NULL,
490
+ \`issuer\` varchar(255) NOT NULL,
491
+ \`account_id\` varchar(255) NOT NULL,
480
492
  \`provider_id\` text NOT NULL,
481
493
  \`user_id\` varchar(36) NOT NULL,
482
494
  \`access_token\` text,
@@ -488,7 +500,8 @@ const mysqlStarterMigration = {
488
500
  \`password\` text,
489
501
  \`created_at\` timestamp(3) NOT NULL,
490
502
  \`updated_at\` timestamp(3) NOT NULL,
491
- CONSTRAINT \`account_id\` PRIMARY KEY(\`id\`)
503
+ CONSTRAINT \`account_id\` PRIMARY KEY(\`id\`),
504
+ CONSTRAINT \`account_issuer_accountId_uidx\` UNIQUE(\`issuer\`,\`account_id\`)
492
505
  );
493
506
  --> statement-breakpoint
494
507
  CREATE TABLE \`session\` (
@@ -585,9 +598,16 @@ CREATE TABLE IF NOT EXISTS \`idempotency_records\` (
585
598
  "notNull": true,
586
599
  "autoincrement": false
587
600
  },
601
+ "issuer": {
602
+ "name": "issuer",
603
+ "type": "varchar(255)",
604
+ "primaryKey": false,
605
+ "notNull": true,
606
+ "autoincrement": false
607
+ },
588
608
  "account_id": {
589
609
  "name": "account_id",
590
- "type": "text",
610
+ "type": "varchar(255)",
591
611
  "primaryKey": false,
592
612
  "notNull": true,
593
613
  "autoincrement": false
@@ -670,7 +690,16 @@ CREATE TABLE IF NOT EXISTS \`idempotency_records\` (
670
690
  "autoincrement": false
671
691
  }
672
692
  },
673
- "indexes": {},
693
+ "indexes": {
694
+ "account_issuer_accountId_uidx": {
695
+ "name": "account_issuer_accountId_uidx",
696
+ "columns": [
697
+ "issuer",
698
+ "account_id"
699
+ ],
700
+ "isUnique": true
701
+ }
702
+ },
674
703
  "foreignKeys": {
675
704
  "account_user_id_user_id_fk": {
676
705
  "name": "account_user_id_user_id_fk",