@hyphaene/hexa-ts-kit 1.7.2 → 1.8.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",
@@ -2100,6 +2117,154 @@ function summarizeGeneration(result) {
2100
2117
  return lines.join("\n");
2101
2118
  }
2102
2119
 
2120
+ // src/lint/checkers/contracts/colocation.ts
2121
+ import fg8 from "fast-glob";
2122
+ import { readFile as readFile5 } from "fs/promises";
2123
+ import { basename as basename4, dirname as dirname3 } from "path";
2124
+ var RESPONSE_SCHEMA_PATTERN = /^\d{3}\.schema$/;
2125
+ var REQUEST_SCHEMA_PATTERN = /^request\./;
2126
+ var VALID_RESPONSE_PATHS = ["./dto/", "../common/", "./common/"];
2127
+ var VALID_REQUEST_PATHS = ["./dto/"];
2128
+ var contractsColocationChecker = {
2129
+ name: "contracts-colocation",
2130
+ rules: ["COL-CTR-001", "COL-CTR-002", "COL-CTR-003"],
2131
+ async check(ctx) {
2132
+ const results = [];
2133
+ const configResult = loadConfig(ctx.cwd);
2134
+ if (!configResult.success || configResult.config.project.type !== "contracts-lib") {
2135
+ return [];
2136
+ }
2137
+ const contractFiles = await fg8("**/contracts/**/*.contract.ts", {
2138
+ cwd: ctx.cwd,
2139
+ ignore: ["**/node_modules/**", "**/dist/**"]
2140
+ });
2141
+ for (const contractFile of contractFiles) {
2142
+ const fullPath = `${ctx.cwd}/${contractFile}`;
2143
+ const content = await readFile5(fullPath, "utf-8");
2144
+ const contractDir = dirname3(contractFile);
2145
+ const imports = parseImports(content);
2146
+ for (const imp of imports) {
2147
+ const schemaResults = checkSchemaImport(contractFile, contractDir, imp);
2148
+ results.push(...schemaResults);
2149
+ }
2150
+ }
2151
+ const schemaFiles = await fg8("**/contracts/**/*.schema.ts", {
2152
+ cwd: ctx.cwd,
2153
+ ignore: ["**/node_modules/**", "**/dist/**", "**/dto/**", "**/common/**"]
2154
+ });
2155
+ for (const schemaFile of schemaFiles) {
2156
+ const fileName = basename4(schemaFile, ".ts");
2157
+ if (RESPONSE_SCHEMA_PATTERN.test(fileName)) {
2158
+ results.push({
2159
+ ruleId: "COL-CTR-001",
2160
+ severity: "error",
2161
+ message: `Response schema outside dto/ or common/: ${schemaFile}`,
2162
+ file: schemaFile,
2163
+ suggestion: `Move to ${dirname3(schemaFile)}/dto/${basename4(schemaFile)}`
2164
+ });
2165
+ } else if (REQUEST_SCHEMA_PATTERN.test(fileName)) {
2166
+ results.push({
2167
+ ruleId: "COL-CTR-002",
2168
+ severity: "error",
2169
+ message: `Request schema outside dto/: ${schemaFile}`,
2170
+ file: schemaFile,
2171
+ suggestion: `Move to ${dirname3(schemaFile)}/dto/${basename4(schemaFile)}`
2172
+ });
2173
+ }
2174
+ }
2175
+ const allSchemaFiles = await fg8("**/contracts/**/*.schema.ts", {
2176
+ cwd: ctx.cwd,
2177
+ ignore: ["**/node_modules/**", "**/dist/**"]
2178
+ });
2179
+ for (const schemaFile of allSchemaFiles) {
2180
+ const fileName = basename4(schemaFile, ".schema.ts");
2181
+ const isValidResponseName = RESPONSE_SCHEMA_PATTERN.test(
2182
+ `${fileName}.schema`
2183
+ );
2184
+ const isValidRequestName = REQUEST_SCHEMA_PATTERN.test(fileName);
2185
+ const isInCommon = schemaFile.includes("/common/");
2186
+ if (!isValidResponseName && !isValidRequestName && !isInCommon) {
2187
+ if (/response|res|status/i.test(fileName)) {
2188
+ results.push({
2189
+ ruleId: "COL-CTR-003",
2190
+ severity: "warning",
2191
+ message: `Schema file may be incorrectly named: ${basename4(schemaFile)}`,
2192
+ file: schemaFile,
2193
+ suggestion: `Use {STATUS_CODE}.schema.ts for response schemas (e.g., 200.schema.ts)`
2194
+ });
2195
+ }
2196
+ }
2197
+ }
2198
+ return results;
2199
+ }
2200
+ };
2201
+ function parseImports(content) {
2202
+ const imports = [];
2203
+ const lines = content.split("\n");
2204
+ for (let i = 0; i < lines.length; i++) {
2205
+ const currentLine = lines[i];
2206
+ if (!currentLine) continue;
2207
+ const importMatch = currentLine.match(
2208
+ /import\s+(?:{([^}]+)}|(\w+))\s+from\s+['"]([^'"]+)['"]/
2209
+ );
2210
+ if (importMatch) {
2211
+ const namedImports = importMatch[1];
2212
+ const defaultImport = importMatch[2];
2213
+ const importPath = importMatch[3];
2214
+ if (!importPath) continue;
2215
+ if (namedImports) {
2216
+ const names = namedImports.split(",").map((n) => n.trim().split(" as ")[0]?.trim()).filter((n) => !!n);
2217
+ for (const name of names) {
2218
+ imports.push({ importPath, importedName: name, line: i + 1 });
2219
+ }
2220
+ } else if (defaultImport) {
2221
+ imports.push({ importPath, importedName: defaultImport, line: i + 1 });
2222
+ }
2223
+ }
2224
+ }
2225
+ return imports;
2226
+ }
2227
+ function checkSchemaImport(contractFile, _contractDir, imp) {
2228
+ const results = [];
2229
+ const { importPath, importedName, line } = imp;
2230
+ if (!importPath.startsWith(".")) {
2231
+ return [];
2232
+ }
2233
+ const isResponseSchema = /Schema$/.test(importedName) && (/^\d{3}/.test(importedName) || /Response\d{3}/.test(importedName) || /^Response\d{3}Schema/.test(importedName));
2234
+ const isRequestSchema = /Schema$/.test(importedName) && /Query|Request|Body/i.test(importedName);
2235
+ if (isResponseSchema) {
2236
+ const isValidPath = VALID_RESPONSE_PATHS.some(
2237
+ (valid) => importPath.startsWith(valid)
2238
+ );
2239
+ if (!isValidPath) {
2240
+ results.push({
2241
+ ruleId: "COL-CTR-001",
2242
+ severity: "error",
2243
+ message: `Response schema import from invalid path: ${importPath}`,
2244
+ file: contractFile,
2245
+ line,
2246
+ suggestion: `Import from ./dto/ or ../common/ instead`
2247
+ });
2248
+ }
2249
+ }
2250
+ if (isRequestSchema) {
2251
+ const isValidPath = VALID_REQUEST_PATHS.some(
2252
+ (valid) => importPath.startsWith(valid)
2253
+ );
2254
+ if (!isValidPath) {
2255
+ results.push({
2256
+ ruleId: "COL-CTR-002",
2257
+ severity: "error",
2258
+ message: `Request schema import from invalid path: ${importPath}`,
2259
+ file: contractFile,
2260
+ line,
2261
+ suggestion: `Import from ./dto/ instead`
2262
+ });
2263
+ }
2264
+ }
2265
+ return results;
2266
+ }
2267
+
2103
2268
  // src/lint/checkers/contracts/index.ts
2104
2269
  var contractsChecker = {
2105
2270
  name: "contracts",
@@ -2179,13 +2344,15 @@ var contractsChecker = {
2179
2344
  suggestion: issue.suggestion
2180
2345
  });
2181
2346
  }
2347
+ const colocationResults = await contractsColocationChecker.check(ctx);
2348
+ results.push(...colocationResults);
2182
2349
  return results;
2183
2350
  }
2184
2351
  };
2185
2352
 
2186
2353
  // src/lint/checkers/vault/index.ts
2187
2354
  import { existsSync as existsSync5, readdirSync } from "fs";
2188
- import { join as join3, dirname as dirname3, resolve as resolve2 } from "path";
2355
+ import { join as join3, dirname as dirname4 } from "path";
2189
2356
  import { Project, SyntaxKind } from "ts-morph";
2190
2357
  var VAULT_PATHS = [
2191
2358
  "src/common/config/vault",
@@ -2206,7 +2373,7 @@ function findVaultLocation(cwd) {
2206
2373
  for (const file of LEGACY_FILES) {
2207
2374
  const fullPath = join3(cwd, file);
2208
2375
  if (existsSync5(fullPath)) {
2209
- return { basePath: dirname3(fullPath), files: [fullPath] };
2376
+ return { basePath: dirname4(fullPath), files: [fullPath] };
2210
2377
  }
2211
2378
  }
2212
2379
  return null;
@@ -2223,49 +2390,6 @@ function isInsideZodObject(node) {
2223
2390
  }
2224
2391
  return false;
2225
2392
  }
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
2393
  function checkAstPatterns(sourceFile) {
2270
2394
  const results = [];
2271
2395
  const filePath = sourceFile.getFilePath();
@@ -2375,7 +2499,6 @@ var vaultChecker = {
2375
2499
  }
2376
2500
  }
2377
2501
  for (const sourceFile of sourceFiles) {
2378
- results.push(...checkExternalImports(sourceFile, vault));
2379
2502
  results.push(...checkAstPatterns(sourceFile));
2380
2503
  }
2381
2504
  return results;
@@ -2503,7 +2626,7 @@ function getChangedFiles(cwd) {
2503
2626
  }
2504
2627
  }
2505
2628
  async function lintCore(options) {
2506
- const cwd = resolve3(options.path || ".");
2629
+ const cwd = resolve2(options.path || ".");
2507
2630
  if (options.rules) {
2508
2631
  const standaloneKey = options.rules.toUpperCase();
2509
2632
  if (standaloneCheckers[standaloneKey]) {
@@ -2583,8 +2706,8 @@ async function lintCommand(path = ".", options) {
2583
2706
  const startTime = performance.now();
2584
2707
  const targetPath = options.cwd || path;
2585
2708
  if (options.debug) {
2586
- console.log(`Linting: ${resolve3(targetPath)}`);
2587
- const projectConfig = detectProjectType(resolve3(targetPath));
2709
+ console.log(`Linting: ${resolve2(targetPath)}`);
2710
+ const projectConfig = detectProjectType(resolve2(targetPath));
2588
2711
  console.log(`Project type: ${projectConfig.type ?? "not configured"}`);
2589
2712
  console.log(
2590
2713
  `Checkers: ${projectConfig.checkers.map((c) => c.name).join(", ") || "none"}`
@@ -2614,10 +2737,10 @@ Completed in ${elapsed}ms`);
2614
2737
  }
2615
2738
 
2616
2739
  // src/commands/analyze.ts
2617
- import { basename as basename4 } from "path";
2740
+ import { basename as basename5 } from "path";
2618
2741
  import { execSync as execSync2 } from "child_process";
2619
2742
  import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
2620
- import fg8 from "fast-glob";
2743
+ import fg9 from "fast-glob";
2621
2744
  import matter from "gray-matter";
2622
2745
  import { minimatch } from "minimatch";
2623
2746
  function expandPath(p) {
@@ -2646,7 +2769,7 @@ function loadKnowledgeMappings(knowledgePath) {
2646
2769
  if (!existsSync6(expandedPath)) {
2647
2770
  return [];
2648
2771
  }
2649
- const knowledgeFiles = fg8.sync("**/*.knowledge.md", {
2772
+ const knowledgeFiles = fg9.sync("**/*.knowledge.md", {
2650
2773
  cwd: expandedPath,
2651
2774
  absolute: true
2652
2775
  });
@@ -2657,7 +2780,7 @@ function loadKnowledgeMappings(knowledgePath) {
2657
2780
  const { data } = matter(content);
2658
2781
  if (data.match) {
2659
2782
  mappings.push({
2660
- name: data.name || basename4(file, ".knowledge.md"),
2783
+ name: data.name || basename5(file, ".knowledge.md"),
2661
2784
  path: file,
2662
2785
  match: data.match,
2663
2786
  description: data.description
@@ -2670,7 +2793,7 @@ function loadKnowledgeMappings(knowledgePath) {
2670
2793
  }
2671
2794
  function matchFileToKnowledges(file, mappings) {
2672
2795
  const results = [];
2673
- const fileName = basename4(file);
2796
+ const fileName = basename5(file);
2674
2797
  for (const mapping of mappings) {
2675
2798
  if (minimatch(fileName, mapping.match) || minimatch(file, mapping.match)) {
2676
2799
  results.push({
@@ -2732,7 +2855,7 @@ async function analyzeCommand(files = [], options) {
2732
2855
  }
2733
2856
 
2734
2857
  // src/commands/scaffold.ts
2735
- import { resolve as resolve4, dirname as dirname4, basename as basename5, join as join4 } from "path";
2858
+ import { resolve as resolve3, dirname as dirname5, basename as basename6, join as join4 } from "path";
2736
2859
  import { mkdirSync, writeFileSync, existsSync as existsSync7 } from "fs";
2737
2860
  function toPascalCase(str) {
2738
2861
  return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
@@ -2742,7 +2865,7 @@ function toCamelCase(str) {
2742
2865
  return pascal.charAt(0).toLowerCase() + pascal.slice(1);
2743
2866
  }
2744
2867
  function generateVueFeature(featurePath) {
2745
- const featureName = basename5(featurePath);
2868
+ const featureName = basename6(featurePath);
2746
2869
  const pascalName = toPascalCase(featureName);
2747
2870
  const camelName = toCamelCase(featureName);
2748
2871
  return [
@@ -2859,7 +2982,7 @@ describe('${pascalName}Rules', () => {
2859
2982
  ];
2860
2983
  }
2861
2984
  function generateNestJSFeature(featurePath) {
2862
- const featureName = basename5(featurePath);
2985
+ const featureName = basename6(featurePath);
2863
2986
  const pascalName = toPascalCase(featureName);
2864
2987
  const camelName = toCamelCase(featureName);
2865
2988
  return [
@@ -2950,7 +3073,7 @@ describe('${pascalName}Controller', () => {
2950
3073
  ];
2951
3074
  }
2952
3075
  function generatePlaywrightFeature(featurePath) {
2953
- const featureName = basename5(featurePath);
3076
+ const featureName = basename6(featurePath);
2954
3077
  const pascalName = toPascalCase(featureName);
2955
3078
  const camelName = toCamelCase(featureName);
2956
3079
  return [
@@ -3053,8 +3176,8 @@ async function scaffoldCore(options) {
3053
3176
  };
3054
3177
  }
3055
3178
  const contractsLib = options.contractsLib ?? "@adeo/ahs-operator-execution-contracts";
3056
- const absolutePath2 = resolve4(options.path);
3057
- const absoluteContractPath = resolve4(options.contractPath);
3179
+ const absolutePath2 = resolve3(options.path);
3180
+ const absoluteContractPath = resolve3(options.contractPath);
3058
3181
  const bffResult = generateNestJsBffFeature(
3059
3182
  absolutePath2,
3060
3183
  absoluteContractPath,
@@ -3085,7 +3208,7 @@ async function scaffoldCore(options) {
3085
3208
  const errors2 = [];
3086
3209
  for (const file of bffResult.files) {
3087
3210
  try {
3088
- const dir = dirname4(file.path);
3211
+ const dir = dirname5(file.path);
3089
3212
  if (!existsSync7(dir)) {
3090
3213
  mkdirSync(dir, { recursive: true });
3091
3214
  }
@@ -3116,7 +3239,7 @@ async function scaffoldCore(options) {
3116
3239
  availableTypes: [...Object.keys(generators), "nestjs-bff-feature"]
3117
3240
  };
3118
3241
  }
3119
- const absolutePath = resolve4(options.path);
3242
+ const absolutePath = resolve3(options.path);
3120
3243
  const files = generator(absolutePath);
3121
3244
  if (options.dryRun) {
3122
3245
  return {
@@ -3136,7 +3259,7 @@ async function scaffoldCore(options) {
3136
3259
  const errors = [];
3137
3260
  for (const file of files) {
3138
3261
  try {
3139
- const dir = dirname4(file.path);
3262
+ const dir = dirname5(file.path);
3140
3263
  if (!existsSync7(dir)) {
3141
3264
  mkdirSync(dir, { recursive: true });
3142
3265
  }
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-QTZTVZQA.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-QTZTVZQA.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.8.0",
4
4
  "description": "TypeScript dev kit for Claude Code agents: architecture linting, scaffolding, knowledge analysis",
5
5
  "type": "module",
6
6
  "bin": {