@hyphaene/hexa-ts-kit 1.11.0 → 1.12.1
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/dist/{chunk-IBVI6IS2.js → chunk-EB6S5X3P.js} +592 -12
- package/dist/cli.js +1 -1
- package/dist/mcp-server.js +2 -2
- package/package.json +1 -1
|
@@ -753,6 +753,114 @@ var rulesRegistry = {
|
|
|
753
753
|
}
|
|
754
754
|
},
|
|
755
755
|
// ==========================================================================
|
|
756
|
+
// Contracts Nomenclature Rules (CTR-ENUM-*, CTR-RESPONSE-*, etc.)
|
|
757
|
+
// ==========================================================================
|
|
758
|
+
"CTR-ENUM-001": {
|
|
759
|
+
title: "Enum bundle missing required exports",
|
|
760
|
+
severity: "error",
|
|
761
|
+
category: "contracts",
|
|
762
|
+
why: "Enum bundles must export {name}Schema, {NAME}, and {NAME}_VALUES for consistent API.",
|
|
763
|
+
autoFixable: false
|
|
764
|
+
},
|
|
765
|
+
"CTR-ENUM-002": {
|
|
766
|
+
title: "Enum bundle missing createEnumBundle()",
|
|
767
|
+
severity: "warning",
|
|
768
|
+
category: "contracts",
|
|
769
|
+
why: "Enum bundles should use createEnumBundle() for consistent schema/enum/values creation.",
|
|
770
|
+
autoFixable: false
|
|
771
|
+
},
|
|
772
|
+
"CTR-ENUM-003": {
|
|
773
|
+
title: "Enum bundle missing inferred type export",
|
|
774
|
+
severity: "error",
|
|
775
|
+
category: "contracts",
|
|
776
|
+
why: "Enum bundles must export a type via z.infer for type safety.",
|
|
777
|
+
autoFixable: false
|
|
778
|
+
},
|
|
779
|
+
"CTR-RESPONSE-001": {
|
|
780
|
+
title: "Response file missing export ending with {HTTP}Schema",
|
|
781
|
+
severity: "error",
|
|
782
|
+
category: "contracts",
|
|
783
|
+
why: "Response files must export a schema whose name ends with {HTTP}Schema (e.g. MyFeature200Schema or MyFeatureResponse200Schema).",
|
|
784
|
+
autoFixable: false
|
|
785
|
+
},
|
|
786
|
+
"CTR-RESPONSE-002": {
|
|
787
|
+
title: "Response file missing default export",
|
|
788
|
+
severity: "error",
|
|
789
|
+
category: "contracts",
|
|
790
|
+
why: "Response files must have a default export for fixture validation scripts.",
|
|
791
|
+
autoFixable: false
|
|
792
|
+
},
|
|
793
|
+
"CTR-RESPONSE-003": {
|
|
794
|
+
title: "Response file missing OpenAPI documentation",
|
|
795
|
+
severity: "error",
|
|
796
|
+
category: "contracts",
|
|
797
|
+
why: "Response schema fields must have .describe() or .openapi() for OpenAPI documentation generation.",
|
|
798
|
+
autoFixable: false
|
|
799
|
+
},
|
|
800
|
+
"CTR-REQUEST-001": {
|
|
801
|
+
title: "Request file missing typed schema export",
|
|
802
|
+
severity: "error",
|
|
803
|
+
category: "contracts",
|
|
804
|
+
why: "Request files must export a schema whose name ends with QuerySchema, BodySchema, HeadersSchema, or ParamsSchema.",
|
|
805
|
+
autoFixable: false
|
|
806
|
+
},
|
|
807
|
+
"CTR-REQUEST-002": {
|
|
808
|
+
title: "Request file missing OpenAPI documentation",
|
|
809
|
+
severity: "error",
|
|
810
|
+
category: "contracts",
|
|
811
|
+
why: "Request schema fields should use .describe() for OpenAPI documentation.",
|
|
812
|
+
autoFixable: false
|
|
813
|
+
},
|
|
814
|
+
"CTR-CONTRACT-001": {
|
|
815
|
+
title: "Contract file missing {Name}Contract export",
|
|
816
|
+
severity: "error",
|
|
817
|
+
category: "contracts",
|
|
818
|
+
why: "Contract files must export a named {Name}Contract for discoverability.",
|
|
819
|
+
autoFixable: false
|
|
820
|
+
},
|
|
821
|
+
"CTR-CONTRACT-002": {
|
|
822
|
+
title: "Contract file missing strictStatusCodes: true",
|
|
823
|
+
severity: "error",
|
|
824
|
+
category: "contracts",
|
|
825
|
+
why: "Contract files must use strictStatusCodes: true for type safety.",
|
|
826
|
+
autoFixable: false
|
|
827
|
+
},
|
|
828
|
+
"CTR-CONTRACT-003": {
|
|
829
|
+
title: "Contract file missing contractMetadata()",
|
|
830
|
+
severity: "error",
|
|
831
|
+
category: "contracts",
|
|
832
|
+
why: "Contract endpoints must use contractMetadata() for permission and tag declaration.",
|
|
833
|
+
autoFixable: false
|
|
834
|
+
},
|
|
835
|
+
"CTR-INDEX-001": {
|
|
836
|
+
title: "Index file contains local declarations",
|
|
837
|
+
severity: "error",
|
|
838
|
+
category: "contracts",
|
|
839
|
+
why: "Index files must only contain re-exports (export * from / export { } from).",
|
|
840
|
+
autoFixable: false
|
|
841
|
+
},
|
|
842
|
+
"CTR-HELPER-001": {
|
|
843
|
+
title: "Helper file contains Zod data schemas",
|
|
844
|
+
severity: "warning",
|
|
845
|
+
category: "contracts",
|
|
846
|
+
why: "Helper files should contain pure functions, not Zod data schemas.",
|
|
847
|
+
autoFixable: false
|
|
848
|
+
},
|
|
849
|
+
"CTR-NAMING-001": {
|
|
850
|
+
title: "File has non-standard suffix",
|
|
851
|
+
severity: "error",
|
|
852
|
+
category: "contracts",
|
|
853
|
+
why: "All source files must use an allowed suffix from the nomenclature.",
|
|
854
|
+
autoFixable: false
|
|
855
|
+
},
|
|
856
|
+
"CTR-DEPRECATED-001": {
|
|
857
|
+
title: "Deprecated file export missing @deprecated JSDoc",
|
|
858
|
+
severity: "error",
|
|
859
|
+
category: "contracts",
|
|
860
|
+
why: "All exports in .deprecated.ts files must have @deprecated JSDoc annotation.",
|
|
861
|
+
autoFixable: false
|
|
862
|
+
},
|
|
863
|
+
// ==========================================================================
|
|
756
864
|
// Contracts Colocation Rules (COL-CTR-*)
|
|
757
865
|
// ==========================================================================
|
|
758
866
|
"COL-CTR-001": {
|
|
@@ -837,7 +945,7 @@ var ContractsLintConfigSchema = z.object({
|
|
|
837
945
|
metadata: z.object({
|
|
838
946
|
permissions: SourceExtractConfigSchema,
|
|
839
947
|
apiServices: SourceExtractConfigSchema
|
|
840
|
-
}),
|
|
948
|
+
}).optional(),
|
|
841
949
|
disabledRules: z.array(z.string()).optional()
|
|
842
950
|
});
|
|
843
951
|
var VaultLintConfigSchema = z.object({
|
|
@@ -2280,6 +2388,476 @@ function checkSchemaImport(contractFile, _contractDir, imp) {
|
|
|
2280
2388
|
return results;
|
|
2281
2389
|
}
|
|
2282
2390
|
|
|
2391
|
+
// src/lint/checkers/contracts/nomenclature.ts
|
|
2392
|
+
import fg9 from "fast-glob";
|
|
2393
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2394
|
+
import { basename as basename5 } from "path";
|
|
2395
|
+
import { parse as parse3 } from "@typescript-eslint/typescript-estree";
|
|
2396
|
+
var ALLOWED_SUFFIXES = [
|
|
2397
|
+
".enum-bundle.ts",
|
|
2398
|
+
".schema.ts",
|
|
2399
|
+
".response.ts",
|
|
2400
|
+
".request.ts",
|
|
2401
|
+
".presenter.ts",
|
|
2402
|
+
".contract.ts",
|
|
2403
|
+
".helpers.ts",
|
|
2404
|
+
".constants.ts",
|
|
2405
|
+
".deprecated.ts",
|
|
2406
|
+
".test.ts",
|
|
2407
|
+
".contract.test.ts",
|
|
2408
|
+
".migration.test.ts",
|
|
2409
|
+
".rules.ts"
|
|
2410
|
+
];
|
|
2411
|
+
var ALLOWED_SPECIAL_FILES = ["index.ts", "openapi.ts"];
|
|
2412
|
+
function parseFile(filePath) {
|
|
2413
|
+
try {
|
|
2414
|
+
const content = readFileSync4(filePath, "utf-8");
|
|
2415
|
+
return parse3(content, {
|
|
2416
|
+
loc: true,
|
|
2417
|
+
range: true,
|
|
2418
|
+
comment: true,
|
|
2419
|
+
jsx: false
|
|
2420
|
+
});
|
|
2421
|
+
} catch {
|
|
2422
|
+
return null;
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
function readFileContent(filePath) {
|
|
2426
|
+
return readFileSync4(filePath, "utf-8");
|
|
2427
|
+
}
|
|
2428
|
+
function getExportedNames(ast) {
|
|
2429
|
+
const names = [];
|
|
2430
|
+
for (const node of ast.body) {
|
|
2431
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
2432
|
+
if (node.declaration) {
|
|
2433
|
+
if (node.declaration.type === "VariableDeclaration") {
|
|
2434
|
+
for (const decl of node.declaration.declarations) {
|
|
2435
|
+
if (decl.id.type === "Identifier") {
|
|
2436
|
+
names.push(decl.id.name);
|
|
2437
|
+
} else if (decl.id.type === "ObjectPattern") {
|
|
2438
|
+
for (const prop of decl.id.properties) {
|
|
2439
|
+
if (prop.type === "Property" && prop.value.type === "Identifier") {
|
|
2440
|
+
names.push(prop.value.name);
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
if (node.declaration.type === "TSTypeAliasDeclaration") {
|
|
2447
|
+
names.push(node.declaration.id.name);
|
|
2448
|
+
}
|
|
2449
|
+
if (node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
|
|
2450
|
+
names.push(node.declaration.id.name);
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
if (node.specifiers) {
|
|
2454
|
+
for (const spec of node.specifiers) {
|
|
2455
|
+
if (spec.exported.type === "Identifier") {
|
|
2456
|
+
names.push(spec.exported.name);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
return names;
|
|
2463
|
+
}
|
|
2464
|
+
function hasDefaultExport(ast) {
|
|
2465
|
+
return ast.body.some(
|
|
2466
|
+
(node) => node.type === "ExportDefaultDeclaration" || node.type === "ExportNamedDeclaration" && node.specifiers?.some(
|
|
2467
|
+
(s) => s.exported.type === "Identifier" && s.exported.name === "default"
|
|
2468
|
+
)
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
function hasOpenApiDocumentation(content) {
|
|
2472
|
+
return /\.describe\s*\(/.test(content) || /\.openapi\s*\(/.test(content);
|
|
2473
|
+
}
|
|
2474
|
+
function hasCreateEnumBundleCall(content) {
|
|
2475
|
+
return /createEnumBundle\s*\(/.test(content);
|
|
2476
|
+
}
|
|
2477
|
+
function hasZodInferTypeExport(content) {
|
|
2478
|
+
return /export\s+type\s+\w+\s*=\s*z\.infer/.test(content);
|
|
2479
|
+
}
|
|
2480
|
+
function hasZodDataSchemas(content) {
|
|
2481
|
+
return /(?:const|let|var)\s+\w+\s*=\s*z\.(?:object|enum|array)\s*\(/.test(
|
|
2482
|
+
content
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
function getExportsWithoutDeprecatedJsdoc(ast, content) {
|
|
2486
|
+
const missingDeprecated = [];
|
|
2487
|
+
for (const node of ast.body) {
|
|
2488
|
+
if (node.type !== "ExportNamedDeclaration" && node.type !== "ExportDefaultDeclaration") {
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
let exportName = "default";
|
|
2492
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
2493
|
+
if (node.declaration) {
|
|
2494
|
+
if (node.declaration.type === "VariableDeclaration") {
|
|
2495
|
+
const decl = node.declaration.declarations[0];
|
|
2496
|
+
if (decl?.id.type === "Identifier") {
|
|
2497
|
+
exportName = decl.id.name;
|
|
2498
|
+
}
|
|
2499
|
+
} else if (node.declaration.type === "TSTypeAliasDeclaration" || node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
|
|
2500
|
+
exportName = node.declaration.type === "TSTypeAliasDeclaration" ? node.declaration.id.name : node.declaration.id.name;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
if (node.source) continue;
|
|
2504
|
+
}
|
|
2505
|
+
const linesBefore = content.substring(0, node.range?.[0] ?? 0);
|
|
2506
|
+
const lastCommentBlock = linesBefore.match(/\/\*\*[\s\S]*?\*\/\s*$/);
|
|
2507
|
+
if (!lastCommentBlock || !lastCommentBlock[0].includes("@deprecated")) {
|
|
2508
|
+
missingDeprecated.push(exportName);
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
return missingDeprecated;
|
|
2512
|
+
}
|
|
2513
|
+
function getNonReexportStatements(ast) {
|
|
2514
|
+
const violations = [];
|
|
2515
|
+
for (const node of ast.body) {
|
|
2516
|
+
if (node.type === "ImportDeclaration") continue;
|
|
2517
|
+
if (node.type === "ExportAllDeclaration") continue;
|
|
2518
|
+
if (node.type === "ExportNamedDeclaration" && node.source) continue;
|
|
2519
|
+
if (node.type === "ExportNamedDeclaration" && node.exportKind === "type" && node.source)
|
|
2520
|
+
continue;
|
|
2521
|
+
violations.push(node);
|
|
2522
|
+
}
|
|
2523
|
+
return violations;
|
|
2524
|
+
}
|
|
2525
|
+
async function checkEnumBundleRules(cwd) {
|
|
2526
|
+
const results = [];
|
|
2527
|
+
const files = await fg9("src/**/*.enum-bundle.ts", {
|
|
2528
|
+
cwd,
|
|
2529
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
2530
|
+
});
|
|
2531
|
+
for (const file of files) {
|
|
2532
|
+
const fullPath = `${cwd}/${file}`;
|
|
2533
|
+
const content = readFileContent(fullPath);
|
|
2534
|
+
const ast = parseFile(fullPath);
|
|
2535
|
+
if (!ast) continue;
|
|
2536
|
+
const exportedNames = getExportedNames(ast);
|
|
2537
|
+
const fileName = basename5(file, ".enum-bundle.ts");
|
|
2538
|
+
const upperName = fileName.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();
|
|
2539
|
+
const hasSchema = exportedNames.some((n) => n.endsWith("Schema"));
|
|
2540
|
+
const hasEnumMapper = exportedNames.some(
|
|
2541
|
+
(n) => n === n.toUpperCase() && n.length > 1
|
|
2542
|
+
);
|
|
2543
|
+
const hasValues = exportedNames.some((n) => n.endsWith("_VALUES"));
|
|
2544
|
+
if (!hasSchema || !hasEnumMapper || !hasValues) {
|
|
2545
|
+
const missing = [];
|
|
2546
|
+
if (!hasSchema) missing.push("{name}Schema");
|
|
2547
|
+
if (!hasEnumMapper) missing.push("{NAME}");
|
|
2548
|
+
if (!hasValues) missing.push("{NAME}_VALUES");
|
|
2549
|
+
results.push({
|
|
2550
|
+
ruleId: "CTR-ENUM-001",
|
|
2551
|
+
severity: "error",
|
|
2552
|
+
message: `Enum bundle ${file} missing exports: ${missing.join(", ")}`,
|
|
2553
|
+
file,
|
|
2554
|
+
suggestion: `Export ${upperName}_VALUES, ${upperName}, and a Schema const`
|
|
2555
|
+
});
|
|
2556
|
+
}
|
|
2557
|
+
if (!hasCreateEnumBundleCall(content)) {
|
|
2558
|
+
results.push({
|
|
2559
|
+
ruleId: "CTR-ENUM-002",
|
|
2560
|
+
severity: "warning",
|
|
2561
|
+
message: `Enum bundle ${file} does not use createEnumBundle()`,
|
|
2562
|
+
file,
|
|
2563
|
+
suggestion: "Use createEnumBundle() for consistent schema/enum/values creation"
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
if (!hasZodInferTypeExport(content)) {
|
|
2567
|
+
results.push({
|
|
2568
|
+
ruleId: "CTR-ENUM-003",
|
|
2569
|
+
severity: "error",
|
|
2570
|
+
message: `Enum bundle ${file} missing type export via z.infer`,
|
|
2571
|
+
file,
|
|
2572
|
+
suggestion: "Add: export type {Name} = z.infer<typeof {name}Schema>"
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
return results;
|
|
2577
|
+
}
|
|
2578
|
+
async function checkResponseRules(cwd) {
|
|
2579
|
+
const results = [];
|
|
2580
|
+
const files = await fg9("src/**/*.response.ts", {
|
|
2581
|
+
cwd,
|
|
2582
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
2583
|
+
});
|
|
2584
|
+
for (const file of files) {
|
|
2585
|
+
const fullPath = `${cwd}/${file}`;
|
|
2586
|
+
const content = readFileContent(fullPath);
|
|
2587
|
+
const ast = parseFile(fullPath);
|
|
2588
|
+
if (!ast) continue;
|
|
2589
|
+
const exportedNames = getExportedNames(ast);
|
|
2590
|
+
const fileName = basename5(file, ".response.ts");
|
|
2591
|
+
const httpCode = fileName.match(/^(\d{3})$/)?.[1];
|
|
2592
|
+
if (httpCode) {
|
|
2593
|
+
if (!exportedNames.some((n) => n.includes(httpCode) && n.endsWith("Schema"))) {
|
|
2594
|
+
results.push({
|
|
2595
|
+
ruleId: "CTR-RESPONSE-001",
|
|
2596
|
+
severity: "error",
|
|
2597
|
+
message: `Response file ${file} missing export containing "${httpCode}" and ending with "Schema"`,
|
|
2598
|
+
file,
|
|
2599
|
+
suggestion: `Export a schema whose name contains ${httpCode} and ends with Schema (e.g. MyFeature${httpCode}Schema)`
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
if (!hasDefaultExport(ast)) {
|
|
2604
|
+
results.push({
|
|
2605
|
+
ruleId: "CTR-RESPONSE-002",
|
|
2606
|
+
severity: "error",
|
|
2607
|
+
message: `Response file ${file} missing default export`,
|
|
2608
|
+
file,
|
|
2609
|
+
suggestion: "Add: export default Response{HTTP}Schema"
|
|
2610
|
+
});
|
|
2611
|
+
}
|
|
2612
|
+
if (!hasOpenApiDocumentation(content)) {
|
|
2613
|
+
results.push({
|
|
2614
|
+
ruleId: "CTR-RESPONSE-003",
|
|
2615
|
+
severity: "error",
|
|
2616
|
+
message: `Response file ${file} has no OpenAPI documentation (.describe() or .openapi())`,
|
|
2617
|
+
file,
|
|
2618
|
+
suggestion: "Add .describe('...') or .openapi({ description: '...' }) to schema fields"
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
return results;
|
|
2623
|
+
}
|
|
2624
|
+
async function checkRequestRules(cwd) {
|
|
2625
|
+
const results = [];
|
|
2626
|
+
const files = await fg9("src/**/*.request.ts", {
|
|
2627
|
+
cwd,
|
|
2628
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
2629
|
+
});
|
|
2630
|
+
for (const file of files) {
|
|
2631
|
+
const fullPath = `${cwd}/${file}`;
|
|
2632
|
+
const content = readFileContent(fullPath);
|
|
2633
|
+
const ast = parseFile(fullPath);
|
|
2634
|
+
if (!ast) continue;
|
|
2635
|
+
const exportedNames = getExportedNames(ast);
|
|
2636
|
+
const hasTypedSchema = exportedNames.some(
|
|
2637
|
+
(n) => /(Query|Body|Headers|Params)Schema$/.test(n)
|
|
2638
|
+
);
|
|
2639
|
+
if (!hasTypedSchema) {
|
|
2640
|
+
results.push({
|
|
2641
|
+
ruleId: "CTR-REQUEST-001",
|
|
2642
|
+
severity: "error",
|
|
2643
|
+
message: `Request file ${file} missing typed schema export (ending with QuerySchema, BodySchema, HeadersSchema, or ParamsSchema)`,
|
|
2644
|
+
file,
|
|
2645
|
+
suggestion: "Export a schema whose name ends with: QuerySchema, BodySchema, HeadersSchema, or ParamsSchema"
|
|
2646
|
+
});
|
|
2647
|
+
}
|
|
2648
|
+
if (!hasOpenApiDocumentation(content)) {
|
|
2649
|
+
results.push({
|
|
2650
|
+
ruleId: "CTR-REQUEST-002",
|
|
2651
|
+
severity: "error",
|
|
2652
|
+
message: `Request file ${file} has no OpenAPI documentation (.describe() or .openapi())`,
|
|
2653
|
+
file,
|
|
2654
|
+
suggestion: "Add .describe('...') or .openapi({ description: '...' }) to schema fields"
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
return results;
|
|
2659
|
+
}
|
|
2660
|
+
async function checkContractFileRules(cwd) {
|
|
2661
|
+
const results = [];
|
|
2662
|
+
const files = await fg9("src/**/*.contract.ts", {
|
|
2663
|
+
cwd,
|
|
2664
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/*.contract.test.ts"]
|
|
2665
|
+
});
|
|
2666
|
+
for (const file of files) {
|
|
2667
|
+
const fullPath = `${cwd}/${file}`;
|
|
2668
|
+
const content = readFileContent(fullPath);
|
|
2669
|
+
const ast = parseFile(fullPath);
|
|
2670
|
+
if (!ast) continue;
|
|
2671
|
+
const exportedNames = getExportedNames(ast);
|
|
2672
|
+
const hasContractExport = exportedNames.some((n) => n.endsWith("Contract"));
|
|
2673
|
+
if (!hasContractExport) {
|
|
2674
|
+
results.push({
|
|
2675
|
+
ruleId: "CTR-CONTRACT-001",
|
|
2676
|
+
severity: "error",
|
|
2677
|
+
message: `Contract file ${file} missing {Name}Contract export`,
|
|
2678
|
+
file,
|
|
2679
|
+
suggestion: "Export const {Name}Contract = c.router({...})"
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
if (!content.includes("strictStatusCodes")) {
|
|
2683
|
+
results.push({
|
|
2684
|
+
ruleId: "CTR-CONTRACT-002",
|
|
2685
|
+
severity: "error",
|
|
2686
|
+
message: `Contract file ${file} missing strictStatusCodes: true`,
|
|
2687
|
+
file,
|
|
2688
|
+
suggestion: "Add strictStatusCodes: true to all endpoints"
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
if (!content.includes("contractMetadata")) {
|
|
2692
|
+
results.push({
|
|
2693
|
+
ruleId: "CTR-CONTRACT-003",
|
|
2694
|
+
severity: "error",
|
|
2695
|
+
message: `Contract file ${file} missing contractMetadata() usage`,
|
|
2696
|
+
file,
|
|
2697
|
+
suggestion: "Use metadata: contractMetadata({...}) for each endpoint"
|
|
2698
|
+
});
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
return results;
|
|
2702
|
+
}
|
|
2703
|
+
async function checkIndexRules(cwd) {
|
|
2704
|
+
const results = [];
|
|
2705
|
+
const files = await fg9("src/**/index.ts", {
|
|
2706
|
+
cwd,
|
|
2707
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/lib/**"]
|
|
2708
|
+
});
|
|
2709
|
+
for (const file of files) {
|
|
2710
|
+
const fullPath = `${cwd}/${file}`;
|
|
2711
|
+
const ast = parseFile(fullPath);
|
|
2712
|
+
if (!ast) continue;
|
|
2713
|
+
const violations = getNonReexportStatements(ast);
|
|
2714
|
+
if (violations.length > 0) {
|
|
2715
|
+
results.push({
|
|
2716
|
+
ruleId: "CTR-INDEX-001",
|
|
2717
|
+
severity: "error",
|
|
2718
|
+
message: `Index file ${file} contains ${violations.length} non-reexport statement(s)`,
|
|
2719
|
+
file,
|
|
2720
|
+
line: violations[0]?.loc?.start.line,
|
|
2721
|
+
suggestion: "Index files should only contain: export * from '...' or export { X } from '...'"
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
return results;
|
|
2726
|
+
}
|
|
2727
|
+
async function checkHelperRules(cwd) {
|
|
2728
|
+
const results = [];
|
|
2729
|
+
const files = await fg9("src/**/*.helpers.ts", {
|
|
2730
|
+
cwd,
|
|
2731
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
2732
|
+
});
|
|
2733
|
+
for (const file of files) {
|
|
2734
|
+
const fullPath = `${cwd}/${file}`;
|
|
2735
|
+
const content = readFileContent(fullPath);
|
|
2736
|
+
if (hasZodDataSchemas(content)) {
|
|
2737
|
+
results.push({
|
|
2738
|
+
ruleId: "CTR-HELPER-001",
|
|
2739
|
+
severity: "warning",
|
|
2740
|
+
message: `Helper file ${file} contains Zod data schemas`,
|
|
2741
|
+
file,
|
|
2742
|
+
suggestion: "Move Zod data schemas to .schema.ts or .enum-bundle.ts files"
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
return results;
|
|
2747
|
+
}
|
|
2748
|
+
async function checkNamingRules(cwd) {
|
|
2749
|
+
const results = [];
|
|
2750
|
+
const files = await fg9("src/**/*.ts", {
|
|
2751
|
+
cwd,
|
|
2752
|
+
ignore: [
|
|
2753
|
+
"**/node_modules/**",
|
|
2754
|
+
"**/dist/**",
|
|
2755
|
+
"**/lib/**",
|
|
2756
|
+
"**/__tests__/**"
|
|
2757
|
+
]
|
|
2758
|
+
});
|
|
2759
|
+
for (const file of files) {
|
|
2760
|
+
const fileName = basename5(file);
|
|
2761
|
+
if (ALLOWED_SPECIAL_FILES.includes(fileName)) continue;
|
|
2762
|
+
const matchesSuffix = ALLOWED_SUFFIXES.some(
|
|
2763
|
+
(suffix) => fileName.endsWith(suffix)
|
|
2764
|
+
);
|
|
2765
|
+
if (!matchesSuffix) {
|
|
2766
|
+
results.push({
|
|
2767
|
+
ruleId: "CTR-NAMING-001",
|
|
2768
|
+
severity: "error",
|
|
2769
|
+
message: `File ${file} has non-standard suffix. Allowed: ${ALLOWED_SUFFIXES.join(", ")}`,
|
|
2770
|
+
file,
|
|
2771
|
+
suggestion: `Rename to use one of: ${ALLOWED_SUFFIXES.map((s) => `*${s}`).join(", ")}`
|
|
2772
|
+
});
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
return results;
|
|
2776
|
+
}
|
|
2777
|
+
async function checkDeprecatedRules(cwd) {
|
|
2778
|
+
const results = [];
|
|
2779
|
+
const files = await fg9("src/**/*.deprecated.ts", {
|
|
2780
|
+
cwd,
|
|
2781
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
2782
|
+
});
|
|
2783
|
+
for (const file of files) {
|
|
2784
|
+
const fullPath = `${cwd}/${file}`;
|
|
2785
|
+
const content = readFileContent(fullPath);
|
|
2786
|
+
const ast = parseFile(fullPath);
|
|
2787
|
+
if (!ast) continue;
|
|
2788
|
+
const missing = getExportsWithoutDeprecatedJsdoc(ast, content);
|
|
2789
|
+
if (missing.length > 0) {
|
|
2790
|
+
results.push({
|
|
2791
|
+
ruleId: "CTR-DEPRECATED-001",
|
|
2792
|
+
severity: "error",
|
|
2793
|
+
message: `Deprecated file ${file} has exports without @deprecated JSDoc: ${missing.join(", ")}`,
|
|
2794
|
+
file,
|
|
2795
|
+
suggestion: "Add /** @deprecated Use X instead */ before each export"
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
return results;
|
|
2800
|
+
}
|
|
2801
|
+
var contractsNomenclatureChecker = {
|
|
2802
|
+
name: "contracts-nomenclature",
|
|
2803
|
+
rules: [
|
|
2804
|
+
"CTR-ENUM-001",
|
|
2805
|
+
"CTR-ENUM-002",
|
|
2806
|
+
"CTR-ENUM-003",
|
|
2807
|
+
"CTR-RESPONSE-001",
|
|
2808
|
+
"CTR-RESPONSE-002",
|
|
2809
|
+
"CTR-RESPONSE-003",
|
|
2810
|
+
"CTR-REQUEST-001",
|
|
2811
|
+
"CTR-REQUEST-002",
|
|
2812
|
+
"CTR-CONTRACT-001",
|
|
2813
|
+
"CTR-CONTRACT-002",
|
|
2814
|
+
"CTR-CONTRACT-003",
|
|
2815
|
+
"CTR-INDEX-001",
|
|
2816
|
+
"CTR-HELPER-001",
|
|
2817
|
+
"CTR-NAMING-001",
|
|
2818
|
+
"CTR-DEPRECATED-001"
|
|
2819
|
+
],
|
|
2820
|
+
async check(ctx) {
|
|
2821
|
+
const configResult = loadConfig(ctx.cwd);
|
|
2822
|
+
if (!configResult.success || configResult.config.project.type !== "contracts-lib") {
|
|
2823
|
+
return [];
|
|
2824
|
+
}
|
|
2825
|
+
const disabledRules = getDisabledRules(configResult.config);
|
|
2826
|
+
const results = [];
|
|
2827
|
+
const [
|
|
2828
|
+
enumResults,
|
|
2829
|
+
responseResults,
|
|
2830
|
+
requestResults,
|
|
2831
|
+
contractResults,
|
|
2832
|
+
indexResults,
|
|
2833
|
+
helperResults,
|
|
2834
|
+
namingResults,
|
|
2835
|
+
deprecatedResults
|
|
2836
|
+
] = await Promise.all([
|
|
2837
|
+
checkEnumBundleRules(ctx.cwd),
|
|
2838
|
+
checkResponseRules(ctx.cwd),
|
|
2839
|
+
checkRequestRules(ctx.cwd),
|
|
2840
|
+
checkContractFileRules(ctx.cwd),
|
|
2841
|
+
checkIndexRules(ctx.cwd),
|
|
2842
|
+
checkHelperRules(ctx.cwd),
|
|
2843
|
+
checkNamingRules(ctx.cwd),
|
|
2844
|
+
checkDeprecatedRules(ctx.cwd)
|
|
2845
|
+
]);
|
|
2846
|
+
results.push(
|
|
2847
|
+
...enumResults,
|
|
2848
|
+
...responseResults,
|
|
2849
|
+
...requestResults,
|
|
2850
|
+
...contractResults,
|
|
2851
|
+
...indexResults,
|
|
2852
|
+
...helperResults,
|
|
2853
|
+
...namingResults,
|
|
2854
|
+
...deprecatedResults
|
|
2855
|
+
);
|
|
2856
|
+
if (disabledRules.length === 0) return results;
|
|
2857
|
+
return results.filter((r) => !disabledRules.includes(r.ruleId));
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2860
|
+
|
|
2283
2861
|
// src/lint/checkers/contracts/index.ts
|
|
2284
2862
|
var contractsChecker = {
|
|
2285
2863
|
name: "contracts",
|
|
@@ -2361,6 +2939,8 @@ var contractsChecker = {
|
|
|
2361
2939
|
}
|
|
2362
2940
|
const colocationResults = await contractsColocationChecker.check(ctx);
|
|
2363
2941
|
results.push(...colocationResults);
|
|
2942
|
+
const nomenclatureResults = await contractsNomenclatureChecker.check(ctx);
|
|
2943
|
+
results.push(...nomenclatureResults);
|
|
2364
2944
|
return results;
|
|
2365
2945
|
}
|
|
2366
2946
|
};
|
|
@@ -2759,10 +3339,10 @@ Completed in ${elapsed}ms`);
|
|
|
2759
3339
|
}
|
|
2760
3340
|
|
|
2761
3341
|
// src/commands/analyze.ts
|
|
2762
|
-
import { basename as
|
|
3342
|
+
import { basename as basename6 } from "path";
|
|
2763
3343
|
import { execSync as execSync2 } from "child_process";
|
|
2764
|
-
import { readFileSync as
|
|
2765
|
-
import
|
|
3344
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
3345
|
+
import fg10 from "fast-glob";
|
|
2766
3346
|
import matter from "gray-matter";
|
|
2767
3347
|
import { minimatch } from "minimatch";
|
|
2768
3348
|
function expandPath(p) {
|
|
@@ -2791,18 +3371,18 @@ function loadKnowledgeMappings(knowledgePath) {
|
|
|
2791
3371
|
if (!existsSync6(expandedPath)) {
|
|
2792
3372
|
return [];
|
|
2793
3373
|
}
|
|
2794
|
-
const knowledgeFiles =
|
|
3374
|
+
const knowledgeFiles = fg10.sync("**/*.knowledge.md", {
|
|
2795
3375
|
cwd: expandedPath,
|
|
2796
3376
|
absolute: true
|
|
2797
3377
|
});
|
|
2798
3378
|
const mappings = [];
|
|
2799
3379
|
for (const file of knowledgeFiles) {
|
|
2800
3380
|
try {
|
|
2801
|
-
const content =
|
|
3381
|
+
const content = readFileSync5(file, "utf-8");
|
|
2802
3382
|
const { data } = matter(content);
|
|
2803
3383
|
if (data.match) {
|
|
2804
3384
|
mappings.push({
|
|
2805
|
-
name: data.name ||
|
|
3385
|
+
name: data.name || basename6(file, ".knowledge.md"),
|
|
2806
3386
|
path: file,
|
|
2807
3387
|
match: data.match,
|
|
2808
3388
|
description: data.description
|
|
@@ -2815,7 +3395,7 @@ function loadKnowledgeMappings(knowledgePath) {
|
|
|
2815
3395
|
}
|
|
2816
3396
|
function matchFileToKnowledges(file, mappings) {
|
|
2817
3397
|
const results = [];
|
|
2818
|
-
const fileName =
|
|
3398
|
+
const fileName = basename6(file);
|
|
2819
3399
|
for (const mapping of mappings) {
|
|
2820
3400
|
if (minimatch(fileName, mapping.match) || minimatch(file, mapping.match)) {
|
|
2821
3401
|
results.push({
|
|
@@ -2877,7 +3457,7 @@ async function analyzeCommand(files = [], options) {
|
|
|
2877
3457
|
}
|
|
2878
3458
|
|
|
2879
3459
|
// src/commands/scaffold.ts
|
|
2880
|
-
import { resolve as resolve3, dirname as dirname5, basename as
|
|
3460
|
+
import { resolve as resolve3, dirname as dirname5, basename as basename7, join as join4 } from "path";
|
|
2881
3461
|
import { mkdirSync, writeFileSync, existsSync as existsSync7 } from "fs";
|
|
2882
3462
|
function toPascalCase(str) {
|
|
2883
3463
|
return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
@@ -2887,7 +3467,7 @@ function toCamelCase(str) {
|
|
|
2887
3467
|
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
2888
3468
|
}
|
|
2889
3469
|
function generateVueFeature(featurePath) {
|
|
2890
|
-
const featureName =
|
|
3470
|
+
const featureName = basename7(featurePath);
|
|
2891
3471
|
const pascalName = toPascalCase(featureName);
|
|
2892
3472
|
const camelName = toCamelCase(featureName);
|
|
2893
3473
|
return [
|
|
@@ -3004,7 +3584,7 @@ describe('${pascalName}Rules', () => {
|
|
|
3004
3584
|
];
|
|
3005
3585
|
}
|
|
3006
3586
|
function generateNestJSFeature(featurePath) {
|
|
3007
|
-
const featureName =
|
|
3587
|
+
const featureName = basename7(featurePath);
|
|
3008
3588
|
const pascalName = toPascalCase(featureName);
|
|
3009
3589
|
const camelName = toCamelCase(featureName);
|
|
3010
3590
|
return [
|
|
@@ -3095,7 +3675,7 @@ describe('${pascalName}Controller', () => {
|
|
|
3095
3675
|
];
|
|
3096
3676
|
}
|
|
3097
3677
|
function generatePlaywrightFeature(featurePath) {
|
|
3098
|
-
const featureName =
|
|
3678
|
+
const featureName = basename7(featurePath);
|
|
3099
3679
|
const pascalName = toPascalCase(featureName);
|
|
3100
3680
|
const camelName = toCamelCase(featureName);
|
|
3101
3681
|
return [
|
package/dist/cli.js
CHANGED
package/dist/mcp-server.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
analyzeCore,
|
|
4
4
|
lintCore,
|
|
5
5
|
scaffoldCore
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-EB6S5X3P.js";
|
|
7
7
|
|
|
8
8
|
// src/mcp-server.ts
|
|
9
9
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -18,7 +18,7 @@ var server = new McpServer({
|
|
|
18
18
|
});
|
|
19
19
|
server.tool(
|
|
20
20
|
"lint",
|
|
21
|
-
"Lint files for colocation architecture rules. Checks
|
|
21
|
+
"Lint files for colocation architecture rules. Checks 96 rules across structure (COL, NAM, DOM, CTR) and AST (VUE, RUL, TSP) categories. Use --changed for incremental mode on git-modified files only.",
|
|
22
22
|
{
|
|
23
23
|
cwd: z.string().optional().describe("Working directory to lint (default: current directory)"),
|
|
24
24
|
path: z.string().optional().describe("Alias for cwd - path to lint"),
|