@beignet/cli 0.0.47 → 0.0.49

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 (144) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +126 -42
  3. package/dist/analysis/contract-factories.d.ts +17 -0
  4. package/dist/analysis/contract-factories.d.ts.map +1 -0
  5. package/dist/analysis/contract-factories.js +176 -0
  6. package/dist/analysis/contract-factories.js.map +1 -0
  7. package/dist/analysis/layers.d.ts +5 -0
  8. package/dist/analysis/layers.d.ts.map +1 -0
  9. package/dist/analysis/layers.js +172 -0
  10. package/dist/analysis/layers.js.map +1 -0
  11. package/dist/analysis/port-wiring.d.ts +23 -0
  12. package/dist/analysis/port-wiring.d.ts.map +1 -0
  13. package/dist/analysis/port-wiring.js +379 -0
  14. package/dist/analysis/port-wiring.js.map +1 -0
  15. package/dist/analysis/source-index.d.ts +5 -0
  16. package/dist/analysis/source-index.d.ts.map +1 -1
  17. package/dist/analysis/source-index.js +171 -42
  18. package/dist/analysis/source-index.js.map +1 -1
  19. package/dist/analysis/workspace.d.ts +10 -4
  20. package/dist/analysis/workspace.d.ts.map +1 -1
  21. package/dist/analysis/workspace.js +66 -6
  22. package/dist/analysis/workspace.js.map +1 -1
  23. package/dist/app-map.d.ts.map +1 -1
  24. package/dist/app-map.js +311 -92
  25. package/dist/app-map.js.map +1 -1
  26. package/dist/check.d.ts +8 -1
  27. package/dist/check.d.ts.map +1 -1
  28. package/dist/check.js +55 -9
  29. package/dist/check.js.map +1 -1
  30. package/dist/db.d.ts +38 -0
  31. package/dist/db.d.ts.map +1 -1
  32. package/dist/db.js +52 -0
  33. package/dist/db.js.map +1 -1
  34. package/dist/explain.js +4 -4
  35. package/dist/explain.js.map +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +73 -6
  38. package/dist/index.js.map +1 -1
  39. package/dist/inspect.d.ts.map +1 -1
  40. package/dist/inspect.js +454 -170
  41. package/dist/inspect.js.map +1 -1
  42. package/dist/lib.d.ts +2 -2
  43. package/dist/lib.d.ts.map +1 -1
  44. package/dist/lib.js +1 -1
  45. package/dist/lib.js.map +1 -1
  46. package/dist/lint.d.ts.map +1 -1
  47. package/dist/lint.js +309 -487
  48. package/dist/lint.js.map +1 -1
  49. package/dist/make/inbox.d.ts.map +1 -1
  50. package/dist/make/inbox.js +12 -5
  51. package/dist/make/inbox.js.map +1 -1
  52. package/dist/make/payments.js +3 -3
  53. package/dist/make/payments.js.map +1 -1
  54. package/dist/make/tenancy.js +1 -1
  55. package/dist/make/tenancy.js.map +1 -1
  56. package/dist/make.d.ts.map +1 -1
  57. package/dist/make.js +144 -8
  58. package/dist/make.js.map +1 -1
  59. package/dist/mcp.d.ts +1 -1
  60. package/dist/mcp.d.ts.map +1 -1
  61. package/dist/mcp.js +56 -19
  62. package/dist/mcp.js.map +1 -1
  63. package/dist/preflight.d.ts +11 -3
  64. package/dist/preflight.d.ts.map +1 -1
  65. package/dist/preflight.js +139 -16
  66. package/dist/preflight.js.map +1 -1
  67. package/dist/provider-add.d.ts.map +1 -1
  68. package/dist/provider-add.js +149 -9
  69. package/dist/provider-add.js.map +1 -1
  70. package/dist/provider-audit.d.ts +13 -0
  71. package/dist/provider-audit.d.ts.map +1 -1
  72. package/dist/provider-audit.js +122 -8
  73. package/dist/provider-audit.js.map +1 -1
  74. package/dist/registry-edits.d.ts +7 -0
  75. package/dist/registry-edits.d.ts.map +1 -1
  76. package/dist/registry-edits.js +237 -53
  77. package/dist/registry-edits.js.map +1 -1
  78. package/dist/templates/agents.d.ts.map +1 -1
  79. package/dist/templates/agents.js +29 -7
  80. package/dist/templates/agents.js.map +1 -1
  81. package/dist/templates/base.d.ts +1 -0
  82. package/dist/templates/base.d.ts.map +1 -1
  83. package/dist/templates/base.js +20 -6
  84. package/dist/templates/base.js.map +1 -1
  85. package/dist/templates/db/index.d.ts +1 -0
  86. package/dist/templates/db/index.d.ts.map +1 -1
  87. package/dist/templates/db/index.js.map +1 -1
  88. package/dist/templates/db/migration-status.d.ts +9 -0
  89. package/dist/templates/db/migration-status.d.ts.map +1 -0
  90. package/dist/templates/db/migration-status.js +117 -0
  91. package/dist/templates/db/migration-status.js.map +1 -0
  92. package/dist/templates/db/mysql.d.ts.map +1 -1
  93. package/dist/templates/db/mysql.js +23 -0
  94. package/dist/templates/db/mysql.js.map +1 -1
  95. package/dist/templates/db/postgres.d.ts.map +1 -1
  96. package/dist/templates/db/postgres.js +21 -0
  97. package/dist/templates/db/postgres.js.map +1 -1
  98. package/dist/templates/db/sqlite.d.ts.map +1 -1
  99. package/dist/templates/db/sqlite.js +53 -0
  100. package/dist/templates/db/sqlite.js.map +1 -1
  101. package/dist/templates/index.d.ts.map +1 -1
  102. package/dist/templates/index.js +5 -0
  103. package/dist/templates/index.js.map +1 -1
  104. package/dist/templates/server.d.ts.map +1 -1
  105. package/dist/templates/server.js +2 -0
  106. package/dist/templates/server.js.map +1 -1
  107. package/dist/templates/shared.d.ts +1 -0
  108. package/dist/templates/shared.d.ts.map +1 -1
  109. package/dist/templates/shared.js +4 -2
  110. package/dist/templates/shared.js.map +1 -1
  111. package/package.json +7 -6
  112. package/skills/app-structure/SKILL.md +56 -11
  113. package/src/analysis/contract-factories.ts +256 -0
  114. package/src/analysis/layers.ts +266 -0
  115. package/src/analysis/port-wiring.ts +506 -0
  116. package/src/analysis/source-index.ts +225 -57
  117. package/src/analysis/workspace.ts +115 -6
  118. package/src/app-map.ts +418 -109
  119. package/src/check.ts +86 -10
  120. package/src/db.ts +97 -0
  121. package/src/explain.ts +4 -4
  122. package/src/index.ts +108 -7
  123. package/src/inspect.ts +659 -216
  124. package/src/lib.ts +9 -1
  125. package/src/lint.ts +393 -643
  126. package/src/make/inbox.ts +12 -5
  127. package/src/make/payments.ts +3 -3
  128. package/src/make/tenancy.ts +1 -1
  129. package/src/make.ts +225 -8
  130. package/src/mcp.ts +73 -21
  131. package/src/preflight.ts +195 -16
  132. package/src/provider-add.ts +193 -9
  133. package/src/provider-audit.ts +210 -18
  134. package/src/registry-edits.ts +313 -63
  135. package/src/templates/agents.ts +29 -7
  136. package/src/templates/base.ts +21 -4
  137. package/src/templates/db/index.ts +1 -0
  138. package/src/templates/db/migration-status.ts +126 -0
  139. package/src/templates/db/mysql.ts +23 -0
  140. package/src/templates/db/postgres.ts +21 -0
  141. package/src/templates/db/sqlite.ts +53 -0
  142. package/src/templates/index.ts +5 -0
  143. package/src/templates/server.ts +2 -0
  144. package/src/templates/shared.ts +4 -2
@@ -308,24 +308,324 @@ export function outboxRegistryValueInfo(
308
308
  source: string,
309
309
  property: "events" | "jobs",
310
310
  ): InitializerInfo | undefined {
311
- const call = /\bdefineOutboxRegistry\s*(?:<[^>]*>\s*)?\(/.exec(source);
312
- if (!call) return undefined;
311
+ const match = outboxRegistryPropertyAnalysis(source, property);
312
+ if (!match?.value || match.indeterminate) return undefined;
313
+ const start = match.value.getStart(match.sourceFile);
314
+ return {
315
+ text: source.slice(start, match.value.end),
316
+ start,
317
+ end: match.value.end,
318
+ };
319
+ }
320
+
321
+ export type OutboxRegistryEntryState =
322
+ | "absent"
323
+ | "empty"
324
+ | "present"
325
+ | "indeterminate";
326
+
327
+ /** Static state of one outbox registry property. */
328
+ export function outboxRegistryEntryState(
329
+ source: string,
330
+ property: "events" | "jobs",
331
+ ): OutboxRegistryEntryState {
332
+ const match = outboxRegistryPropertyAnalysis(source, property);
333
+ if (!match) return "absent";
334
+ if (match.indeterminate) return "indeterminate";
335
+ if (!match.value) return "empty";
336
+ return resolvedRegistryExpressions(match.value, match.sourceFile).length > 0
337
+ ? "present"
338
+ : "empty";
339
+ }
313
340
 
314
- const openBrace = source.indexOf("{", call.index);
315
- if (openBrace === -1) return undefined;
341
+ /** Whether a registry property is non-empty or cannot be proven empty. */
342
+ export function outboxRegistryHasEntries(
343
+ source: string,
344
+ property: "events" | "jobs",
345
+ ): boolean {
346
+ const state = outboxRegistryEntryState(source, property);
347
+ return state === "present" || state === "indeterminate";
348
+ }
316
349
 
317
- const closeBrace = matchingDelimiterIndex(source, openBrace, "{", "}");
318
- if (closeBrace === -1) return undefined;
350
+ /** Identifiers referenced by a registry property after resolving local array aliases. */
351
+ export function outboxRegistryIdentifiers(
352
+ source: string,
353
+ property: "events" | "jobs",
354
+ ): Set<string> {
355
+ const match = outboxRegistryPropertyAnalysis(source, property);
356
+ if (!match?.value || match.indeterminate) return new Set();
357
+
358
+ const identifiers = new Set<string>();
359
+ for (const expression of resolvedRegistryExpressions(
360
+ match.value,
361
+ match.sourceFile,
362
+ )) {
363
+ const start = expression.getStart(match.sourceFile);
364
+ for (const identifier of identifiersFromArrayExpression(
365
+ source.slice(start, expression.end),
366
+ )) {
367
+ identifiers.add(identifier);
368
+ }
369
+ }
370
+ return identifiers;
371
+ }
319
372
 
320
- const objectText = source.slice(openBrace, closeBrace + 1);
321
- const propertyMatch = new RegExp(`\\b${property}\\s*:`).exec(objectText);
322
- if (!propertyMatch) return undefined;
373
+ type OutboxRegistryPropertyAnalysis = {
374
+ sourceFile: ts.SourceFile;
375
+ value?: ts.Expression;
376
+ indeterminate: boolean;
377
+ };
323
378
 
324
- const valueStart = openBrace + propertyMatch.index + propertyMatch[0].length;
325
- const value = topLevelValueSlice(source, valueStart, closeBrace);
326
- if (!value) return undefined;
379
+ function outboxRegistryPropertyAnalysis(
380
+ source: string,
381
+ property: "events" | "jobs",
382
+ ): OutboxRegistryPropertyAnalysis | undefined {
383
+ const sourceFile = ts.createSourceFile(
384
+ "outbox-registry.ts",
385
+ source,
386
+ ts.ScriptTarget.Latest,
387
+ true,
388
+ ts.ScriptKind.TS,
389
+ );
390
+ const initializers = registryInitializers(sourceFile);
391
+ const selected = selectOutboxRegistryCall(sourceFile);
392
+ if (!selected.call) {
393
+ return selected.indeterminate
394
+ ? { sourceFile, indeterminate: true }
395
+ : undefined;
396
+ }
327
397
 
328
- return value;
398
+ const firstArgument = selected.call.arguments[0];
399
+ const object = firstArgument
400
+ ? resolveRegistryExpression(firstArgument, initializers)
401
+ : undefined;
402
+ if (!object || !ts.isObjectLiteralExpression(object)) {
403
+ return { sourceFile, indeterminate: true };
404
+ }
405
+
406
+ let value: ts.Expression | undefined;
407
+ let indeterminate = false;
408
+ for (const member of object.properties) {
409
+ if (ts.isSpreadAssignment(member)) {
410
+ value = undefined;
411
+ indeterminate = true;
412
+ continue;
413
+ }
414
+ const name = member.name ? registryPropertyName(member.name) : undefined;
415
+ if (!name) {
416
+ value = undefined;
417
+ indeterminate = true;
418
+ continue;
419
+ }
420
+ if (name !== property) continue;
421
+ if (
422
+ ts.isPropertyAssignment(member) ||
423
+ ts.isShorthandPropertyAssignment(member)
424
+ ) {
425
+ value = ts.isPropertyAssignment(member)
426
+ ? member.initializer
427
+ : member.name;
428
+ indeterminate = false;
429
+ } else {
430
+ value = undefined;
431
+ indeterminate = true;
432
+ }
433
+ }
434
+ if (value && !isStaticRegistryValue(value, initializers)) {
435
+ return { sourceFile, indeterminate: true };
436
+ }
437
+ return { sourceFile, value, indeterminate };
438
+ }
439
+
440
+ function isStaticRegistryValue(
441
+ expression: ts.Expression,
442
+ initializers: Map<string, ts.Expression>,
443
+ visited = new Set<string>(),
444
+ ): boolean {
445
+ const current = unwrapRegistryExpression(expression);
446
+ if (ts.isArrayLiteralExpression(current)) return true;
447
+ if (!ts.isIdentifier(current)) return false;
448
+
449
+ const initializer = initializers.get(current.text);
450
+ if (!initializer) return true;
451
+ if (visited.has(current.text)) return false;
452
+
453
+ const nextVisited = new Set(visited);
454
+ nextVisited.add(current.text);
455
+ return isStaticRegistryValue(initializer, initializers, nextVisited);
456
+ }
457
+
458
+ function selectOutboxRegistryCall(sourceFile: ts.SourceFile): {
459
+ call?: ts.CallExpression;
460
+ indeterminate: boolean;
461
+ } {
462
+ const exportedNames = locallyExportedRegistryNames(sourceFile);
463
+ const candidates: {
464
+ name?: string;
465
+ exported: boolean;
466
+ calls: ts.CallExpression[];
467
+ }[] = [];
468
+
469
+ for (const statement of sourceFile.statements) {
470
+ if (!ts.isVariableStatement(statement)) continue;
471
+ const statementExported = Boolean(
472
+ statement.modifiers?.some(
473
+ (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
474
+ ),
475
+ );
476
+ for (const declaration of statement.declarationList.declarations) {
477
+ if (!declaration.initializer) continue;
478
+ const calls = outboxRegistryCalls(declaration.initializer);
479
+ if (calls.length === 0) continue;
480
+ const name = ts.isIdentifier(declaration.name)
481
+ ? declaration.name.text
482
+ : undefined;
483
+ candidates.push({
484
+ ...(name ? { name } : {}),
485
+ exported: statementExported || (name ? exportedNames.has(name) : false),
486
+ calls,
487
+ });
488
+ }
489
+ }
490
+
491
+ const canonical = candidates.filter(
492
+ (candidate) => candidate.name === "outboxRegistry" && candidate.exported,
493
+ );
494
+ const exported = candidates.filter((candidate) => candidate.exported);
495
+ const selected =
496
+ canonical.length > 0
497
+ ? canonical
498
+ : exported.length > 0
499
+ ? exported
500
+ : candidates;
501
+ if (selected.length === 1 && selected[0].calls.length === 1) {
502
+ return { call: selected[0].calls[0], indeterminate: false };
503
+ }
504
+ if (selected.length > 0) return { indeterminate: true };
505
+
506
+ const fallback = outboxRegistryCalls(sourceFile);
507
+ return fallback.length === 1
508
+ ? { call: fallback[0], indeterminate: false }
509
+ : { indeterminate: fallback.length > 0 };
510
+ }
511
+
512
+ function locallyExportedRegistryNames(sourceFile: ts.SourceFile): Set<string> {
513
+ const names = new Set<string>();
514
+ for (const statement of sourceFile.statements) {
515
+ if (
516
+ !ts.isExportDeclaration(statement) ||
517
+ !statement.exportClause ||
518
+ !ts.isNamedExports(statement.exportClause) ||
519
+ statement.moduleSpecifier
520
+ ) {
521
+ continue;
522
+ }
523
+ for (const element of statement.exportClause.elements) {
524
+ names.add(element.propertyName?.text ?? element.name.text);
525
+ }
526
+ }
527
+ return names;
528
+ }
529
+
530
+ function outboxRegistryCalls(node: ts.Node): ts.CallExpression[] {
531
+ const calls: ts.CallExpression[] = [];
532
+ const visit = (child: ts.Node): void => {
533
+ if (
534
+ ts.isCallExpression(child) &&
535
+ ((ts.isIdentifier(child.expression) &&
536
+ child.expression.text === "defineOutboxRegistry") ||
537
+ (ts.isPropertyAccessExpression(child.expression) &&
538
+ child.expression.name.text === "defineOutboxRegistry"))
539
+ ) {
540
+ calls.push(child);
541
+ }
542
+ ts.forEachChild(child, visit);
543
+ };
544
+ visit(node);
545
+ return calls;
546
+ }
547
+
548
+ function resolvedRegistryExpressions(
549
+ expression: ts.Expression,
550
+ sourceFile: ts.SourceFile,
551
+ ): ts.Expression[] {
552
+ const initializers = registryInitializers(sourceFile);
553
+
554
+ const resolve = (
555
+ candidate: ts.Expression,
556
+ visited: Set<string>,
557
+ ): ts.Expression[] => {
558
+ const current = unwrapRegistryExpression(candidate);
559
+ if (ts.isArrayLiteralExpression(current)) {
560
+ return current.elements.flatMap((element) =>
561
+ ts.isSpreadElement(element)
562
+ ? resolve(element.expression, new Set(visited))
563
+ : ts.isExpression(element)
564
+ ? [element]
565
+ : [],
566
+ );
567
+ }
568
+ if (ts.isIdentifier(current) && !visited.has(current.text)) {
569
+ const initializer = initializers.get(current.text);
570
+ if (initializer) {
571
+ const nextVisited = new Set(visited);
572
+ nextVisited.add(current.text);
573
+ return resolve(initializer, nextVisited);
574
+ }
575
+ }
576
+ return [current];
577
+ };
578
+
579
+ return resolve(expression, new Set());
580
+ }
581
+
582
+ function registryInitializers(
583
+ sourceFile: ts.SourceFile,
584
+ ): Map<string, ts.Expression> {
585
+ const initializers = new Map<string, ts.Expression>();
586
+ for (const statement of sourceFile.statements) {
587
+ if (!ts.isVariableStatement(statement)) continue;
588
+ for (const declaration of statement.declarationList.declarations) {
589
+ if (ts.isIdentifier(declaration.name) && declaration.initializer) {
590
+ initializers.set(declaration.name.text, declaration.initializer);
591
+ }
592
+ }
593
+ }
594
+ return initializers;
595
+ }
596
+
597
+ function resolveRegistryExpression(
598
+ expression: ts.Expression,
599
+ initializers: Map<string, ts.Expression>,
600
+ visited = new Set<string>(),
601
+ ): ts.Expression {
602
+ const current = unwrapRegistryExpression(expression);
603
+ if (!ts.isIdentifier(current) || visited.has(current.text)) return current;
604
+ const initializer = initializers.get(current.text);
605
+ if (!initializer) return current;
606
+ const nextVisited = new Set(visited);
607
+ nextVisited.add(current.text);
608
+ return resolveRegistryExpression(initializer, initializers, nextVisited);
609
+ }
610
+
611
+ function unwrapRegistryExpression(expression: ts.Expression): ts.Expression {
612
+ let current = expression;
613
+ while (
614
+ ts.isParenthesizedExpression(current) ||
615
+ ts.isAsExpression(current) ||
616
+ ts.isSatisfiesExpression(current) ||
617
+ ts.isTypeAssertionExpression(current) ||
618
+ ts.isNonNullExpression(current)
619
+ ) {
620
+ current = current.expression;
621
+ }
622
+ return current;
623
+ }
624
+
625
+ function registryPropertyName(name: ts.PropertyName): string | undefined {
626
+ return ts.isIdentifier(name) || ts.isStringLiteralLike(name)
627
+ ? name.text
628
+ : undefined;
329
629
  }
330
630
 
331
631
  /**
@@ -390,53 +690,3 @@ function appendEntry(
390
690
  if (next === source) return { kind: "unchanged" };
391
691
  return { kind: "updated", source: next };
392
692
  }
393
-
394
- function topLevelValueSlice(
395
- source: string,
396
- valueStart: number,
397
- hardEnd: number,
398
- ): InitializerInfo | undefined {
399
- let start = valueStart;
400
- while (start < hardEnd && /\s/.test(source[start])) start++;
401
- if (start >= hardEnd) return undefined;
402
-
403
- let depth = 0;
404
-
405
- for (const index of codeCharacterIndexes(source, start, hardEnd + 1)) {
406
- const char = source[index];
407
-
408
- if (char === "[" || char === "(" || char === "{") {
409
- depth++;
410
- continue;
411
- }
412
-
413
- if (char === "]" || char === ")" || char === "}") {
414
- if (depth === 0) {
415
- return trimmedInfo(source, start, index);
416
- }
417
- depth--;
418
- continue;
419
- }
420
-
421
- if (char === "," && depth === 0) {
422
- return trimmedInfo(source, start, index);
423
- }
424
- }
425
-
426
- return undefined;
427
- }
428
-
429
- function trimmedInfo(
430
- source: string,
431
- start: number,
432
- end: number,
433
- ): InitializerInfo {
434
- let trimmedEnd = end;
435
- while (trimmedEnd > start && /\s/.test(source[trimmedEnd - 1])) trimmedEnd--;
436
-
437
- return {
438
- text: source.slice(start, trimmedEnd),
439
- start,
440
- end: trimmedEnd,
441
- };
442
- }
@@ -41,6 +41,9 @@ 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.
46
+
44
47
  ## Registration is explicit
45
48
 
46
49
  Hand-writing an artifact file does not wire it into the app. Non-registration
@@ -54,6 +57,11 @@ is a silent failure — the file exists but never runs:
54
57
  - Tasks must be added to \`defineTasks([...])\` in \`server/tasks.ts\`.
55
58
  - Outbox events and jobs must be registered in \`defineOutboxRegistry({...})\`
56
59
  in \`server/outbox.ts\`.
60
+ - An outbox registry containing jobs also requires \`jobs: JobDispatcherPort\`
61
+ to be bound directly or deferred to a provider. \`${cli} make job\` installs
62
+ an app-owned inline dispatcher only when no dispatcher is already wired;
63
+ adding an Inngest or BullMQ preset later replaces only that marked generated
64
+ fallback and rejects unmarked custom inline wiring as a conflict.
57
65
  - Listeners must be added to the \`listeners\` array in \`server/listeners.ts\`
58
66
  and wired through a \`registerListeners(...)\` call in server provider wiring.
59
67
  - Apps using Inngest must also spread feature job registries into
@@ -79,7 +87,8 @@ array anchors are unambiguous. It also syncs missing default Beignet
79
87
  provider-table exports when \`infra/db/schema/beignet.ts\` is missing or still
80
88
  matches \`${cli} db schema sync\` output and the schema index has no custom
81
89
  named re-export for that file. Run \`${cli} db generate\` and
82
- \`${cli} db migrate\` after accepting that source repair. Custom or ambiguous
90
+ \`${cli} db migrate\`, then \`${cli} db status\` after accepting that source
91
+ repair. Custom or ambiguous
83
92
  code stays diagnostic-only.
84
93
 
85
94
  ## Prefer generators
@@ -89,10 +98,12 @@ files — prefer it over hand-writing them. See \`${cli} make --help\` for the
89
98
  artifact list. Use \`${cli} make feature <name> --recipe full-slice\` when you
90
99
  need a richer reference slice with policy, client helpers, workflow artifacts,
91
100
  events, listener registration, jobs, and outbox wiring. After changing the Drizzle schema in
92
- \`infra/db/schema/\`, run \`${cli} db generate\` then \`${cli} db migrate\`.
101
+ \`infra/db/schema/\`, run \`${cli} db generate\`, \`${cli} db migrate\`, then
102
+ \`${cli} db status\`.
93
103
  When MCP is available, use \`db_schema_sync\` for Beignet provider table
94
104
  re-exports and \`db\` for \`generate\`, \`migrate\`, \`seed\`, or \`reset\`
95
- instead of falling back to a shell. Use \`task_run\`, \`schedule_run\`,
105
+ instead of falling back to a shell. Use the read-only \`db_status\` tool before
106
+ deploying. Use \`task_run\`, \`schedule_run\`,
96
107
  \`outbox_inspect\`, and \`outbox_run\` for registered operational workflows
97
108
  and outbox recovery.
98
109
 
@@ -103,6 +114,7 @@ apps have reimplemented by accident:
103
114
 
104
115
  | Need | Use |
105
116
  | --- | --- |${capabilityClientRow}
117
+ | Query parameters on an HTTP contract | Pass both the Standard Schema and an explicit \`defineQueryTransport({ ... })\` to \`.query(schema, transport)\`. Use \`query.string()\`, \`query.number()\`, \`query.integer()\`, \`query.boolean()\`, \`query.dateTime()\`, \`query.date()\`, repeated scalar arrays, or one flat \`deepObject\`; never make schema coercion define the URL format. |
106
118
  | App context outside HTTP — agents, queues, scripts, backfills | \`server.createServiceContext(input)\`, or \`server.runServiceContext(input, fn)\` in one-off scripts. Never hand-assemble a context or call \`gate.attach(...)\` — the server owns gate attachment. |
107
119
  | Ports outside a request — auth callbacks, module-level helpers | \`const { ports } = await getServer()\` (dynamic \`import("@/server")\` breaks module cycles). Do not construct parallel provider clients or fall back to \`console.*\` when \`ports.logger\` exists. |
108
120
  | Routes that cannot be contracts — webhooks, third-party callbacks, streaming | \`createWebhookRoute\`, \`createPaymentWebhookRoute\`, \`createScheduleRoute\`, \`createOutboxDrainRoute\` from \`@beignet/next\`; \`server.rawRoute(...)\` for anything else. All run the hooks pipeline — never hand-enforce rate limits in a route body. |
@@ -130,6 +142,13 @@ formatting. The individual commands (\`${lint}\`, \`${cli} lint\`,
130
142
  \`${cli} doctor --strict\`, \`${test}\`, \`${typecheck}\`) still work when you
131
143
  need one check alone.
132
144
 
145
+ Beignet's architecture lint parses TypeScript imports and re-exports,
146
+ \`require()\`, import assignments, and literal dynamic imports, then follows
147
+ local value imports through feature-root helpers. When lint cannot verify a
148
+ computed module reference inside a constrained layer, use a string-literal
149
+ specifier or move runtime loading behind an allowed boundary. Move near-miss
150
+ feature layers to the canonical path named in the diagnostic.
151
+
133
152
  Before exploring unfamiliar code, run \`${cli} map\` for the human inventory
134
153
  or \`${cli} map --json --feature <name>\` for an evidence-backed graph scoped
135
154
  to one feature. The graph honors the app's complete TypeScript \`baseUrl\` and
@@ -175,7 +194,7 @@ skill-loading block.
175
194
  \`.mcp.json\` registers the app-local \`@beignet/cli\` bin at
176
195
  \`./node_modules/.bin/beignet mcp\`, which exposes app context, validation,
177
196
  generation, and operations as structured tools named exactly: \`app_map\`,
178
- \`explain\`, \`check\`, \`db\`, \`db_schema_sync\`, \`task_run\`,
197
+ \`explain\`, \`check\`, \`db\`, \`db_status\`, \`db_schema_sync\`, \`task_run\`,
179
198
  \`schedule_run\`, \`outbox_inspect\`, \`outbox_run\`, \`routes\`, \`doctor\`,
180
199
  \`doctor_fix_plan\`, \`doctor_fix\`, \`lint\`, \`make\`, \`provider_add\`.
181
200
  It also publishes \`beignet://app/guidance\` and focused
@@ -190,11 +209,14 @@ relationships, conventions, findings, and suggested files for any mapped
190
209
  concept or diagnostic; its kinds match the app map. Use \`check\` after edits
191
210
  to run the complete validation loop; it returns the same versioned result as
192
211
  \`${cli} check --json\`, bounds failure output, and cancels the active package
193
- script when the MCP request is cancelled. It does not apply Beignet fixes, but
194
- app-owned scripts retain their normal side effects. Use \`db_schema_sync\` to
212
+ script when the MCP request is cancelled. Pass \`preflightConnect: true\` to
213
+ include migration status and dependency health checks; \`connectTimeoutMs\`
214
+ sets their per-check timeout. It does not apply Beignet fixes, but app-owned
215
+ scripts retain their normal side effects. Use \`db_schema_sync\` to
195
216
  preview or apply idempotent provider-table schema re-exports, then call \`db\`
196
217
  with \`generate\` and \`migrate\`; database output is bounded, commands time
197
- out, and cancellation stops the active process tree. Treat \`seed\` and
218
+ out, and cancellation stops the active process tree. Use \`db_status\` for a
219
+ read-only current, pending, or failed migration report. Treat \`seed\` and
198
220
  especially \`reset\` as app-owned mutations. Lifecycle \`dryRun\` validates
199
221
  and reports the script without executing it; it does not simulate SQL or data
200
222
  changes. Use \`task_run\` and \`schedule_run\` for registered operational
@@ -103,6 +103,7 @@ export function packageJson(ctx: TemplateContext): string {
103
103
  "db:generate": "drizzle-kit generate",
104
104
  "db:migrate": "drizzle-kit migrate",
105
105
  "db:reset": currentGeneratedPackageScripts["db:reset"],
106
+ "db:status": currentGeneratedPackageScripts["db:status"],
106
107
  };
107
108
 
108
109
  return json({
@@ -110,6 +111,9 @@ export function packageJson(ctx: TemplateContext): string {
110
111
  version: "0.1.0",
111
112
  private: true,
112
113
  type: "module",
114
+ engines: {
115
+ node: ">=22.12.0",
116
+ },
113
117
  scripts,
114
118
  intent: {
115
119
  skills: intentSkillPackages(ctx),
@@ -243,6 +247,9 @@ route groups such as \`app/(app)\` are not part of the URL.
243
247
 
244
248
  Beignet app scaffolded with \`@beignet/cli\`.
245
249
 
250
+ Node.js 22.12 or newer is required. Bun 1.3.14 or newer is supported for
251
+ Beignet commands.
252
+
246
253
  ## Getting started
247
254
 
248
255
  \`\`\`bash
@@ -254,9 +261,10 @@ ${startDatabase}## Prepare the database
254
261
 
255
262
  \`\`\`bash
256
263
  ${cli} db migrate
264
+ ${cli} db status
257
265
  \`\`\`
258
266
 
259
- The starter ships its initial Drizzle migration in \`drizzle/\`, so the first run only applies it. After you change \`infra/db/schema/\`, run \`${cli} db generate\` and \`${cli} db migrate\` together.
267
+ The starter ships its initial Drizzle migration in \`drizzle/\`, so the first run only applies it. After you change \`infra/db/schema/\`, run \`${cli} db generate\` and \`${cli} db migrate\` together, then verify the target database with \`${cli} db status\`.
260
268
 
261
269
  ## Start the app
262
270
 
@@ -280,7 +288,9 @@ use cases, workflows, ports, providers, and tests connect. \`check\` runs the
280
288
  whole validation loop in one pass: \`${cli} lint\` (dependency direction),
281
289
  \`${cli} doctor --strict\` (route, OpenAPI, and resource drift), and the
282
290
  \`lint\`, \`typecheck\`, and \`test\` package scripts. Use \`${format}\` to
283
- apply Biome formatting.
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.
284
294
  ${testingNotes}
285
295
  ## Coding agents
286
296
 
@@ -303,10 +313,11 @@ ${start}
303
313
  ${cli} make feature projects
304
314
  ${cli} db generate
305
315
  ${cli} db migrate
316
+ ${cli} db status
306
317
  ${cli} check
307
318
  \`\`\`
308
319
 
309
- \`make feature\` creates a contract-to-test vertical slice with Drizzle schema and repository files, so regenerate and migrate the database before running the app against the new feature.
320
+ \`make feature\` creates a contract-to-test vertical slice with Drizzle schema and repository files, so regenerate, migrate, and verify the database before running the app against the new feature.
310
321
  Use \`${cli} make feature projects --recipe full-slice\` when you want a richer reference slice with policy, feature client helpers, workflow artifacts, events, listener registration, jobs, and outbox wiring.
311
322
 
312
323
  ## App map
@@ -352,7 +363,8 @@ ${optionalAuthRedirects}
352
363
  " db migrate` against it before starting the app."
353
364
  }
354
365
  - Size the process-wide database client for the deployment. Postgres and MySQL use the generated pool-limit environment variable; count web, worker, preview, and task processes against the database connection limit, and keep writable SQLite files on one host.
355
- - Run \`${cli} db generate\` and \`${cli} db migrate\` after changing the Drizzle schema.
366
+ - Run \`${cli} db generate\`, \`${cli} db migrate\`, and \`${cli} db status\` after changing the Drizzle schema.
367
+ - Run \`${cli} db migrate\` in one serialized migration job per environment, then \`${cli} db status\` and \`${cli} preflight --connect\` before shifting traffic. Do not migrate independently from every web replica.
356
368
  - Run \`${cli} db reset\` to rebuild a local ${
357
369
  ctx.database === "sqlite"
358
370
  ? "SQLite"
@@ -429,6 +441,7 @@ export function providersDoc(ctx: TemplateContext): string {
429
441
  "- Peer dependencies: `@upstash/ratelimit` and `@upstash/redis`",
430
442
  "- The starter wires `createUpstashRateLimitProvider()` in `server/providers.ts` and adds `rateLimit: RateLimitPort` to `AppPorts`.",
431
443
  "- `.env.example` ships dev placeholders so the app boots; set real `UPSTASH_REDIS_REST_URL` and `UPSTASH_REDIS_REST_TOKEN` values before rate limiting requests.",
444
+ "- Rate-limit decisions time out after 5000ms and fail closed by default. Opt into `UPSTASH_TIMEOUT_POLICY=fail-open` only when availability outweighs enforcement across this provider during an outage.",
432
445
  "",
433
446
  );
434
447
  }
@@ -510,6 +523,8 @@ export function envExample(ctx: TemplateContext): string {
510
523
  "UPSTASH_REDIS_REST_URL=https://dev-placeholder.upstash.io",
511
524
  "UPSTASH_REDIS_REST_TOKEN=dev-placeholder-change-me",
512
525
  "UPSTASH_PREFIX=beignet:ratelimit",
526
+ "# UPSTASH_TIMEOUT_MS=5000",
527
+ "# UPSTASH_TIMEOUT_POLICY=fail-closed",
513
528
  "",
514
529
  );
515
530
  }
@@ -518,6 +533,8 @@ export function envExample(ctx: TemplateContext): string {
518
533
  }
519
534
 
520
535
  const files = {
536
+ gitattributes: `**/drizzle/*.sql text eol=lf
537
+ `,
521
538
  gitignore: `.next
522
539
  node_modules
523
540
  dist
@@ -15,6 +15,7 @@ export type DbTemplateFiles = {
15
15
  drizzleTodoRepository: string;
16
16
  dbRepositories: string;
17
17
  databaseReady: string;
18
+ dbMigrationStatus: string;
18
19
  dbProvider: string;
19
20
  dbReset: string;
20
21
  dbTestDatabase: string;