@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.
- package/CHANGELOG.md +46 -0
- package/README.md +126 -42
- package/dist/analysis/contract-factories.d.ts +17 -0
- package/dist/analysis/contract-factories.d.ts.map +1 -0
- package/dist/analysis/contract-factories.js +176 -0
- package/dist/analysis/contract-factories.js.map +1 -0
- package/dist/analysis/layers.d.ts +5 -0
- package/dist/analysis/layers.d.ts.map +1 -0
- package/dist/analysis/layers.js +172 -0
- package/dist/analysis/layers.js.map +1 -0
- package/dist/analysis/port-wiring.d.ts +23 -0
- package/dist/analysis/port-wiring.d.ts.map +1 -0
- package/dist/analysis/port-wiring.js +379 -0
- package/dist/analysis/port-wiring.js.map +1 -0
- package/dist/analysis/source-index.d.ts +5 -0
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +171 -42
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace.d.ts +10 -4
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +66 -6
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +311 -92
- package/dist/app-map.js.map +1 -1
- package/dist/check.d.ts +8 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +55 -9
- package/dist/check.js.map +1 -1
- package/dist/db.d.ts +38 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +52 -0
- package/dist/db.js.map +1 -1
- package/dist/explain.js +4 -4
- package/dist/explain.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +73 -6
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +454 -170
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +2 -2
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +309 -487
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +12 -5
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +3 -3
- package/dist/make/payments.js.map +1 -1
- package/dist/make/tenancy.js +1 -1
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +144 -8
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +56 -19
- package/dist/mcp.js.map +1 -1
- package/dist/preflight.d.ts +11 -3
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +139 -16
- package/dist/preflight.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +149 -9
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.d.ts +13 -0
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +122 -8
- package/dist/provider-audit.js.map +1 -1
- package/dist/registry-edits.d.ts +7 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +237 -53
- package/dist/registry-edits.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +29 -7
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts +1 -0
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +20 -6
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db/index.d.ts +1 -0
- package/dist/templates/db/index.d.ts.map +1 -1
- package/dist/templates/db/index.js.map +1 -1
- package/dist/templates/db/migration-status.d.ts +9 -0
- package/dist/templates/db/migration-status.d.ts.map +1 -0
- package/dist/templates/db/migration-status.js +117 -0
- package/dist/templates/db/migration-status.js.map +1 -0
- package/dist/templates/db/mysql.d.ts.map +1 -1
- package/dist/templates/db/mysql.js +23 -0
- package/dist/templates/db/mysql.js.map +1 -1
- package/dist/templates/db/postgres.d.ts.map +1 -1
- package/dist/templates/db/postgres.js +21 -0
- package/dist/templates/db/postgres.js.map +1 -1
- package/dist/templates/db/sqlite.d.ts.map +1 -1
- package/dist/templates/db/sqlite.js +53 -0
- package/dist/templates/db/sqlite.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +5 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +2 -0
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +1 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +4 -2
- package/dist/templates/shared.js.map +1 -1
- package/package.json +7 -6
- package/skills/app-structure/SKILL.md +56 -11
- package/src/analysis/contract-factories.ts +256 -0
- package/src/analysis/layers.ts +266 -0
- package/src/analysis/port-wiring.ts +506 -0
- package/src/analysis/source-index.ts +225 -57
- package/src/analysis/workspace.ts +115 -6
- package/src/app-map.ts +418 -109
- package/src/check.ts +86 -10
- package/src/db.ts +97 -0
- package/src/explain.ts +4 -4
- package/src/index.ts +108 -7
- package/src/inspect.ts +659 -216
- package/src/lib.ts +9 -1
- package/src/lint.ts +393 -643
- package/src/make/inbox.ts +12 -5
- package/src/make/payments.ts +3 -3
- package/src/make/tenancy.ts +1 -1
- package/src/make.ts +225 -8
- package/src/mcp.ts +73 -21
- package/src/preflight.ts +195 -16
- package/src/provider-add.ts +193 -9
- package/src/provider-audit.ts +210 -18
- package/src/registry-edits.ts +313 -63
- package/src/templates/agents.ts +29 -7
- package/src/templates/base.ts +21 -4
- package/src/templates/db/index.ts +1 -0
- package/src/templates/db/migration-status.ts +126 -0
- package/src/templates/db/mysql.ts +23 -0
- package/src/templates/db/postgres.ts +21 -0
- package/src/templates/db/sqlite.ts +53 -0
- package/src/templates/index.ts +5 -0
- package/src/templates/server.ts +2 -0
- 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,
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
468
|
-
const
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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
|
|
522
|
-
const
|
|
523
|
-
|
|
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
|
|
526
|
-
const
|
|
527
|
-
|
|
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 =
|
|
666
|
+
const method = methodValue.toUpperCase();
|
|
531
667
|
if (!isHttpMethod(method))
|
|
532
668
|
return undefined;
|
|
533
|
-
return { method, path:
|
|
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
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
name
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
|
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
|
|
589
|
-
|
|
761
|
+
function applyCatalogErrorCall(current, expression) {
|
|
762
|
+
const errors = objectLiteral(expression);
|
|
763
|
+
if (!errors)
|
|
590
764
|
return [];
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
|
597
|
-
if (expression
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
1489
|
-
!/\
|
|
1698
|
+
!/\bauthorize\s*(?::|\()/.test(source) &&
|
|
1699
|
+
!/\ballowUnauthenticatedDevelopmentAccess\s*:\s*true\b/.test(source)) {
|
|
1490
1700
|
diagnostics.push({
|
|
1491
1701
|
severity: "warning",
|
|
1492
|
-
code: "
|
|
1702
|
+
code: "BEIGNET_DEVTOOLS_ROUTE_POLICY_MISSING",
|
|
1493
1703
|
file,
|
|
1494
|
-
message: `${file}
|
|
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 =
|
|
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 (
|
|
2311
|
-
|
|
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 =
|
|
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
|
|
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
|
|
2359
|
-
const
|
|
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
|
|
2363
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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) {
|