@hyphaene/hexa-ts-kit 1.7.2 → 1.9.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.
@@ -1,5 +1,5 @@
1
1
  // src/commands/lint.ts
2
- import { resolve as resolve3 } from "path";
2
+ import { resolve as resolve2 } from "path";
3
3
  import { execSync } from "child_process";
4
4
 
5
5
  // src/lint/checkers/structure/colocation.ts
@@ -753,15 +753,32 @@ var rulesRegistry = {
753
753
  }
754
754
  },
755
755
  // ==========================================================================
756
- // Vault Schema Rules (VAULT-*) - Critical rules for AST parsing
756
+ // Contracts Colocation Rules (COL-CTR-*)
757
757
  // ==========================================================================
758
- "VAULT-IMP-001": {
759
- title: "External schema import",
758
+ "COL-CTR-001": {
759
+ title: "Response schema outside dto/ or common/",
760
760
  severity: "error",
761
- category: "vault",
762
- why: "External imports break static AST parsing. Duplicate schema with @sync-check.",
761
+ category: "contracts",
762
+ why: "Response schemas ({STATUS}.schema.ts) must be in ./dto/ or ../common/ for colocation.",
763
+ autoFixable: false
764
+ },
765
+ "COL-CTR-002": {
766
+ title: "Request schema outside dto/",
767
+ severity: "error",
768
+ category: "contracts",
769
+ why: "Request schemas (request.*.ts) must be in ./dto/ for colocation with contract.",
770
+ autoFixable: false
771
+ },
772
+ "COL-CTR-003": {
773
+ title: "Invalid schema file naming",
774
+ severity: "warning",
775
+ category: "contracts",
776
+ why: "Schema files should follow naming convention: {STATUS}.schema.ts or request.{type}.ts.",
763
777
  autoFixable: false
764
778
  },
779
+ // ==========================================================================
780
+ // Vault Schema Rules (VAULT-*) - Critical rules for AST parsing
781
+ // ==========================================================================
765
782
  "VAULT-AST-001": {
766
783
  title: "Forbidden z.lazy()",
767
784
  severity: "error",
@@ -823,6 +840,9 @@ var ContractsLintConfigSchema = z.object({
823
840
  }),
824
841
  disabledRules: z.array(z.string()).optional()
825
842
  });
843
+ var VaultLintConfigSchema = z.object({
844
+ disabledRules: z.array(z.string()).optional()
845
+ });
826
846
  var ContractsScaffoldConfigSchema = z.object({
827
847
  path: z.string(),
828
848
  // Absolute path to contracts lib
@@ -835,7 +855,8 @@ var ProjectConfigSchema = z.object({
835
855
  var HexaTsKitConfigSchema = z.object({
836
856
  project: ProjectConfigSchema,
837
857
  lint: z.object({
838
- contracts: ContractsLintConfigSchema.optional()
858
+ contracts: ContractsLintConfigSchema.optional(),
859
+ vault: VaultLintConfigSchema.optional()
839
860
  }).optional(),
840
861
  scaffold: z.object({
841
862
  contracts: ContractsScaffoldConfigSchema.optional()
@@ -900,6 +921,9 @@ function resolveConfigPath(repoPath, relativePath) {
900
921
  function getDisabledRules(config) {
901
922
  return config.lint?.contracts?.disabledRules ?? [];
902
923
  }
924
+ function getDisabledVaultRules(config) {
925
+ return config.lint?.vault?.disabledRules ?? [];
926
+ }
903
927
 
904
928
  // src/lib/contracts/permissions-extractor.ts
905
929
  import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
@@ -2100,6 +2124,154 @@ function summarizeGeneration(result) {
2100
2124
  return lines.join("\n");
2101
2125
  }
2102
2126
 
2127
+ // src/lint/checkers/contracts/colocation.ts
2128
+ import fg8 from "fast-glob";
2129
+ import { readFile as readFile5 } from "fs/promises";
2130
+ import { basename as basename4, dirname as dirname3 } from "path";
2131
+ var RESPONSE_SCHEMA_PATTERN = /^\d{3}\.schema$/;
2132
+ var REQUEST_SCHEMA_PATTERN = /^request\./;
2133
+ var VALID_RESPONSE_PATHS = ["./dto/", "../common/", "./common/"];
2134
+ var VALID_REQUEST_PATHS = ["./dto/"];
2135
+ var contractsColocationChecker = {
2136
+ name: "contracts-colocation",
2137
+ rules: ["COL-CTR-001", "COL-CTR-002", "COL-CTR-003"],
2138
+ async check(ctx) {
2139
+ const results = [];
2140
+ const configResult = loadConfig(ctx.cwd);
2141
+ if (!configResult.success || configResult.config.project.type !== "contracts-lib") {
2142
+ return [];
2143
+ }
2144
+ const contractFiles = await fg8("**/contracts/**/*.contract.ts", {
2145
+ cwd: ctx.cwd,
2146
+ ignore: ["**/node_modules/**", "**/dist/**"]
2147
+ });
2148
+ for (const contractFile of contractFiles) {
2149
+ const fullPath = `${ctx.cwd}/${contractFile}`;
2150
+ const content = await readFile5(fullPath, "utf-8");
2151
+ const contractDir = dirname3(contractFile);
2152
+ const imports = parseImports(content);
2153
+ for (const imp of imports) {
2154
+ const schemaResults = checkSchemaImport(contractFile, contractDir, imp);
2155
+ results.push(...schemaResults);
2156
+ }
2157
+ }
2158
+ const schemaFiles = await fg8("**/contracts/**/*.schema.ts", {
2159
+ cwd: ctx.cwd,
2160
+ ignore: ["**/node_modules/**", "**/dist/**", "**/dto/**", "**/common/**"]
2161
+ });
2162
+ for (const schemaFile of schemaFiles) {
2163
+ const fileName = basename4(schemaFile, ".ts");
2164
+ if (RESPONSE_SCHEMA_PATTERN.test(fileName)) {
2165
+ results.push({
2166
+ ruleId: "COL-CTR-001",
2167
+ severity: "error",
2168
+ message: `Response schema outside dto/ or common/: ${schemaFile}`,
2169
+ file: schemaFile,
2170
+ suggestion: `Move to ${dirname3(schemaFile)}/dto/${basename4(schemaFile)}`
2171
+ });
2172
+ } else if (REQUEST_SCHEMA_PATTERN.test(fileName)) {
2173
+ results.push({
2174
+ ruleId: "COL-CTR-002",
2175
+ severity: "error",
2176
+ message: `Request schema outside dto/: ${schemaFile}`,
2177
+ file: schemaFile,
2178
+ suggestion: `Move to ${dirname3(schemaFile)}/dto/${basename4(schemaFile)}`
2179
+ });
2180
+ }
2181
+ }
2182
+ const allSchemaFiles = await fg8("**/contracts/**/*.schema.ts", {
2183
+ cwd: ctx.cwd,
2184
+ ignore: ["**/node_modules/**", "**/dist/**"]
2185
+ });
2186
+ for (const schemaFile of allSchemaFiles) {
2187
+ const fileName = basename4(schemaFile, ".schema.ts");
2188
+ const isValidResponseName = RESPONSE_SCHEMA_PATTERN.test(
2189
+ `${fileName}.schema`
2190
+ );
2191
+ const isValidRequestName = REQUEST_SCHEMA_PATTERN.test(fileName);
2192
+ const isInCommon = schemaFile.includes("/common/");
2193
+ if (!isValidResponseName && !isValidRequestName && !isInCommon) {
2194
+ if (/response|res|status/i.test(fileName)) {
2195
+ results.push({
2196
+ ruleId: "COL-CTR-003",
2197
+ severity: "warning",
2198
+ message: `Schema file may be incorrectly named: ${basename4(schemaFile)}`,
2199
+ file: schemaFile,
2200
+ suggestion: `Use {STATUS_CODE}.schema.ts for response schemas (e.g., 200.schema.ts)`
2201
+ });
2202
+ }
2203
+ }
2204
+ }
2205
+ return results;
2206
+ }
2207
+ };
2208
+ function parseImports(content) {
2209
+ const imports = [];
2210
+ const lines = content.split("\n");
2211
+ for (let i = 0; i < lines.length; i++) {
2212
+ const currentLine = lines[i];
2213
+ if (!currentLine) continue;
2214
+ const importMatch = currentLine.match(
2215
+ /import\s+(?:{([^}]+)}|(\w+))\s+from\s+['"]([^'"]+)['"]/
2216
+ );
2217
+ if (importMatch) {
2218
+ const namedImports = importMatch[1];
2219
+ const defaultImport = importMatch[2];
2220
+ const importPath = importMatch[3];
2221
+ if (!importPath) continue;
2222
+ if (namedImports) {
2223
+ const names = namedImports.split(",").map((n) => n.trim().split(" as ")[0]?.trim()).filter((n) => !!n);
2224
+ for (const name of names) {
2225
+ imports.push({ importPath, importedName: name, line: i + 1 });
2226
+ }
2227
+ } else if (defaultImport) {
2228
+ imports.push({ importPath, importedName: defaultImport, line: i + 1 });
2229
+ }
2230
+ }
2231
+ }
2232
+ return imports;
2233
+ }
2234
+ function checkSchemaImport(contractFile, _contractDir, imp) {
2235
+ const results = [];
2236
+ const { importPath, importedName, line } = imp;
2237
+ if (!importPath.startsWith(".")) {
2238
+ return [];
2239
+ }
2240
+ const isResponseSchema = /Schema$/.test(importedName) && (/^\d{3}/.test(importedName) || /Response\d{3}/.test(importedName) || /^Response\d{3}Schema/.test(importedName));
2241
+ const isRequestSchema = /Schema$/.test(importedName) && /Query|Request|Body/i.test(importedName);
2242
+ if (isResponseSchema) {
2243
+ const isValidPath = VALID_RESPONSE_PATHS.some(
2244
+ (valid) => importPath.startsWith(valid)
2245
+ );
2246
+ if (!isValidPath) {
2247
+ results.push({
2248
+ ruleId: "COL-CTR-001",
2249
+ severity: "error",
2250
+ message: `Response schema import from invalid path: ${importPath}`,
2251
+ file: contractFile,
2252
+ line,
2253
+ suggestion: `Import from ./dto/ or ../common/ instead`
2254
+ });
2255
+ }
2256
+ }
2257
+ if (isRequestSchema) {
2258
+ const isValidPath = VALID_REQUEST_PATHS.some(
2259
+ (valid) => importPath.startsWith(valid)
2260
+ );
2261
+ if (!isValidPath) {
2262
+ results.push({
2263
+ ruleId: "COL-CTR-002",
2264
+ severity: "error",
2265
+ message: `Request schema import from invalid path: ${importPath}`,
2266
+ file: contractFile,
2267
+ line,
2268
+ suggestion: `Import from ./dto/ instead`
2269
+ });
2270
+ }
2271
+ }
2272
+ return results;
2273
+ }
2274
+
2103
2275
  // src/lint/checkers/contracts/index.ts
2104
2276
  var contractsChecker = {
2105
2277
  name: "contracts",
@@ -2179,13 +2351,15 @@ var contractsChecker = {
2179
2351
  suggestion: issue.suggestion
2180
2352
  });
2181
2353
  }
2354
+ const colocationResults = await contractsColocationChecker.check(ctx);
2355
+ results.push(...colocationResults);
2182
2356
  return results;
2183
2357
  }
2184
2358
  };
2185
2359
 
2186
2360
  // src/lint/checkers/vault/index.ts
2187
2361
  import { existsSync as existsSync5, readdirSync } from "fs";
2188
- import { join as join3, dirname as dirname3, resolve as resolve2 } from "path";
2362
+ import { join as join3, dirname as dirname4 } from "path";
2189
2363
  import { Project, SyntaxKind } from "ts-morph";
2190
2364
  var VAULT_PATHS = [
2191
2365
  "src/common/config/vault",
@@ -2206,7 +2380,7 @@ function findVaultLocation(cwd) {
2206
2380
  for (const file of LEGACY_FILES) {
2207
2381
  const fullPath = join3(cwd, file);
2208
2382
  if (existsSync5(fullPath)) {
2209
- return { basePath: dirname3(fullPath), files: [fullPath] };
2383
+ return { basePath: dirname4(fullPath), files: [fullPath] };
2210
2384
  }
2211
2385
  }
2212
2386
  return null;
@@ -2223,49 +2397,6 @@ function isInsideZodObject(node) {
2223
2397
  }
2224
2398
  return false;
2225
2399
  }
2226
- function checkExternalImports(sourceFile, vault) {
2227
- const results = [];
2228
- const filePath = sourceFile.getFilePath();
2229
- sourceFile.getImportDeclarations().forEach((imp) => {
2230
- const moduleSpec = imp.getModuleSpecifierValue();
2231
- const namedImports = imp.getNamedImports();
2232
- if (moduleSpec === "zod") return;
2233
- if (moduleSpec.startsWith(".")) {
2234
- const resolvedPath = resolve2(dirname3(filePath), moduleSpec);
2235
- if (!resolvedPath.startsWith(vault.basePath)) {
2236
- const hasSchemaImport = namedImports.some(
2237
- (ni) => ni.getName().toLowerCase().includes("schema")
2238
- );
2239
- if (hasSchemaImport) {
2240
- results.push({
2241
- ruleId: "VAULT-IMP-001",
2242
- severity: getRuleSeverity("VAULT-IMP-001"),
2243
- message: `External schema import from "${moduleSpec}"`,
2244
- file: filePath,
2245
- line: imp.getStartLineNumber(),
2246
- suggestion: "Move schema to vault/ or duplicate with @sync-check"
2247
- });
2248
- }
2249
- }
2250
- }
2251
- if (!moduleSpec.startsWith(".")) {
2252
- const hasSchemaImport = namedImports.some(
2253
- (ni) => ni.getName().toLowerCase().includes("schema")
2254
- );
2255
- if (hasSchemaImport) {
2256
- results.push({
2257
- ruleId: "VAULT-IMP-001",
2258
- severity: getRuleSeverity("VAULT-IMP-001"),
2259
- message: `External schema import from package "${moduleSpec}"`,
2260
- file: filePath,
2261
- line: imp.getStartLineNumber(),
2262
- suggestion: "Define schema locally in vault/ instead"
2263
- });
2264
- }
2265
- }
2266
- });
2267
- return results;
2268
- }
2269
2400
  function checkAstPatterns(sourceFile) {
2270
2401
  const results = [];
2271
2402
  const filePath = sourceFile.getFilePath();
@@ -2361,6 +2492,8 @@ var vaultChecker = {
2361
2492
  if (!vault) {
2362
2493
  return results;
2363
2494
  }
2495
+ const configResult = loadConfig(ctx.cwd);
2496
+ const disabledRules = configResult.success ? getDisabledVaultRules(configResult.config) : [];
2364
2497
  const project = new Project({
2365
2498
  skipAddingFilesFromTsConfig: true,
2366
2499
  skipFileDependencyResolution: true
@@ -2375,8 +2508,12 @@ var vaultChecker = {
2375
2508
  }
2376
2509
  }
2377
2510
  for (const sourceFile of sourceFiles) {
2378
- results.push(...checkExternalImports(sourceFile, vault));
2379
- results.push(...checkAstPatterns(sourceFile));
2511
+ const astResults = checkAstPatterns(sourceFile);
2512
+ for (const result of astResults) {
2513
+ if (!disabledRules.includes(result.ruleId)) {
2514
+ results.push(result);
2515
+ }
2516
+ }
2380
2517
  }
2381
2518
  return results;
2382
2519
  }
@@ -2503,7 +2640,7 @@ function getChangedFiles(cwd) {
2503
2640
  }
2504
2641
  }
2505
2642
  async function lintCore(options) {
2506
- const cwd = resolve3(options.path || ".");
2643
+ const cwd = resolve2(options.path || ".");
2507
2644
  if (options.rules) {
2508
2645
  const standaloneKey = options.rules.toUpperCase();
2509
2646
  if (standaloneCheckers[standaloneKey]) {
@@ -2583,8 +2720,8 @@ async function lintCommand(path = ".", options) {
2583
2720
  const startTime = performance.now();
2584
2721
  const targetPath = options.cwd || path;
2585
2722
  if (options.debug) {
2586
- console.log(`Linting: ${resolve3(targetPath)}`);
2587
- const projectConfig = detectProjectType(resolve3(targetPath));
2723
+ console.log(`Linting: ${resolve2(targetPath)}`);
2724
+ const projectConfig = detectProjectType(resolve2(targetPath));
2588
2725
  console.log(`Project type: ${projectConfig.type ?? "not configured"}`);
2589
2726
  console.log(
2590
2727
  `Checkers: ${projectConfig.checkers.map((c) => c.name).join(", ") || "none"}`
@@ -2614,10 +2751,10 @@ Completed in ${elapsed}ms`);
2614
2751
  }
2615
2752
 
2616
2753
  // src/commands/analyze.ts
2617
- import { basename as basename4 } from "path";
2754
+ import { basename as basename5 } from "path";
2618
2755
  import { execSync as execSync2 } from "child_process";
2619
2756
  import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
2620
- import fg8 from "fast-glob";
2757
+ import fg9 from "fast-glob";
2621
2758
  import matter from "gray-matter";
2622
2759
  import { minimatch } from "minimatch";
2623
2760
  function expandPath(p) {
@@ -2646,7 +2783,7 @@ function loadKnowledgeMappings(knowledgePath) {
2646
2783
  if (!existsSync6(expandedPath)) {
2647
2784
  return [];
2648
2785
  }
2649
- const knowledgeFiles = fg8.sync("**/*.knowledge.md", {
2786
+ const knowledgeFiles = fg9.sync("**/*.knowledge.md", {
2650
2787
  cwd: expandedPath,
2651
2788
  absolute: true
2652
2789
  });
@@ -2657,7 +2794,7 @@ function loadKnowledgeMappings(knowledgePath) {
2657
2794
  const { data } = matter(content);
2658
2795
  if (data.match) {
2659
2796
  mappings.push({
2660
- name: data.name || basename4(file, ".knowledge.md"),
2797
+ name: data.name || basename5(file, ".knowledge.md"),
2661
2798
  path: file,
2662
2799
  match: data.match,
2663
2800
  description: data.description
@@ -2670,7 +2807,7 @@ function loadKnowledgeMappings(knowledgePath) {
2670
2807
  }
2671
2808
  function matchFileToKnowledges(file, mappings) {
2672
2809
  const results = [];
2673
- const fileName = basename4(file);
2810
+ const fileName = basename5(file);
2674
2811
  for (const mapping of mappings) {
2675
2812
  if (minimatch(fileName, mapping.match) || minimatch(file, mapping.match)) {
2676
2813
  results.push({
@@ -2732,7 +2869,7 @@ async function analyzeCommand(files = [], options) {
2732
2869
  }
2733
2870
 
2734
2871
  // src/commands/scaffold.ts
2735
- import { resolve as resolve4, dirname as dirname4, basename as basename5, join as join4 } from "path";
2872
+ import { resolve as resolve3, dirname as dirname5, basename as basename6, join as join4 } from "path";
2736
2873
  import { mkdirSync, writeFileSync, existsSync as existsSync7 } from "fs";
2737
2874
  function toPascalCase(str) {
2738
2875
  return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
@@ -2742,7 +2879,7 @@ function toCamelCase(str) {
2742
2879
  return pascal.charAt(0).toLowerCase() + pascal.slice(1);
2743
2880
  }
2744
2881
  function generateVueFeature(featurePath) {
2745
- const featureName = basename5(featurePath);
2882
+ const featureName = basename6(featurePath);
2746
2883
  const pascalName = toPascalCase(featureName);
2747
2884
  const camelName = toCamelCase(featureName);
2748
2885
  return [
@@ -2859,7 +2996,7 @@ describe('${pascalName}Rules', () => {
2859
2996
  ];
2860
2997
  }
2861
2998
  function generateNestJSFeature(featurePath) {
2862
- const featureName = basename5(featurePath);
2999
+ const featureName = basename6(featurePath);
2863
3000
  const pascalName = toPascalCase(featureName);
2864
3001
  const camelName = toCamelCase(featureName);
2865
3002
  return [
@@ -2950,7 +3087,7 @@ describe('${pascalName}Controller', () => {
2950
3087
  ];
2951
3088
  }
2952
3089
  function generatePlaywrightFeature(featurePath) {
2953
- const featureName = basename5(featurePath);
3090
+ const featureName = basename6(featurePath);
2954
3091
  const pascalName = toPascalCase(featureName);
2955
3092
  const camelName = toCamelCase(featureName);
2956
3093
  return [
@@ -3053,8 +3190,8 @@ async function scaffoldCore(options) {
3053
3190
  };
3054
3191
  }
3055
3192
  const contractsLib = options.contractsLib ?? "@adeo/ahs-operator-execution-contracts";
3056
- const absolutePath2 = resolve4(options.path);
3057
- const absoluteContractPath = resolve4(options.contractPath);
3193
+ const absolutePath2 = resolve3(options.path);
3194
+ const absoluteContractPath = resolve3(options.contractPath);
3058
3195
  const bffResult = generateNestJsBffFeature(
3059
3196
  absolutePath2,
3060
3197
  absoluteContractPath,
@@ -3085,7 +3222,7 @@ async function scaffoldCore(options) {
3085
3222
  const errors2 = [];
3086
3223
  for (const file of bffResult.files) {
3087
3224
  try {
3088
- const dir = dirname4(file.path);
3225
+ const dir = dirname5(file.path);
3089
3226
  if (!existsSync7(dir)) {
3090
3227
  mkdirSync(dir, { recursive: true });
3091
3228
  }
@@ -3116,7 +3253,7 @@ async function scaffoldCore(options) {
3116
3253
  availableTypes: [...Object.keys(generators), "nestjs-bff-feature"]
3117
3254
  };
3118
3255
  }
3119
- const absolutePath = resolve4(options.path);
3256
+ const absolutePath = resolve3(options.path);
3120
3257
  const files = generator(absolutePath);
3121
3258
  if (options.dryRun) {
3122
3259
  return {
@@ -3136,7 +3273,7 @@ async function scaffoldCore(options) {
3136
3273
  const errors = [];
3137
3274
  for (const file of files) {
3138
3275
  try {
3139
- const dir = dirname4(file.path);
3276
+ const dir = dirname5(file.path);
3140
3277
  if (!existsSync7(dir)) {
3141
3278
  mkdirSync(dir, { recursive: true });
3142
3279
  }
package/dist/cli.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  analyzeCommand,
4
4
  lintCommand,
5
5
  scaffoldCommand
6
- } from "./chunk-UNAQG5HI.js";
6
+ } from "./chunk-OI2LN5K5.js";
7
7
 
8
8
  // src/cli.ts
9
9
  import { program } from "commander";
@@ -3,7 +3,7 @@ import {
3
3
  analyzeCore,
4
4
  lintCore,
5
5
  scaffoldCore
6
- } from "./chunk-UNAQG5HI.js";
6
+ } from "./chunk-OI2LN5K5.js";
7
7
 
8
8
  // src/mcp-server.ts
9
9
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphaene/hexa-ts-kit",
3
- "version": "1.7.2",
3
+ "version": "1.9.0",
4
4
  "description": "TypeScript dev kit for Claude Code agents: architecture linting, scaffolding, knowledge analysis",
5
5
  "type": "module",
6
6
  "bin": {