@beignet/cli 0.0.1 → 0.0.4

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.
Files changed (98) hide show
  1. package/CHANGELOG.md +231 -0
  2. package/README.md +497 -90
  3. package/dist/ansi.d.ts +10 -0
  4. package/dist/ansi.d.ts.map +1 -0
  5. package/dist/ansi.js +20 -0
  6. package/dist/ansi.js.map +1 -0
  7. package/dist/choices.d.ts +72 -0
  8. package/dist/choices.d.ts.map +1 -0
  9. package/dist/choices.js +88 -0
  10. package/dist/choices.js.map +1 -0
  11. package/dist/completion.d.ts +47 -0
  12. package/dist/completion.d.ts.map +1 -0
  13. package/dist/completion.js +123 -0
  14. package/dist/completion.js.map +1 -0
  15. package/dist/config.d.ts +39 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +26 -0
  18. package/dist/config.js.map +1 -1
  19. package/dist/create-prompts.d.ts +42 -0
  20. package/dist/create-prompts.d.ts.map +1 -0
  21. package/dist/create-prompts.js +136 -0
  22. package/dist/create-prompts.js.map +1 -0
  23. package/dist/create.d.ts +12 -0
  24. package/dist/create.d.ts.map +1 -1
  25. package/dist/create.js +19 -24
  26. package/dist/create.js.map +1 -1
  27. package/dist/db.d.ts +37 -0
  28. package/dist/db.d.ts.map +1 -0
  29. package/dist/db.js +146 -0
  30. package/dist/db.js.map +1 -0
  31. package/dist/github-annotations.d.ts +18 -0
  32. package/dist/github-annotations.d.ts.map +1 -0
  33. package/dist/github-annotations.js +22 -0
  34. package/dist/github-annotations.js.map +1 -0
  35. package/dist/index.d.ts +1 -7
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +710 -400
  38. package/dist/index.js.map +1 -1
  39. package/dist/inspect.d.ts +45 -2
  40. package/dist/inspect.d.ts.map +1 -1
  41. package/dist/inspect.js +2191 -100
  42. package/dist/inspect.js.map +1 -1
  43. package/dist/lib.d.ts +20 -0
  44. package/dist/lib.d.ts.map +1 -0
  45. package/dist/lib.js +17 -0
  46. package/dist/lib.js.map +1 -0
  47. package/dist/lint.d.ts +22 -1
  48. package/dist/lint.d.ts.map +1 -1
  49. package/dist/lint.js +370 -38
  50. package/dist/lint.js.map +1 -1
  51. package/dist/make.d.ts +109 -1
  52. package/dist/make.d.ts.map +1 -1
  53. package/dist/make.js +2225 -333
  54. package/dist/make.js.map +1 -1
  55. package/dist/outbox.d.ts +24 -0
  56. package/dist/outbox.d.ts.map +1 -0
  57. package/dist/outbox.js +138 -0
  58. package/dist/outbox.js.map +1 -0
  59. package/dist/schedule.d.ts +36 -0
  60. package/dist/schedule.d.ts.map +1 -0
  61. package/dist/schedule.js +155 -0
  62. package/dist/schedule.js.map +1 -0
  63. package/dist/task.d.ts +26 -0
  64. package/dist/task.d.ts.map +1 -0
  65. package/dist/task.js +106 -0
  66. package/dist/task.js.map +1 -0
  67. package/dist/templates.d.ts +12 -8
  68. package/dist/templates.d.ts.map +1 -1
  69. package/dist/templates.js +2144 -385
  70. package/dist/templates.js.map +1 -1
  71. package/dist/version.d.ts +8 -0
  72. package/dist/version.d.ts.map +1 -0
  73. package/dist/version.js +18 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +9 -8
  76. package/src/ansi.ts +30 -0
  77. package/src/choices.ts +137 -0
  78. package/src/completion.ts +169 -0
  79. package/src/config.ts +47 -0
  80. package/src/create-prompts.ts +182 -0
  81. package/src/create.ts +32 -28
  82. package/src/db.ts +222 -0
  83. package/src/github-annotations.ts +37 -0
  84. package/src/index.ts +1119 -535
  85. package/src/inspect.ts +3372 -134
  86. package/src/lib.ts +45 -0
  87. package/src/lint.ts +533 -45
  88. package/src/make.ts +3010 -397
  89. package/src/outbox.ts +249 -0
  90. package/src/schedule.ts +272 -0
  91. package/src/task.ts +169 -0
  92. package/src/templates.ts +2282 -462
  93. package/src/version.ts +20 -0
  94. package/dist/create-bin.d.ts +0 -3
  95. package/dist/create-bin.d.ts.map +0 -1
  96. package/dist/create-bin.js +0 -9
  97. package/dist/create-bin.js.map +0 -1
  98. package/src/create-bin.ts +0 -11
package/dist/lint.js CHANGED
@@ -1,6 +1,11 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import { createPainter } from "./ansi.js";
3
4
  import { directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
5
+ import { formatGithubAnnotation } from "./github-annotations.js";
6
+ /**
7
+ * Inspect app imports and report dependency-direction violations.
8
+ */
4
9
  export async function lintApp(options = {}) {
5
10
  const targetDir = path.resolve(options.cwd ?? process.cwd());
6
11
  await assertDirectory(targetDir);
@@ -9,34 +14,98 @@ export async function lintApp(options = {}) {
9
14
  ? resolveConfig(options.config)
10
15
  : await loadBeignetConfig(targetDir, files);
11
16
  const diagnostics = [];
12
- for (const file of lintSourceFiles(files)) {
17
+ const sourceFiles = lintSourceFiles(files);
18
+ const sourceFileSet = new Set(sourceFiles);
19
+ const sourceByFile = new Map();
20
+ const importGraph = new Map();
21
+ for (const file of sourceFiles) {
13
22
  const sourceLayer = classifyPath(file, config);
23
+ const source = await readFile(path.join(targetDir, file), "utf8");
24
+ sourceByFile.set(file, source);
25
+ importGraph.set(file, parseImports(source, file, sourceFileSet));
14
26
  if (sourceLayer === "test" || sourceLayer === "unknown")
15
27
  continue;
16
- const source = await readFile(path.join(targetDir, file), "utf8");
17
- for (const reference of parseImports(source, file)) {
28
+ const lineStarts = computeLineStarts(source);
29
+ for (const reference of importGraph.get(file) ?? []) {
18
30
  const diagnostic = lintImport(file, sourceLayer, reference, config);
19
- if (diagnostic)
20
- diagnostics.push(diagnostic);
31
+ if (diagnostic) {
32
+ diagnostics.push({
33
+ ...diagnostic,
34
+ ...positionFromOffset(lineStarts, reference.offset),
35
+ });
36
+ }
21
37
  }
22
38
  }
39
+ diagnostics.push(...lintRuntimeBoundary({
40
+ config,
41
+ graph: importGraph,
42
+ sourceByFile,
43
+ }));
23
44
  return {
45
+ schemaVersion: 1,
24
46
  targetDir,
25
47
  config,
26
48
  diagnostics: dedupeDiagnostics(diagnostics),
27
49
  };
28
50
  }
29
- export function formatLint(result) {
51
+ function computeLineStarts(source) {
52
+ const lineStarts = [0];
53
+ for (let index = 0; index < source.length; index++) {
54
+ if (source[index] === "\n") {
55
+ lineStarts.push(index + 1);
56
+ }
57
+ }
58
+ return lineStarts;
59
+ }
60
+ function positionFromOffset(lineStarts, offset) {
61
+ let low = 0;
62
+ let high = lineStarts.length - 1;
63
+ while (low < high) {
64
+ const middle = Math.ceil((low + high) / 2);
65
+ if (lineStarts[middle] <= offset) {
66
+ low = middle;
67
+ }
68
+ else {
69
+ high = middle - 1;
70
+ }
71
+ }
72
+ return { line: low + 1, column: offset - lineStarts[low] + 1 };
73
+ }
74
+ /**
75
+ * Format dependency-direction lint diagnostics for CLI output.
76
+ */
77
+ export function formatLint(result, options = {}) {
78
+ const paint = createPainter(options.color);
30
79
  if (result.diagnostics.length === 0) {
31
80
  return `No Beignet lint issues found in ${result.targetDir}.`;
32
81
  }
33
82
  return [
34
83
  `Found ${result.diagnostics.length} Beignet lint issue${result.diagnostics.length === 1 ? "" : "s"} in ${result.targetDir}:`,
35
84
  "",
36
- ...result.diagnostics.map((diagnostic) => `ERROR ${diagnostic.code} ${diagnostic.file}
85
+ ...result.diagnostics.map((diagnostic) => `${paint("ERROR", "red")} ${diagnostic.code} ${lintDiagnosticLocation(diagnostic)}
37
86
  ${diagnostic.message}`),
38
87
  ].join("\n");
39
88
  }
89
+ function lintDiagnosticLocation(diagnostic) {
90
+ if (diagnostic.line === undefined || diagnostic.column === undefined) {
91
+ return diagnostic.file;
92
+ }
93
+ return `${diagnostic.file}:${diagnostic.line}:${diagnostic.column}`;
94
+ }
95
+ /**
96
+ * Format dependency-direction lint diagnostics as GitHub Actions annotations.
97
+ */
98
+ export function formatLintGithub(result) {
99
+ return result.diagnostics
100
+ .map((diagnostic) => formatGithubAnnotation({
101
+ severity: "error",
102
+ file: diagnostic.file,
103
+ line: diagnostic.line,
104
+ col: diagnostic.column,
105
+ message: `${diagnostic.code}: ${diagnostic.message}`,
106
+ }))
107
+ .join("\n");
108
+ }
40
109
  async function assertDirectory(targetDir) {
41
110
  try {
42
111
  const stats = await stat(targetDir);
@@ -86,11 +155,11 @@ function lintSourceFiles(files) {
86
155
  return /\.(c|m)?tsx?$/.test(file);
87
156
  });
88
157
  }
89
- function parseImports(source, importerFile) {
158
+ function parseImports(source, importerFile, files) {
90
159
  const references = parseImportSpecifiers(source);
91
- return references.map((importPath) => ({
92
- importPath,
93
- ...resolveImport(importPath, importerFile),
160
+ return references.map((reference) => ({
161
+ ...reference,
162
+ ...resolveImport(reference.importPath, importerFile, files),
94
163
  }));
95
164
  }
96
165
  function parseImportSpecifiers(source) {
@@ -100,15 +169,25 @@ function parseImportSpecifiers(source) {
100
169
  index = skipNonCode(source, index);
101
170
  if (isKeywordAt(source, index, "import")) {
102
171
  const parsed = parseImportSpecifierAt(source, index + "import".length);
103
- if (parsed?.specifier)
104
- specifiers.push(parsed.specifier);
172
+ if (parsed?.specifier) {
173
+ specifiers.push({
174
+ importPath: parsed.specifier,
175
+ kind: parsed.kind ?? "value",
176
+ offset: index,
177
+ });
178
+ }
105
179
  index = parsed?.end ?? index + "import".length;
106
180
  continue;
107
181
  }
108
182
  if (isKeywordAt(source, index, "export")) {
109
183
  const parsed = parseExportSpecifierAt(source, index + "export".length);
110
- if (parsed?.specifier)
111
- specifiers.push(parsed.specifier);
184
+ if (parsed?.specifier) {
185
+ specifiers.push({
186
+ importPath: parsed.specifier,
187
+ kind: parsed.kind ?? "value",
188
+ offset: index,
189
+ });
190
+ }
112
191
  index = parsed?.end ?? index + "export".length;
113
192
  continue;
114
193
  }
@@ -132,9 +211,16 @@ function parseImportSpecifierAt(source, index) {
132
211
  if (sideEffectImport) {
133
212
  return {
134
213
  specifier: sideEffectImport.value,
214
+ kind: "value",
135
215
  end: sideEffectImport.end,
136
216
  };
137
217
  }
218
+ const importClauseStart = cursor;
219
+ let isImportType = false;
220
+ if (isKeywordAt(source, cursor, "type")) {
221
+ isImportType = true;
222
+ cursor = cursorAfterKeyword("type", cursor);
223
+ }
138
224
  while (cursor < source.length) {
139
225
  cursor = skipWhitespaceAndComments(source, cursor);
140
226
  if (source[cursor] === ";" || source[cursor] === "\n") {
@@ -147,6 +233,9 @@ function parseImportSpecifierAt(source, index) {
147
233
  return { end: fromCursor };
148
234
  return {
149
235
  specifier: stringLiteral.value,
236
+ kind: isImportType
237
+ ? "type"
238
+ : inferImportClauseKind(source.slice(importClauseStart, cursor)),
150
239
  end: stringLiteral.end,
151
240
  };
152
241
  }
@@ -156,8 +245,15 @@ function parseImportSpecifierAt(source, index) {
156
245
  }
157
246
  function parseExportSpecifierAt(source, index) {
158
247
  let cursor = index;
248
+ let isExportType = false;
249
+ const exportClauseStart = cursor;
159
250
  while (cursor < source.length) {
160
251
  cursor = skipWhitespaceAndComments(source, cursor);
252
+ if (isKeywordAt(source, cursor, "type")) {
253
+ isExportType = true;
254
+ cursor = cursorAfterKeyword("type", cursor);
255
+ continue;
256
+ }
161
257
  if (source[cursor] === ";" || source[cursor] === "\n") {
162
258
  return { end: cursor + 1 };
163
259
  }
@@ -168,6 +264,9 @@ function parseExportSpecifierAt(source, index) {
168
264
  return { end: fromCursor };
169
265
  return {
170
266
  specifier: stringLiteral.value,
267
+ kind: isExportType
268
+ ? "type"
269
+ : inferImportClauseKind(source.slice(exportClauseStart, cursor)),
171
270
  end: stringLiteral.end,
172
271
  };
173
272
  }
@@ -175,6 +274,13 @@ function parseExportSpecifierAt(source, index) {
175
274
  }
176
275
  return undefined;
177
276
  }
277
+ function inferImportClauseKind(importClause) {
278
+ const trimmed = importClause.trim();
279
+ if (/^\{\s*type\s+[^,}]+(?:,\s*type\s+[^,}]+)*,?\s*\}$/.test(trimmed)) {
280
+ return "type";
281
+ }
282
+ return "value";
283
+ }
178
284
  function cursorAfterKeyword(keyword, index) {
179
285
  return index + keyword.length;
180
286
  }
@@ -292,21 +398,42 @@ function isKeywordAt(source, index, keyword) {
292
398
  function isIdentifierChar(char) {
293
399
  return Boolean(char && /[A-Za-z0-9_$]/.test(char));
294
400
  }
295
- function resolveImport(importPath, importerFile) {
401
+ function resolveImport(importPath, importerFile, files) {
296
402
  if (importPath.startsWith("@/")) {
297
403
  return {
298
- resolvedPath: stripKnownExtension(importPath.slice(2)),
404
+ resolvedPath: resolveLocalImportPath(stripKnownExtension(importPath.slice(2)), files),
299
405
  };
300
406
  }
301
407
  if (importPath.startsWith(".")) {
302
408
  return {
303
- resolvedPath: stripKnownExtension(normalizePath(path.join(path.dirname(importerFile), importPath))),
409
+ resolvedPath: resolveLocalImportPath(stripKnownExtension(normalizePath(path.join(path.dirname(importerFile), importPath))), files),
304
410
  };
305
411
  }
306
412
  return {
307
413
  packageName: packageName(importPath),
308
414
  };
309
415
  }
416
+ function resolveLocalImportPath(importPath, files) {
417
+ const candidates = [
418
+ importPath,
419
+ `${importPath}.ts`,
420
+ `${importPath}.tsx`,
421
+ `${importPath}.mts`,
422
+ `${importPath}.cts`,
423
+ `${importPath}/index.ts`,
424
+ `${importPath}/index.tsx`,
425
+ `${importPath}/index.mts`,
426
+ `${importPath}/index.cts`,
427
+ ];
428
+ for (const candidate of candidates) {
429
+ for (const file of files) {
430
+ if (stripKnownExtension(file) === stripKnownExtension(candidate)) {
431
+ return file;
432
+ }
433
+ }
434
+ }
435
+ return stripKnownExtension(importPath);
436
+ }
310
437
  function packageName(importPath) {
311
438
  if (!importPath.startsWith("@"))
312
439
  return importPath.split("/")[0];
@@ -317,29 +444,45 @@ function stripKnownExtension(filePath) {
317
444
  return filePath.replace(/\.(c|m)?tsx?$/, "");
318
445
  }
319
446
  function lintImport(file, sourceLayer, reference, config) {
447
+ if (reference.kind === "type")
448
+ return undefined;
320
449
  const targetLayer = reference.resolvedPath
321
450
  ? classifyPath(reference.resolvedPath, config)
322
451
  : undefined;
452
+ const featureImport = reference.resolvedPath
453
+ ? featureImportViolation(file, sourceLayer, reference.resolvedPath, config)
454
+ : undefined;
455
+ if (featureImport) {
456
+ return {
457
+ severity: "error",
458
+ code: "BEIGNET_IMPORT_DIRECTION",
459
+ file,
460
+ importPath: reference.importPath,
461
+ message: featureImport,
462
+ };
463
+ }
323
464
  const appImport = reference.resolvedPath
324
465
  ? appImportViolation(sourceLayer, targetLayer)
325
466
  : undefined;
326
467
  if (appImport) {
327
468
  return {
328
469
  severity: "error",
329
- code: "CK_IMPORT_DIRECTION",
470
+ code: "BEIGNET_IMPORT_DIRECTION",
330
471
  file,
331
472
  importPath: reference.importPath,
332
473
  message: appImportMessage(sourceLayer, reference.importPath, appImport),
333
474
  };
334
475
  }
335
- if (reference.packageName &&
336
- packageImportViolation(sourceLayer, reference.packageName)) {
476
+ const bannedPackage = reference.packageName
477
+ ? packageImportViolation(sourceLayer, reference.importPath)
478
+ : undefined;
479
+ if (bannedPackage) {
337
480
  return {
338
481
  severity: "error",
339
- code: "CK_IMPORT_DIRECTION",
482
+ code: "BEIGNET_IMPORT_DIRECTION",
340
483
  file,
341
484
  importPath: reference.importPath,
342
- message: packageImportMessage(sourceLayer, reference.packageName),
485
+ message: packageImportMessage(sourceLayer, bannedPackage),
343
486
  };
344
487
  }
345
488
  return undefined;
@@ -362,42 +505,211 @@ function appImportViolation(sourceLayer, targetLayer) {
362
505
  "route",
363
506
  "server",
364
507
  "use-case",
508
+ "workflow",
365
509
  ].includes(targetLayer)) {
366
510
  return targetLayer;
367
511
  }
368
- if (sourceLayer === "use-case" &&
512
+ if ((sourceLayer === "use-case" || sourceLayer === "workflow") &&
369
513
  ["app", "client", "component", "infra", "route", "server"].includes(targetLayer)) {
370
514
  return targetLayer;
371
515
  }
372
516
  if (["feature-port", "policy", "port"].includes(sourceLayer) &&
373
- ["app", "client", "component", "infra", "route", "server"].includes(targetLayer)) {
517
+ [
518
+ "app",
519
+ "client",
520
+ "component",
521
+ "infra",
522
+ "route",
523
+ "server",
524
+ "workflow",
525
+ ].includes(targetLayer)) {
374
526
  return targetLayer;
375
527
  }
376
528
  if (sourceLayer === "contract" &&
377
- ["app", "client", "component", "infra", "route"].includes(targetLayer)) {
529
+ ["app", "client", "component", "infra", "route", "workflow"].includes(targetLayer)) {
378
530
  return targetLayer;
379
531
  }
380
532
  if (sourceLayer === "route" &&
381
- ["app", "client", "component", "infra"].includes(targetLayer)) {
533
+ ["app", "client", "component", "infra", "workflow"].includes(targetLayer)) {
534
+ return targetLayer;
535
+ }
536
+ if (sourceLayer === "infra" &&
537
+ ["app", "client", "component", "route", "server"].includes(targetLayer)) {
382
538
  return targetLayer;
383
539
  }
384
540
  return undefined;
385
541
  }
386
- function packageImportViolation(sourceLayer, packageName) {
542
+ function packageImportViolation(sourceLayer, importPath) {
387
543
  if (sourceLayer === "domain") {
388
- return (domainBannedPackages.has(packageName) ||
389
- packageName.startsWith("@beignet/provider-"));
390
- }
391
- if (["contract", "feature-port", "policy", "port", "use-case"].includes(sourceLayer)) {
392
- return (coreBannedPackages.has(packageName) ||
393
- serverRuntimePackages.has(packageName) ||
394
- packageName.startsWith("@beignet/provider-"));
544
+ return (matchBannedPackage(domainBannedPackages, importPath) ??
545
+ matchBeignetProviderPackage(importPath));
546
+ }
547
+ if ([
548
+ "contract",
549
+ "feature-port",
550
+ "policy",
551
+ "port",
552
+ "use-case",
553
+ "workflow",
554
+ ].includes(sourceLayer)) {
555
+ return (matchBannedPackage(coreBannedPackages, importPath) ??
556
+ matchBannedPackage(serverRuntimePackages, importPath) ??
557
+ matchBeignetProviderPackage(importPath));
395
558
  }
396
559
  if (sourceLayer === "route") {
397
- return (coreBannedPackages.has(packageName) ||
398
- packageName.startsWith("@beignet/provider-"));
560
+ return (matchBannedPackage(coreBannedPackages, importPath) ??
561
+ matchBeignetProviderPackage(importPath));
562
+ }
563
+ return undefined;
564
+ }
565
+ function matchBannedPackage(bannedPackages, importPath) {
566
+ let candidate = importPath;
567
+ while (candidate.length > 0) {
568
+ if (bannedPackages.has(candidate))
569
+ return candidate;
570
+ const separator = candidate.lastIndexOf("/");
571
+ if (separator === -1)
572
+ return undefined;
573
+ candidate = candidate.slice(0, separator);
574
+ }
575
+ return undefined;
576
+ }
577
+ function matchBeignetProviderPackage(importPath) {
578
+ const name = packageName(importPath);
579
+ return name.startsWith("@beignet/provider-") ? name : undefined;
580
+ }
581
+ function lintRuntimeBoundary({ config, graph, sourceByFile, }) {
582
+ const diagnostics = [];
583
+ for (const file of graph.keys()) {
584
+ const rootKind = runtimeBoundaryRootKind(file, sourceByFile.get(file) ?? "", config);
585
+ if (!rootKind)
586
+ continue;
587
+ diagnostics.push(...lintRuntimeBoundaryRoot({
588
+ root: file,
589
+ rootKind,
590
+ config,
591
+ graph,
592
+ }));
593
+ }
594
+ return diagnostics;
595
+ }
596
+ function lintRuntimeBoundaryRoot({ root, rootKind, config, graph, }) {
597
+ const diagnostics = [];
598
+ const visited = new Set();
599
+ const queue = [
600
+ { file: root, chain: [root] },
601
+ ];
602
+ while (queue.length > 0) {
603
+ const current = queue.shift();
604
+ if (!current)
605
+ continue;
606
+ if (visited.has(current.file))
607
+ continue;
608
+ visited.add(current.file);
609
+ for (const reference of graph.get(current.file) ?? []) {
610
+ if (reference.kind === "type" &&
611
+ reference.importPath !== "@beignet/core/server-only") {
612
+ continue;
613
+ }
614
+ const chain = reference.resolvedPath
615
+ ? [...current.chain, reference.resolvedPath]
616
+ : current.chain;
617
+ const violation = runtimeBoundaryViolation(reference, config);
618
+ if (violation) {
619
+ diagnostics.push({
620
+ severity: "error",
621
+ code: "BEIGNET_RUNTIME_BOUNDARY",
622
+ file: root,
623
+ importPath: reference.importPath,
624
+ message: runtimeBoundaryMessage(rootKind, violation, chain),
625
+ });
626
+ continue;
627
+ }
628
+ if (reference.resolvedPath && graph.has(reference.resolvedPath)) {
629
+ queue.push({
630
+ file: reference.resolvedPath,
631
+ chain,
632
+ });
633
+ }
634
+ }
635
+ }
636
+ return diagnostics;
637
+ }
638
+ function runtimeBoundaryRootKind(file, source, config) {
639
+ const layer = classifyPath(file, config);
640
+ if (layer === "contract")
641
+ return "contract";
642
+ if (layer === "client")
643
+ return "client";
644
+ if (hasUseClientDirective(source))
645
+ return "client";
646
+ if (isGeneratedClientAdapter(file))
647
+ return "client";
648
+ return undefined;
649
+ }
650
+ function hasUseClientDirective(source) {
651
+ const trimmed = source.trimStart();
652
+ return (trimmed.startsWith('"use client";') ||
653
+ trimmed.startsWith('"use client"') ||
654
+ trimmed.startsWith("'use client';") ||
655
+ trimmed.startsWith("'use client'"));
656
+ }
657
+ function isGeneratedClientAdapter(file) {
658
+ return (file === "client/index.ts" ||
659
+ file === "client/api-client.ts" ||
660
+ file === "client/rhf.ts" ||
661
+ file === "client/rq.ts" ||
662
+ file === "client/uploads.ts");
663
+ }
664
+ function runtimeBoundaryViolation(reference, config) {
665
+ if (reference.resolvedPath) {
666
+ const targetLayer = classifyPath(reference.resolvedPath, config);
667
+ if (targetLayer === "use-case" ||
668
+ targetLayer === "route" ||
669
+ targetLayer === "infra" ||
670
+ targetLayer === "server") {
671
+ return `${layerLabel(targetLayer)} module ${reference.resolvedPath}`;
672
+ }
673
+ if (targetLayer === "app-context") {
674
+ return `app context module ${reference.resolvedPath}`;
675
+ }
676
+ }
677
+ if (reference.importPath === "@beignet/core/server-only") {
678
+ return "server-only marker @beignet/core/server-only";
679
+ }
680
+ if (reference.importPath.startsWith("node:")) {
681
+ return `Node runtime module ${reference.importPath}`;
682
+ }
683
+ if (serverRuntimePackages.has(reference.importPath) ||
684
+ reference.importPath.startsWith("@beignet/provider-")) {
685
+ return `server runtime package ${reference.importPath}`;
686
+ }
687
+ return undefined;
688
+ }
689
+ function runtimeBoundaryMessage(rootKind, violation, chain) {
690
+ return `${rootKindLabel(rootKind)} roots must not value-import server-only code. Reached ${violation} through ${formatImportChain(chain)}. Move shared DTO and validation schemas to features/<feature>/schemas.ts, keep use cases/routes/infra/server modules behind server entrypoints, or import server-shaped modules with import type only.`;
691
+ }
692
+ function rootKindLabel(rootKind) {
693
+ return rootKind === "contract" ? "contract" : "client";
694
+ }
695
+ function formatImportChain(chain) {
696
+ return chain.join(" -> ");
697
+ }
698
+ function featureImportViolation(file, sourceLayer, resolvedPath, config) {
699
+ if (sourceLayer !== "domain")
700
+ return undefined;
701
+ const sourceFeature = featureLayerInfo(file, config);
702
+ const targetFeature = featureLayerInfo(resolvedPath, config);
703
+ if (!sourceFeature || !targetFeature)
704
+ return undefined;
705
+ if (sourceFeature.layer !== "domain" || targetFeature.layer !== "domain") {
706
+ return undefined;
399
707
  }
400
- return false;
708
+ if (sourceFeature.feature === targetFeature.feature)
709
+ return undefined;
710
+ if (targetFeature.feature === "shared")
711
+ return undefined;
712
+ return `feature-specific domain files must not import another feature's domain (${stripKnownExtension(resolvedPath)}). Move shared concepts to features/shared/domain.`;
401
713
  }
402
714
  const domainBannedPackages = new Set([
403
715
  "@aws-sdk/client-s3",
@@ -520,6 +832,16 @@ function classifyPath(filePath, config) {
520
832
  return "policy";
521
833
  if (layerSegment === "use-cases")
522
834
  return "use-case";
835
+ if ([
836
+ "jobs",
837
+ "listeners",
838
+ "notifications",
839
+ "schedules",
840
+ "tasks",
841
+ "uploads",
842
+ ].includes(layerSegment)) {
843
+ return "workflow";
844
+ }
523
845
  if (layerSegment === "components")
524
846
  return "component";
525
847
  }
@@ -551,6 +873,16 @@ function featureRelativePath(filePath, featuresPath) {
551
873
  const prefix = `${directoryPath(featuresPath)}/`;
552
874
  return filePath.slice(prefix.length);
553
875
  }
876
+ function featureLayerInfo(filePath, config) {
877
+ const normalizedPath = stripKnownExtension(normalizePath(filePath));
878
+ const relativePath = featureRelativePath(normalizedPath, directoryPath(config.paths.features));
879
+ if (!relativePath)
880
+ return undefined;
881
+ const [feature, layer] = relativePath.split("/");
882
+ if (!feature)
883
+ return undefined;
884
+ return { feature, layer };
885
+ }
554
886
  function sharedDomainPath(config) {
555
887
  const featuresDir = directoryPath(config.paths.features);
556
888
  const featuresParent = directoryPath(path.dirname(featuresDir));