@pattern-stack/codegen 0.6.8 → 0.7.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.
Files changed (41) hide show
  1. package/dist/src/cli/index.js +516 -73
  2. package/dist/src/cli/index.js.map +1 -1
  3. package/dist/src/index.d.ts +208 -1
  4. package/dist/src/index.js +147 -0
  5. package/dist/src/index.js.map +1 -1
  6. package/package.json +1 -1
  7. package/src/patterns/library/base-junction-fields.ts +32 -0
  8. package/src/patterns/library/index.ts +7 -0
  9. package/src/patterns/library/junction.pattern.ts +41 -0
  10. package/templates/entity/new/backend/application/queries/get-by-id.ejs.t +3 -3
  11. package/templates/entity/new/backend/application/queries/grouped-index.ejs.t +5 -5
  12. package/templates/entity/new/backend/application/queries/index.ejs.t +3 -0
  13. package/templates/entity/new/backend/application/queries/list.ejs.t +3 -3
  14. package/templates/entity/new/backend/application/queries/relationships.queries.ejs.t +147 -0
  15. package/templates/entity/new/backend/database/repository.ejs.t +36 -176
  16. package/templates/entity/new/backend/domain/entity.ejs.t +0 -44
  17. package/templates/entity/new/backend/domain/grouped-index.ejs.t +4 -60
  18. package/templates/entity/new/backend/domain/index.ejs.t +2 -2
  19. package/templates/entity/new/backend/domain/repository-interface.ejs.t +16 -17
  20. package/templates/entity/new/backend/modules/core/module.ejs.t +10 -0
  21. package/templates/entity/new/backend/presentation/controller.ejs.t +2 -34
  22. package/templates/entity/new/clean-lite-ps/entity.ejs.t +15 -2
  23. package/templates/entity/new/clean-lite-ps/module.ejs.t +27 -2
  24. package/templates/entity/new/clean-lite-ps/prompt-extension.js +72 -5
  25. package/templates/entity/new/clean-lite-ps/repository.ejs.t +33 -1
  26. package/templates/entity/new/clean-lite-ps/service.ejs.t +79 -0
  27. package/templates/entity/new/prompt.js +1 -0
  28. package/templates/junction/new/_inject-parent-module-clp-left.ejs.t +8 -0
  29. package/templates/junction/new/_inject-parent-module-clp-right.ejs.t +8 -0
  30. package/templates/junction/new/_inject-parent-module-import-clp-left.ejs.t +9 -0
  31. package/templates/junction/new/_inject-parent-module-import-clp-right.ejs.t +9 -0
  32. package/templates/junction/new/_inject-parent-service-clp-left.ejs.t +51 -0
  33. package/templates/junction/new/_inject-parent-service-clp-right.ejs.t +48 -0
  34. package/templates/junction/new/_inject-parent-service-import-clp-left.ejs.t +11 -0
  35. package/templates/junction/new/_inject-parent-service-import-clp-right.ejs.t +11 -0
  36. package/templates/junction/new/entity.ejs.t +111 -0
  37. package/templates/junction/new/index.ejs.t +15 -0
  38. package/templates/junction/new/module.ejs.t +37 -0
  39. package/templates/junction/new/prompt.js +492 -0
  40. package/templates/junction/new/repository.ejs.t +67 -0
  41. package/templates/junction/new/service.ejs.t +174 -0
@@ -14,7 +14,7 @@ var __decorateParam = (index2, decorator) => (target, key) => decorator(target,
14
14
  // src/cli/index.ts
15
15
  import { readFileSync as readFileSync6 } from "fs";
16
16
  import { join as join11 } from "path";
17
- import { Builtins, Cli, Command as Command10 } from "clipanion";
17
+ import { Builtins, Cli, Command as Command11 } from "clipanion";
18
18
 
19
19
  // src/cli/noun-module.ts
20
20
  import { Command, Option } from "clipanion";
@@ -986,13 +986,13 @@ function renderPane(pane) {
986
986
  }
987
987
 
988
988
  // src/cli/ui/hints.ts
989
- function renderHints(hints8) {
989
+ function renderHints(hints9) {
990
990
  if (isJsonMode()) return;
991
- if (hints8.length === 0) return;
991
+ if (hints9.length === 0) return;
992
992
  console.log("");
993
993
  console.log(theme.muted(" Next:"));
994
- const maxCmd = Math.max(...hints8.map((h) => h.command.length));
995
- for (const h of hints8) {
994
+ const maxCmd = Math.max(...hints9.map((h) => h.command.length));
995
+ for (const h of hints9) {
996
996
  const pad = " ".repeat(maxCmd - h.command.length + 2);
997
997
  console.log(` ${theme.system(h.command)}${pad}${theme.muted(h.description)}`);
998
998
  }
@@ -1017,13 +1017,13 @@ function buildNounSummaryCommand(noun) {
1017
1017
  json: this.json,
1018
1018
  verbose: this.verbose
1019
1019
  });
1020
- const [pane, hints8] = await Promise.all([noun.summary(ctx), noun.hints(ctx)]);
1020
+ const [pane, hints9] = await Promise.all([noun.summary(ctx), noun.hints(ctx)]);
1021
1021
  if (isJsonMode()) {
1022
- printJson({ noun: noun.name, summary: pane, hints: hints8 });
1022
+ printJson({ noun: noun.name, summary: pane, hints: hints9 });
1023
1023
  return 0;
1024
1024
  }
1025
1025
  renderPane(pane);
1026
- renderHints(hints8);
1026
+ renderHints(hints9);
1027
1027
  return 0;
1028
1028
  }
1029
1029
  }
@@ -2791,6 +2791,80 @@ function deriveUniqueConstraint(config) {
2791
2791
  return columns;
2792
2792
  }
2793
2793
 
2794
+ // src/schema/junction-definition.schema.ts
2795
+ import { z as z6 } from "zod";
2796
+
2797
+ // src/patterns/library/base-junction-fields.ts
2798
+ var BaseJunctionFields = [
2799
+ { name: "is_primary", type: "boolean" },
2800
+ { name: "started_at", type: "timestamp" },
2801
+ { name: "ended_at", type: "timestamp" },
2802
+ { name: "sourced_from", type: "text" },
2803
+ { name: "confidence", type: "numeric(5,4)" },
2804
+ { name: "matched_at", type: "timestamp" }
2805
+ ];
2806
+ var BASE_JUNCTION_FIELD_NAMES = new Set(
2807
+ BaseJunctionFields.map((c) => c.name)
2808
+ );
2809
+
2810
+ // src/schema/junction-definition.schema.ts
2811
+ var EntityNameSchema = z6.string().regex(/^[a-z][a-z0-9_]*$/, "Entity reference must be snake_case");
2812
+ var JunctionDefinitionSchema = z6.object({
2813
+ /** Discriminator literal — `pattern: Junction`. */
2814
+ pattern: z6.literal("Junction"),
2815
+ /**
2816
+ * Exactly two endpoint entity names. Both intra- and cross-domain
2817
+ * pairings are accepted; entity existence is validated by the
2818
+ * analyzer in a later leaf.
2819
+ */
2820
+ between: z6.tuple([EntityNameSchema, EntityNameSchema]),
2821
+ /**
2822
+ * Emit BaseJunctionFields temporal columns (`started_at`, `ended_at`,
2823
+ * `matched_at`). Default true. Matches Relationship's `temporal` toggle.
2824
+ */
2825
+ temporal: z6.boolean().optional().default(true),
2826
+ /**
2827
+ * Emit BaseJunctionFields sourcing columns (`sourced_from`,
2828
+ * `confidence`). Default true. Matches Relationship's `sourced` toggle.
2829
+ */
2830
+ sourced: z6.boolean().optional().default(true),
2831
+ /**
2832
+ * Junction-specific fields beyond `BaseJunctionFields`. Includes the
2833
+ * per-pairing role enum (declared inline; never shared across
2834
+ * pairings). Shape is validated downstream by the codegen layer
2835
+ * using the existing entity FieldDefinitionSchema.
2836
+ */
2837
+ fields: z6.record(z6.string(), z6.any()).optional(),
2838
+ /**
2839
+ * Declarative queries — same syntax as entity queries. Shape is
2840
+ * validated downstream by the codegen layer.
2841
+ */
2842
+ queries: z6.array(z6.any()).optional(),
2843
+ /**
2844
+ * Per-side opt-out for parent-service fan-out (CGP-60). When a side
2845
+ * is `false`, the `_inject-parent-service-*` templates emit nothing
2846
+ * on that side (and the corresponding module wiring is skipped).
2847
+ * The junction service body is always emitted regardless. Defaults
2848
+ * to `{ left: true, right: true }`.
2849
+ */
2850
+ expose_on_parent: z6.object({
2851
+ left: z6.boolean().optional().default(true),
2852
+ right: z6.boolean().optional().default(true)
2853
+ }).optional().default({ left: true, right: true })
2854
+ }).strict().refine((d) => d.between[0] !== d.between[1], {
2855
+ message: "`between` endpoints must be distinct",
2856
+ path: ["between"]
2857
+ }).refine(
2858
+ (d) => {
2859
+ const fieldNames = Object.keys(d.fields ?? {});
2860
+ return !fieldNames.some((n) => BASE_JUNCTION_FIELD_NAMES.has(n));
2861
+ },
2862
+ {
2863
+ message: "`fields:` block redeclares a reserved BaseJunctionFields column (is_primary, started_at, ended_at, sourced_from, confidence, matched_at)",
2864
+ path: ["fields"]
2865
+ }
2866
+ );
2867
+
2794
2868
  // src/utils/yaml-loader.ts
2795
2869
  function loadEntityFromYaml(filePath) {
2796
2870
  if (!existsSync6(filePath)) {
@@ -2839,8 +2913,8 @@ function loadEntityFromYaml(filePath) {
2839
2913
  }
2840
2914
  function formatZodErrors(error) {
2841
2915
  return error.errors.map((err) => {
2842
- const path29 = err.path.join(".");
2843
- const location = path29 ? `at '${path29}'` : "at root";
2916
+ const path30 = err.path.join(".");
2917
+ const location = path30 ? `at '${path30}'` : "at root";
2844
2918
  return `${err.message} ${location}`;
2845
2919
  });
2846
2920
  }
@@ -2934,12 +3008,58 @@ function loadEventFromYaml(filePath) {
2934
3008
  filePath
2935
3009
  };
2936
3010
  }
3011
+ function loadJunctionFromYaml(filePath) {
3012
+ if (!existsSync6(filePath)) {
3013
+ return {
3014
+ success: false,
3015
+ error: `File not found: ${filePath}`,
3016
+ filePath
3017
+ };
3018
+ }
3019
+ let content;
3020
+ try {
3021
+ content = readFileSync4(filePath, "utf-8");
3022
+ } catch (err) {
3023
+ return {
3024
+ success: false,
3025
+ error: `Failed to read file: ${filePath}`,
3026
+ details: [err instanceof Error ? err.message : String(err)],
3027
+ filePath
3028
+ };
3029
+ }
3030
+ let parsed;
3031
+ try {
3032
+ parsed = parseYaml(content);
3033
+ } catch (err) {
3034
+ return {
3035
+ success: false,
3036
+ error: `Invalid YAML syntax in ${filePath}`,
3037
+ details: [err instanceof Error ? err.message : String(err)],
3038
+ filePath
3039
+ };
3040
+ }
3041
+ const result = JunctionDefinitionSchema.safeParse(parsed);
3042
+ if (!result.success) {
3043
+ return {
3044
+ success: false,
3045
+ error: `Validation failed for ${filePath}`,
3046
+ details: formatZodErrors(result.error),
3047
+ filePath
3048
+ };
3049
+ }
3050
+ return {
3051
+ success: true,
3052
+ definition: result.data,
3053
+ filePath
3054
+ };
3055
+ }
2937
3056
  function detectYamlType(filePath) {
2938
3057
  if (!existsSync6(filePath)) return "unknown";
2939
3058
  try {
2940
3059
  const content = readFileSync4(filePath, "utf-8");
2941
3060
  const parsed = parseYaml(content);
2942
3061
  if (parsed && typeof parsed === "object") {
3062
+ if (parsed.pattern === "Junction") return "junction";
2943
3063
  if ("entity" in parsed) return "entity";
2944
3064
  if ("relationship" in parsed) return "relationship";
2945
3065
  }
@@ -3392,19 +3512,19 @@ function findCircularDependencies(graph) {
3392
3512
  const cycles = [];
3393
3513
  const visited = /* @__PURE__ */ new Set();
3394
3514
  const recursionStack = /* @__PURE__ */ new Set();
3395
- function dfs(node, path29) {
3515
+ function dfs(node, path30) {
3396
3516
  visited.add(node);
3397
3517
  recursionStack.add(node);
3398
3518
  const outgoingEdges = graph.edges.filter((e) => e.from === node);
3399
3519
  for (const edge of outgoingEdges) {
3400
3520
  if (!visited.has(edge.to)) {
3401
- dfs(edge.to, [...path29, edge.to]);
3521
+ dfs(edge.to, [...path30, edge.to]);
3402
3522
  } else if (recursionStack.has(edge.to)) {
3403
- const cycleStart = path29.indexOf(edge.to);
3523
+ const cycleStart = path30.indexOf(edge.to);
3404
3524
  if (cycleStart !== -1) {
3405
- cycles.push([...path29.slice(cycleStart), edge.to]);
3525
+ cycles.push([...path30.slice(cycleStart), edge.to]);
3406
3526
  } else {
3407
- cycles.push([...path29, edge.to]);
3527
+ cycles.push([...path30, edge.to]);
3408
3528
  }
3409
3529
  }
3410
3530
  }
@@ -4001,8 +4121,8 @@ function suggestTransitiveRelationships(graph, options) {
4001
4121
  for (const [entityName, entity] of graph.entities) {
4002
4122
  if (shouldExcludeEntity(entityName, opts)) continue;
4003
4123
  const paths = findTransitivePaths(graph, entityName, opts);
4004
- for (const path29 of paths) {
4005
- suggestions.push(createSuggestion(path29));
4124
+ for (const path30 of paths) {
4125
+ suggestions.push(createSuggestion(path30));
4006
4126
  }
4007
4127
  }
4008
4128
  return suggestions;
@@ -4033,7 +4153,7 @@ function findTransitivePaths(graph, sourceEntity, opts) {
4033
4153
  while (queue.length > 0) {
4034
4154
  const current = queue.shift();
4035
4155
  if (!current) continue;
4036
- const { entity, depth, path: path29, visited } = current;
4156
+ const { entity, depth, path: path30, visited } = current;
4037
4157
  if (depth >= opts.maxDepth) continue;
4038
4158
  const currentEntity = graph.entities.get(entity);
4039
4159
  if (!currentEntity) continue;
@@ -4044,7 +4164,7 @@ function findTransitivePaths(graph, sourceEntity, opts) {
4044
4164
  if (shouldExcludeEntity(target, opts)) continue;
4045
4165
  if (visited.has(target)) continue;
4046
4166
  const newPath = [
4047
- ...path29,
4167
+ ...path30,
4048
4168
  {
4049
4169
  via: entity,
4050
4170
  relationship: relName,
@@ -4112,15 +4232,15 @@ function generateYamlSnippet(name, target, throughPath) {
4112
4232
  target: ${target}
4113
4233
  through: "${throughPath}"`;
4114
4234
  }
4115
- function createSuggestion(path29) {
4116
- const pathDescription = [path29.source, ...path29.hops.map((h) => h.via), path29.target].join(" -> ");
4235
+ function createSuggestion(path30) {
4236
+ const pathDescription = [path30.source, ...path30.hops.map((h) => h.via), path30.target].join(" -> ");
4117
4237
  return {
4118
4238
  severity: "info",
4119
4239
  type: "transitive_suggestion",
4120
- entity: path29.source,
4240
+ entity: path30.source,
4121
4241
  message: `Potential transitive relationship: ${pathDescription}`,
4122
- suggestion: `Add "${path29.suggestedName}" relationship via "${path29.throughPath}"`,
4123
- path: path29
4242
+ suggestion: `Add "${path30.suggestedName}" relationship via "${path30.throughPath}"`,
4243
+ path: path30
4124
4244
  };
4125
4245
  }
4126
4246
 
@@ -5277,6 +5397,16 @@ var BasePattern = definePattern({
5277
5397
  description: "Identity pattern \u2014 base CRUD, no extra columns or methods"
5278
5398
  });
5279
5399
 
5400
+ // src/patterns/library/junction.pattern.ts
5401
+ import { z as z7 } from "zod";
5402
+ var JunctionPatternConfigSchema = z7.object({}).strict();
5403
+ var JunctionPattern = definePattern({
5404
+ name: "Junction",
5405
+ description: "Explicit many-to-many junction with role + temporal + sourcing metadata",
5406
+ columns: [...BaseJunctionFields],
5407
+ configSchema: JunctionPatternConfigSchema
5408
+ });
5409
+
5280
5410
  // src/patterns/library/knowledge.pattern.ts
5281
5411
  var KnowledgePattern = definePattern({
5282
5412
  name: "Knowledge",
@@ -5341,6 +5471,7 @@ registerLibraryPattern(SyncedPattern);
5341
5471
  registerLibraryPattern(ActivityPattern);
5342
5472
  registerLibraryPattern(KnowledgePattern);
5343
5473
  registerLibraryPattern(MetadataPattern);
5474
+ registerLibraryPattern(JunctionPattern);
5344
5475
 
5345
5476
  // src/index.ts
5346
5477
  async function analyzeDomain(entitiesDir, relationshipsOrOptions) {
@@ -5448,6 +5579,14 @@ function invokeRelationshipNew(absoluteYamlPath, cwd) {
5448
5579
  cwd
5449
5580
  });
5450
5581
  }
5582
+ function invokeJunctionNew(absoluteYamlPath, cwd) {
5583
+ return invokeHygen({
5584
+ generator: "junction",
5585
+ action: "new",
5586
+ args: ["--yaml", absoluteYamlPath],
5587
+ cwd
5588
+ });
5589
+ }
5451
5590
 
5452
5591
  // src/cli/shared/git-safety.ts
5453
5592
  import { execSync as execSync2 } from "child_process";
@@ -5538,6 +5677,27 @@ function collectRelationships(relationshipsDir) {
5538
5677
  junctions.sort((a, b) => a.name.localeCompare(b.name));
5539
5678
  return junctions;
5540
5679
  }
5680
+ function listJunctionYamls(junctionsDir) {
5681
+ if (!fs2.existsSync(junctionsDir)) return [];
5682
+ return fs2.readdirSync(junctionsDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml")).map((f) => path4.join(junctionsDir, f)).filter((full) => detectYamlType(full) === "junction").sort();
5683
+ }
5684
+ function deriveJunctionName(def) {
5685
+ return def.name ?? `${def.between[0]}_${def.between[1]}`;
5686
+ }
5687
+ function collectJunctions(junctionsDir) {
5688
+ const files = listJunctionYamls(junctionsDir);
5689
+ const junctions = [];
5690
+ for (const file of files) {
5691
+ const result = loadJunctionFromYaml(file);
5692
+ if (!result.success) continue;
5693
+ const def = result.definition;
5694
+ const name = deriveJunctionName(def);
5695
+ const plural = def.table ?? pluralize(name);
5696
+ junctions.push({ name, plural });
5697
+ }
5698
+ junctions.sort((a, b) => a.name.localeCompare(b.name));
5699
+ return junctions;
5700
+ }
5541
5701
  function entityFilePaths(info, architecture, backendSrc) {
5542
5702
  const name = info.name;
5543
5703
  const plural = info.plural;
@@ -5605,6 +5765,7 @@ async function regenerateBarrels(opts) {
5605
5765
  ctx,
5606
5766
  entitiesDir,
5607
5767
  relationshipsDir = path4.resolve(ctx.cwd, "relationships"),
5768
+ junctionsDir = path4.resolve(ctx.cwd, "junctions"),
5608
5769
  generatedDir,
5609
5770
  architecture,
5610
5771
  backendSrc = resolveBackendSrc(ctx),
@@ -5613,7 +5774,8 @@ async function regenerateBarrels(opts) {
5613
5774
  const cwd = ctx.cwd;
5614
5775
  const entities = [
5615
5776
  ...collectEntities(entitiesDir),
5616
- ...collectRelationships(relationshipsDir)
5777
+ ...collectRelationships(relationshipsDir),
5778
+ ...collectJunctions(junctionsDir)
5617
5779
  ].sort((a, b) => a.name.localeCompare(b.name));
5618
5780
  const generatedRel = path4.relative(cwd, generatedDir) || path4.basename(generatedDir);
5619
5781
  const modulesRel = path4.posix.join(
@@ -12434,11 +12596,291 @@ var relationshipNoun = {
12434
12596
  };
12435
12597
  var relationship_default = relationshipNoun;
12436
12598
 
12599
+ // src/cli/commands/junction.ts
12600
+ import path27 from "path";
12601
+ import { Command as Command8, Option as Option8 } from "clipanion";
12602
+ function summarizeJunctionFile(filePath) {
12603
+ const result = loadJunctionFromYaml(filePath);
12604
+ if (!result.success) return null;
12605
+ const def = result.definition;
12606
+ const name = def.name ?? `${def.between[0]}_${def.between[1]}`;
12607
+ const roleChoices = def.fields?.role?.choices;
12608
+ const hasRole = Array.isArray(roleChoices) && roleChoices.length > 0;
12609
+ return {
12610
+ name,
12611
+ left: def.between[0],
12612
+ right: def.between[1],
12613
+ hasRole,
12614
+ temporal: def.temporal ?? true,
12615
+ sourced: def.sourced ?? true,
12616
+ file: filePath
12617
+ };
12618
+ }
12619
+ function padRight3(s, n) {
12620
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
12621
+ }
12622
+ async function summary6(ctx) {
12623
+ const junctionDir = path27.resolve(ctx.cwd, "junctions");
12624
+ const files = listJunctionYamls(junctionDir);
12625
+ if (files.length === 0) {
12626
+ return {
12627
+ title: "junctions",
12628
+ body: [
12629
+ "No junction definitions found.",
12630
+ "",
12631
+ `Create one at ${theme.system("junctions/<name>.yaml")} to get started.`
12632
+ ]
12633
+ };
12634
+ }
12635
+ const rows = files.map(summarizeJunctionFile).filter((r) => r !== null);
12636
+ const nameCol = Math.max(4, ...rows.map((r) => r.name.length));
12637
+ const pairingCol = Math.max(7, ...rows.map((r) => `${r.left} \xD7 ${r.right}`.length));
12638
+ const body = rows.map((r) => {
12639
+ const pairing = `${r.left} \xD7 ${r.right}`;
12640
+ const flags = [
12641
+ r.hasRole ? "role" : "",
12642
+ r.temporal ? "temporal" : "",
12643
+ r.sourced ? "sourced" : ""
12644
+ ].filter(Boolean).join(", ");
12645
+ return `${theme.system(icons.bullet)} ${padRight3(r.name, nameCol)} ${theme.muted(
12646
+ padRight3(pairing, pairingCol)
12647
+ )} ${theme.muted(flags)}`;
12648
+ });
12649
+ return {
12650
+ title: "junctions",
12651
+ body,
12652
+ footer: `${rows.length} junctions`
12653
+ };
12654
+ }
12655
+ async function hints6(_ctx) {
12656
+ return [
12657
+ { command: "codegen junction new <file>", description: "Generate one junction" },
12658
+ { command: "codegen junction new --all", description: "Generate all junctions" },
12659
+ { command: "codegen junction list", description: "List junction definitions" }
12660
+ ];
12661
+ }
12662
+ var JunctionNewCommand = class extends Command8 {
12663
+ static paths = [["junction", "new"]];
12664
+ static usage = Command8.Usage({
12665
+ description: "Generate code for one or more junctions from YAML",
12666
+ examples: [
12667
+ ["Generate a single junction", "codegen junction new junctions/opportunity_contact.yaml"],
12668
+ ["Generate all junctions", "codegen junction new --all"],
12669
+ ["Preview without writing", "codegen junction new junctions/opportunity_contact.yaml --dry-run"]
12670
+ ]
12671
+ });
12672
+ yaml = Option8.String({ required: false });
12673
+ all = Option8.Boolean("--all", false);
12674
+ dryRun = Option8.Boolean("--dry-run", false);
12675
+ force = Option8.Boolean("--force", false);
12676
+ json = Option8.Boolean("--json", false);
12677
+ cwd = Option8.String("--cwd", { required: false });
12678
+ configPath = Option8.String("--config", { required: false });
12679
+ async execute() {
12680
+ if (this.json) setJsonMode(true);
12681
+ const ctx = await loadContext({
12682
+ cwd: this.cwd,
12683
+ configPath: this.configPath,
12684
+ json: this.json,
12685
+ skipDetection: true
12686
+ });
12687
+ if (this.all && this.yaml) {
12688
+ printError("Pass either a YAML path or --all, not both.");
12689
+ return 2;
12690
+ }
12691
+ let targets = [];
12692
+ if (this.all) {
12693
+ const dir = path27.resolve(ctx.cwd, "junctions");
12694
+ targets = listJunctionYamls(dir);
12695
+ if (targets.length === 0) {
12696
+ printError(`No junction YAML files found in ${dir}`);
12697
+ return 1;
12698
+ }
12699
+ } else if (this.yaml) {
12700
+ targets = [path27.resolve(ctx.cwd, this.yaml)];
12701
+ } else {
12702
+ printError("Missing YAML path. Pass a file or --all.");
12703
+ return 2;
12704
+ }
12705
+ const validated = [];
12706
+ const invalid = [];
12707
+ for (const file of targets) {
12708
+ const result = loadJunctionFromYaml(file);
12709
+ if (result.success) {
12710
+ const def = result.definition;
12711
+ const name = def.name ?? `${def.between[0]}_${def.between[1]}`;
12712
+ validated.push({ file, name });
12713
+ } else {
12714
+ invalid.push({ file, message: result.error });
12715
+ }
12716
+ }
12717
+ if (invalid.length > 0) {
12718
+ for (const i of invalid) {
12719
+ printError(`${path27.basename(i.file)} \u2014 ${i.message}`);
12720
+ }
12721
+ if (!isJsonMode()) return 1;
12722
+ }
12723
+ if (!this.force) {
12724
+ const gitCheck = checkGitSafety(["src"], ctx.cwd);
12725
+ if (gitCheck.inRepo && !gitCheck.clean) {
12726
+ printWarning(
12727
+ `Uncommitted changes in ${gitCheck.dirty.length} files under src/. Pass --force to overwrite.`
12728
+ );
12729
+ if (!isJsonMode()) return 1;
12730
+ }
12731
+ }
12732
+ if (this.dryRun) {
12733
+ if (isJsonMode()) {
12734
+ printJson({
12735
+ command: "junction new",
12736
+ dryRun: true,
12737
+ junctions: validated.map((v) => ({ name: v.name, file: v.file })),
12738
+ totals: { planned: validated.length, invalid: invalid.length }
12739
+ });
12740
+ } else {
12741
+ printInfo(`Dry run \u2014 ${validated.length} junctions would be generated:`);
12742
+ for (const v of validated) {
12743
+ console.log(` ${theme.muted(icons.arrow)} ${v.name} ${theme.muted(v.file)}`);
12744
+ }
12745
+ }
12746
+ return invalid.length > 0 ? 1 : 0;
12747
+ }
12748
+ const succeeded = [];
12749
+ const failed = [
12750
+ ...invalid.map((i) => ({ name: path27.basename(i.file), file: i.file, message: i.message }))
12751
+ ];
12752
+ for (const v of validated) {
12753
+ if (!isJsonMode()) {
12754
+ printInfo(`generating ${v.name}`);
12755
+ }
12756
+ const res = invokeJunctionNew(v.file, ctx.cwd);
12757
+ if (res.ok) {
12758
+ succeeded.push(v.name);
12759
+ if (!isJsonMode()) printSuccess(`${v.name}`);
12760
+ } else {
12761
+ failed.push({
12762
+ name: v.name,
12763
+ file: v.file,
12764
+ message: res.stderr ?? "Hygen invocation failed"
12765
+ });
12766
+ if (!isJsonMode()) printError(`${v.name} \u2014 ${res.stderr ?? "failed"}`);
12767
+ }
12768
+ }
12769
+ const entitiesDir = ctx.entitiesDir ?? path27.resolve(ctx.cwd, "entities");
12770
+ const relationshipsDir = path27.resolve(ctx.cwd, "relationships");
12771
+ const junctionsDir = path27.resolve(ctx.cwd, "junctions");
12772
+ const generatedDir = resolveGeneratedDir(ctx);
12773
+ const architecture = resolveArchitecture(ctx);
12774
+ let barrelResult = null;
12775
+ try {
12776
+ barrelResult = await regenerateBarrels({
12777
+ ctx,
12778
+ entitiesDir,
12779
+ relationshipsDir,
12780
+ junctionsDir,
12781
+ generatedDir,
12782
+ architecture
12783
+ });
12784
+ } catch (err) {
12785
+ const msg = err instanceof Error ? err.message : String(err);
12786
+ if (!isJsonMode()) {
12787
+ printWarning(`barrel regeneration failed \u2014 ${msg}`);
12788
+ }
12789
+ }
12790
+ if (isJsonMode()) {
12791
+ printJson({
12792
+ command: "junction new",
12793
+ totals: {
12794
+ succeeded: succeeded.length,
12795
+ failed: failed.length
12796
+ },
12797
+ succeeded,
12798
+ failed,
12799
+ barrels: barrelResult ? {
12800
+ modules: barrelResult.modulesBarrel,
12801
+ schema: barrelResult.schemaBarrel,
12802
+ entityCount: barrelResult.entityCount
12803
+ } : null
12804
+ });
12805
+ } else {
12806
+ const total = validated.length + invalid.length;
12807
+ console.log("");
12808
+ if (failed.length === 0) {
12809
+ printSuccess(`${total} junctions \xB7 ${succeeded.length} succeeded`);
12810
+ } else {
12811
+ printWarning(
12812
+ `${total} junctions \xB7 ${succeeded.length} succeeded \xB7 ${failed.length} failed`
12813
+ );
12814
+ }
12815
+ if (barrelResult) {
12816
+ printInfo(
12817
+ `barrels regenerated (${barrelResult.entityCount} modules) \u2192 ${path27.relative(ctx.cwd, barrelResult.modulesBarrel)}, ${path27.relative(ctx.cwd, barrelResult.schemaBarrel)}`
12818
+ );
12819
+ }
12820
+ }
12821
+ return failed.length === 0 ? 0 : 1;
12822
+ }
12823
+ };
12824
+ var JunctionListCommand = class extends Command8 {
12825
+ static paths = [["junction", "list"]];
12826
+ static usage = Command8.Usage({
12827
+ description: "List defined junctions as a table"
12828
+ });
12829
+ json = Option8.Boolean("--json", false);
12830
+ cwd = Option8.String("--cwd", { required: false });
12831
+ configPath = Option8.String("--config", { required: false });
12832
+ async execute() {
12833
+ if (this.json) setJsonMode(true);
12834
+ const ctx = await loadContext({
12835
+ cwd: this.cwd,
12836
+ configPath: this.configPath,
12837
+ json: this.json,
12838
+ skipDetection: true
12839
+ });
12840
+ const junctionDir = path27.resolve(ctx.cwd, "junctions");
12841
+ const files = listJunctionYamls(junctionDir);
12842
+ if (files.length === 0) {
12843
+ printInfo("No junction definitions found.");
12844
+ return 0;
12845
+ }
12846
+ const rows = files.map(summarizeJunctionFile).filter((r) => r !== null);
12847
+ if (isJsonMode()) {
12848
+ printJson({
12849
+ command: "junction list",
12850
+ junctions: rows
12851
+ });
12852
+ return 0;
12853
+ }
12854
+ const nameW = Math.max(4, ...rows.map((r) => r.name.length));
12855
+ const leftW = Math.max(4, ...rows.map((r) => r.left.length));
12856
+ const rightW = Math.max(5, ...rows.map((r) => r.right.length));
12857
+ console.log(
12858
+ theme.muted(
12859
+ `${padRight3("NAME", nameW)} ${padRight3("LEFT", leftW)} ${padRight3("RIGHT", rightW)} ROLE FLAGS`
12860
+ )
12861
+ );
12862
+ for (const r of rows) {
12863
+ const flags = [r.temporal ? "T" : "", r.sourced ? "S" : ""].filter(Boolean).join(",");
12864
+ console.log(
12865
+ `${padRight3(r.name, nameW)} ${padRight3(r.left, leftW)} ${padRight3(r.right, rightW)} ${padRight3(r.hasRole ? "yes" : "no", 4)} ${flags}`
12866
+ );
12867
+ }
12868
+ return 0;
12869
+ }
12870
+ };
12871
+ var junctionNoun = {
12872
+ name: "junction",
12873
+ commandClasses: [JunctionNewCommand, JunctionListCommand],
12874
+ summary: summary6,
12875
+ hints: hints6
12876
+ };
12877
+ var junction_default = junctionNoun;
12878
+
12437
12879
  // src/cli/commands/events.ts
12438
12880
  import fs16 from "fs";
12439
- import path27 from "path";
12881
+ import path28 from "path";
12440
12882
  import ts2 from "typescript";
12441
- import { Command as Command8, Option as Option8 } from "clipanion";
12883
+ import { Command as Command9, Option as Option9 } from "clipanion";
12442
12884
  function scanSourceFileForConsumers(sourceFile, filePath, eventType) {
12443
12885
  const tier2 = [];
12444
12886
  const tier1 = [];
@@ -12572,7 +13014,7 @@ function suggestEventTypes(target, known, limit = 3) {
12572
13014
  return known.map((t) => ({ t, d: levenshtein(target, t) })).sort((a, b) => a.d - b.d).slice(0, limit).map((x) => x.t);
12573
13015
  }
12574
13016
  function renderConsumerReport(result, cwd) {
12575
- const rel = (p) => path27.relative(cwd, p) || p;
13017
+ const rel = (p) => path28.relative(cwd, p) || p;
12576
13018
  const lines = [];
12577
13019
  const total = result.tier3.length + result.tier2.length + result.tier1.length;
12578
13020
  lines.push(`Event: ${result.eventType}`);
@@ -12608,7 +13050,7 @@ function renderConsumerReport(result, cwd) {
12608
13050
  return lines;
12609
13051
  }
12610
13052
  function runConsumersScan(opts) {
12611
- const scanRoot = opts.scanRoot ?? path27.join(opts.cwd, "src");
13053
+ const scanRoot = opts.scanRoot ?? path28.join(opts.cwd, "src");
12612
13054
  const handlersDir = opts.handlersDir ?? scanRoot;
12613
13055
  const allTriggers = scanHandlerFiles(handlersDir);
12614
13056
  const tier3 = allTriggers.filter((t) => t.event === opts.eventType).map((t) => ({
@@ -12618,7 +13060,7 @@ function runConsumersScan(opts) {
12618
13060
  sourceLine: t.sourceLine
12619
13061
  }));
12620
13062
  const tier21 = fs16.existsSync(scanRoot) ? scanDirectoryForConsumers(scanRoot, opts.eventType) : { tier2: [], tier1: [], hasEventFlowImport: false };
12621
- const eventsGeneratedDir = opts.eventsGeneratedDir ?? path27.join(
13063
+ const eventsGeneratedDir = opts.eventsGeneratedDir ?? path28.join(
12622
13064
  resolveSubsystemsRootFromContext(opts.cwd, opts.config),
12623
13065
  "events",
12624
13066
  "generated"
@@ -12639,15 +13081,15 @@ function runConsumersScan(opts) {
12639
13081
  function resolveSubsystemsRootFromContext(cwd, config) {
12640
13082
  const configured = config?.paths?.subsystems;
12641
13083
  if (typeof configured === "string" && configured.length > 0) {
12642
- return path27.resolve(cwd, configured);
13084
+ return path28.resolve(cwd, configured);
12643
13085
  }
12644
13086
  const backendSrc = config?.paths?.backend_src;
12645
13087
  const base = typeof backendSrc === "string" && backendSrc.length > 0 ? backendSrc : "src";
12646
- return path27.resolve(cwd, base, "shared", "subsystems");
13088
+ return path28.resolve(cwd, base, "shared", "subsystems");
12647
13089
  }
12648
- var EventsConsumersCommand = class extends Command8 {
13090
+ var EventsConsumersCommand = class extends Command9 {
12649
13091
  static paths = [["events", "consumers"]];
12650
- static usage = Command8.Usage({
13092
+ static usage = Command9.Usage({
12651
13093
  description: "List all consumers of an event across the three tiers",
12652
13094
  examples: [
12653
13095
  [
@@ -12656,10 +13098,10 @@ var EventsConsumersCommand = class extends Command8 {
12656
13098
  ]
12657
13099
  ]
12658
13100
  });
12659
- eventType = Option8.String({ required: true });
12660
- json = Option8.Boolean("--json", false);
12661
- cwd = Option8.String("--cwd", { required: false });
12662
- configPath = Option8.String("--config", { required: false });
13101
+ eventType = Option9.String({ required: true });
13102
+ json = Option9.Boolean("--json", false);
13103
+ cwd = Option9.String("--cwd", { required: false });
13104
+ configPath = Option9.String("--config", { required: false });
12663
13105
  async execute() {
12664
13106
  if (this.json) setJsonMode(true);
12665
13107
  const ctx = await loadContext({
@@ -12704,7 +13146,7 @@ var EventsConsumersCommand = class extends Command8 {
12704
13146
  return 0;
12705
13147
  }
12706
13148
  };
12707
- async function summary6(_ctx) {
13149
+ async function summary7(_ctx) {
12708
13150
  return {
12709
13151
  title: "events",
12710
13152
  body: [
@@ -12715,7 +13157,7 @@ async function summary6(_ctx) {
12715
13157
  ]
12716
13158
  };
12717
13159
  }
12718
- async function hints6(_ctx) {
13160
+ async function hints7(_ctx) {
12719
13161
  return [
12720
13162
  {
12721
13163
  command: "codegen events consumers <type>",
@@ -12726,14 +13168,14 @@ async function hints6(_ctx) {
12726
13168
  var eventsNoun = {
12727
13169
  name: "events",
12728
13170
  commandClasses: [EventsConsumersCommand],
12729
- summary: summary6,
12730
- hints: hints6
13171
+ summary: summary7,
13172
+ hints: hints7
12731
13173
  };
12732
13174
  var events_default = eventsNoun;
12733
13175
 
12734
13176
  // src/cli/commands/orchestration.ts
12735
- import path28 from "path";
12736
- import { Command as Command9, Option as Option9 } from "clipanion";
13177
+ import path29 from "path";
13178
+ import { Command as Command10, Option as Option10 } from "clipanion";
12737
13179
  var DEFAULT_PATTERN_GLOBS = ["src/patterns/*.pattern.ts"];
12738
13180
  function resolvePatternGlobs(ctx) {
12739
13181
  const fromConfig = ctx.config?.patterns;
@@ -12746,26 +13188,26 @@ function resolveOrchestrationOutputRoot(ctx) {
12746
13188
  const paths = ctx.config?.paths;
12747
13189
  const explicit = paths?.orchestration_src;
12748
13190
  if (typeof explicit === "string" && explicit.length > 0) {
12749
- return path28.resolve(ctx.cwd, explicit);
13191
+ return path29.resolve(ctx.cwd, explicit);
12750
13192
  }
12751
13193
  const backendSrc = typeof paths?.backend_src === "string" && paths.backend_src.length > 0 ? paths.backend_src : "app/backend/src";
12752
- return path28.resolve(ctx.cwd, backendSrc, "orchestration");
13194
+ return path29.resolve(ctx.cwd, backendSrc, "orchestration");
12753
13195
  }
12754
13196
  async function reloadRegistry(ctx) {
12755
13197
  _resetRegistryForTests({ includeLibrary: false });
12756
13198
  return loadAppPatterns(resolvePatternGlobs(ctx), ctx.cwd);
12757
13199
  }
12758
- var OrchestrationGenCommand = class extends Command9 {
13200
+ var OrchestrationGenCommand = class extends Command10 {
12759
13201
  static paths = [["orchestration", "gen"]];
12760
- static usage = Command9.Usage({
13202
+ static usage = Command10.Usage({
12761
13203
  description: "Emit token / providers / dispatcher / module files per orchestration pattern (ADR-032 Phase 3-2/3)."
12762
13204
  });
12763
- pattern = Option9.String("--pattern", { required: false });
12764
- all = Option9.Boolean("--all", false);
12765
- dryRun = Option9.Boolean("--dry-run", false);
12766
- json = Option9.Boolean("--json", false);
12767
- cwd = Option9.String("--cwd", { required: false });
12768
- configPath = Option9.String("--config", { required: false });
13205
+ pattern = Option10.String("--pattern", { required: false });
13206
+ all = Option10.Boolean("--all", false);
13207
+ dryRun = Option10.Boolean("--dry-run", false);
13208
+ json = Option10.Boolean("--json", false);
13209
+ cwd = Option10.String("--cwd", { required: false });
13210
+ configPath = Option10.String("--config", { required: false });
12769
13211
  async execute() {
12770
13212
  if (this.json) setJsonMode(true);
12771
13213
  const ctx = await loadContext({
@@ -12838,12 +13280,12 @@ var OrchestrationGenCommand = class extends Command9 {
12838
13280
  );
12839
13281
  for (const f of result.files) {
12840
13282
  console.log(
12841
- ` ${theme.muted(icons.arrow)} ${path28.relative(ctx.cwd, f.outputPath)}`
13283
+ ` ${theme.muted(icons.arrow)} ${path29.relative(ctx.cwd, f.outputPath)}`
12842
13284
  );
12843
13285
  }
12844
13286
  } else {
12845
13287
  printSuccess(
12846
- `Emitted ${result.files.length} file(s) across ${targets.length} pattern(s) \u2192 ${path28.relative(ctx.cwd, outputRoot)}`
13288
+ `Emitted ${result.files.length} file(s) across ${targets.length} pattern(s) \u2192 ${path29.relative(ctx.cwd, outputRoot)}`
12847
13289
  );
12848
13290
  }
12849
13291
  return 0;
@@ -12856,14 +13298,14 @@ var OrchestrationGenCommand = class extends Command9 {
12856
13298
  }
12857
13299
  }
12858
13300
  };
12859
- var OrchestrationListCommand = class extends Command9 {
13301
+ var OrchestrationListCommand = class extends Command10 {
12860
13302
  static paths = [["orchestration", "list"]];
12861
- static usage = Command9.Usage({
13303
+ static usage = Command10.Usage({
12862
13304
  description: "List registered orchestration patterns"
12863
13305
  });
12864
- json = Option9.Boolean("--json", false);
12865
- cwd = Option9.String("--cwd", { required: false });
12866
- configPath = Option9.String("--config", { required: false });
13306
+ json = Option10.Boolean("--json", false);
13307
+ cwd = Option10.String("--cwd", { required: false });
13308
+ configPath = Option10.String("--config", { required: false });
12867
13309
  async execute() {
12868
13310
  if (this.json) setJsonMode(true);
12869
13311
  const ctx = await loadContext({
@@ -12907,14 +13349,14 @@ var OrchestrationListCommand = class extends Command9 {
12907
13349
  return 0;
12908
13350
  }
12909
13351
  };
12910
- var OrchestrationValidateCommand = class extends Command9 {
13352
+ var OrchestrationValidateCommand = class extends Command10 {
12911
13353
  static paths = [["orchestration", "validate"]];
12912
- static usage = Command9.Usage({
13354
+ static usage = Command10.Usage({
12913
13355
  description: "Run the Phase 3-1 project-level orchestration validator (ADR-032)"
12914
13356
  });
12915
- json = Option9.Boolean("--json", false);
12916
- cwd = Option9.String("--cwd", { required: false });
12917
- configPath = Option9.String("--config", { required: false });
13357
+ json = Option10.Boolean("--json", false);
13358
+ cwd = Option10.String("--cwd", { required: false });
13359
+ configPath = Option10.String("--config", { required: false });
12918
13360
  async execute() {
12919
13361
  if (this.json) setJsonMode(true);
12920
13362
  const ctx = await loadContext({
@@ -12950,7 +13392,7 @@ var OrchestrationValidateCommand = class extends Command9 {
12950
13392
  return errors.length === 0 && loadResult.errors.length === 0 ? 0 : 1;
12951
13393
  }
12952
13394
  };
12953
- async function summary7(ctx) {
13395
+ async function summary8(ctx) {
12954
13396
  try {
12955
13397
  await reloadRegistry(ctx);
12956
13398
  } catch {
@@ -12964,7 +13406,7 @@ async function summary7(ctx) {
12964
13406
  ]
12965
13407
  };
12966
13408
  }
12967
- async function hints7(_ctx) {
13409
+ async function hints8(_ctx) {
12968
13410
  return [
12969
13411
  {
12970
13412
  command: "codegen orchestration gen",
@@ -12987,8 +13429,8 @@ var orchestrationNoun = {
12987
13429
  OrchestrationListCommand,
12988
13430
  OrchestrationValidateCommand
12989
13431
  ],
12990
- summary: summary7,
12991
- hints: hints7
13432
+ summary: summary8,
13433
+ hints: hints8
12992
13434
  };
12993
13435
  var orchestration_default = orchestrationNoun;
12994
13436
 
@@ -13009,9 +13451,9 @@ function readVersion() {
13009
13451
  return "0.0.0";
13010
13452
  }
13011
13453
  }
13012
- var RootSummaryCommand = class extends Command10 {
13013
- static paths = [Command10.Default];
13014
- static usage = Command10.Usage({
13454
+ var RootSummaryCommand = class extends Command11 {
13455
+ static paths = [Command11.Default];
13456
+ static usage = Command11.Usage({
13015
13457
  description: "Show project status and available noun commands"
13016
13458
  });
13017
13459
  async execute() {
@@ -13055,6 +13497,7 @@ var nouns = [
13055
13497
  project_default,
13056
13498
  dev_default,
13057
13499
  relationship_default,
13500
+ junction_default,
13058
13501
  events_default,
13059
13502
  orchestration_default
13060
13503
  ];