@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
package/dist/inspect.js CHANGED
@@ -1,14 +1,19 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { parseProviderPackageMetadata } from "@beignet/core/providers";
4
+ import ts from "typescript";
5
+ import { contractFactoryBindings, isImportedContractFactoryCall, } from "./analysis/contract-factories.js";
6
+ import { canonicalFeatureLayerPath } from "./analysis/layers.js";
7
+ import { analyzePortWiringFiles } from "./analysis/port-wiring.js";
8
+ import { exportedVariableDeclarations } from "./analysis/source-index.js";
4
9
  import { createPainter } from "./ansi.js";
5
10
  import { clientDirPath, clientFormsPath, clientIndexPath, defaultBeignetConfig, directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
6
11
  import { renderDatabaseSchema, resolveDatabaseSchemaDialect } from "./db.js";
7
12
  import { applyPlannedDoctorFixes, createDoctorFixPlan, planDoctorFixFileChange, publicDoctorFixPlan, } from "./doctor-fixes.js";
8
13
  import { formatGithubAnnotation, } from "./github-annotations.js";
9
14
  import { createListenersRegistrySource, listenersFilePath, providersFilePath, updateListenersRegistrySource, wireListenersProviderSource, } from "./make/shared.js";
10
- import { detectedProviderVariants, diagnosticPackageJsonFile, formatProviderMetadataIssues, installedPackageNames, providerDoctorRulesForInstalledPackages, providerOperationalConfigFiles, providerRequiredEnvExists, providerRequiredTableExists, providerVariantRegistrationSeverity, readProviderListEntries, readProviderPackageMetadataSource, registrationTokenHint, serverProviderFiles, } from "./provider-audit.js";
11
- import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, codeCharacterIndexes, identifiersFromArrayExpression, insertAfterImports, matchingDelimiterIndex, outboxRegistryValueInfo, } from "./registry-edits.js";
15
+ import { detectedProviderVariants, diagnosticPackageJsonFile, formatProviderEnvAlternatives, formatProviderMetadataIssues, installedPackageNames, providerDoctorRulesForInstalledPackages, providerOperationalConfigFiles, providerRequiredEnvExists, providerRequiredTableExists, providerVariantRegistrationSeverity, readProviderListEntries, readProviderPackageMetadataSource, registrationTokenHint, serverProviderFiles, } from "./provider-audit.js";
16
+ import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, codeCharacterIndexes, identifiersFromArrayExpression, insertAfterImports, matchingDelimiterIndex, outboxRegistryEntryState, outboxRegistryIdentifiers, } from "./registry-edits.js";
12
17
  import { currentGeneratedPackageScripts, externalVersions, isLegacyGeneratedPackageScript, legacyGeneratedPackageScripts, } from "./templates/shared.js";
13
18
  import { testSupportTemplateFiles } from "./templates/testing.js";
14
19
  import { getCliVersion } from "./version.js";
@@ -20,9 +25,10 @@ export async function inspectApp(options = {}) {
20
25
  ? resolveConfig(options.config)
21
26
  : await loadBeignetConfig(targetDir, files);
22
27
  const convention = inspectConvention(files, config);
23
- const contracts = await readContracts(targetDir, files, config);
28
+ const inspectedContracts = await readContracts(targetDir, files, config);
29
+ const { contracts } = inspectedContracts;
24
30
  const matchedRoutes = await inspectRouteSurface(targetDir, files, config, contracts);
25
- const diagnostics = await inspectDiagnostics(targetDir, files, config, convention, contracts, matchedRoutes, Boolean(options.strict));
31
+ const diagnostics = await inspectDiagnostics(targetDir, files, config, convention, contracts, matchedRoutes, Boolean(options.strict), inspectedContracts.queryTransportDiagnostics);
26
32
  return {
27
33
  schemaVersion: 1,
28
34
  targetDir,
@@ -76,7 +82,7 @@ async function buildDoctorFixPlan(options = {}) {
76
82
  ? resolveConfig(options.config)
77
83
  : await loadBeignetConfig(targetDir, files);
78
84
  const convention = inspectConvention(files, config);
79
- const contracts = await readContracts(targetDir, files, config);
85
+ const { contracts } = await readContracts(targetDir, files, config);
80
86
  const matchedRoutes = await inspectRouteSurface(targetDir, files, config, contracts);
81
87
  const operations = [];
82
88
  const packageFix = await planPackageScripts(targetDir, files, convention);
@@ -318,7 +324,6 @@ export const productionHardeningDiagnosticCodes = new Set([
318
324
  "BEIGNET_BILLING_IDEMPOTENCY_PORT_MISSING",
319
325
  "BEIGNET_CORS_CREDENTIALS_WILDCARD",
320
326
  "BEIGNET_CRON_SECRET_MISSING",
321
- "BEIGNET_DEVTOOLS_ROUTE_EXPOSED",
322
327
  "BEIGNET_ENTITLEMENTS_PORT_MISSING",
323
328
  "BEIGNET_PAYMENTS_STRIPE_MEMORY_PROVIDER",
324
329
  "BEIGNET_PAYMENTS_WEBHOOK_ROUTE_MISSING",
@@ -409,12 +414,28 @@ async function listFiles(targetDir) {
409
414
  }
410
415
  async function readContracts(targetDir, files, config) {
411
416
  const contracts = [];
417
+ const queryTransportDiagnostics = [];
412
418
  const contractFiles = contractSourceFiles(files, config);
413
419
  for (const file of contractFiles) {
414
420
  const source = await readFile(path.join(targetDir, file), "utf8");
415
- contracts.push(...parseContracts(source, file));
421
+ for (const parsed of parseContracts(source, file)) {
422
+ contracts.push(parsed.contract);
423
+ if (!parsed.queryTransportMissing)
424
+ continue;
425
+ queryTransportDiagnostics.push({
426
+ severity: "error",
427
+ code: "BEIGNET_QUERY_TRANSPORT_MISSING",
428
+ file,
429
+ contract: parsed.contract.exportName,
430
+ subject: { file, exportName: parsed.contract.exportName },
431
+ message: `${parsed.contract.exportName} declares a query schema without an explicit query transport. Pass defineQueryTransport(...) as the second .query(...) argument so the client, server, and OpenAPI document share one wire format.`,
432
+ });
433
+ }
416
434
  }
417
- return contracts.sort(compareContracts);
435
+ return {
436
+ contracts: contracts.sort(compareContracts),
437
+ queryTransportDiagnostics,
438
+ };
418
439
  }
419
440
  function inspectConvention(files, config) {
420
441
  const missingNextLayout = missingRequirements(files, [
@@ -464,47 +485,77 @@ function hasRequirement(files, requirement) {
464
485
  }
465
486
  function parseContracts(source, file) {
466
487
  const contracts = [];
467
- const groupPrefixes = parseContractGroupPrefixes(source);
468
- const groupErrorRefs = parseContractGroupErrorRefs(source);
469
- const exportRegex = /(?:^|\n)export const\s+([A-Za-z_$][\w$]*)\s*=([\s\S]*?)(?=\nexport const\s+[A-Za-z_$][\w$]*\s*=|$)/g;
470
- for (const exportMatch of source.matchAll(exportRegex)) {
471
- const block = exportMatch[2];
472
- const methodMatch = /([A-Za-z_$][\w$]*)(?:\s*\.\s*[A-Za-z_$][\w$]*\([^)]*\))*\s*\.\s*(get|post|put|patch|delete|head|options)\(\s*["']([^"']+)["'](?:\s*,\s*["'][^"']+["'])?\s*\)/.exec(block);
473
- if (methodMatch) {
474
- const receiver = methodMatch[1];
475
- const pathPrefix = groupPrefixes.get(receiver) ?? "";
476
- const routePath = joinContractPaths(pathPrefix, methodMatch[3]);
477
- contracts.push({
478
- exportName: exportMatch[1],
479
- file,
480
- metadata: parseContractMetadata(block, groupErrorRefs.get(receiver) ?? []),
481
- method: methodMatch[2].toUpperCase(),
482
- path: routePath,
483
- });
488
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
489
+ const assignments = variableAssignments(sourceFile);
490
+ const assignmentsByName = new Map(assignments.map((assignment) => [assignment.name, assignment.expression]));
491
+ const factoryBindings = contractFactoryBindings(sourceFile);
492
+ for (const { exportName, declaration } of exportedVariableDeclarations(sourceFile)) {
493
+ if (!declaration.initializer)
484
494
  continue;
485
- }
486
- const directContract = parseDirectContract(block);
495
+ const chain = resolveContractCallChain(declaration.initializer, assignmentsByName);
496
+ if (!chain)
497
+ continue;
498
+ const directContract = parseDirectContract(chain, factoryBindings);
487
499
  if (directContract) {
488
500
  contracts.push({
489
- exportName: exportMatch[1],
490
- file,
491
- metadata: parseContractMetadata(block),
492
- method: directContract.method,
493
- path: normalizeContractPath(directContract.path),
501
+ contract: {
502
+ exportName,
503
+ file,
504
+ metadata: parseContractMetadata(applyContractMetadataCalls(emptyContractMetadataState(), chain.calls)),
505
+ method: directContract.method,
506
+ path: normalizeContractPath(directContract.path),
507
+ },
508
+ queryTransportMissing: contractQueryHasTransport(chain) === false,
494
509
  });
510
+ continue;
495
511
  }
512
+ const methodIndex = chain.calls.findIndex((call) => isHttpMethod(call.name.toUpperCase()));
513
+ if (methodIndex === -1)
514
+ continue;
515
+ const methodCall = chain.calls[methodIndex];
516
+ const routePath = staticString(methodCall.arguments[0]);
517
+ if (!routePath)
518
+ continue;
519
+ const hasGroupFactory = contractFactoryCall(chain, "defineContractGroup", factoryBindings);
520
+ if (!hasGroupFactory)
521
+ continue;
522
+ const pathPrefix = applyStaticPrefixes("", chain.calls.slice(0, methodIndex));
523
+ if (pathPrefix === undefined)
524
+ continue;
525
+ contracts.push({
526
+ contract: {
527
+ exportName,
528
+ file,
529
+ metadata: parseContractMetadata(applyContractMetadataCalls(emptyContractMetadataState(), chain.calls)),
530
+ method: methodCall.name.toUpperCase(),
531
+ path: joinContractPaths(pathPrefix, routePath),
532
+ },
533
+ queryTransportMissing: contractQueryHasTransport(chain) === false,
534
+ });
496
535
  }
497
536
  return contracts;
498
537
  }
499
- function parseContractMetadata(block, inheritedCatalogErrors = []) {
500
- const authRequired = /\bauth\s*:\s*["']required["']/.test(block);
501
- const authorizationAbility = /\bauthorization\s*:\s*\{[\s\S]*?\bability\s*:\s*["']([^"']+)["']/.exec(block)?.[1];
502
- const auditRequired = /\baudit\s*:\s*["']required["']/.test(block) ||
503
- /\bauditRequired\s*:\s*true\b/.test(block);
504
- const catalogErrors = dedupeCatalogErrorRefs([
505
- ...inheritedCatalogErrors,
506
- ...parseCatalogErrorRefs(block),
507
- ]);
538
+ function unwrapContractExpression(expression) {
539
+ let current = expression;
540
+ while (ts.isParenthesizedExpression(current) ||
541
+ ts.isAsExpression(current) ||
542
+ ts.isTypeAssertionExpression(current) ||
543
+ ts.isNonNullExpression(current) ||
544
+ ts.isSatisfiesExpression(current)) {
545
+ current = current.expression;
546
+ }
547
+ return current;
548
+ }
549
+ function emptyContractMetadataState() {
550
+ return { catalogErrors: [] };
551
+ }
552
+ function parseContractMetadata(state) {
553
+ const authRequired = state.authRequired === true;
554
+ const authorizationAbility = state.authorizationAbility;
555
+ const auditRequired = state.audit === true ||
556
+ state.auditRequired === true ||
557
+ state.authorizationAuditRequired === true;
558
+ const catalogErrors = state.catalogErrors;
508
559
  if (!authRequired &&
509
560
  !authorizationAbility &&
510
561
  !auditRequired &&
@@ -518,100 +569,271 @@ function parseContractMetadata(block, inheritedCatalogErrors = []) {
518
569
  catalogErrors,
519
570
  };
520
571
  }
521
- function parseDirectContract(block) {
522
- const configMatch = /defineContract\s*\(\s*\{([\s\S]*?)\}\s*\)/.exec(block);
523
- if (!configMatch)
572
+ function applyContractMetadataCalls(initialState, calls) {
573
+ const state = {
574
+ ...initialState,
575
+ catalogErrors: [...initialState.catalogErrors],
576
+ };
577
+ for (const call of calls) {
578
+ if (call.name === "meta") {
579
+ applyContractMetaObject(state, objectLiteral(call.arguments[0]));
580
+ continue;
581
+ }
582
+ if (call.name === "errors") {
583
+ state.catalogErrors = applyCatalogErrorCall(state.catalogErrors, call.arguments[0]);
584
+ }
585
+ }
586
+ return state;
587
+ }
588
+ function applyContractMetaObject(state, metadata) {
589
+ if (!metadata) {
590
+ state.authRequired = undefined;
591
+ state.authorizationAbility = undefined;
592
+ state.authorizationAuditRequired = undefined;
593
+ state.audit = undefined;
594
+ state.auditRequired = undefined;
595
+ return;
596
+ }
597
+ const auth = objectPropertyResult(metadata, "auth");
598
+ if (auth.kind !== "absent") {
599
+ state.authRequired =
600
+ auth.kind === "value" ? staticRequiredValue(auth.value) : undefined;
601
+ }
602
+ const authorization = objectPropertyResult(metadata, "authorization");
603
+ if (authorization.kind !== "absent") {
604
+ const value = authorization.kind === "value"
605
+ ? objectLiteral(authorization.value)
606
+ : undefined;
607
+ if (!value) {
608
+ state.authorizationAbility = undefined;
609
+ state.authorizationAuditRequired = undefined;
610
+ }
611
+ else {
612
+ const ability = objectPropertyResult(value, "ability");
613
+ state.authorizationAbility =
614
+ ability.kind === "value" ? staticString(ability.value) : undefined;
615
+ state.authorizationAuditRequired = objectRequiresAudit(value);
616
+ }
617
+ }
618
+ const audit = objectPropertyResult(metadata, "audit");
619
+ const auditRequired = objectPropertyResult(metadata, "auditRequired");
620
+ if (audit.kind !== "absent") {
621
+ state.audit =
622
+ audit.kind === "value" ? staticRequiredValue(audit.value) : undefined;
623
+ }
624
+ if (auditRequired.kind !== "absent") {
625
+ state.auditRequired =
626
+ auditRequired.kind === "value"
627
+ ? staticBoolean(auditRequired.value)
628
+ : undefined;
629
+ }
630
+ }
631
+ function objectRequiresAudit(metadata) {
632
+ const audit = objectPropertyResult(metadata, "audit");
633
+ const auditRequired = objectPropertyResult(metadata, "auditRequired");
634
+ if (audit.kind === "unknown" || auditRequired.kind === "unknown") {
635
+ return undefined;
636
+ }
637
+ const auditValue = audit.kind === "value" ? staticRequiredValue(audit.value) : false;
638
+ const auditRequiredValue = auditRequired.kind === "value" ? staticBoolean(auditRequired.value) : false;
639
+ if (auditValue === undefined || auditRequiredValue === undefined) {
640
+ return undefined;
641
+ }
642
+ return auditValue || auditRequiredValue;
643
+ }
644
+ function staticRequiredValue(expression) {
645
+ const value = staticString(expression);
646
+ return value === undefined ? undefined : value === "required";
647
+ }
648
+ function staticBoolean(expression) {
649
+ if (expression.kind === ts.SyntaxKind.TrueKeyword)
650
+ return true;
651
+ if (expression.kind === ts.SyntaxKind.FalseKeyword)
652
+ return false;
653
+ return undefined;
654
+ }
655
+ function parseDirectContract(chain, factoryBindings) {
656
+ const factoryCall = contractFactoryCall(chain, "defineContract", factoryBindings);
657
+ if (!factoryCall)
658
+ return undefined;
659
+ const options = objectLiteral(factoryCall.arguments[0]);
660
+ if (!options)
524
661
  return undefined;
525
- const configSource = configMatch[1];
526
- const methodMatch = /method\s*:\s*["']([^"']+)["']/.exec(configSource);
527
- const pathMatch = /path\s*:\s*["']([^"']+)["']/.exec(configSource);
528
- if (!methodMatch || !pathMatch)
662
+ const methodValue = staticString(objectProperty(options, "method"));
663
+ const contractPath = staticString(objectProperty(options, "path"));
664
+ if (!methodValue || !contractPath)
529
665
  return undefined;
530
- const method = methodMatch[1].toUpperCase();
666
+ const method = methodValue.toUpperCase();
531
667
  if (!isHttpMethod(method))
532
668
  return undefined;
533
- return { method, path: pathMatch[1] };
669
+ return { method, path: contractPath };
534
670
  }
535
671
  function isHttpMethod(method) {
536
672
  return ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"].includes(method);
537
673
  }
538
- function parseContractGroupPrefixes(source) {
539
- const assignments = [
540
- ...source.matchAll(/(?:^|\n)(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*([\s\S]*?);/g),
541
- ].map((match) => ({
542
- name: match[1],
543
- expression: match[2],
544
- }));
545
- const prefixes = new Map();
546
- let changed = true;
547
- while (changed) {
548
- changed = false;
549
- for (const assignment of assignments) {
550
- if (prefixes.has(assignment.name))
551
- continue;
552
- const basePrefix = baseGroupPrefix(assignment.expression, prefixes);
553
- if (basePrefix === undefined)
674
+ function variableAssignments(sourceFile) {
675
+ return sourceFile.statements
676
+ .filter(ts.isVariableStatement)
677
+ .flatMap((statement) => [...statement.declarationList.declarations]
678
+ .filter((declaration) => ts.isIdentifier(declaration.name) &&
679
+ declaration.initializer !== undefined)
680
+ .map((declaration) => ({
681
+ name: declaration.name.text,
682
+ expression: declaration.initializer,
683
+ })));
684
+ }
685
+ function resolveContractCallChain(expression, assignments, resolving = new Set()) {
686
+ const chain = contractCallChain(expression);
687
+ if (!chain || chain.rootCall || !chain.baseIdentifier)
688
+ return chain;
689
+ const baseExpression = assignments.get(chain.baseIdentifier);
690
+ if (!baseExpression || resolving.has(chain.baseIdentifier))
691
+ return chain;
692
+ const nextResolving = new Set(resolving);
693
+ nextResolving.add(chain.baseIdentifier);
694
+ const baseChain = resolveContractCallChain(baseExpression, assignments, nextResolving);
695
+ if (!baseChain)
696
+ return chain;
697
+ return {
698
+ ...baseChain,
699
+ calls: [...baseChain.calls, ...chain.calls],
700
+ };
701
+ }
702
+ function contractCallChain(rawExpression) {
703
+ const calls = [];
704
+ let expression = rawExpression;
705
+ while (true) {
706
+ expression = unwrapContractExpression(expression);
707
+ if (ts.isPropertyAccessExpression(expression)) {
708
+ expression = expression.expression;
709
+ continue;
710
+ }
711
+ if (ts.isCallExpression(expression)) {
712
+ const callee = unwrapContractExpression(expression.expression);
713
+ if (ts.isPropertyAccessExpression(callee)) {
714
+ calls.unshift({
715
+ name: callee.name.text,
716
+ arguments: [...expression.arguments],
717
+ node: expression,
718
+ });
719
+ expression = callee.expression;
554
720
  continue;
555
- const prefix = prefixCalls(assignment.expression).reduce((current, next) => joinContractPaths(current, next), basePrefix);
556
- prefixes.set(assignment.name, prefix);
557
- changed = true;
721
+ }
722
+ if (ts.isIdentifier(callee)) {
723
+ return {
724
+ baseIdentifier: callee.text,
725
+ rootCall: {
726
+ name: callee.text,
727
+ arguments: [...expression.arguments],
728
+ node: expression,
729
+ },
730
+ calls,
731
+ };
732
+ }
733
+ return undefined;
734
+ }
735
+ if (ts.isIdentifier(expression)) {
736
+ return { baseIdentifier: expression.text, calls };
558
737
  }
738
+ return undefined;
559
739
  }
560
- return prefixes;
561
740
  }
562
- function parseContractGroupErrorRefs(source) {
563
- const assignments = [
564
- ...source.matchAll(/(?:^|\n)(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*([\s\S]*?);/g),
565
- ].map((match) => ({
566
- name: match[1],
567
- expression: match[2],
568
- }));
569
- const groupErrors = new Map();
570
- let changed = true;
571
- while (changed) {
572
- changed = false;
573
- for (const assignment of assignments) {
574
- if (groupErrors.has(assignment.name))
575
- continue;
576
- const baseErrors = baseGroupErrors(assignment.expression, groupErrors);
577
- if (baseErrors === undefined)
578
- continue;
579
- groupErrors.set(assignment.name, dedupeCatalogErrorRefs([
580
- ...baseErrors,
581
- ...parseCatalogErrorRefs(assignment.expression),
582
- ]));
583
- changed = true;
584
- }
741
+ function contractFactoryCall(chain, name, factoryBindings) {
742
+ const calls = chain.rootCall ? [chain.rootCall] : chain.calls;
743
+ return calls.find((call) => isImportedContractFactoryCall(call.node, name, factoryBindings));
744
+ }
745
+ function applyStaticPrefixes(initialPrefix, calls) {
746
+ let prefix = initialPrefix;
747
+ for (const call of calls) {
748
+ if (call.name !== "prefix")
749
+ continue;
750
+ const next = staticString(call.arguments[0]);
751
+ if (next === undefined)
752
+ return undefined;
753
+ prefix = joinContractPaths(prefix, next);
585
754
  }
586
- return groupErrors;
755
+ return prefix;
756
+ }
757
+ function contractQueryHasTransport(chain) {
758
+ const queryCall = chain.calls.find((call) => call.name === "query");
759
+ return queryCall ? queryCall.arguments.length >= 2 : undefined;
587
760
  }
588
- function baseGroupErrors(expression, groupErrors) {
589
- if (expression.includes("defineContractGroup("))
761
+ function applyCatalogErrorCall(current, expression) {
762
+ const errors = objectLiteral(expression);
763
+ if (!errors)
590
764
  return [];
591
- const baseMatch = /^\s*([A-Za-z_$][\w$]*)\b/.exec(expression);
592
- if (!baseMatch)
593
- return undefined;
594
- return groupErrors.get(baseMatch[1]);
765
+ let merged = [...current];
766
+ for (const property of errors.properties) {
767
+ if (ts.isSpreadAssignment(property)) {
768
+ merged = [];
769
+ continue;
770
+ }
771
+ const key = staticPropertyName(property.name);
772
+ if (!key) {
773
+ merged = [];
774
+ continue;
775
+ }
776
+ merged = merged.filter((ref) => ref.key !== key);
777
+ if (!ts.isPropertyAssignment(property))
778
+ continue;
779
+ const value = unwrapContractExpression(property.initializer);
780
+ if (!ts.isPropertyAccessExpression(value) ||
781
+ !ts.isIdentifier(value.expression)) {
782
+ continue;
783
+ }
784
+ merged.push({
785
+ key,
786
+ catalog: value.expression.text,
787
+ catalogKey: value.name.text,
788
+ });
789
+ }
790
+ return merged;
595
791
  }
596
- function baseGroupPrefix(expression, prefixes) {
597
- if (expression.includes("defineContractGroup("))
598
- return "";
599
- const baseMatch = /^\s*([A-Za-z_$][\w$]*)\b/.exec(expression);
600
- if (!baseMatch)
792
+ function objectLiteral(expression) {
793
+ if (!expression)
601
794
  return undefined;
602
- return prefixes.get(baseMatch[1]);
603
- }
604
- function prefixCalls(expression) {
605
- return [...expression.matchAll(/\.prefix\(\s*["']([^"']+)["']\s*\)/g)].map((match) => match[1]);
795
+ const unwrapped = unwrapContractExpression(expression);
796
+ return ts.isObjectLiteralExpression(unwrapped) ? unwrapped : undefined;
797
+ }
798
+ function objectProperty(object, name) {
799
+ const result = objectPropertyResult(object, name);
800
+ return result.kind === "value" ? result.value : undefined;
801
+ }
802
+ function objectPropertyResult(object, name) {
803
+ for (const property of [...object.properties].reverse()) {
804
+ if (ts.isSpreadAssignment(property))
805
+ return { kind: "unknown" };
806
+ const propertyName = staticPropertyName(property.name);
807
+ if (propertyName === undefined)
808
+ return { kind: "unknown" };
809
+ if (propertyName !== name)
810
+ continue;
811
+ if (ts.isPropertyAssignment(property)) {
812
+ return {
813
+ kind: "value",
814
+ value: unwrapContractExpression(property.initializer),
815
+ };
816
+ }
817
+ return { kind: "unknown" };
818
+ }
819
+ return { kind: "absent" };
820
+ }
821
+ function staticPropertyName(name) {
822
+ if (ts.isIdentifier(name) ||
823
+ ts.isStringLiteral(name) ||
824
+ ts.isNumericLiteral(name)) {
825
+ return name.text;
826
+ }
827
+ return undefined;
606
828
  }
607
- function parseCatalogErrorRefs(source) {
608
- return dedupeCatalogErrorRefs(objectArgumentBodies(source, /\.errors\s*\(/g).flatMap((body) => [...body.matchAll(errorCatalogRefRegex)].map((match) => ({
609
- key: match[1],
610
- catalog: match[2],
611
- catalogKey: match[3],
612
- }))));
829
+ function staticString(expression) {
830
+ if (!expression)
831
+ return undefined;
832
+ const value = unwrapContractExpression(expression);
833
+ return ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value)
834
+ ? value.text
835
+ : undefined;
613
836
  }
614
- const errorCatalogRefRegex = /(?:^|[,{])\s*([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)\s*\.\s*([A-Za-z_$][\w$]*)/g;
615
837
  function parseErrorCatalogKeys(source) {
616
838
  const keys = new Set();
617
839
  const catalogBodies = objectArgumentBodies(source, /\bdefineErrors\s*\(/g);
@@ -691,18 +913,6 @@ function topLevelObjectKeys(body) {
691
913
  }
692
914
  return keys;
693
915
  }
694
- function dedupeCatalogErrorRefs(refs) {
695
- const seen = new Set();
696
- const deduped = [];
697
- for (const ref of refs) {
698
- const key = `${ref.key}:${ref.catalog}:${ref.catalogKey}`;
699
- if (seen.has(key))
700
- continue;
701
- seen.add(key);
702
- deduped.push(ref);
703
- }
704
- return deduped;
705
- }
706
916
  function joinContractPaths(prefix, routePath) {
707
917
  if (!prefix)
708
918
  return normalizeContractPath(routePath);
@@ -985,8 +1195,8 @@ function contractPathMatchesCatchAll(contractPath, catchAllPrefix) {
985
1195
  return (contractPath === catchAllPrefix ||
986
1196
  contractPath.startsWith(`${catchAllPrefix}/`));
987
1197
  }
988
- async function inspectDiagnostics(targetDir, files, config, convention, contracts, matchedRoutes, strict) {
989
- const diagnostics = [];
1198
+ async function inspectDiagnostics(targetDir, files, config, convention, contracts, matchedRoutes, strict, queryTransportDiagnostics) {
1199
+ const diagnostics = [...queryTransportDiagnostics];
990
1200
  const frameworkLayout = config.framework === "next" ? convention.nextLayout : convention.webLayout;
991
1201
  const missingFrameworkLayout = config.framework === "next"
992
1202
  ? convention.missingNextLayout
@@ -1025,7 +1235,7 @@ async function inspectDiagnostics(targetDir, files, config, convention, contract
1025
1235
  diagnostics.push(unmatchedRouteDiagnostic(routeHandler));
1026
1236
  }
1027
1237
  }
1028
- diagnostics.push(...(await inspectPackageScripts(targetDir, files, convention)), ...(await inspectOpenApiDrift(targetDir, files, config, convention, contracts, matchedRoutes)), ...(await inspectFeatureRouteRegistration(targetDir, files, config, convention, contracts)), ...(await inspectRouteErrorCatalogDrift(targetDir, files, config, convention, contracts, strict)), ...(await inspectCanonicalConformance(targetDir, files, config, convention, strict)), ...(await inspectVersionSkew(targetDir, files)), ...(await inspectProviderMetadataDrift(targetDir, files, convention)), ...(await inspectPortProviderDrift(targetDir, files, config, convention)), ...(await inspectProviderRegistrationDrift(targetDir, files, config, convention)), ...(await inspectInngestProductionPath(targetDir, files, config)), ...(await inspectDatabaseLifecycleDrift(targetDir, files, config, convention, strict)), ...(await inspectAuthzAuditDrift(targetDir, files, config, convention, contracts)), ...(await inspectServerlessFootguns(targetDir, files, config, convention)), ...(await inspectProductionReadiness(targetDir, files, config, convention)), ...(await inspectFeatureArtifactDrift(targetDir, files, config, convention)), ...(await inspectWorkflowRegistrationDrift(targetDir, files, config, convention)), ...(await inspectRuntimeManifestDrift(targetDir, files, config, convention)), ...(await inspectResourceSlices(targetDir, files, config, convention, strict)));
1238
+ diagnostics.push(...(await inspectPackageScripts(targetDir, files, convention)), ...(await inspectOpenApiDrift(targetDir, files, config, convention, contracts, matchedRoutes)), ...(await inspectFeatureRouteRegistration(targetDir, files, config, convention, contracts)), ...(await inspectRouteErrorCatalogDrift(targetDir, files, config, convention, contracts, strict)), ...(await inspectCanonicalConformance(targetDir, files, config, convention, strict)), ...(await inspectVersionSkew(targetDir, files)), ...(await inspectProviderMetadataDrift(targetDir, files, convention)), ...(await inspectPortProviderDrift(targetDir, files, config, convention)), ...(await inspectProviderRegistrationDrift(targetDir, files, config, convention)), ...(await inspectInngestProductionPath(targetDir, files, config)), ...(await inspectDatabaseLifecycleDrift(targetDir, files, config, convention, strict)), ...(await inspectAuthzAuditDrift(targetDir, files, config, convention, contracts)), ...(await inspectServerlessFootguns(targetDir, files, config, convention)), ...(await inspectOutboxJobDispatcherDrift(targetDir, files, config)), ...(await inspectProductionReadiness(targetDir, files, config, convention)), ...(await inspectFeatureArtifactDrift(targetDir, files, config, convention)), ...(await inspectWorkflowRegistrationDrift(targetDir, files, config, convention)), ...(await inspectRuntimeManifestDrift(targetDir, files, config, convention)), ...(await inspectResourceSlices(targetDir, files, config, convention, strict)));
1029
1239
  return dedupeDiagnostics(diagnostics);
1030
1240
  }
1031
1241
  async function inspectFrameworkAdapter(targetDir, files, config) {
@@ -1485,13 +1695,13 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
1485
1695
  hasSecurityHeadersHook = true;
1486
1696
  }
1487
1697
  if (containsCallExpression(source, "createDevtoolsRoute") &&
1488
- /\benabled\s*:\s*true\b/.test(source) &&
1489
- !/\bauthorize\s*:/.test(source)) {
1698
+ !/\bauthorize\s*(?::|\()/.test(source) &&
1699
+ !/\ballowUnauthenticatedDevelopmentAccess\s*:\s*true\b/.test(source)) {
1490
1700
  diagnostics.push({
1491
1701
  severity: "warning",
1492
- code: "BEIGNET_DEVTOOLS_ROUTE_EXPOSED",
1702
+ code: "BEIGNET_DEVTOOLS_ROUTE_POLICY_MISSING",
1493
1703
  file,
1494
- message: `${file} enables devtools with enabled: true and no authorize callback. Keep devtools disabled in production or add app-owned authorization before exposing diagnostic data.`,
1704
+ message: `${file} creates a devtools route without an access policy, so the route remains hidden. Add app-owned authorize(...), or explicitly set allowUnauthenticatedDevelopmentAccess: true only for a development server bound exclusively to loopback.`,
1495
1705
  });
1496
1706
  }
1497
1707
  if (fileWithinDirectory(file, cronDirectory) &&
@@ -1576,6 +1786,16 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
1576
1786
  // Multi-variant packages only require the env vars of the variants the
1577
1787
  // app actually registers; an unused variant must not warn.
1578
1788
  for (const variant of detectedProviderVariants(provider.variants, providerEntries)) {
1789
+ const envAlternatives = configuredProviderRequiredEnvAlternatives(variant.requiredEnvAlternatives, config);
1790
+ if (envAlternatives.length > 0 &&
1791
+ !(await providerRequiredEnvAlternativeExists(targetDir, configFiles, envAlternatives, sourceCache))) {
1792
+ diagnostics.push({
1793
+ severity: "warning",
1794
+ code: "BEIGNET_PROVIDER_ENV_MISSING",
1795
+ file: "package.json",
1796
+ message: `${provider.packageName} is installed and ${variant.displayName} is registered, but none of its required environment configurations are fully mentioned in app env/config files. Configure ${formatProviderEnvAlternatives(envAlternatives)}, or remove the provider registration.`,
1797
+ });
1798
+ }
1579
1799
  for (const envVar of configuredProviderRequiredEnv(variant.requiredEnv, config)) {
1580
1800
  if (await providerRequiredEnvExists(targetDir, configFiles, envVar, sourceCache)) {
1581
1801
  continue;
@@ -1601,6 +1821,16 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
1601
1821
  }
1602
1822
  continue;
1603
1823
  }
1824
+ const envAlternatives = configuredProviderRequiredEnvAlternatives(provider.requiredEnvAlternatives ?? [], config);
1825
+ if (envAlternatives.length > 0 &&
1826
+ !(await providerRequiredEnvAlternativeExists(targetDir, configFiles, envAlternatives, sourceCache))) {
1827
+ diagnostics.push({
1828
+ severity: "warning",
1829
+ code: "BEIGNET_PROVIDER_ENV_MISSING",
1830
+ file: "package.json",
1831
+ message: `${provider.packageName} is installed, but none of its required environment configurations are fully mentioned in app env/config files. Configure ${formatProviderEnvAlternatives(envAlternatives)}, or remove the unused provider package.`,
1832
+ });
1833
+ }
1604
1834
  for (const envVar of configuredProviderRequiredEnv(provider.requiredEnv ?? [], config)) {
1605
1835
  if (await providerRequiredEnvExists(targetDir, configFiles, envVar, sourceCache)) {
1606
1836
  continue;
@@ -2296,7 +2526,7 @@ async function inspectFeatureArtifactDrift(targetDir, files, config, convention)
2296
2526
  });
2297
2527
  }
2298
2528
  for (const file of files) {
2299
- const match = new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.test\\.ts$`).exec(file);
2529
+ const match = directFeatureTestMatch(file, featuresPath);
2300
2530
  if (!match)
2301
2531
  continue;
2302
2532
  diagnostics.push({
@@ -2307,20 +2537,25 @@ async function inspectFeatureArtifactDrift(targetDir, files, config, convention)
2307
2537
  });
2308
2538
  }
2309
2539
  for (const file of files) {
2310
- if (!file.endsWith(".ts") ||
2311
- file.endsWith(".test.ts") ||
2312
- file.endsWith(".d.ts")) {
2540
+ if (!/\.(?:ts|tsx|mts|cts)$/.test(file) ||
2541
+ /\.d\.(?:ts|mts|cts)$/.test(file)) {
2313
2542
  continue;
2314
2543
  }
2315
- const misplaced = misplacedFeatureArtifactFolder(file, featuresPath);
2544
+ const misplaced = directFeatureTestMatch(file, featuresPath)
2545
+ ? undefined
2546
+ : misplacedFeatureLayer(file, config);
2316
2547
  if (misplaced) {
2317
2548
  diagnostics.push({
2318
2549
  severity: "warning",
2319
2550
  code: "BEIGNET_FEATURE_ARTIFACT_FOLDER",
2320
2551
  file,
2321
- message: `${file} uses ${misplaced.actual}, but Beignet expects ${misplaced.expected}. Keep feature-owned workflow artifacts in the canonical feature folders so CLI generators, docs, and architecture lint stay aligned.`,
2552
+ message: `${file} uses ${misplaced.actual}, but Beignet expects ${misplaced.expected}. Keep feature-owned files in the canonical feature folders so CLI generators, docs, and architecture lint stay aligned.`,
2322
2553
  });
2323
2554
  }
2555
+ if (isTestSourceFile(file))
2556
+ continue;
2557
+ if (!file.endsWith(".ts"))
2558
+ continue;
2324
2559
  if (isNotificationDefinitionFile(file, featuresPath) ||
2325
2560
  isInfraOrServerFile(file, config)) {
2326
2561
  continue;
@@ -2337,6 +2572,9 @@ async function inspectFeatureArtifactDrift(targetDir, files, config, convention)
2337
2572
  }
2338
2573
  return diagnostics;
2339
2574
  }
2575
+ function directFeatureTestMatch(file, featuresPath) {
2576
+ return new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/[^/]+\\.(?:test|spec)\\.(?:ts|tsx|mts|cts)$`).exec(file);
2577
+ }
2340
2578
  function isFeatureArtifactFile(file, featuresPath, folder) {
2341
2579
  const match = new RegExp(`^${escapeRegExp(featuresPath)}/[^/]+/${folder}/([^/]+)\\.ts$`).exec(file);
2342
2580
  return Boolean(match && match[1] !== "index");
@@ -2355,28 +2593,23 @@ async function appHasUploadRoute(targetDir, files, config) {
2355
2593
  }
2356
2594
  return false;
2357
2595
  }
2358
- function misplacedFeatureArtifactFolder(file, featuresPath) {
2359
- const match = new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/([^/]+)/`).exec(file);
2596
+ function misplacedFeatureLayer(file, config) {
2597
+ const featuresPath = directoryPath(config.paths.features);
2598
+ const match = new RegExp(`^${escapeRegExp(featuresPath)}/([^/]+)/(.+)$`).exec(file);
2360
2599
  if (!match)
2361
2600
  return undefined;
2362
- const folder = match[2];
2363
- const mappings = {
2364
- command: "tasks/",
2365
- commands: "tasks/",
2366
- event: "domain/events/",
2367
- events: "domain/events/",
2368
- job: "jobs/",
2369
- listener: "listeners/",
2370
- notification: "notifications/",
2371
- schedule: "schedules/",
2372
- scheduled: "schedules/",
2373
- task: "tasks/",
2374
- upload: "uploads/",
2375
- };
2376
- const expected = mappings[folder];
2377
- if (!expected)
2601
+ const canonicalPath = canonicalFeatureLayerPath(file, config);
2602
+ if (!canonicalPath)
2378
2603
  return undefined;
2379
- return { actual: `${folder}/`, expected };
2604
+ const featurePrefix = `${featuresPath}/${match[1]}/`;
2605
+ const actualRelative = file.slice(featurePrefix.length);
2606
+ const expectedRelative = canonicalPath.slice(featurePrefix.length);
2607
+ const actual = actualRelative.includes("/")
2608
+ ? `${actualRelative.split("/")[0]}/`
2609
+ : actualRelative;
2610
+ const expectedDirectory = path.posix.dirname(expectedRelative);
2611
+ const expected = expectedDirectory === "." ? expectedRelative : `${expectedDirectory}/`;
2612
+ return { actual, expected };
2380
2613
  }
2381
2614
  function isNotificationDefinitionFile(file, featuresPath) {
2382
2615
  return isFeatureArtifactFile(file, featuresPath, "notifications");
@@ -2596,17 +2829,11 @@ async function workflowRegistrationDrift(targetDir, files, config) {
2596
2829
  // Events without listeners have nothing to deliver on drain, so only
2597
2830
  // record-only events stay out of the registry without a warning.
2598
2831
  const listened = await listenedEventNames(targetDir, files, config);
2599
- const value = outboxRegistryValueInfo(outboxSource, "events");
2600
- const registered = value
2601
- ? identifiersFromArrayExpression(value.text)
2602
- : new Set();
2832
+ const registered = outboxRegistryIdentifiers(outboxSource, "events");
2603
2833
  drift.outbox.events = unregisteredWorkflowRegistries(eventRegistries, registered, (member) => listened.has(member));
2604
2834
  }
2605
2835
  if (jobRegistries.length > 0) {
2606
- const value = outboxRegistryValueInfo(outboxSource, "jobs");
2607
- const registered = value
2608
- ? identifiersFromArrayExpression(value.text)
2609
- : new Set();
2836
+ const registered = outboxRegistryIdentifiers(outboxSource, "jobs");
2610
2837
  drift.outbox.jobs = unregisteredWorkflowRegistries(jobRegistries, registered);
2611
2838
  }
2612
2839
  }
@@ -3094,6 +3321,45 @@ async function appHasOutboxDrainEntrypoint(targetDir, files, config) {
3094
3321
  }
3095
3322
  return false;
3096
3323
  }
3324
+ async function inspectOutboxJobDispatcherDrift(targetDir, files, config) {
3325
+ if (!hasOutboxRegistry(files, config))
3326
+ return [];
3327
+ const source = await readFile(path.join(targetDir, config.paths.outbox), "utf8");
3328
+ const jobsState = outboxRegistryEntryState(source, "jobs");
3329
+ if (jobsState === "absent" || jobsState === "empty")
3330
+ return [];
3331
+ const analysis = await analyzePortWiringFiles({ targetDir, files, config });
3332
+ const declaresJobs = analysis.declared.has("jobs");
3333
+ const wiresJobs = analysis.bound.has("jobs") || analysis.deferred.has("jobs");
3334
+ if (declaresJobs && wiresJobs)
3335
+ return [];
3336
+ const file = jobsState === "indeterminate"
3337
+ ? config.paths.outbox
3338
+ : !declaresJobs
3339
+ ? config.paths.ports
3340
+ : config.paths.portWiring;
3341
+ const reason = !analysis.portsFileExists
3342
+ ? `${config.paths.ports} does not exist at the configured path`
3343
+ : !declaresJobs
3344
+ ? analysis.declarationIndeterminate
3345
+ ? `${config.paths.ports} does not expose a statically verifiable jobs declaration`
3346
+ : `${config.paths.ports} does not declare a jobs port`
3347
+ : !analysis.portWiringFileExists
3348
+ ? `${config.paths.portWiring} does not exist at the configured path`
3349
+ : analysis.indeterminate
3350
+ ? `${config.paths.portWiring} does not expose statically verifiable jobs wiring`
3351
+ : `${config.paths.portWiring} does not bind or defer the jobs port`;
3352
+ return [
3353
+ {
3354
+ severity: "warning",
3355
+ code: "BEIGNET_OUTBOX_JOB_DISPATCHER_MISSING",
3356
+ file,
3357
+ message: jobsState === "indeterminate"
3358
+ ? `The jobs entries in the outbox registry at ${config.paths.outbox} could not be statically resolved, and ${reason}. Keep the jobs property explicit so doctor can verify it, or add jobs: JobDispatcherPort to AppPorts and bind it directly or list "jobs" as deferred with an inline or provider-backed dispatcher. If the app uses custom paths, configure paths.ports and paths.portWiring in beignet.config.ts.`
3359
+ : `The outbox registry in ${config.paths.outbox} contains jobs, but ${reason}. Add jobs: JobDispatcherPort to AppPorts and bind it directly or list "jobs" as deferred with an inline or provider-backed dispatcher. If the app uses custom paths, configure paths.ports and paths.portWiring in beignet.config.ts.`,
3360
+ },
3361
+ ];
3362
+ }
3097
3363
  async function inspectPortProviderDrift(targetDir, files, config, convention) {
3098
3364
  if (!convention.resourceGenerator)
3099
3365
  return [];
@@ -3187,7 +3453,7 @@ async function inspectDatabaseLifecycleDrift(targetDir, files, config, conventio
3187
3453
  });
3188
3454
  }
3189
3455
  if (hasDrizzleArtifacts) {
3190
- for (const script of ["db:generate", "db:migrate"]) {
3456
+ for (const script of ["db:generate", "db:migrate", "db:status"]) {
3191
3457
  if (packageScripts[script])
3192
3458
  continue;
3193
3459
  diagnostics.push({
@@ -3246,6 +3512,12 @@ async function inspectDatabaseLifecycleDrift(targetDir, files, config, conventio
3246
3512
  file: `${infrastructurePath}/db/reset.ts`,
3247
3513
  code: "BEIGNET_DB_RESET_ENTRYPOINT_MISSING",
3248
3514
  },
3515
+ {
3516
+ script: "db:status",
3517
+ command: "status",
3518
+ file: `${infrastructurePath}/db/migration-status.ts`,
3519
+ code: "BEIGNET_DB_STATUS_ENTRYPOINT_MISSING",
3520
+ },
3249
3521
  ];
3250
3522
  for (const entrypoint of standardEntrypoints) {
3251
3523
  const script = packageScripts[entrypoint.script];
@@ -3881,6 +4153,17 @@ function configuredProviderRequiredEnv(envVars, config) {
3881
4153
  const ignored = new Set(config.providerAudit.ignoreRequiredEnv);
3882
4154
  return envVars.filter((envVar) => !ignored.has(envVar));
3883
4155
  }
4156
+ function configuredProviderRequiredEnvAlternatives(alternatives, config) {
4157
+ return alternatives.map((alternative) => configuredProviderRequiredEnv(alternative, config));
4158
+ }
4159
+ async function providerRequiredEnvAlternativeExists(targetDir, configFiles, alternatives, sourceCache) {
4160
+ for (const alternative of alternatives) {
4161
+ const exists = await Promise.all(alternative.map((envVar) => providerRequiredEnvExists(targetDir, configFiles, envVar, sourceCache)));
4162
+ if (exists.every(Boolean))
4163
+ return true;
4164
+ }
4165
+ return false;
4166
+ }
3884
4167
  function configuredProviderRequiredTable(tableName, config) {
3885
4168
  for (const [key, defaultTableName] of Object.entries(defaultBeignetConfig.database.tables)) {
3886
4169
  if (tableName === defaultTableName)
@@ -5239,7 +5522,8 @@ async function contractsFromRoutesSource(targetDir, files, config, file, source,
5239
5522
  return undefined;
5240
5523
  const routeGroups = await readFeatureRouteGroups(targetDir, files, config);
5241
5524
  const registeredGroups = await registeredRouteGroupsForSource(targetDir, files, file, source, routeIdentifier);
5242
- const registeredContracts = contractsForRegisteredRouteGroups(await readContracts(targetDir, files, config), routeGroups, registeredGroups);
5525
+ const { contracts } = await readContracts(targetDir, files, config);
5526
+ const registeredContracts = contractsForRegisteredRouteGroups(contracts, routeGroups, registeredGroups);
5243
5527
  return new Set(registeredContracts.map((contract) => contract.exportName));
5244
5528
  }
5245
5529
  function contractsFromRoutesIdentifier(source, exportName) {