@hyphaene/hexa-ts-kit 1.11.0 → 1.12.0
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-TGZA3GHG.js} +569 -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,453 @@ 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, { loc: true, range: true, comment: true, jsx: false });
|
|
2416
|
+
} catch {
|
|
2417
|
+
return null;
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
function readFileContent(filePath) {
|
|
2421
|
+
return readFileSync4(filePath, "utf-8");
|
|
2422
|
+
}
|
|
2423
|
+
function getExportedNames(ast) {
|
|
2424
|
+
const names = [];
|
|
2425
|
+
for (const node of ast.body) {
|
|
2426
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
2427
|
+
if (node.declaration) {
|
|
2428
|
+
if (node.declaration.type === "VariableDeclaration") {
|
|
2429
|
+
for (const decl of node.declaration.declarations) {
|
|
2430
|
+
if (decl.id.type === "Identifier") {
|
|
2431
|
+
names.push(decl.id.name);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
if (node.declaration.type === "TSTypeAliasDeclaration") {
|
|
2436
|
+
names.push(node.declaration.id.name);
|
|
2437
|
+
}
|
|
2438
|
+
if (node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
|
|
2439
|
+
names.push(node.declaration.id.name);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
if (node.specifiers) {
|
|
2443
|
+
for (const spec of node.specifiers) {
|
|
2444
|
+
if (spec.exported.type === "Identifier") {
|
|
2445
|
+
names.push(spec.exported.name);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
return names;
|
|
2452
|
+
}
|
|
2453
|
+
function hasDefaultExport(ast) {
|
|
2454
|
+
return ast.body.some(
|
|
2455
|
+
(node) => node.type === "ExportDefaultDeclaration" || node.type === "ExportNamedDeclaration" && node.specifiers?.some(
|
|
2456
|
+
(s) => s.exported.type === "Identifier" && s.exported.name === "default"
|
|
2457
|
+
)
|
|
2458
|
+
);
|
|
2459
|
+
}
|
|
2460
|
+
function hasOpenApiDocumentation(content) {
|
|
2461
|
+
return /\.describe\s*\(/.test(content) || /\.openapi\s*\(/.test(content);
|
|
2462
|
+
}
|
|
2463
|
+
function hasCreateEnumBundleCall(content) {
|
|
2464
|
+
return /createEnumBundle\s*\(/.test(content);
|
|
2465
|
+
}
|
|
2466
|
+
function hasZodInferTypeExport(content) {
|
|
2467
|
+
return /export\s+type\s+\w+\s*=\s*z\.infer/.test(content);
|
|
2468
|
+
}
|
|
2469
|
+
function hasZodDataSchemas(content) {
|
|
2470
|
+
return /(?:const|let|var)\s+\w+\s*=\s*z\.(?:object|enum|array)\s*\(/.test(content);
|
|
2471
|
+
}
|
|
2472
|
+
function getExportsWithoutDeprecatedJsdoc(ast, content) {
|
|
2473
|
+
const missingDeprecated = [];
|
|
2474
|
+
for (const node of ast.body) {
|
|
2475
|
+
if (node.type !== "ExportNamedDeclaration" && node.type !== "ExportDefaultDeclaration") {
|
|
2476
|
+
continue;
|
|
2477
|
+
}
|
|
2478
|
+
let exportName = "default";
|
|
2479
|
+
if (node.type === "ExportNamedDeclaration") {
|
|
2480
|
+
if (node.declaration) {
|
|
2481
|
+
if (node.declaration.type === "VariableDeclaration") {
|
|
2482
|
+
const decl = node.declaration.declarations[0];
|
|
2483
|
+
if (decl?.id.type === "Identifier") {
|
|
2484
|
+
exportName = decl.id.name;
|
|
2485
|
+
}
|
|
2486
|
+
} else if (node.declaration.type === "TSTypeAliasDeclaration" || node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
|
|
2487
|
+
exportName = node.declaration.type === "TSTypeAliasDeclaration" ? node.declaration.id.name : node.declaration.id.name;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
if (node.source) continue;
|
|
2491
|
+
}
|
|
2492
|
+
const linesBefore = content.substring(0, node.range?.[0] ?? 0);
|
|
2493
|
+
const lastCommentBlock = linesBefore.match(/\/\*\*[\s\S]*?\*\/\s*$/);
|
|
2494
|
+
if (!lastCommentBlock || !lastCommentBlock[0].includes("@deprecated")) {
|
|
2495
|
+
missingDeprecated.push(exportName);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
return missingDeprecated;
|
|
2499
|
+
}
|
|
2500
|
+
function getNonReexportStatements(ast) {
|
|
2501
|
+
const violations = [];
|
|
2502
|
+
for (const node of ast.body) {
|
|
2503
|
+
if (node.type === "ImportDeclaration") continue;
|
|
2504
|
+
if (node.type === "ExportAllDeclaration") continue;
|
|
2505
|
+
if (node.type === "ExportNamedDeclaration" && node.source) continue;
|
|
2506
|
+
if (node.type === "ExportNamedDeclaration" && node.exportKind === "type" && node.source) continue;
|
|
2507
|
+
violations.push(node);
|
|
2508
|
+
}
|
|
2509
|
+
return violations;
|
|
2510
|
+
}
|
|
2511
|
+
async function checkEnumBundleRules(cwd) {
|
|
2512
|
+
const results = [];
|
|
2513
|
+
const files = await fg9("src/**/*.enum-bundle.ts", {
|
|
2514
|
+
cwd,
|
|
2515
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
2516
|
+
});
|
|
2517
|
+
for (const file of files) {
|
|
2518
|
+
const fullPath = `${cwd}/${file}`;
|
|
2519
|
+
const content = readFileContent(fullPath);
|
|
2520
|
+
const ast = parseFile(fullPath);
|
|
2521
|
+
if (!ast) continue;
|
|
2522
|
+
const exportedNames = getExportedNames(ast);
|
|
2523
|
+
const fileName = basename5(file, ".enum-bundle.ts");
|
|
2524
|
+
const upperName = fileName.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase();
|
|
2525
|
+
const hasSchema = exportedNames.some((n) => n.endsWith("Schema"));
|
|
2526
|
+
const hasEnumMapper = exportedNames.some((n) => n === n.toUpperCase() && n.length > 1);
|
|
2527
|
+
const hasValues = exportedNames.some((n) => n.endsWith("_VALUES"));
|
|
2528
|
+
if (!hasSchema || !hasEnumMapper || !hasValues) {
|
|
2529
|
+
const missing = [];
|
|
2530
|
+
if (!hasSchema) missing.push("{name}Schema");
|
|
2531
|
+
if (!hasEnumMapper) missing.push("{NAME}");
|
|
2532
|
+
if (!hasValues) missing.push("{NAME}_VALUES");
|
|
2533
|
+
results.push({
|
|
2534
|
+
ruleId: "CTR-ENUM-001",
|
|
2535
|
+
severity: "error",
|
|
2536
|
+
message: `Enum bundle ${file} missing exports: ${missing.join(", ")}`,
|
|
2537
|
+
file,
|
|
2538
|
+
suggestion: `Export ${upperName}_VALUES, ${upperName}, and a Schema const`
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
if (!hasCreateEnumBundleCall(content)) {
|
|
2542
|
+
results.push({
|
|
2543
|
+
ruleId: "CTR-ENUM-002",
|
|
2544
|
+
severity: "warning",
|
|
2545
|
+
message: `Enum bundle ${file} does not use createEnumBundle()`,
|
|
2546
|
+
file,
|
|
2547
|
+
suggestion: "Use createEnumBundle() for consistent schema/enum/values creation"
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2550
|
+
if (!hasZodInferTypeExport(content)) {
|
|
2551
|
+
results.push({
|
|
2552
|
+
ruleId: "CTR-ENUM-003",
|
|
2553
|
+
severity: "error",
|
|
2554
|
+
message: `Enum bundle ${file} missing type export via z.infer`,
|
|
2555
|
+
file,
|
|
2556
|
+
suggestion: "Add: export type {Name} = z.infer<typeof {name}Schema>"
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
return results;
|
|
2561
|
+
}
|
|
2562
|
+
async function checkResponseRules(cwd) {
|
|
2563
|
+
const results = [];
|
|
2564
|
+
const files = await fg9("src/**/*.response.ts", {
|
|
2565
|
+
cwd,
|
|
2566
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
2567
|
+
});
|
|
2568
|
+
for (const file of files) {
|
|
2569
|
+
const fullPath = `${cwd}/${file}`;
|
|
2570
|
+
const content = readFileContent(fullPath);
|
|
2571
|
+
const ast = parseFile(fullPath);
|
|
2572
|
+
if (!ast) continue;
|
|
2573
|
+
const exportedNames = getExportedNames(ast);
|
|
2574
|
+
const fileName = basename5(file, ".response.ts");
|
|
2575
|
+
const httpCode = fileName.match(/^(\d{3})$/)?.[1];
|
|
2576
|
+
if (httpCode) {
|
|
2577
|
+
if (!exportedNames.some((n) => n.includes(httpCode) && n.endsWith("Schema"))) {
|
|
2578
|
+
results.push({
|
|
2579
|
+
ruleId: "CTR-RESPONSE-001",
|
|
2580
|
+
severity: "error",
|
|
2581
|
+
message: `Response file ${file} missing export containing "${httpCode}" and ending with "Schema"`,
|
|
2582
|
+
file,
|
|
2583
|
+
suggestion: `Export a schema whose name contains ${httpCode} and ends with Schema (e.g. MyFeature${httpCode}Schema)`
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
if (!hasDefaultExport(ast)) {
|
|
2588
|
+
results.push({
|
|
2589
|
+
ruleId: "CTR-RESPONSE-002",
|
|
2590
|
+
severity: "error",
|
|
2591
|
+
message: `Response file ${file} missing default export`,
|
|
2592
|
+
file,
|
|
2593
|
+
suggestion: "Add: export default Response{HTTP}Schema"
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
if (!hasOpenApiDocumentation(content)) {
|
|
2597
|
+
results.push({
|
|
2598
|
+
ruleId: "CTR-RESPONSE-003",
|
|
2599
|
+
severity: "error",
|
|
2600
|
+
message: `Response file ${file} has no OpenAPI documentation (.describe() or .openapi())`,
|
|
2601
|
+
file,
|
|
2602
|
+
suggestion: "Add .describe('...') or .openapi({ description: '...' }) to schema fields"
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
return results;
|
|
2607
|
+
}
|
|
2608
|
+
async function checkRequestRules(cwd) {
|
|
2609
|
+
const results = [];
|
|
2610
|
+
const files = await fg9("src/**/*.request.ts", {
|
|
2611
|
+
cwd,
|
|
2612
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
2613
|
+
});
|
|
2614
|
+
for (const file of files) {
|
|
2615
|
+
const fullPath = `${cwd}/${file}`;
|
|
2616
|
+
const content = readFileContent(fullPath);
|
|
2617
|
+
const ast = parseFile(fullPath);
|
|
2618
|
+
if (!ast) continue;
|
|
2619
|
+
const exportedNames = getExportedNames(ast);
|
|
2620
|
+
const hasTypedSchema = exportedNames.some(
|
|
2621
|
+
(n) => /(Query|Body|Headers|Params)Schema$/.test(n)
|
|
2622
|
+
);
|
|
2623
|
+
if (!hasTypedSchema) {
|
|
2624
|
+
results.push({
|
|
2625
|
+
ruleId: "CTR-REQUEST-001",
|
|
2626
|
+
severity: "error",
|
|
2627
|
+
message: `Request file ${file} missing typed schema export (ending with QuerySchema, BodySchema, HeadersSchema, or ParamsSchema)`,
|
|
2628
|
+
file,
|
|
2629
|
+
suggestion: "Export a schema whose name ends with: QuerySchema, BodySchema, HeadersSchema, or ParamsSchema"
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
if (!hasOpenApiDocumentation(content)) {
|
|
2633
|
+
results.push({
|
|
2634
|
+
ruleId: "CTR-REQUEST-002",
|
|
2635
|
+
severity: "error",
|
|
2636
|
+
message: `Request file ${file} has no OpenAPI documentation (.describe() or .openapi())`,
|
|
2637
|
+
file,
|
|
2638
|
+
suggestion: "Add .describe('...') or .openapi({ description: '...' }) to schema fields"
|
|
2639
|
+
});
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
return results;
|
|
2643
|
+
}
|
|
2644
|
+
async function checkContractFileRules(cwd) {
|
|
2645
|
+
const results = [];
|
|
2646
|
+
const files = await fg9("src/**/*.contract.ts", {
|
|
2647
|
+
cwd,
|
|
2648
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/*.contract.test.ts"]
|
|
2649
|
+
});
|
|
2650
|
+
for (const file of files) {
|
|
2651
|
+
const fullPath = `${cwd}/${file}`;
|
|
2652
|
+
const content = readFileContent(fullPath);
|
|
2653
|
+
const ast = parseFile(fullPath);
|
|
2654
|
+
if (!ast) continue;
|
|
2655
|
+
const exportedNames = getExportedNames(ast);
|
|
2656
|
+
const hasContractExport = exportedNames.some((n) => n.endsWith("Contract"));
|
|
2657
|
+
if (!hasContractExport) {
|
|
2658
|
+
results.push({
|
|
2659
|
+
ruleId: "CTR-CONTRACT-001",
|
|
2660
|
+
severity: "error",
|
|
2661
|
+
message: `Contract file ${file} missing {Name}Contract export`,
|
|
2662
|
+
file,
|
|
2663
|
+
suggestion: "Export const {Name}Contract = c.router({...})"
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
if (!content.includes("strictStatusCodes")) {
|
|
2667
|
+
results.push({
|
|
2668
|
+
ruleId: "CTR-CONTRACT-002",
|
|
2669
|
+
severity: "error",
|
|
2670
|
+
message: `Contract file ${file} missing strictStatusCodes: true`,
|
|
2671
|
+
file,
|
|
2672
|
+
suggestion: "Add strictStatusCodes: true to all endpoints"
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
if (!content.includes("contractMetadata")) {
|
|
2676
|
+
results.push({
|
|
2677
|
+
ruleId: "CTR-CONTRACT-003",
|
|
2678
|
+
severity: "error",
|
|
2679
|
+
message: `Contract file ${file} missing contractMetadata() usage`,
|
|
2680
|
+
file,
|
|
2681
|
+
suggestion: "Use metadata: contractMetadata({...}) for each endpoint"
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
return results;
|
|
2686
|
+
}
|
|
2687
|
+
async function checkIndexRules(cwd) {
|
|
2688
|
+
const results = [];
|
|
2689
|
+
const files = await fg9("src/**/index.ts", {
|
|
2690
|
+
cwd,
|
|
2691
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/lib/**"]
|
|
2692
|
+
});
|
|
2693
|
+
for (const file of files) {
|
|
2694
|
+
const fullPath = `${cwd}/${file}`;
|
|
2695
|
+
const ast = parseFile(fullPath);
|
|
2696
|
+
if (!ast) continue;
|
|
2697
|
+
const violations = getNonReexportStatements(ast);
|
|
2698
|
+
if (violations.length > 0) {
|
|
2699
|
+
results.push({
|
|
2700
|
+
ruleId: "CTR-INDEX-001",
|
|
2701
|
+
severity: "error",
|
|
2702
|
+
message: `Index file ${file} contains ${violations.length} non-reexport statement(s)`,
|
|
2703
|
+
file,
|
|
2704
|
+
line: violations[0]?.loc?.start.line,
|
|
2705
|
+
suggestion: "Index files should only contain: export * from '...' or export { X } from '...'"
|
|
2706
|
+
});
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
return results;
|
|
2710
|
+
}
|
|
2711
|
+
async function checkHelperRules(cwd) {
|
|
2712
|
+
const results = [];
|
|
2713
|
+
const files = await fg9("src/**/*.helpers.ts", {
|
|
2714
|
+
cwd,
|
|
2715
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
2716
|
+
});
|
|
2717
|
+
for (const file of files) {
|
|
2718
|
+
const fullPath = `${cwd}/${file}`;
|
|
2719
|
+
const content = readFileContent(fullPath);
|
|
2720
|
+
if (hasZodDataSchemas(content)) {
|
|
2721
|
+
results.push({
|
|
2722
|
+
ruleId: "CTR-HELPER-001",
|
|
2723
|
+
severity: "warning",
|
|
2724
|
+
message: `Helper file ${file} contains Zod data schemas`,
|
|
2725
|
+
file,
|
|
2726
|
+
suggestion: "Move Zod data schemas to .schema.ts or .enum-bundle.ts files"
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return results;
|
|
2731
|
+
}
|
|
2732
|
+
async function checkNamingRules(cwd) {
|
|
2733
|
+
const results = [];
|
|
2734
|
+
const files = await fg9("src/**/*.ts", {
|
|
2735
|
+
cwd,
|
|
2736
|
+
ignore: ["**/node_modules/**", "**/dist/**", "**/lib/**", "**/__tests__/**"]
|
|
2737
|
+
});
|
|
2738
|
+
for (const file of files) {
|
|
2739
|
+
const fileName = basename5(file);
|
|
2740
|
+
if (ALLOWED_SPECIAL_FILES.includes(fileName)) continue;
|
|
2741
|
+
const matchesSuffix = ALLOWED_SUFFIXES.some((suffix) => fileName.endsWith(suffix));
|
|
2742
|
+
if (!matchesSuffix) {
|
|
2743
|
+
results.push({
|
|
2744
|
+
ruleId: "CTR-NAMING-001",
|
|
2745
|
+
severity: "error",
|
|
2746
|
+
message: `File ${file} has non-standard suffix. Allowed: ${ALLOWED_SUFFIXES.join(", ")}`,
|
|
2747
|
+
file,
|
|
2748
|
+
suggestion: `Rename to use one of: ${ALLOWED_SUFFIXES.map((s) => `*${s}`).join(", ")}`
|
|
2749
|
+
});
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
return results;
|
|
2753
|
+
}
|
|
2754
|
+
async function checkDeprecatedRules(cwd) {
|
|
2755
|
+
const results = [];
|
|
2756
|
+
const files = await fg9("src/**/*.deprecated.ts", {
|
|
2757
|
+
cwd,
|
|
2758
|
+
ignore: ["**/node_modules/**", "**/dist/**"]
|
|
2759
|
+
});
|
|
2760
|
+
for (const file of files) {
|
|
2761
|
+
const fullPath = `${cwd}/${file}`;
|
|
2762
|
+
const content = readFileContent(fullPath);
|
|
2763
|
+
const ast = parseFile(fullPath);
|
|
2764
|
+
if (!ast) continue;
|
|
2765
|
+
const missing = getExportsWithoutDeprecatedJsdoc(ast, content);
|
|
2766
|
+
if (missing.length > 0) {
|
|
2767
|
+
results.push({
|
|
2768
|
+
ruleId: "CTR-DEPRECATED-001",
|
|
2769
|
+
severity: "error",
|
|
2770
|
+
message: `Deprecated file ${file} has exports without @deprecated JSDoc: ${missing.join(", ")}`,
|
|
2771
|
+
file,
|
|
2772
|
+
suggestion: "Add /** @deprecated Use X instead */ before each export"
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
return results;
|
|
2777
|
+
}
|
|
2778
|
+
var contractsNomenclatureChecker = {
|
|
2779
|
+
name: "contracts-nomenclature",
|
|
2780
|
+
rules: [
|
|
2781
|
+
"CTR-ENUM-001",
|
|
2782
|
+
"CTR-ENUM-002",
|
|
2783
|
+
"CTR-ENUM-003",
|
|
2784
|
+
"CTR-RESPONSE-001",
|
|
2785
|
+
"CTR-RESPONSE-002",
|
|
2786
|
+
"CTR-RESPONSE-003",
|
|
2787
|
+
"CTR-REQUEST-001",
|
|
2788
|
+
"CTR-REQUEST-002",
|
|
2789
|
+
"CTR-CONTRACT-001",
|
|
2790
|
+
"CTR-CONTRACT-002",
|
|
2791
|
+
"CTR-CONTRACT-003",
|
|
2792
|
+
"CTR-INDEX-001",
|
|
2793
|
+
"CTR-HELPER-001",
|
|
2794
|
+
"CTR-NAMING-001",
|
|
2795
|
+
"CTR-DEPRECATED-001"
|
|
2796
|
+
],
|
|
2797
|
+
async check(ctx) {
|
|
2798
|
+
const configResult = loadConfig(ctx.cwd);
|
|
2799
|
+
if (!configResult.success || configResult.config.project.type !== "contracts-lib") {
|
|
2800
|
+
return [];
|
|
2801
|
+
}
|
|
2802
|
+
const disabledRules = getDisabledRules(configResult.config);
|
|
2803
|
+
const results = [];
|
|
2804
|
+
const [
|
|
2805
|
+
enumResults,
|
|
2806
|
+
responseResults,
|
|
2807
|
+
requestResults,
|
|
2808
|
+
contractResults,
|
|
2809
|
+
indexResults,
|
|
2810
|
+
helperResults,
|
|
2811
|
+
namingResults,
|
|
2812
|
+
deprecatedResults
|
|
2813
|
+
] = await Promise.all([
|
|
2814
|
+
checkEnumBundleRules(ctx.cwd),
|
|
2815
|
+
checkResponseRules(ctx.cwd),
|
|
2816
|
+
checkRequestRules(ctx.cwd),
|
|
2817
|
+
checkContractFileRules(ctx.cwd),
|
|
2818
|
+
checkIndexRules(ctx.cwd),
|
|
2819
|
+
checkHelperRules(ctx.cwd),
|
|
2820
|
+
checkNamingRules(ctx.cwd),
|
|
2821
|
+
checkDeprecatedRules(ctx.cwd)
|
|
2822
|
+
]);
|
|
2823
|
+
results.push(
|
|
2824
|
+
...enumResults,
|
|
2825
|
+
...responseResults,
|
|
2826
|
+
...requestResults,
|
|
2827
|
+
...contractResults,
|
|
2828
|
+
...indexResults,
|
|
2829
|
+
...helperResults,
|
|
2830
|
+
...namingResults,
|
|
2831
|
+
...deprecatedResults
|
|
2832
|
+
);
|
|
2833
|
+
if (disabledRules.length === 0) return results;
|
|
2834
|
+
return results.filter((r) => !disabledRules.includes(r.ruleId));
|
|
2835
|
+
}
|
|
2836
|
+
};
|
|
2837
|
+
|
|
2283
2838
|
// src/lint/checkers/contracts/index.ts
|
|
2284
2839
|
var contractsChecker = {
|
|
2285
2840
|
name: "contracts",
|
|
@@ -2361,6 +2916,8 @@ var contractsChecker = {
|
|
|
2361
2916
|
}
|
|
2362
2917
|
const colocationResults = await contractsColocationChecker.check(ctx);
|
|
2363
2918
|
results.push(...colocationResults);
|
|
2919
|
+
const nomenclatureResults = await contractsNomenclatureChecker.check(ctx);
|
|
2920
|
+
results.push(...nomenclatureResults);
|
|
2364
2921
|
return results;
|
|
2365
2922
|
}
|
|
2366
2923
|
};
|
|
@@ -2759,10 +3316,10 @@ Completed in ${elapsed}ms`);
|
|
|
2759
3316
|
}
|
|
2760
3317
|
|
|
2761
3318
|
// src/commands/analyze.ts
|
|
2762
|
-
import { basename as
|
|
3319
|
+
import { basename as basename6 } from "path";
|
|
2763
3320
|
import { execSync as execSync2 } from "child_process";
|
|
2764
|
-
import { readFileSync as
|
|
2765
|
-
import
|
|
3321
|
+
import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
|
|
3322
|
+
import fg10 from "fast-glob";
|
|
2766
3323
|
import matter from "gray-matter";
|
|
2767
3324
|
import { minimatch } from "minimatch";
|
|
2768
3325
|
function expandPath(p) {
|
|
@@ -2791,18 +3348,18 @@ function loadKnowledgeMappings(knowledgePath) {
|
|
|
2791
3348
|
if (!existsSync6(expandedPath)) {
|
|
2792
3349
|
return [];
|
|
2793
3350
|
}
|
|
2794
|
-
const knowledgeFiles =
|
|
3351
|
+
const knowledgeFiles = fg10.sync("**/*.knowledge.md", {
|
|
2795
3352
|
cwd: expandedPath,
|
|
2796
3353
|
absolute: true
|
|
2797
3354
|
});
|
|
2798
3355
|
const mappings = [];
|
|
2799
3356
|
for (const file of knowledgeFiles) {
|
|
2800
3357
|
try {
|
|
2801
|
-
const content =
|
|
3358
|
+
const content = readFileSync5(file, "utf-8");
|
|
2802
3359
|
const { data } = matter(content);
|
|
2803
3360
|
if (data.match) {
|
|
2804
3361
|
mappings.push({
|
|
2805
|
-
name: data.name ||
|
|
3362
|
+
name: data.name || basename6(file, ".knowledge.md"),
|
|
2806
3363
|
path: file,
|
|
2807
3364
|
match: data.match,
|
|
2808
3365
|
description: data.description
|
|
@@ -2815,7 +3372,7 @@ function loadKnowledgeMappings(knowledgePath) {
|
|
|
2815
3372
|
}
|
|
2816
3373
|
function matchFileToKnowledges(file, mappings) {
|
|
2817
3374
|
const results = [];
|
|
2818
|
-
const fileName =
|
|
3375
|
+
const fileName = basename6(file);
|
|
2819
3376
|
for (const mapping of mappings) {
|
|
2820
3377
|
if (minimatch(fileName, mapping.match) || minimatch(file, mapping.match)) {
|
|
2821
3378
|
results.push({
|
|
@@ -2877,7 +3434,7 @@ async function analyzeCommand(files = [], options) {
|
|
|
2877
3434
|
}
|
|
2878
3435
|
|
|
2879
3436
|
// src/commands/scaffold.ts
|
|
2880
|
-
import { resolve as resolve3, dirname as dirname5, basename as
|
|
3437
|
+
import { resolve as resolve3, dirname as dirname5, basename as basename7, join as join4 } from "path";
|
|
2881
3438
|
import { mkdirSync, writeFileSync, existsSync as existsSync7 } from "fs";
|
|
2882
3439
|
function toPascalCase(str) {
|
|
2883
3440
|
return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
@@ -2887,7 +3444,7 @@ function toCamelCase(str) {
|
|
|
2887
3444
|
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
2888
3445
|
}
|
|
2889
3446
|
function generateVueFeature(featurePath) {
|
|
2890
|
-
const featureName =
|
|
3447
|
+
const featureName = basename7(featurePath);
|
|
2891
3448
|
const pascalName = toPascalCase(featureName);
|
|
2892
3449
|
const camelName = toCamelCase(featureName);
|
|
2893
3450
|
return [
|
|
@@ -3004,7 +3561,7 @@ describe('${pascalName}Rules', () => {
|
|
|
3004
3561
|
];
|
|
3005
3562
|
}
|
|
3006
3563
|
function generateNestJSFeature(featurePath) {
|
|
3007
|
-
const featureName =
|
|
3564
|
+
const featureName = basename7(featurePath);
|
|
3008
3565
|
const pascalName = toPascalCase(featureName);
|
|
3009
3566
|
const camelName = toCamelCase(featureName);
|
|
3010
3567
|
return [
|
|
@@ -3095,7 +3652,7 @@ describe('${pascalName}Controller', () => {
|
|
|
3095
3652
|
];
|
|
3096
3653
|
}
|
|
3097
3654
|
function generatePlaywrightFeature(featurePath) {
|
|
3098
|
-
const featureName =
|
|
3655
|
+
const featureName = basename7(featurePath);
|
|
3099
3656
|
const pascalName = toPascalCase(featureName);
|
|
3100
3657
|
const camelName = toCamelCase(featureName);
|
|
3101
3658
|
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-TGZA3GHG.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"),
|