@metaobjectsdev/codegen-ts 0.20.9 → 0.20.10

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 (114) hide show
  1. package/dist/generators/api-model.d.ts.map +1 -1
  2. package/dist/generators/api-model.js +9 -5
  3. package/dist/generators/api-model.js.map +1 -1
  4. package/dist/generators/barrel.js +1 -1
  5. package/dist/generators/barrel.js.map +1 -1
  6. package/dist/generators/entity-file.d.ts.map +1 -1
  7. package/dist/generators/entity-file.js +7 -1
  8. package/dist/generators/entity-file.js.map +1 -1
  9. package/dist/generators/extractor-file.d.ts.map +1 -1
  10. package/dist/generators/extractor-file.js +6 -1
  11. package/dist/generators/extractor-file.js.map +1 -1
  12. package/dist/generators/output-parser-file.d.ts.map +1 -1
  13. package/dist/generators/output-parser-file.js +6 -1
  14. package/dist/generators/output-parser-file.js.map +1 -1
  15. package/dist/generators/queries-file.d.ts +1 -1
  16. package/dist/generators/queries-file.d.ts.map +1 -1
  17. package/dist/generators/queries-file.js +11 -6
  18. package/dist/generators/queries-file.js.map +1 -1
  19. package/dist/generators/routes-file-hono.d.ts +4 -0
  20. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  21. package/dist/generators/routes-file-hono.js +6 -1
  22. package/dist/generators/routes-file-hono.js.map +1 -1
  23. package/dist/generators/routes-file.d.ts +4 -0
  24. package/dist/generators/routes-file.d.ts.map +1 -1
  25. package/dist/generators/routes-file.js +6 -1
  26. package/dist/generators/routes-file.js.map +1 -1
  27. package/dist/index.d.ts +1 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +1 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/naming/collision-names.d.ts +19 -0
  32. package/dist/naming/collision-names.d.ts.map +1 -0
  33. package/dist/naming/collision-names.js +79 -0
  34. package/dist/naming/collision-names.js.map +1 -0
  35. package/dist/payload-codegen.d.ts.map +1 -1
  36. package/dist/payload-codegen.js +5 -71
  37. package/dist/payload-codegen.js.map +1 -1
  38. package/dist/render-context.d.ts +35 -3
  39. package/dist/render-context.d.ts.map +1 -1
  40. package/dist/render-context.js +23 -0
  41. package/dist/render-context.js.map +1 -1
  42. package/dist/runner.d.ts.map +1 -1
  43. package/dist/runner.js +36 -2
  44. package/dist/runner.js.map +1 -1
  45. package/dist/source-detect.d.ts +10 -0
  46. package/dist/source-detect.d.ts.map +1 -1
  47. package/dist/source-detect.js +22 -0
  48. package/dist/source-detect.js.map +1 -1
  49. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  50. package/dist/templates/drizzle-schema.js +14 -3
  51. package/dist/templates/drizzle-schema.js.map +1 -1
  52. package/dist/templates/entity-file.d.ts +1 -1
  53. package/dist/templates/entity-file.d.ts.map +1 -1
  54. package/dist/templates/entity-file.js +13 -3
  55. package/dist/templates/entity-file.js.map +1 -1
  56. package/dist/templates/extract-delegate-emitter.d.ts +9 -4
  57. package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
  58. package/dist/templates/extract-delegate-emitter.js +46 -30
  59. package/dist/templates/extract-delegate-emitter.js.map +1 -1
  60. package/dist/templates/extractor.d.ts +2 -1
  61. package/dist/templates/extractor.d.ts.map +1 -1
  62. package/dist/templates/extractor.js +48 -33
  63. package/dist/templates/extractor.js.map +1 -1
  64. package/dist/templates/inferred-types.d.ts +1 -1
  65. package/dist/templates/inferred-types.d.ts.map +1 -1
  66. package/dist/templates/inferred-types.js +39 -16
  67. package/dist/templates/inferred-types.js.map +1 -1
  68. package/dist/templates/output-parser.d.ts +2 -1
  69. package/dist/templates/output-parser.d.ts.map +1 -1
  70. package/dist/templates/output-parser.js +37 -12
  71. package/dist/templates/output-parser.js.map +1 -1
  72. package/dist/templates/projection-decl.d.ts +1 -1
  73. package/dist/templates/projection-decl.d.ts.map +1 -1
  74. package/dist/templates/projection-decl.js +20 -9
  75. package/dist/templates/projection-decl.js.map +1 -1
  76. package/dist/templates/value-object-file.d.ts.map +1 -1
  77. package/dist/templates/value-object-file.js +5 -1
  78. package/dist/templates/value-object-file.js.map +1 -1
  79. package/dist/templates/view-decl.d.ts +12 -2
  80. package/dist/templates/view-decl.d.ts.map +1 -1
  81. package/dist/templates/view-decl.js +21 -15
  82. package/dist/templates/view-decl.js.map +1 -1
  83. package/dist/templates/zod-validators.d.ts +1 -1
  84. package/dist/templates/zod-validators.d.ts.map +1 -1
  85. package/dist/templates/zod-validators.js +35 -22
  86. package/dist/templates/zod-validators.js.map +1 -1
  87. package/package.json +6 -6
  88. package/src/generators/api-model.ts +9 -5
  89. package/src/generators/barrel.ts +1 -1
  90. package/src/generators/entity-file.ts +7 -1
  91. package/src/generators/extractor-file.ts +6 -1
  92. package/src/generators/output-parser-file.ts +6 -1
  93. package/src/generators/queries-file.ts +12 -7
  94. package/src/generators/routes-file-hono.ts +7 -1
  95. package/src/generators/routes-file.ts +6 -1
  96. package/src/index.ts +1 -1
  97. package/src/naming/collision-names.ts +87 -0
  98. package/src/payload-codegen.ts +4 -79
  99. package/src/reference/barrel.ts +1 -1
  100. package/src/reference/queries.ts +14 -6
  101. package/src/reference/routes.ts +8 -1
  102. package/src/render-context.ts +51 -3
  103. package/src/runner.ts +36 -4
  104. package/src/source-detect.ts +20 -0
  105. package/src/templates/drizzle-schema.ts +14 -3
  106. package/src/templates/entity-file.ts +14 -6
  107. package/src/templates/extract-delegate-emitter.ts +52 -27
  108. package/src/templates/extractor.ts +49 -32
  109. package/src/templates/inferred-types.ts +39 -17
  110. package/src/templates/output-parser.ts +40 -12
  111. package/src/templates/projection-decl.ts +20 -10
  112. package/src/templates/value-object-file.ts +5 -1
  113. package/src/templates/view-decl.ts +30 -18
  114. package/src/templates/zod-validators.ts +35 -23
@@ -28,6 +28,7 @@ import {
28
28
  resolveObjectRef,
29
29
  } from "@metaobjectsdev/metadata";
30
30
  import { fields, isArray, scalarKind, jsonStringLiteral } from "./fr010-field-mapping.js";
31
+ import type { RenderContext } from "../render-context.js";
31
32
 
32
33
  // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
33
34
  // ADR-0042: resolveObjectRef gives package-local-before-root-level precedence for a bare ref, FQN-exact otherwise.
@@ -50,14 +51,20 @@ function isObjectField(field: MetaData): boolean {
50
51
  return field.subType === FIELD_SUBTYPE_OBJECT;
51
52
  }
52
53
 
53
- /** The extracted-mirror interface name for a value-object (`<Name>Extracted`). */
54
- export function mirrorName(vo: MetaData): string {
55
- return `${vo.name}Extracted`;
54
+ /** The extracted-mirror interface name for a value-object (`<Name>Extracted`). ADR-0044/#228:
55
+ * `ctx` (optional) resolves the collision-scoped entity-domain emitted name (Task 3's
56
+ * `valueObjectEmittedName`), so a cross-package short-name collision qualifies both the
57
+ * entity module AND its extract mirror identically (`AcmeAlphaNoteExtracted`). Omitted →
58
+ * the bare `vo.name` (bare template unit-test calls; byte-identical to pre-#228 output). */
59
+ export function mirrorName(vo: MetaData, ctx?: RenderContext): string {
60
+ const name = ctx ? ctx.valueObjectEmittedName(vo) : vo.name;
61
+ return `${name}Extracted`;
56
62
  }
57
63
 
58
- /** The mapper function name for a value-object (`from<Name>Extracted`). */
59
- function mapperName(vo: MetaData): string {
60
- return `from${vo.name}Extracted`;
64
+ /** The mapper function name for a value-object (`from<Name>Extracted`). See {@link mirrorName}. */
65
+ function mapperName(vo: MetaData, ctx?: RenderContext): string {
66
+ const name = ctx ? ctx.valueObjectEmittedName(vo) : vo.name;
67
+ return `from${name}Extracted`;
61
68
  }
62
69
 
63
70
  // =============================================================================
@@ -65,10 +72,10 @@ function mapperName(vo: MetaData): string {
65
72
  // =============================================================================
66
73
 
67
74
  /** The nullable mirror TS type for one field — nested-aware (recurses into nested mirror names). */
68
- function nestedMirrorType(field: MetaData, root: MetaData): string {
75
+ function nestedMirrorType(field: MetaData, root: MetaData, ctx?: RenderContext): string {
69
76
  if (isObjectField(field)) {
70
77
  const target = refVo(field, root);
71
- const base = target !== undefined ? mirrorName(target) : "unknown";
78
+ const base = target !== undefined ? mirrorName(target, ctx) : "unknown";
72
79
  const elem = `${base} | null`;
73
80
  return isArray(field) ? `(${elem})[] | null` : elem;
74
81
  }
@@ -92,10 +99,15 @@ function nestedMirrorType(field: MetaData, root: MetaData): string {
92
99
  * name (passed in) so the existing self-contained extract<Name>() and the delegating overload
93
100
  * share one mirror type. Returns the joined interface declarations in stable (BFS) order.
94
101
  */
95
- export function nestedMirrorInterfaces(vo: MetaData, root: MetaData, payloadMirror: string): string {
102
+ export function nestedMirrorInterfaces(
103
+ vo: MetaData,
104
+ root: MetaData,
105
+ payloadMirror: string,
106
+ ctx?: RenderContext,
107
+ ): string {
96
108
  const out: string[] = [];
97
109
  const seen = new Set<string>();
98
- emitMirror(vo, root, payloadMirror, seen, out);
110
+ emitMirror(vo, root, payloadMirror, seen, out, ctx);
99
111
  return out.join("\n\n");
100
112
  }
101
113
 
@@ -105,9 +117,14 @@ function emitMirror(
105
117
  interfaceName: string,
106
118
  seen: Set<string>,
107
119
  out: string[],
120
+ ctx?: RenderContext,
108
121
  ): void {
109
- if (seen.has(vo.name)) return;
110
- seen.add(vo.name);
122
+ // ADR-0044/#228: dedupe by resolutionKey(), NOT the bare name — two distinct value-objects
123
+ // sharing a bare short name across packages (the collision case) are DIFFERENT nodes with
124
+ // DIFFERENT resolutionKey()s; bare-name dedupe would treat the second as "already seen" and
125
+ // silently DROP its mirror interface (and every mapper reading it downstream).
126
+ if (seen.has(vo.resolutionKey())) return;
127
+ seen.add(vo.resolutionKey());
111
128
 
112
129
  const base = interfaceName.endsWith("Extracted")
113
130
  ? interfaceName.slice(0, -"Extracted".length)
@@ -118,7 +135,7 @@ function emitMirror(
118
135
  );
119
136
  lines.push(`export interface ${interfaceName} {`);
120
137
  for (const f of fields(vo)) {
121
- lines.push(` ${f.name}: ${nestedMirrorType(f, root)};`);
138
+ lines.push(` ${f.name}: ${nestedMirrorType(f, root, ctx)};`);
122
139
  }
123
140
  lines.push("}");
124
141
  out.push(lines.join("\n"));
@@ -127,7 +144,7 @@ function emitMirror(
127
144
  for (const f of fields(vo)) {
128
145
  if (isObjectField(f)) {
129
146
  const target = refVo(f, root);
130
- if (target !== undefined) emitMirror(target, root, mirrorName(target), seen, out);
147
+ if (target !== undefined) emitMirror(target, root, mirrorName(target, ctx), seen, out, ctx);
131
148
  }
132
149
  }
133
150
  }
@@ -149,10 +166,11 @@ export function nestedMappers(
149
166
  root: MetaData,
150
167
  rootMapperFn: string,
151
168
  rootMirror: string,
169
+ ctx?: RenderContext,
152
170
  ): string {
153
171
  const out: string[] = [];
154
172
  const seen = new Set<string>();
155
- emitMapper(vo, root, seen, out, { fn: rootMapperFn, mirror: rootMirror });
173
+ emitMapper(vo, root, seen, out, { fn: rootMapperFn, mirror: rootMirror }, ctx);
156
174
  return out.join("\n\n");
157
175
  }
158
176
 
@@ -167,13 +185,17 @@ function emitMapper(
167
185
  seen: Set<string>,
168
186
  out: string[],
169
187
  override?: { fn: string; mirror: string },
188
+ ctx?: RenderContext,
170
189
  ): void {
171
- if (seen.has(vo.name)) return;
172
- seen.add(vo.name);
190
+ // ADR-0044/#228: dedupe by resolutionKey() — see emitMirror for why bare-name dedupe drops
191
+ // the second colliding VO's mapper (silently misdirecting its extraction to the FIRST
192
+ // colliding VO's mapper — the exact wrong-data bug closed by this fix).
193
+ if (seen.has(vo.resolutionKey())) return;
194
+ seen.add(vo.resolutionKey());
173
195
 
174
- const fn = override?.fn ?? mapperName(vo);
175
- const mir = override?.mirror ?? mirrorName(vo);
176
- const assigns = fields(vo).map((f) => ` ${f.name}: ${mapperArg(f, root)},`);
196
+ const fn = override?.fn ?? mapperName(vo, ctx);
197
+ const mir = override?.mirror ?? mirrorName(vo, ctx);
198
+ const assigns = fields(vo).map((f) => ` ${f.name}: ${mapperArg(f, root, ctx)},`);
177
199
  const body = [
178
200
  `/** Map an assembled ValueObject graph into a typed \`${mir}\` mirror. Generated; null-tolerant. */`,
179
201
  `function ${fn}(o: unknown): ${mir} | null {`,
@@ -188,19 +210,19 @@ function emitMapper(
188
210
  for (const f of fields(vo)) {
189
211
  if (isObjectField(f)) {
190
212
  const target = refVo(f, root);
191
- if (target !== undefined) emitMapper(target, root, seen, out);
213
+ if (target !== undefined) emitMapper(target, root, seen, out, undefined, ctx);
192
214
  }
193
215
  }
194
216
  }
195
217
 
196
218
  /** The mirror-field initializer expression that reads `field` from the assembled object `o`. */
197
- function mapperArg(field: MetaData, root: MetaData): string {
219
+ function mapperArg(field: MetaData, root: MetaData, ctx?: RenderContext): string {
198
220
  const key = jsonStringLiteral(field.name);
199
221
 
200
222
  if (isObjectField(field)) {
201
223
  const target = refVo(field, root);
202
224
  if (target === undefined) return "null /* unresolved @objectRef */";
203
- const fn = mapperName(target);
225
+ const fn = mapperName(target, ctx);
204
226
  if (isArray(field)) {
205
227
  return `mapObjectList(readProp(o, ${key}), ${fn})`;
206
228
  }
@@ -242,8 +264,10 @@ export function usedHelpers(vo: MetaData, root: MetaData): Set<string> {
242
264
  const stack = [vo];
243
265
  while (stack.length > 0) {
244
266
  const cur = stack.pop()!;
245
- if (seen.has(cur.name)) continue;
246
- seen.add(cur.name);
267
+ // ADR-0044/#228: dedupe by resolutionKey() — bare-name dedupe would skip walking the
268
+ // SECOND colliding VO's fields entirely, silently missing a helper only IT needs.
269
+ if (seen.has(cur.resolutionKey())) continue;
270
+ seen.add(cur.resolutionKey());
247
271
  for (const f of fields(cur)) {
248
272
  if (isObjectField(f)) {
249
273
  const target = refVo(f, root);
@@ -285,8 +309,9 @@ export function hasNested(vo: MetaData, root: MetaData): boolean {
285
309
  const stack = [vo];
286
310
  while (stack.length > 0) {
287
311
  const cur = stack.pop()!;
288
- if (seen.has(cur.name)) continue;
289
- seen.add(cur.name);
312
+ // ADR-0044/#228: dedupe by resolutionKey() (see usedHelpers).
313
+ if (seen.has(cur.resolutionKey())) continue;
314
+ seen.add(cur.resolutionKey());
290
315
  for (const f of cur.children().filter((c) => c.type === TYPE_FIELD)) {
291
316
  if (isObjectField(f)) {
292
317
  const target = refVo(f, root);
@@ -37,6 +37,7 @@ import { fields, isArray } from "./fr010-field-mapping.js";
37
37
  import { mirrorName } from "./extract-delegate-emitter.js";
38
38
  import { enumUnionAliasName } from "./inferred-types.js";
39
39
  import { enumValues } from "../enum-meta.js";
40
+ import type { RenderContext } from "../render-context.js";
40
41
 
41
42
  // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
42
43
  // ADR-0042: resolveObjectRef gives package-local-before-root-level precedence for a bare ref, FQN-exact otherwise.
@@ -87,9 +88,13 @@ function isFieldRequired(field: MetaData): boolean {
87
88
  return field.attr(FIELD_ATTR_REQUIRED) === true;
88
89
  }
89
90
 
90
- /** The mirror→strict mapper name for a value-object (`toStrict<Name>`). */
91
- function mapperName(vo: MetaData): string {
92
- return `toStrict${vo.name}`;
91
+ /** The mirror→strict mapper name for a value-object (`toStrict<Name>`). ADR-0044/#228: `ctx`
92
+ * (optional) resolves the collision-scoped entity-domain emitted name (matches Task 3's
93
+ * entity module), so the mapper name agrees with the strict payload type it targets under a
94
+ * cross-package short-name collision (`toStrictAcmeAlphaNote`). Omitted → bare `vo.name`. */
95
+ function mapperName(vo: MetaData, ctx?: RenderContext): string {
96
+ const name = ctx ? ctx.valueObjectEmittedName(vo) : vo.name;
97
+ return `toStrict${name}`;
93
98
  }
94
99
 
95
100
  /**
@@ -99,7 +104,7 @@ function mapperName(vo: MetaData): string {
99
104
  * `f?: T` (= `T | undefined`, never `T | null`), so an absent optional maps to `undefined`.
100
105
  * Nested single/array objects recurse into their toStrict<Type> mapper, guarding when optional.
101
106
  */
102
- function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
107
+ function strictArg(field: MetaData, root: MetaData, ownerName: string, ctx?: RenderContext): string {
103
108
  const name = field.name;
104
109
  const required = isFieldRequired(field);
105
110
 
@@ -109,7 +114,7 @@ function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
109
114
  // Unresolved @objectRef — the payload type would be `unknown`; pass through as-is.
110
115
  return required ? `m.${name}!` : `m.${name} ?? undefined`;
111
116
  }
112
- const fn = mapperName(target);
117
+ const fn = mapperName(target, ctx);
113
118
  if (isArray(field)) {
114
119
  // Required array-of-objects: each element mapped; element nulls dropped at the type level
115
120
  // via the non-null assertion (extract never yields null elements for a present array).
@@ -164,10 +169,10 @@ function strictArg(field: MetaData, root: MetaData, ownerName: string): string {
164
169
  * payload interface. The ROOT mapper reads the canonically-named root mirror (`<Template>Extracted`)
165
170
  * since the template name may differ from the payload VO name.
166
171
  */
167
- function emitMappers(payloadVo: MetaData, root: MetaData, rootMirror: string): string {
172
+ function emitMappers(payloadVo: MetaData, root: MetaData, rootMirror: string, ctx?: RenderContext): string {
168
173
  const out: string[] = [];
169
174
  const seen = new Set<string>();
170
- emitMapper(payloadVo, root, seen, out, rootMirror);
175
+ emitMapper(payloadVo, root, seen, out, rootMirror, ctx);
171
176
  return out.join("\n\n");
172
177
  }
173
178
 
@@ -177,14 +182,17 @@ function emitMapper(
177
182
  seen: Set<string>,
178
183
  out: string[],
179
184
  mirrorOverride?: string,
185
+ ctx?: RenderContext,
180
186
  ): void {
181
- if (seen.has(vo.name)) return;
182
- seen.add(vo.name);
187
+ // ADR-0044/#228: dedupe by resolutionKey() — see extract-delegate-emitter's emitMirror for why
188
+ // bare-name dedupe silently drops the second colliding VO's toStrict mapper.
189
+ if (seen.has(vo.resolutionKey())) return;
190
+ seen.add(vo.resolutionKey());
183
191
 
184
- const fn = mapperName(vo);
185
- const strict = vo.name;
186
- const mir = mirrorOverride ?? mirrorName(vo);
187
- const assigns = fields(vo).map((f) => ` ${f.name}: ${strictArg(f, root, vo.name)},`);
192
+ const fn = mapperName(vo, ctx);
193
+ const strict = ctx ? ctx.valueObjectEmittedName(vo) : vo.name;
194
+ const mir = mirrorOverride ?? mirrorName(vo, ctx);
195
+ const assigns = fields(vo).map((f) => ` ${f.name}: ${strictArg(f, root, strict, ctx)},`);
188
196
  out.push(
189
197
  [
190
198
  `/** Map the all-nullable \`${mir}\` mirror onto the strict \`${strict}\` payload. Generated. */`,
@@ -199,7 +207,7 @@ function emitMapper(
199
207
  for (const f of fields(vo)) {
200
208
  if (isObjectField(f)) {
201
209
  const target = refVo(f, root);
202
- if (target !== undefined) emitMapper(target, root, seen, out);
210
+ if (target !== undefined) emitMapper(target, root, seen, out, undefined, ctx);
203
211
  }
204
212
  }
205
213
  }
@@ -224,23 +232,29 @@ interface PayloadImportGroup {
224
232
  * VO's interface AND the aliases for its own enum fields are imported from `./<VO>.js` — NOT from a
225
233
  * single `payloads.ts` (which no generator emits). Deduped, in discovery order, one group per VO.
226
234
  */
227
- function reachablePayloadGroups(vo: MetaData, root: MetaData): PayloadImportGroup[] {
235
+ function reachablePayloadGroups(vo: MetaData, root: MetaData, ctx?: RenderContext): PayloadImportGroup[] {
228
236
  const groups: PayloadImportGroup[] = [];
229
237
  const seenVo = new Set<string>();
230
238
  const seenAlias = new Set<string>();
231
239
  const visit = (cur: MetaData) => {
232
- if (seenVo.has(cur.name)) return;
233
- seenVo.add(cur.name);
234
- // The VO interface + its OWN enum aliases share the VO's entity module.
235
- const types: string[] = [cur.name];
240
+ // ADR-0044/#228: dedupe by resolutionKey() — bare-name dedupe would treat a colliding
241
+ // second VO as "already seen" and drop its own import group entirely.
242
+ if (seenVo.has(cur.resolutionKey())) return;
243
+ seenVo.add(cur.resolutionKey());
244
+ // The VO interface + its OWN enum aliases share the VO's entity module. The module target
245
+ // is the ADR-0044/#228 entity-domain EMITTED name (Task 3's entityFile() writes `<emitted>.ts`),
246
+ // so a cross-package short-name collision imports from the SAME qualified module the entity
247
+ // tier emits (e.g. `AcmeAlphaNote` from `./AcmeAlphaNote.js`, never bare `Note`).
248
+ const emittedName = ctx ? ctx.valueObjectEmittedName(cur) : cur.name;
249
+ const types: string[] = [emittedName];
236
250
  for (const f of fields(cur)) {
237
- const alias = enumAlias(f, cur.name);
251
+ const alias = enumAlias(f, emittedName);
238
252
  if (alias !== undefined && !seenAlias.has(alias)) {
239
253
  seenAlias.add(alias);
240
254
  types.push(alias);
241
255
  }
242
256
  }
243
- groups.push({ module: cur.name, types });
257
+ groups.push({ module: emittedName, types });
244
258
  // Recurse into nested object refs (their interfaces live in their own modules).
245
259
  for (const f of fields(cur)) {
246
260
  if (isObjectField(f)) {
@@ -254,16 +268,16 @@ function reachablePayloadGroups(vo: MetaData, root: MetaData): PayloadImportGrou
254
268
  }
255
269
 
256
270
  /** Collect the mirror-interface names reachable from `vo` (root mirror + nested VO mirrors). */
257
- function reachableMirrorTypes(vo: MetaData, root: MetaData, rootMirror: string): string[] {
271
+ function reachableMirrorTypes(vo: MetaData, root: MetaData, rootMirror: string, ctx?: RenderContext): string[] {
258
272
  const out: string[] = [rootMirror];
259
- const seen = new Set<string>([vo.name]);
273
+ const seen = new Set<string>([vo.resolutionKey()]);
260
274
  const visit = (cur: MetaData) => {
261
275
  for (const f of fields(cur)) {
262
276
  if (isObjectField(f)) {
263
277
  const target = refVo(f, root);
264
- if (target !== undefined && !seen.has(target.name)) {
265
- seen.add(target.name);
266
- out.push(mirrorName(target));
278
+ if (target !== undefined && !seen.has(target.resolutionKey())) {
279
+ seen.add(target.resolutionKey());
280
+ out.push(mirrorName(target, ctx));
267
281
  visit(target);
268
282
  }
269
283
  }
@@ -279,7 +293,7 @@ function reachableMirrorTypes(vo: MetaData, root: MetaData, rootMirror: string):
279
293
  * or if the target format is not json/xml (the extract tier requires the extract<Name> API, which
280
294
  * only the json/xml output-parsers emit).
281
295
  */
282
- export function renderExtractor(root: MetaData, templateName: string): string {
296
+ export function renderExtractor(root: MetaData, templateName: string, ctx?: RenderContext): string {
283
297
  const tmpl = findTemplate(root, templateName);
284
298
  if (!tmpl) {
285
299
  throw new Error(`template "${templateName}" not found in metadata root`);
@@ -305,16 +319,19 @@ export function renderExtractor(root: MetaData, templateName: string): string {
305
319
  );
306
320
  }
307
321
 
308
- const strictType = vo.name; // the payload VO's interface name (payload-codegen emits the bare VO name)
322
+ // ADR-0044/#228: the strict payload TYPE name is the entity-domain EMITTED name (Task 3's
323
+ // `valueObjectEmittedName`) — the SAME name entityFile() declared the interface under, so a
324
+ // cross-package short-name collision emits e.g. `AcmeAlphaNote`, matching `./AcmeAlphaNote.js`.
325
+ const strictType = ctx ? ctx.valueObjectEmittedName(vo) : vo.name;
309
326
  const rootMirror = `${templateName}Extracted`;
310
327
  const extractLenientWithName = `extractLenient${templateName}WithLoader`; // the nested-capable lenient extract (output-parser)
311
328
  const extractLenientPublic = `extractLenient${templateName}`; // re-exposed never-throws lenient tier name
312
329
  const extractName = `extract${templateName}`;
313
- const rootMapper = mapperName(vo);
330
+ const rootMapper = mapperName(vo, ctx);
314
331
 
315
- const payloadGroups = reachablePayloadGroups(vo, root);
316
- const mirrorTypes = reachableMirrorTypes(vo, root, rootMirror);
317
- const mappers = emitMappers(vo, root, rootMirror);
332
+ const payloadGroups = reachablePayloadGroups(vo, root, ctx);
333
+ const mirrorTypes = reachableMirrorTypes(vo, root, rootMirror, ctx);
334
+ const mappers = emitMappers(vo, root, rootMirror, ctx);
318
335
 
319
336
  // One type-only import per VO entity module (the VO interface + its own enum
320
337
  // union-aliases co-located there). NOT a single non-existent `./payloads.js`.
@@ -40,7 +40,7 @@ import { enumValues } from "../enum-meta.js";
40
40
  import { renderDocsFor } from "./jsdoc.js";
41
41
  import { sharedEnumForField } from "../enum-shared.js";
42
42
  import { sharedEnumImportSpecifier, providedEnumImportSpecifier } from "../enum-import.js";
43
- import type { RenderContext } from "../render-context.js";
43
+ import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
44
44
 
45
45
  /**
46
46
  * Emit Drizzle's InferSelectModel / InferInsertModel aliases for an entity.
@@ -132,6 +132,11 @@ export function renderEnumTypeAliases(entity: MetaObject, ctx?: RenderContext):
132
132
  // De-duplicate by type-alias name — multiple fields can extend the same abstract enum.
133
133
  const seen = new Set<string>();
134
134
  const lines: string[] = [];
135
+ // ADR-0044/#228 — an inline enum's alias is `<Owner><Field>`; `<Owner>` is this
136
+ // object's EMITTED name so a collision-qualified value object declares (and its
137
+ // interface references) `AcmeAlphaNoteStatus`, not a bare `NoteStatus`. Entities
138
+ // and non-colliding value objects keep their bare name (byte-identical).
139
+ const ownerName = ctx ? ctx.valueObjectEmittedName(entity) : entity.name;
135
140
 
136
141
  for (const field of entity.fields()) {
137
142
  if (field.subType !== FIELD_SUBTYPE_ENUM) continue;
@@ -139,7 +144,7 @@ export function renderEnumTypeAliases(entity: MetaObject, ctx?: RenderContext):
139
144
  const values = enumValues(field);
140
145
  if (values === undefined) continue;
141
146
 
142
- const typeName = enumUnionAliasName(entity.name, field);
147
+ const typeName = enumUnionAliasName(ownerName, field);
143
148
  if (seen.has(typeName)) continue;
144
149
  seen.add(typeName);
145
150
 
@@ -214,6 +219,12 @@ export function fieldTsTypeString(ownerName: string, field: MetaField): string {
214
219
  if (field.subType === FIELD_SUBTYPE_OBJECT) {
215
220
  const ref = field.attr(FIELD_ATTR_OBJECT_REF);
216
221
  if (typeof ref === "string" && ref.length > 0) {
222
+ // #228: docs-tier bare name under collision — this is the deprecated `meta docs`
223
+ // TEXT-shape helper (no ctx/root in scope; callers api-field-shape run under
224
+ // api-model's `{ pkMap } as RenderContext` shim), so it can't resolve the ADR-0044
225
+ // emitted name. Byte-identical to codegen in every non-colliding model; on a
226
+ // cross-package collision it documents the bare `Note` while codegen emits
227
+ // `AcmeAlphaNote`. Threading a real RenderContext into api-docs is out of scope.
217
228
  const base = stripPackage(ref);
218
229
  return field.resolvedIsArray() ? `${base}[]` : base;
219
230
  }
@@ -241,6 +252,10 @@ export function fieldTsTypeString(ownerName: string, field: MetaField): string {
241
252
  * ts-poet `imp(...)` — matching how the Zod emitter hoists `<Ref>InsertSchema`.
242
253
  */
243
254
  function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: RenderContext): Code {
255
+ // ADR-0044/#228 — the owning value-object's EMITTED name (bare when unique in
256
+ // the run, package-qualified on a cross-package short-name collision). Drives
257
+ // the inline enum-union alias so it matches the alias declared for this object.
258
+ const ownerName = ctx ? ctx.valueObjectEmittedName(entity) : entity.name;
244
259
  // `@dbColumnType: jsonb` (open JSON bag) → `unknown`, in lock-step with
245
260
  // fieldTsTypeString above and the `z.unknown()` Zod emission.
246
261
  if (field.attr(FIELD_ATTR_DB_COLUMN_TYPE) === DB_COLUMN_TYPE_JSONB) {
@@ -252,16 +267,19 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
252
267
  if (field.subType === FIELD_SUBTYPE_OBJECT) {
253
268
  const ref = field.attr(FIELD_ATTR_OBJECT_REF);
254
269
  if (typeof ref === "string" && ref.length > 0) {
255
- // @objectRef may be authored fully-qualified (acme::sales::Brief) or bare; the
256
- // referenced interface is named by the BARE short name. The import MODULE is
257
- // resolved through the shared layout/package/extStyle-aware helper (the SAME
258
- // one the Zod schema + Drizzle .$type<> use) so all three agree. Without a
259
- // ctx (bare unit-test calls) fall back to the flat same-dir specifier.
260
- const base = stripPackage(ref);
270
+ // @objectRef may be authored fully-qualified (acme::sales::Brief) or bare.
271
+ // ADR-0044/#228 — the referenced interface is named by its EMITTED name
272
+ // (bare when unique in the run, package-qualified on a cross-package
273
+ // short-name collision), resolved package-locally from the FIELD's declaring
274
+ // package. The import MODULE is resolved through the shared
275
+ // layout/package/extStyle-aware helper (the SAME one the Zod schema +
276
+ // Drizzle .$type<> use) so all three agree. Without a ctx (bare unit-test
277
+ // calls) fall back to the bare name + flat same-dir specifier.
278
+ const refName = ctx ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, entity.package)) : stripPackage(ref);
261
279
  const moduleSpec = ctx
262
- ? valueObjectModuleSpecifier(base, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle)
263
- : `./${base}.js`;
264
- const refImp = imp(`${base}@${moduleSpec}`);
280
+ ? valueObjectModuleSpecifier(refName, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle)
281
+ : `./${refName}.js`;
282
+ const refImp = imp(`${refName}@${moduleSpec}`);
265
283
  return field.resolvedIsArray() ? code`${refImp}[]` : code`${refImp}`;
266
284
  }
267
285
  return field.resolvedIsArray() ? code`unknown[]` : code`unknown`;
@@ -271,11 +289,11 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
271
289
  if (field.subType === FIELD_SUBTYPE_MAP) {
272
290
  const ref = field.attr(FIELD_ATTR_OBJECT_REF);
273
291
  if (typeof ref === "string" && ref.length > 0) {
274
- const base = stripPackage(ref);
292
+ const refName = ctx ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, entity.package)) : stripPackage(ref);
275
293
  const moduleSpec = ctx
276
- ? valueObjectModuleSpecifier(base, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle)
277
- : `./${base}.js`;
278
- const refImp = imp(`${base}@${moduleSpec}`);
294
+ ? valueObjectModuleSpecifier(refName, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle)
295
+ : `./${refName}.js`;
296
+ const refImp = imp(`${refName}@${moduleSpec}`);
279
297
  return code`Record<string, ${refImp}>`;
280
298
  }
281
299
  const vt = field.attr(FIELD_ATTR_VALUE_TYPE);
@@ -287,7 +305,7 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
287
305
  if (field.subType === FIELD_SUBTYPE_ENUM) {
288
306
  const values = enumValues(field);
289
307
  if (values !== undefined) {
290
- const alias = enumUnionAliasName(entity.name, field);
308
+ const alias = enumUnionAliasName(ownerName, field);
291
309
  // FR-019: a shared/provided enum's type lives in another module (./enums or
292
310
  // the provided module). Use imp() so ts-poet hoists `import { type E }` —
293
311
  // the local interface can then reference E. Inline enums reference the
@@ -322,6 +340,10 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
322
340
  export function renderValueObjectInterface(entity: MetaObject, ctx?: RenderContext): Code {
323
341
  const docs = renderDocsFor(entity);
324
342
  const docsPrefix = docs ? `${docs}\n` : "";
343
+ // ADR-0044/#228 — the declared interface name is this value object's EMITTED
344
+ // name (bare when unique in the run, package-qualified on a cross-package
345
+ // short-name collision). Byte-identical (bare) when there is no collision.
346
+ const objName = ctx ? ctx.valueObjectEmittedName(entity) : entity.name;
325
347
 
326
348
  const lines: Code[] = [];
327
349
  for (const field of entity.fields()) {
@@ -333,7 +355,7 @@ export function renderValueObjectInterface(entity: MetaObject, ctx?: RenderConte
333
355
 
334
356
  // joinCode with "\n" interpolates each Code segment on its own line and
335
357
  // keeps the imp() registrations intact so ts-poet hoists the imports.
336
- return code`${docsPrefix}export interface ${entity.name} {
358
+ return code`${docsPrefix}export interface ${objName} {
337
359
  ${joinCode(lines, { on: "\n" })}
338
360
  }
339
361
  `;
@@ -28,6 +28,7 @@ import {
28
28
  usedHelpers,
29
29
  hasNested,
30
30
  } from "./extract-delegate-emitter.js";
31
+ import type { RenderContext } from "../render-context.js";
31
32
 
32
33
  const SCALAR_ZOD: Record<string, string> = {
33
34
  string: "z.string()",
@@ -93,7 +94,7 @@ function renderObjectSchema(vo: MetaData, root: MetaData, seen: ReadonlySet<stri
93
94
  * Throws if the template isn't found, isn't a template.output, or its
94
95
  * @payloadRef doesn't resolve to an object.value.
95
96
  */
96
- export function renderOutputParser(root: MetaData, templateName: string): string {
97
+ export function renderOutputParser(root: MetaData, templateName: string, ctx?: RenderContext): string {
97
98
  const tmpl = findTemplate(root, templateName);
98
99
  if (!tmpl) {
99
100
  throw new Error(`template "${templateName}" not found in metadata root`);
@@ -176,8 +177,10 @@ export function ${safeParseName}(
176
177
  // The nullable mirror is the return shape of the delegating extract. Use the nested-aware
177
178
  // emitter so the payload mirror's nested-object / array-of-object components are typed (not
178
179
  // `unknown`), and so a mirror interface is emitted for every reachable nested value-object.
179
- // The payload mirror keeps the canonical `<Template>Extracted` name.
180
- const mirrorDecls = nestedMirrorInterfaces(vo, root, extractedName);
180
+ // The payload mirror keeps the canonical `<Template>Extracted` name. ADR-0044/#228: `ctx`
181
+ // qualifies a nested mirror's name/dedupe when its VO's bare short name collides across
182
+ // packages, matching Task 3's entity-domain emitted name (e.g. `AcmeAlphaNoteExtracted`).
183
+ const mirrorDecls = nestedMirrorInterfaces(vo, root, extractedName, ctx);
181
184
 
182
185
  // Render-package imports the (single, loader-delegating) extract block needs. Kept minimal so
183
186
  // the file has no unused imports (tsc noUnusedLocals-safe).
@@ -190,19 +193,41 @@ export function ${safeParseName}(
190
193
  // graph is then mapped into the typed nullable mirror graph by the generated from<VO>Extracted
191
194
  // mappers. Codegen-wrapping-runtime (a generated DAO calling the dynamic-metadata runtime).
192
195
  //
193
- // The baked PAYLOAD_NAME is the resolved payload VO's SIMPLE name (root.findObject matches on
194
- // the object's `name`, not its FQN). The root mapper is named for the TEMPLATE (so it returns
195
- // the canonically-named `<Template>Extracted` mirror); nested mappers use their VO names.
196
+ // The baked PAYLOAD_NAME is normally the resolved payload VO's SIMPLE name (root.findObject
197
+ // matches on the object's `name`, not its FQN). The root mapper is named for the TEMPLATE (so
198
+ // it returns the canonically-named `<Template>Extracted` mirror); nested mappers use their VO
199
+ // names (via the entity-domain name map, so they agree with the imported mirror types above).
200
+ //
201
+ // ADR-0044/#228 — `root.findObject()` (MetaRoot's public runtime API) is a BARE-name-only,
202
+ // first-match lookup with no package awareness. If THIS PAYLOAD's own bare name collides with
203
+ // a same-short-name value-object elsewhere in the run (the identical signal Option A already
204
+ // computes: `ctx.valueObjectEmittedName(vo)` diverges from the bare name), a bare lookup could
205
+ // silently resolve to the WRONG package's object at runtime (load-order-dependent — the exact
206
+ // hazard class ADR-0042 closed everywhere else). When it does collide, bake the FQN
207
+ // (`resolutionKey()`) instead and resolve it via the SAME canonical ADR-0042 `resolveObjectRef`
208
+ // this file's own build-time `findObject()` wraps (FQN-exact, load-order-independent). A
209
+ // non-colliding payload keeps the bare name + `root.findObject()` path — byte-identical to
210
+ // pre-#228 output.
196
211
  const payloadName = vo.name;
212
+ const emittedPayloadName = ctx ? ctx.valueObjectEmittedName(vo) : payloadName;
213
+ const payloadNameCollides = emittedPayloadName !== payloadName;
214
+ const bakedPayloadName = payloadNameCollides ? vo.resolutionKey() : payloadName;
197
215
  const rootMapper = rootMapperName(templateName);
198
216
  void hasNested;
217
+ const lookupExpr = payloadNameCollides
218
+ ? `resolveObjectRef(root, ${payloadFqnConst}, "").node`
219
+ : `root.findObject(${payloadFqnConst})`;
199
220
  const delegating = `
200
- /** Payload value-object name this parser extracts — resolved against a loaded MetaRoot at runtime. */
201
- export const ${payloadFqnConst} = ${JSON.stringify(payloadName)};
221
+ /** Payload value-object name this parser extracts — resolved against a loaded MetaRoot at runtime.${
222
+ payloadNameCollides
223
+ ? " ADR-0042 FQN (this payload's bare name collides with a same-short-name value object elsewhere in the run)."
224
+ : ""
225
+ } */
226
+ export const ${payloadFqnConst} = ${JSON.stringify(bakedPayloadName)};
202
227
 
203
228
  ${mirrorDecls}
204
229
 
205
- ${nestedMappers(vo, root, rootMapper, extractedName)}
230
+ ${nestedMappers(vo, root, rootMapper, extractedName, ctx)}
206
231
 
207
232
  ${delegateHelpers(usedHelpers(vo, root))}
208
233
 
@@ -221,7 +246,7 @@ export function ${extractLenientWithName}(
221
246
  text: string,
222
247
  opts?: Partial<ExtractOptions> | null,
223
248
  ): ExtractionResult<${extractedName}> {
224
- const mo = root.findObject(${payloadFqnConst});
249
+ const mo = ${lookupExpr};
225
250
  if (mo === undefined) {
226
251
  throw new Error(\`${extractLenientWithName}: payload "\${${payloadFqnConst}}" not found in the supplied MetaRoot\`);
227
252
  }
@@ -230,8 +255,11 @@ export function ${extractLenientWithName}(
230
255
  }
231
256
  `;
232
257
 
233
- // The delegating overload needs runtime-ts (extractObject) + the MetaRoot type from metadata.
234
- const metadataImport = `import type { MetaRoot } from "@metaobjectsdev/metadata";\n`;
258
+ // The delegating overload needs runtime-ts (extractObject) + the MetaRoot type from metadata
259
+ // (+ resolveObjectRef, ADR-0044/#228, only when this payload's own bare name collides).
260
+ const metadataImport = payloadNameCollides
261
+ ? `import type { MetaRoot } from "@metaobjectsdev/metadata";\nimport { resolveObjectRef } from "@metaobjectsdev/metadata";\n`
262
+ : `import type { MetaRoot } from "@metaobjectsdev/metadata";\n`;
235
263
  const runtimeImport = `import { extractObject } from "@metaobjectsdev/runtime-ts";\n`;
236
264
 
237
265
  return (
@@ -10,12 +10,13 @@
10
10
  import { code, imp, joinCode, type Code } from "ts-poet";
11
11
  import {
12
12
  MetaField, MetaObject, type MetaRoot,
13
+ FIELD_ATTR_OBJECT_REF, stripPackage,
13
14
  } from "@metaobjectsdev/metadata";
14
15
  import { projectionViewName } from "../projection/extract-view-spec.js";
15
16
  import { columnNameFromField, toSnakeCase, pluralize } from "../naming.js";
16
17
  import { GENERATED_HEADER } from "../constants.js";
17
18
  import type { ColumnNamingStrategy } from "../metaobjects-config.js";
18
- import type { RenderContext } from "../render-context.js";
19
+ import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
19
20
  import { valueObjectModuleSpecifier } from "../import-path.js";
20
21
  import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
21
22
  import { renderFilterType } from "./filter-type.js";
@@ -92,13 +93,22 @@ export function renderProjectionDecl(
92
93
  ): string {
93
94
  const { dialect, columnNamingStrategy, apiPrefix = "", timestampMode = "string", allowlists = true, ctx, includeViewDecl = true } = opts;
94
95
 
95
- // Resolve a value-object name its import module. Layout/package/extStyle-aware
96
- // when a render context is present (so the projection's VO imports match the
97
- // entity's), else a flat same-dir import identical to zodFieldExpr's fallback.
98
- const voModule = (refBase: string): string =>
99
- ctx
100
- ? valueObjectModuleSpecifier(refBase, ctx.packageOf, projection.package, ctx.outputLayout, ctx.extStyle)
101
- : `./${refBase}.js`;
96
+ // ADR-0044/#228 — resolve a projection field's `@objectRef` to the value object's
97
+ // EMITTED name + module TOGETHER (lock-step): bare when unique in the run,
98
+ // package-qualified on a cross-package short-name collision, so the projection's
99
+ // VO import matches the entity's. Layout/package/extStyle-aware when a render
100
+ // context is present, else a flat same-dir import (zodFieldExpr's fallback).
101
+ const voRef = (field: MetaField): { name: string; module: string } => {
102
+ const ref = field.attr(FIELD_ATTR_OBJECT_REF);
103
+ const rawRef = typeof ref === "string" ? ref : "";
104
+ const name = ctx
105
+ ? ctx.resolveValueObjectName(rawRef, fieldDeclaringPackage(field, projection.package))
106
+ : stripPackage(rawRef);
107
+ const module = ctx
108
+ ? valueObjectModuleSpecifier(name, ctx.packageOf, projection.package, ctx.outputLayout, ctx.extStyle)
109
+ : `./${name}.js`;
110
+ return { name, module };
111
+ };
102
112
 
103
113
  const z = imp("z@zod");
104
114
 
@@ -145,12 +155,12 @@ export function renderProjectionDecl(
145
155
  const sections: Code[] = [
146
156
  ...(includeViewDecl
147
157
  ? [renderExistingViewDecl(allFields, viewName, `${camelName}View`, {
148
- dialect, columnNamingStrategy, timestampMode, voModule,
158
+ dialect, columnNamingStrategy, timestampMode, voRef,
149
159
  })]
150
160
  : []),
151
161
  code`
152
162
  export const ${projName}Schema = ${renderViewReadZodObject(allFields, {
153
- dialect, columnNamingStrategy, timestampMode, voModule,
163
+ dialect, columnNamingStrategy, timestampMode, voRef,
154
164
  })};
155
165
  `,
156
166
  code`