@metaobjectsdev/codegen-ts 0.14.1 → 0.15.0-rc.1

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 (154) hide show
  1. package/dist/column-mapper.d.ts.map +1 -1
  2. package/dist/column-mapper.js +36 -16
  3. package/dist/column-mapper.js.map +1 -1
  4. package/dist/generators/api-model.js +21 -13
  5. package/dist/generators/api-model.js.map +1 -1
  6. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  7. package/dist/generators/docs-data-builder.js +27 -22
  8. package/dist/generators/docs-data-builder.js.map +1 -1
  9. package/dist/generators/docs-file.d.ts.map +1 -1
  10. package/dist/generators/docs-file.js +2 -1
  11. package/dist/generators/docs-file.js.map +1 -1
  12. package/dist/generators/extractor-file.d.ts.map +1 -1
  13. package/dist/generators/extractor-file.js +4 -2
  14. package/dist/generators/extractor-file.js.map +1 -1
  15. package/dist/generators/output-prompt-file.d.ts.map +1 -1
  16. package/dist/generators/output-prompt-file.js +5 -3
  17. package/dist/generators/output-prompt-file.js.map +1 -1
  18. package/dist/generators/render-helper-file.d.ts.map +1 -1
  19. package/dist/generators/render-helper-file.js +5 -3
  20. package/dist/generators/render-helper-file.js.map +1 -1
  21. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  22. package/dist/generators/routes-file-hono.js +2 -1
  23. package/dist/generators/routes-file-hono.js.map +1 -1
  24. package/dist/generators/routes-file.d.ts.map +1 -1
  25. package/dist/generators/routes-file.js +3 -1
  26. package/dist/generators/routes-file.js.map +1 -1
  27. package/dist/generators/template-doc-builder.d.ts.map +1 -1
  28. package/dist/generators/template-doc-builder.js +14 -11
  29. package/dist/generators/template-doc-builder.js.map +1 -1
  30. package/dist/generators/trace-helper-file.d.ts.map +1 -1
  31. package/dist/generators/trace-helper-file.js +11 -5
  32. package/dist/generators/trace-helper-file.js.map +1 -1
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/naming.d.ts +10 -0
  38. package/dist/naming.d.ts.map +1 -1
  39. package/dist/naming.js +47 -0
  40. package/dist/naming.js.map +1 -1
  41. package/dist/payload-codegen.d.ts.map +1 -1
  42. package/dist/payload-codegen.js +12 -9
  43. package/dist/payload-codegen.js.map +1 -1
  44. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  45. package/dist/projection/extract-view-spec.js +26 -3
  46. package/dist/projection/extract-view-spec.js.map +1 -1
  47. package/dist/projection/projection-detector.d.ts.map +1 -1
  48. package/dist/projection/projection-detector.js +4 -0
  49. package/dist/projection/projection-detector.js.map +1 -1
  50. package/dist/relation-resolver.d.ts.map +1 -1
  51. package/dist/relation-resolver.js +20 -12
  52. package/dist/relation-resolver.js.map +1 -1
  53. package/dist/source-detect.d.ts.map +1 -1
  54. package/dist/source-detect.js +4 -1
  55. package/dist/source-detect.js.map +1 -1
  56. package/dist/template-codegen/template-data.js +1 -1
  57. package/dist/template-codegen/template-data.js.map +1 -1
  58. package/dist/templates/callable-file.d.ts.map +1 -1
  59. package/dist/templates/callable-file.js +8 -4
  60. package/dist/templates/callable-file.js.map +1 -1
  61. package/dist/templates/entity-constants.d.ts.map +1 -1
  62. package/dist/templates/entity-constants.js +15 -11
  63. package/dist/templates/entity-constants.js.map +1 -1
  64. package/dist/templates/entity-file.d.ts.map +1 -1
  65. package/dist/templates/entity-file.js +7 -2
  66. package/dist/templates/entity-file.js.map +1 -1
  67. package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
  68. package/dist/templates/extract-delegate-emitter.js +2 -1
  69. package/dist/templates/extract-delegate-emitter.js.map +1 -1
  70. package/dist/templates/extractor.d.ts.map +1 -1
  71. package/dist/templates/extractor.js +8 -4
  72. package/dist/templates/extractor.js.map +1 -1
  73. package/dist/templates/field-meta.d.ts.map +1 -1
  74. package/dist/templates/field-meta.js +22 -4
  75. package/dist/templates/field-meta.js.map +1 -1
  76. package/dist/templates/find-templates.d.ts.map +1 -1
  77. package/dist/templates/find-templates.js +3 -0
  78. package/dist/templates/find-templates.js.map +1 -1
  79. package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
  80. package/dist/templates/fr010-field-mapping.js +5 -2
  81. package/dist/templates/fr010-field-mapping.js.map +1 -1
  82. package/dist/templates/inferred-types.d.ts.map +1 -1
  83. package/dist/templates/inferred-types.js +18 -14
  84. package/dist/templates/inferred-types.js.map +1 -1
  85. package/dist/templates/output-format-spec-emitter.js +7 -3
  86. package/dist/templates/output-format-spec-emitter.js.map +1 -1
  87. package/dist/templates/output-parser.d.ts.map +1 -1
  88. package/dist/templates/output-parser.js +9 -5
  89. package/dist/templates/output-parser.js.map +1 -1
  90. package/dist/templates/output-prompt.d.ts.map +1 -1
  91. package/dist/templates/output-prompt.js +8 -4
  92. package/dist/templates/output-prompt.js.map +1 -1
  93. package/dist/templates/projection-decl.d.ts.map +1 -1
  94. package/dist/templates/projection-decl.js +4 -1
  95. package/dist/templates/projection-decl.js.map +1 -1
  96. package/dist/templates/queries-file.d.ts.map +1 -1
  97. package/dist/templates/queries-file.js +12 -1
  98. package/dist/templates/queries-file.js.map +1 -1
  99. package/dist/templates/queries.d.ts +19 -1
  100. package/dist/templates/queries.d.ts.map +1 -1
  101. package/dist/templates/queries.js +49 -7
  102. package/dist/templates/queries.js.map +1 -1
  103. package/dist/templates/render-helper.d.ts.map +1 -1
  104. package/dist/templates/render-helper.js +17 -10
  105. package/dist/templates/render-helper.js.map +1 -1
  106. package/dist/templates/routes-file.js +2 -1
  107. package/dist/templates/routes-file.js.map +1 -1
  108. package/dist/templates/tph-discriminator.d.ts.map +1 -1
  109. package/dist/templates/tph-discriminator.js +7 -0
  110. package/dist/templates/tph-discriminator.js.map +1 -1
  111. package/dist/templates/zod-validators.d.ts.map +1 -1
  112. package/dist/templates/zod-validators.js +62 -16
  113. package/dist/templates/zod-validators.js.map +1 -1
  114. package/package.json +6 -6
  115. package/src/column-mapper.ts +37 -16
  116. package/src/generators/api-model.ts +21 -13
  117. package/src/generators/docs-data-builder.ts +27 -22
  118. package/src/generators/docs-file.ts +2 -1
  119. package/src/generators/extractor-file.ts +4 -2
  120. package/src/generators/output-prompt-file.ts +5 -3
  121. package/src/generators/render-helper-file.ts +5 -3
  122. package/src/generators/routes-file-hono.ts +2 -1
  123. package/src/generators/routes-file.ts +2 -1
  124. package/src/generators/template-doc-builder.ts +14 -11
  125. package/src/generators/trace-helper-file.ts +11 -5
  126. package/src/index.ts +2 -0
  127. package/src/naming.ts +51 -0
  128. package/src/payload-codegen.ts +12 -9
  129. package/src/projection/extract-view-spec.ts +26 -3
  130. package/src/projection/projection-detector.ts +4 -0
  131. package/src/reference/queries.ts +9 -0
  132. package/src/reference/routes.ts +2 -1
  133. package/src/relation-resolver.ts +20 -12
  134. package/src/source-detect.ts +4 -1
  135. package/src/template-codegen/template-data.ts +1 -1
  136. package/src/templates/callable-file.ts +8 -4
  137. package/src/templates/entity-constants.ts +15 -11
  138. package/src/templates/entity-file.ts +7 -2
  139. package/src/templates/extract-delegate-emitter.ts +2 -1
  140. package/src/templates/extractor.ts +8 -4
  141. package/src/templates/field-meta.ts +25 -3
  142. package/src/templates/find-templates.ts +3 -0
  143. package/src/templates/fr010-field-mapping.ts +6 -1
  144. package/src/templates/inferred-types.ts +19 -13
  145. package/src/templates/output-format-spec-emitter.ts +7 -3
  146. package/src/templates/output-parser.ts +9 -5
  147. package/src/templates/output-prompt.ts +8 -4
  148. package/src/templates/projection-decl.ts +4 -1
  149. package/src/templates/queries-file.ts +14 -0
  150. package/src/templates/queries.ts +68 -8
  151. package/src/templates/render-helper.ts +17 -10
  152. package/src/templates/routes-file.ts +2 -1
  153. package/src/templates/tph-discriminator.ts +7 -0
  154. package/src/templates/zod-validators.ts +64 -15
@@ -18,6 +18,8 @@ import {
18
18
  FIELD_SUBTYPE_MAP,
19
19
  FIELD_SUBTYPE_ENUM,
20
20
  FIELD_SUBTYPE_UUID,
21
+ FIELD_SUBTYPE_URI,
22
+ FIELD_SUBTYPE_INET,
21
23
  VALIDATOR_SUBTYPE_REQUIRED,
22
24
  VALIDATOR_SUBTYPE_LENGTH,
23
25
  FIELD_ATTR_MAX_LENGTH,
@@ -34,7 +36,7 @@ import {
34
36
  FIELD_ATTR_DB_COLUMN_TYPE,
35
37
  DB_COLUMN_TYPE_UUID,
36
38
  DB_COLUMN_TYPE_JSONB,
37
- DB_COLUMN_TYPE_TIMESTAMP_WITH_TZ,
39
+ FIELD_ATTR_LOCAL_TIME,
38
40
  } from "@metaobjectsdev/metadata";
39
41
  import { columnNameFromField } from "./naming.js";
40
42
  import { enumValues } from "./enum-meta.js";
@@ -160,19 +162,22 @@ export interface ColumnSpec {
160
162
  * migrate-ts/src/expected-schema.ts (override checked first, wins over the
161
163
  * subtype default), so codegen and DDL agree.
162
164
  *
163
- * Postgres-only: uuid/jsonb/timestamptz are Postgres physical column types with
164
- * no native SQLite analogue, so on SQLite the attribute is ignored (the caller
165
- * falls through to the subtype default). The native TS binding is keyed on
165
+ * Postgres-only: uuid/jsonb are Postgres physical column types with no native
166
+ * SQLite analogue, so on SQLite the attribute is ignored (the caller falls
167
+ * through to the subtype default). The native TS binding is keyed on
166
168
  * field.subType elsewhere and is unaffected — a `field.string @dbColumnType:uuid`
167
169
  * field stays a TS `string`; only the Drizzle column function changes.
168
170
  *
171
+ * ADR-0036 Wave 2: the `timestamp_with_tz` value is retired — `field.timestamp`
172
+ * is now instant/tz-aware BY DEFAULT (handled in the subtype switch below), and
173
+ * the naive opt-out is the `@localTime` boolean, not a physical override.
174
+ *
169
175
  * The loader has already validated the (subtype × value) pairing, so an
170
176
  * unrecognized value never reaches here; an unknown value returns undefined
171
177
  * (fall through to subtype default).
172
178
  */
173
179
  function pgColumnTypeOverride(
174
180
  field: MetaField,
175
- timestampMode: "date" | "string" = "string",
176
181
  ): { fnName: string; fnOptions?: Record<string, unknown> } | undefined {
177
182
  const dbColumnType = field.attr(FIELD_ATTR_DB_COLUMN_TYPE);
178
183
  if (typeof dbColumnType !== "string") return undefined;
@@ -181,12 +186,6 @@ function pgColumnTypeOverride(
181
186
  return { fnName: "uuid" };
182
187
  case DB_COLUMN_TYPE_JSONB:
183
188
  return { fnName: "jsonb" };
184
- case DB_COLUMN_TYPE_TIMESTAMP_WITH_TZ:
185
- // Drizzle pg-core: timestamp(col, { withTimezone: true }) → timestamptz.
186
- // mode defaults to "string" (ISO-8601 wire contract, matching the generated
187
- // Zod); a consumer can opt into "date" (drizzle's native mode) via
188
- // codegen.timestampMode when its hand-written code works with JS Dates.
189
- return { fnName: "timestamp", fnOptions: { mode: timestampMode, withTimezone: true } };
190
189
  default:
191
190
  return undefined;
192
191
  }
@@ -199,7 +198,8 @@ function getMaxLength(field: MetaField): number | undefined {
199
198
  if (typeof lenAttr === "number") return lenAttr;
200
199
  for (const child of field.validators()) {
201
200
  if (child.subType === VALIDATOR_SUBTYPE_LENGTH) {
202
- const max = child.ownAttr(VALIDATOR_ATTR_MAX);
201
+ // ADR-0039: resolving — a validator may inherit @max via extends.
202
+ const max = child.attr(VALIDATOR_ATTR_MAX);
203
203
  if (typeof max === "number") return max;
204
204
  }
205
205
  }
@@ -231,7 +231,7 @@ export function mapColumnType(
231
231
  const dbName = field.column ?? columnNameFromField(field.name, strategy);
232
232
  const importModule = dialect === "sqlite" ? "drizzle-orm/sqlite-core" : "drizzle-orm/pg-core";
233
233
  const subType = field.subType;
234
- const isArray = field.isArray;
234
+ const isArray = field.resolvedIsArray();
235
235
 
236
236
  let fnName: string;
237
237
  let fnOptions: Record<string, unknown> | undefined;
@@ -279,7 +279,9 @@ export function mapColumnType(
279
279
  case FIELD_SUBTYPE_STRING:
280
280
  case FIELD_SUBTYPE_ENUM:
281
281
  case FIELD_SUBTYPE_UUID:
282
- // SQLite has no native uuid type; store as TEXT (string native binding).
282
+ case FIELD_SUBTYPE_URI:
283
+ case FIELD_SUBTYPE_INET:
284
+ // SQLite has no native uuid/inet type; store as TEXT (string native binding).
283
285
  fnName = "text";
284
286
  break;
285
287
  default:
@@ -291,7 +293,7 @@ export function mapColumnType(
291
293
  // A physical @dbColumnType override wins over the subtype default (Postgres
292
294
  // only; SQLite has no native analogue and falls through above). Resolved
293
295
  // first so the override-precedence matches migrate-ts's expected-schema.
294
- const override = pgColumnTypeOverride(field, timestampMode);
296
+ const override = pgColumnTypeOverride(field);
295
297
  if (override !== undefined) {
296
298
  // Override fully determines the physical type; skip the subtype switch.
297
299
  fnName = override.fnName;
@@ -322,6 +324,11 @@ export function mapColumnType(
322
324
  fnName = "time";
323
325
  break;
324
326
  case FIELD_SUBTYPE_TIMESTAMP:
327
+ // ADR-0036 Wave 2: field.timestamp is instant / tz-aware BY DEFAULT →
328
+ // timestamp({ withTimezone: true }) = timestamptz. A naive wall-clock
329
+ // value opts out with @localTime:true → timestamp({ withTimezone:
330
+ // false }) = `timestamp without time zone`.
331
+ //
325
332
  // mode:"string" so the column round-trips ISO-8601 strings — the
326
333
  // generated Zod schema validates timestamp fields as z.string() and
327
334
  // the cross-port wire format carries timestamps as JSON strings.
@@ -330,12 +337,26 @@ export function mapColumnType(
330
337
  // inconsistent with the string-typed schema + wire contract and
331
338
  // throws on a string write. See SP-B api-contract-generated lane.
332
339
  fnName = "timestamp";
333
- fnOptions = { mode: timestampMode };
340
+ fnOptions = {
341
+ mode: timestampMode,
342
+ withTimezone: field.attr(FIELD_ATTR_LOCAL_TIME) !== true,
343
+ };
334
344
  break;
335
345
  case FIELD_SUBTYPE_UUID:
336
346
  // Postgres native uuid column; native TS binding stays `string`.
337
347
  fnName = "uuid";
338
348
  break;
349
+ case FIELD_SUBTYPE_URI:
350
+ // ADR-0036/0037 Wave 3: Postgres has no uri type → text. Native TS
351
+ // binding stays `string` (validated as a URL via Zod .url()).
352
+ fnName = "text";
353
+ break;
354
+ case FIELD_SUBTYPE_INET:
355
+ // ADR-0036/0037 Wave 3: Postgres-native `inet` column (Drizzle's
356
+ // pg-core `inet()` infers as `string`). Native TS binding stays
357
+ // `string` (validated as an IP via Zod .ip()).
358
+ fnName = "inet";
359
+ break;
339
360
  case FIELD_SUBTYPE_DECIMAL: {
340
361
  // Drizzle pg `numeric` infers as a TS `string` (precision-exact); the
341
362
  // native TS binding for field.decimal is `string` to match. Read the
@@ -41,7 +41,7 @@
41
41
  // render handle promptRender() emits into a single
42
42
  // aggregated `prompts.ts` (payload-codegen generateRenderHandle).
43
43
  // ONLY for TOP-LEVEL template.prompt nodes (matching
44
- // prompt-render-file.ts's `ctx.loadedRoot.ownChildren()`).
44
+ // prompt-render-file.ts's top-level template collection).
45
45
  //
46
46
  // DEFERRALS (tracked follow-ups — NOT documented by this builder yet, stated here
47
47
  // so the gap is known + intentional):
@@ -353,7 +353,8 @@ function isQueryable(obj: MetaObject): boolean {
353
353
  * generators which always emit. So REST symbols are gated separately from the
354
354
  * other queryable kinds. */
355
355
  function emitsRoutes(obj: MetaObject): boolean {
356
- return obj.ownAttr(CODEGEN_ATTR_EMIT_ROUTES) !== false;
356
+ // ADR-0039: resolving — a concrete entity may inherit @emitRoutes via extends.
357
+ return obj.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false;
357
358
  }
358
359
 
359
360
  function buildEntityUnit(
@@ -750,13 +751,16 @@ function callableSymbol(
750
751
  * undefined for a zero-arg proc. Mirrors the callable template's resolution
751
752
  * (the source child's SOURCE_ATTR_PARAMETER_REF). */
752
753
  function callableArgsRef(obj: MetaObject, root: MetaRoot): string | undefined {
753
- for (const child of obj.ownChildren()) {
754
+ // ADR-0039: resolving a callable entity's source (and its @parameterRef) may
755
+ // be inherited via extends.
756
+ for (const child of obj.children()) {
754
757
  if (child.type !== TYPE_SOURCE) continue;
755
- const ref = child.ownAttr(SOURCE_ATTR_PARAMETER_REF);
758
+ const ref = child.attr(SOURCE_ATTR_PARAMETER_REF);
756
759
  if (typeof ref === "string" && ref !== "") {
757
760
  // Only count it when it resolves to a value object (the template's guard).
761
+ // ADR-0039: resolving — root has no super (children()==ownChildren()).
758
762
  const vo = root
759
- .ownChildren()
763
+ .children()
760
764
  .find((c) => c.subType === OBJECT_SUBTYPE_VALUE && refMatchesObject(c, ref));
761
765
  if (vo !== undefined) return ref;
762
766
  }
@@ -806,8 +810,9 @@ function restHonoSymbols(obj: MetaObject, layout: OutputLayout): ApiSymbol[] {
806
810
  // ---------------------------------------------------------------------------
807
811
 
808
812
  function templateOutputs(root: MetaRoot): MetaData[] {
813
+ // ADR-0039: resolving — root has no super (children()==ownChildren()).
809
814
  return root
810
- .ownChildren()
815
+ .children()
811
816
  .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_OUTPUT);
812
817
  }
813
818
 
@@ -819,10 +824,11 @@ function buildTemplateUnit(tmpl: MetaData, root: MetaRoot, _layout: OutputLayout
819
824
  const extractorMod = templateModulePath(`${name}.extractor`);
820
825
  const renderMod = templateModulePath(`${name}.render`);
821
826
 
822
- const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
827
+ // ADR-0039: resolving — a template may inherit @payloadRef/@format/@kind via extends.
828
+ const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
823
829
  const payload = typeof payloadRef === "string" ? payloadRef : undefined;
824
- const format = ((tmpl.ownAttr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
825
- const kind = ((tmpl.ownAttr(TEMPLATE_ATTR_KIND) as string | undefined) ?? TEMPLATE_KIND_DEFAULT).toLowerCase();
830
+ const format = ((tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
831
+ const kind = ((tmpl.attr(TEMPLATE_ATTR_KIND) as string | undefined) ?? TEMPLATE_KIND_DEFAULT).toLowerCase();
826
832
 
827
833
  // --- extractor: only json/xml output-parsers expose the extract API (matches
828
834
  // extractor-file.ts's `if (format !== "json" && format !== "xml") continue`). ---
@@ -893,13 +899,14 @@ function buildTemplateUnit(tmpl: MetaData, root: MetaRoot, _layout: OutputLayout
893
899
  // template.prompt nodes — the prompt-render handle.
894
900
  // ---------------------------------------------------------------------------
895
901
 
896
- /** TOP-LEVEL template.prompt nodes — matching the promptRender generator's own
897
- * collection (`ctx.loadedRoot.ownChildren()` filtered to TYPE_TEMPLATE +
902
+ /** TOP-LEVEL template.prompt nodes — matching the promptRender generator's
903
+ * top-level collection (root children filtered to TYPE_TEMPLATE +
898
904
  * TEMPLATE_SUBTYPE_PROMPT). A prompt nested INSIDE an entity is not collected by
899
905
  * the generator, so the builder must not document it either (no over-doc). */
900
906
  function templatePrompts(root: MetaRoot): MetaData[] {
907
+ // ADR-0039: resolving — root has no super (children()==ownChildren()).
901
908
  return root
902
- .ownChildren()
909
+ .children()
903
910
  .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_PROMPT);
904
911
  }
905
912
 
@@ -919,7 +926,8 @@ function buildPromptUnit(tmpl: MetaData, root: MetaRoot): ApiUnitDoc {
919
926
  // promptRender writes the aggregated handles to `outFile` (default "prompts.ts").
920
927
  const promptsMod = templateModulePath("prompts");
921
928
 
922
- const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
929
+ // ADR-0039: resolving — a template may inherit @payloadRef via extends.
930
+ const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
923
931
  const payload = typeof payloadRef === "string" ? payloadRef : undefined;
924
932
 
925
933
  if (payload) {
@@ -108,20 +108,21 @@ function collectValidatorParts(field: MetaField): ValidatorParts {
108
108
  const regexParts: string[] = [];
109
109
  const lengthParts: string[] = [];
110
110
  const numericParts: string[] = [];
111
+ // ADR-0039: resolving — a validator may inherit @min/@max/@pattern via extends.
111
112
  for (const v of field.validators()) {
112
113
  if (v.subType === VALIDATOR_SUBTYPE_REGEX) {
113
- const pattern = v.ownAttr(VALIDATOR_ATTR_PATTERN);
114
+ const pattern = v.attr(VALIDATOR_ATTR_PATTERN);
114
115
  if (typeof pattern === "string" && pattern.length > 0) {
115
116
  regexParts.push(`pattern \`${pattern}\``);
116
117
  }
117
118
  } else if (v.subType === VALIDATOR_SUBTYPE_LENGTH) {
118
- const min = v.ownAttr(VALIDATOR_ATTR_MIN);
119
- const max = v.ownAttr(VALIDATOR_ATTR_MAX);
119
+ const min = v.attr(VALIDATOR_ATTR_MIN);
120
+ const max = v.attr(VALIDATOR_ATTR_MAX);
120
121
  if (typeof min === "number") lengthParts.push(`minLength: ${min}`);
121
122
  if (typeof max === "number" && typeof maxLenAttr !== "number") lengthParts.push(`maxLength: ${max}`);
122
123
  } else if (v.subType === VALIDATOR_SUBTYPE_NUMERIC) {
123
- const min = v.ownAttr(VALIDATOR_ATTR_MIN);
124
- const max = v.ownAttr(VALIDATOR_ATTR_MAX);
124
+ const min = v.attr(VALIDATOR_ATTR_MIN);
125
+ const max = v.attr(VALIDATOR_ATTR_MAX);
125
126
  if (typeof min === "number") numericParts.push(`min: ${min}`);
126
127
  if (typeof max === "number") numericParts.push(`max: ${max}`);
127
128
  }
@@ -148,7 +149,7 @@ export function neutralTypeStr(field: MetaField): string {
148
149
  } else {
149
150
  base = field.subType;
150
151
  }
151
- if (field.isArray) base = `${base}[]`;
152
+ if (field.resolvedIsArray()) base = `${base}[]`;
152
153
  return base;
153
154
  }
154
155
 
@@ -160,7 +161,7 @@ function neutralTypeCell(field: MetaField): string {
160
161
 
161
162
  /** Neutral PHYSICAL type cell for the Storage table. Metadata-driven, no DDL
162
163
  * re-derivation (ADR-0020): if the field declares a `@dbColumnType` physical
163
- * override (e.g. `uuid`, `jsonb`, `timestamp_with_tz`) show it UPPERCASED;
164
+ * override (e.g. `uuid`, `jsonb`) show it UPPERCASED;
164
165
  * otherwise fall back to the same neutral LOGICAL type the Constraints table
165
166
  * uses. Deliberately does NOT derive ANSI/ORM SQL so it can't drift vs the
166
167
  * migrate engine or re-introduce language-specific DDL. Wrapped in backticks. */
@@ -192,7 +193,7 @@ function buildConstraintRow(
192
193
  if (isPk) rules.push("primary key");
193
194
  if (field.attr(FIELD_ATTR_UNIQUE) === true) rules.push("unique");
194
195
 
195
- if (field.subType === FIELD_SUBTYPE_ENUM && !field.isArray) {
196
+ if (field.subType === FIELD_SUBTYPE_ENUM && !field.resolvedIsArray()) {
196
197
  const values = enumValues(field);
197
198
  if (values !== undefined && values.length > 0) {
198
199
  const list = values.map((v) => `\`${v}\``).join(", ");
@@ -289,7 +290,7 @@ function buildFieldRow(
289
290
  const rules: string[] = [];
290
291
  if (field.attr(FIELD_ATTR_UNIQUE) === true) rules.push("unique");
291
292
 
292
- if (field.subType === FIELD_SUBTYPE_ENUM && !field.isArray) {
293
+ if (field.subType === FIELD_SUBTYPE_ENUM && !field.resolvedIsArray()) {
293
294
  const values = enumValues(field);
294
295
  if (values !== undefined && values.length > 0) {
295
296
  const list = values.map((v) => `\`${v}\``).join(", ");
@@ -344,7 +345,7 @@ function buildFieldDetail(
344
345
  const columnName = field.column;
345
346
  const dbColumnType = field.attr(FIELD_ATTR_DB_COLUMN_TYPE);
346
347
  const isUnique = field.attr(FIELD_ATTR_UNIQUE) === true;
347
- const isEnum = field.subType === FIELD_SUBTYPE_ENUM && !field.isArray;
348
+ const isEnum = field.subType === FIELD_SUBTYPE_ENUM && !field.resolvedIsArray();
348
349
  const enumVals = isEnum ? enumValues(field) : undefined;
349
350
  const validators = field.validators();
350
351
  const hasValidatorChildren = validators.some(
@@ -423,22 +424,23 @@ function buildFieldDetail(
423
424
  // Validators — one bullet per validator subtype (regex / length / numeric),
424
425
  // rendered in declaration order so authors can rely on the order they
425
426
  // wrote.
427
+ // ADR-0039: resolving — a validator may inherit @min/@max/@pattern via extends.
426
428
  for (const v of validators) {
427
429
  if (v.subType === VALIDATOR_SUBTYPE_REGEX) {
428
- const pattern = v.ownAttr(VALIDATOR_ATTR_PATTERN);
430
+ const pattern = v.attr(VALIDATOR_ATTR_PATTERN);
429
431
  if (typeof pattern === "string" && pattern.length > 0) {
430
432
  bullets.push(`**Validator (regex):** pattern \`${pattern}\``);
431
433
  }
432
434
  } else if (v.subType === VALIDATOR_SUBTYPE_LENGTH) {
433
- const min = v.ownAttr(VALIDATOR_ATTR_MIN);
434
- const max = v.ownAttr(VALIDATOR_ATTR_MAX);
435
+ const min = v.attr(VALIDATOR_ATTR_MIN);
436
+ const max = v.attr(VALIDATOR_ATTR_MAX);
435
437
  const fragments: string[] = [];
436
438
  if (typeof min === "number") fragments.push(`min ${min}`);
437
439
  if (typeof max === "number") fragments.push(`max ${max}`);
438
440
  if (fragments.length > 0) bullets.push(`**Validator (length):** ${fragments.join(", ")}`);
439
441
  } else if (v.subType === VALIDATOR_SUBTYPE_NUMERIC) {
440
- const min = v.ownAttr(VALIDATOR_ATTR_MIN);
441
- const max = v.ownAttr(VALIDATOR_ATTR_MAX);
442
+ const min = v.attr(VALIDATOR_ATTR_MIN);
443
+ const max = v.attr(VALIDATOR_ATTR_MAX);
442
444
  const fragments: string[] = [];
443
445
  if (typeof min === "number") fragments.push(`min ${min}`);
444
446
  if (typeof max === "number") fragments.push(`max ${max}`);
@@ -524,9 +526,10 @@ function describeIdentity(id: MetaIdentity): string {
524
526
  }
525
527
 
526
528
  function relationshipBullet(r: ReturnType<MetaObject["relationships"]>[number]): string {
527
- const cardinality = r.ownAttr(RELATIONSHIP_ATTR_CARDINALITY);
529
+ // ADR-0039: resolving — a relationship may inherit its attrs via extends.
530
+ const cardinality = r.attr(RELATIONSHIP_ATTR_CARDINALITY);
528
531
  const card = typeof cardinality === "string" ? cardinality : "?";
529
- const targetRaw = r.ownAttr(RELATIONSHIP_ATTR_OBJECT_REF);
532
+ const targetRaw = r.attr(RELATIONSHIP_ATTR_OBJECT_REF);
530
533
  const target = typeof targetRaw === "string" ? stripPackage(targetRaw) : "?";
531
534
  const subtype = r.subType;
532
535
  let label: string;
@@ -542,14 +545,14 @@ function relationshipBullet(r: ReturnType<MetaObject["relationships"]>[number]):
542
545
  // symmetric (undirected) → "symmetric self-join"
543
546
  // @sourceRefField set (directed) → "directed self-join via `<field>`"
544
547
  // The junction/disambiguator are DECLARED facts (ADR-0020 — no re-derivation).
545
- const throughRaw = r.ownAttr(RELATIONSHIP_ATTR_THROUGH);
548
+ const throughRaw = r.attr(RELATIONSHIP_ATTR_THROUGH);
546
549
  if (typeof throughRaw === "string" && throughRaw.length > 0) {
547
550
  const through = stripPackage(throughRaw);
548
551
  const noteParts = [`${label}, through \`${through}\``];
549
- if (r.ownAttr(RELATIONSHIP_ATTR_SYMMETRIC) === true) {
552
+ if (r.attr(RELATIONSHIP_ATTR_SYMMETRIC) === true) {
550
553
  noteParts.push("symmetric self-join");
551
554
  } else {
552
- const srcRef = r.ownAttr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
555
+ const srcRef = r.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD);
553
556
  if (typeof srcRef === "string" && srcRef.length > 0) {
554
557
  noteParts.push(`directed self-join via \`${srcRef}\``);
555
558
  }
@@ -664,9 +667,11 @@ export function buildEntityDocData(
664
667
 
665
668
  // ---- UsedBy
666
669
  const usedByMatches: UsedByDoc[] = [];
667
- for (const child of root.ownChildren()) {
670
+ // ADR-0039: resolving root has no super (children()==ownChildren()), and a
671
+ // template may inherit @payloadRef via extends; resolve rather than work-by-accident.
672
+ for (const child of root.children()) {
668
673
  if (child.type !== TYPE_TEMPLATE) continue;
669
- const ref = child.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
674
+ const ref = child.attr(TEMPLATE_ATTR_PAYLOAD_REF);
670
675
  if (typeof ref !== "string") continue;
671
676
  if (stripPackage(ref) !== entity.name) continue;
672
677
  // Link to the template's own doc page. The href is derived from the SAME
@@ -135,7 +135,8 @@ export const docsFile = function docsFile(opts?: DocsFileOpts): Generator {
135
135
  // ALSO emit one NEUTRAL render-contract page per `template.output` node —
136
136
  // a sibling artifact, distinct from the entity page. Raw node name → file
137
137
  // (`<name>.md`), agreeing with the entity Used-by back-link target.
138
- for (const child of ctx.loadedRoot.ownChildren()) {
138
+ // ADR-0039: resolving root has no super (children()==ownChildren()).
139
+ for (const child of ctx.loadedRoot.children()) {
139
140
  if (child.type !== TYPE_TEMPLATE || child.subType !== TEMPLATE_SUBTYPE_OUTPUT) continue;
140
141
  const node = docPageNode(child);
141
142
  templateNodes.push(node);
@@ -35,13 +35,15 @@ export const extractor = function extractor(opts?: ExtractorOpts): Generator {
35
35
  const generator: Generator = {
36
36
  name: "extractor",
37
37
  generate: oncePerRun((_entities, ctx) => {
38
+ // ADR-0039: resolving — root has no super (children()==ownChildren()).
38
39
  const outputs = ctx.loadedRoot
39
- .ownChildren()
40
+ .children()
40
41
  .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_OUTPUT);
41
42
  const files: EmittedFile[] = [];
42
43
  for (const t of outputs) {
43
44
  // The extract tier requires the extract API, which only json/xml output-parsers emit.
44
- const format = ((t.ownAttr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
45
+ // ADR-0039: resolving a template may inherit @format via extends.
46
+ const format = ((t.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
45
47
  if (format !== "json" && format !== "xml") continue;
46
48
  files.push({
47
49
  path: `${dirPrefix}${t.name}.extractor.ts`,
@@ -40,17 +40,19 @@ export const outputPrompt = function outputPrompt(opts?: OutputPromptOpts): Gene
40
40
  name: "output-prompt",
41
41
  generate: oncePerRun((_entities, ctx) => {
42
42
  const root = ctx.loadedRoot;
43
+ // ADR-0039: resolving — root has no super (children()==ownChildren()).
43
44
  const outputs = root
44
- .ownChildren()
45
+ .children()
45
46
  .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_OUTPUT);
46
47
  const files: EmittedFile[] = [];
47
48
  for (const t of outputs) {
48
49
  // Only json/xml outputs get a renderable prompt fragment.
49
50
  if (!templateSupportsPrompt(t)) continue;
50
51
  // @payloadRef must resolve to a value-object (same contract as the parser).
51
- const payloadRef = t.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
52
+ // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
53
+ const payloadRef = t.attr(TEMPLATE_ATTR_PAYLOAD_REF);
52
54
  if (typeof payloadRef !== "string") continue;
53
- const vo = root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, payloadRef));
55
+ const vo = root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, payloadRef));
54
56
  if (!vo) continue;
55
57
  files.push({
56
58
  path: `${dirPrefix}${t.name}.prompt.ts`,
@@ -48,15 +48,17 @@ export const renderHelper = function renderHelper(opts?: RenderHelperOpts): Gene
48
48
  // framework defaults; used to resolve + verify each referenced mustache so
49
49
  // the build-time drift gate runs against the same texts render() will see.
50
50
  const provider = projectProvider(ctx.projectRoot);
51
+ // ADR-0039: resolving — root has no super (children()==ownChildren()).
51
52
  const outputs = root
52
- .ownChildren()
53
+ .children()
53
54
  .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_OUTPUT);
54
55
  const files: EmittedFile[] = [];
55
56
  for (const t of outputs) {
56
57
  // @payloadRef must resolve to a value-object (same contract as the parser).
57
- const payloadRef = t.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
58
+ // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
59
+ const payloadRef = t.attr(TEMPLATE_ATTR_PAYLOAD_REF);
58
60
  if (typeof payloadRef !== "string") continue;
59
- const vo = root.ownChildren().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, payloadRef));
61
+ const vo = root.children().find((c) => c.type === TYPE_OBJECT && refMatchesObject(c, payloadRef));
60
62
  if (!vo) continue;
61
63
  files.push({
62
64
  // renderRenderHelper THROWS (fails codegen) on a mustache↔VO drift —
@@ -30,7 +30,8 @@ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts)
30
30
  // Marks this as the Hono routes generator so the runner can aggregate
31
31
  // `ctx.config.includeHonoRoutes` and api-docs auto-documents the Hono surface.
32
32
  emitsHonoRoutes: true,
33
- filter: (e: MetaObject) => e.ownAttr(CODEGEN_ATTR_EMIT_ROUTES) !== false && userFilter(e),
33
+ // ADR-0039: resolving a concrete entity may inherit @emitRoutes via extends.
34
+ filter: (e: MetaObject) => e.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false && userFilter(e),
34
35
  generate: perEntity(async (entity, ctx) => {
35
36
  if (!ctx.renderContext) {
36
37
  throw new Error("routes-file-hono: renderContext is required (provided by runGen)");
@@ -24,7 +24,8 @@ export const routesFile = function routesFile(opts?: RoutesFileOpts): Generator
24
24
  name: "routes-file",
25
25
  // Always set: AND-composes metadata opt-out with optional user filter.
26
26
  filter: (e: MetaObject) =>
27
- e.ownAttr(CODEGEN_ATTR_EMIT_ROUTES) !== false && !isTphSubtype(e) && userFilter(e),
27
+ // ADR-0039: resolving a concrete entity may inherit its @emit* opt-out flag via extends.
28
+ e.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false && !isTphSubtype(e) && userFilter(e),
28
29
  generate: perEntity(async (entity, ctx) => {
29
30
  if (!ctx.renderContext) {
30
31
  throw new Error("routes-file: renderContext is required (provided by runGen)");
@@ -107,28 +107,29 @@ export function buildTemplateDocData(
107
107
  ): TemplateDocData {
108
108
  const layout = opts?.layout ?? "flat";
109
109
  const root = opts?.loadedRoot;
110
- const kindRaw = ((template.ownAttr(TEMPLATE_ATTR_KIND) as string | undefined) ??
110
+ // ADR-0039: resolving — a template may inherit its @kind/@*Ref/@format/etc via extends.
111
+ const kindRaw = ((template.attr(TEMPLATE_ATTR_KIND) as string | undefined) ??
111
112
  TEMPLATE_KIND_DEFAULT).toLowerCase();
112
113
  const isEmail = kindRaw === TEMPLATE_KIND_EMAIL;
113
114
  const kind: "document" | "email" = isEmail ? TEMPLATE_KIND_EMAIL : TEMPLATE_KIND_DOCUMENT;
114
115
 
115
- const payloadRefRaw = template.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
116
+ const payloadRefRaw = template.attr(TEMPLATE_ATTR_PAYLOAD_REF);
116
117
  const payloadName =
117
118
  typeof payloadRefRaw === "string" && payloadRefRaw.length > 0
118
119
  ? stripPackage(payloadRefRaw)
119
120
  : "unknown";
120
121
 
121
- const requiredTags = attrStringList(template.ownAttr(TEMPLATE_ATTR_REQUIRED_TAGS));
122
- const maxChars = readMaxChars(template.ownAttr(TEMPLATE_ATTR_MAX_CHARS));
122
+ const requiredTags = attrStringList(template.attr(TEMPLATE_ATTR_REQUIRED_TAGS));
123
+ const maxChars = readMaxChars(template.attr(TEMPLATE_ATTR_MAX_CHARS));
123
124
 
124
125
  let format = "";
125
126
  let parts: TemplateOutputPart[] | undefined;
126
127
  const sourceRefs: string[] = [];
127
128
 
128
129
  if (isEmail) {
129
- const subjectRef = template.ownAttr(TEMPLATE_ATTR_SUBJECT_REF);
130
- const htmlBodyRef = template.ownAttr(TEMPLATE_ATTR_HTML_BODY_REF);
131
- const textBodyRef = template.ownAttr(TEMPLATE_ATTR_TEXT_BODY_REF);
130
+ const subjectRef = template.attr(TEMPLATE_ATTR_SUBJECT_REF);
131
+ const htmlBodyRef = template.attr(TEMPLATE_ATTR_HTML_BODY_REF);
132
+ const textBodyRef = template.attr(TEMPLATE_ATTR_TEXT_BODY_REF);
132
133
  parts = [];
133
134
  if (typeof subjectRef === "string") {
134
135
  parts.push({ label: "Subject", ref: subjectRef, format: "text", escaped: false });
@@ -143,8 +144,8 @@ export function buildTemplateDocData(
143
144
  sourceRefs.push(textBodyRef);
144
145
  }
145
146
  } else {
146
- format = ((template.ownAttr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
147
- const textRef = template.ownAttr(TEMPLATE_ATTR_TEXT_REF);
147
+ format = ((template.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
148
+ const textRef = template.attr(TEMPLATE_ATTR_TEXT_REF);
148
149
  if (typeof textRef === "string") sourceRefs.push(textRef);
149
150
  }
150
151
 
@@ -285,7 +286,8 @@ function makePartialHrefResolver(
285
286
  ): (ref: string) => string | undefined {
286
287
  // Map each documented source ref → the template node that documents it.
287
288
  const refToTemplate = new Map<string, MetaData>();
288
- for (const child of root.ownChildren()) {
289
+ // ADR-0039: resolving root has no super (children()==ownChildren()).
290
+ for (const child of root.children()) {
289
291
  if (child.type !== TYPE_TEMPLATE || child.subType !== TEMPLATE_SUBTYPE_OUTPUT) continue;
290
292
  for (const attr of [
291
293
  TEMPLATE_ATTR_TEXT_REF,
@@ -293,7 +295,8 @@ function makePartialHrefResolver(
293
295
  TEMPLATE_ATTR_HTML_BODY_REF,
294
296
  TEMPLATE_ATTR_TEXT_BODY_REF,
295
297
  ]) {
296
- const v = child.ownAttr(attr);
298
+ // ADR-0039: resolving — a template may inherit these refs via extends.
299
+ const v = child.attr(attr);
297
300
  if (typeof v === "string" && v.length > 0 && !refToTemplate.has(v)) {
298
301
  refToTemplate.set(v, child);
299
302
  }
@@ -72,13 +72,17 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
72
72
  if (!extendsBase(entity)) return [];
73
73
 
74
74
  // Find the nested template.prompt.
75
- const prompt = entity.ownChildren().find(
75
+ // ADR-0039: resolving — a concrete trace entity may inherit its
76
+ // template.prompt from an abstract base (it extendsBase); own-only would
77
+ // miss the inherited prompt.
78
+ const prompt = entity.children().find(
76
79
  (c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_PROMPT,
77
80
  );
78
81
  if (prompt === undefined) return [];
79
82
 
80
- const payloadRef = prompt.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
81
- const responseRef = prompt.ownAttr(TEMPLATE_ATTR_RESPONSE_REF);
83
+ // ADR-0039: resolving — a prompt may inherit @payloadRef/@responseRef via extends.
84
+ const payloadRef = prompt.attr(TEMPLATE_ATTR_PAYLOAD_REF);
85
+ const responseRef = prompt.attr(TEMPLATE_ATTR_RESPONSE_REF);
82
86
 
83
87
  // @responseRef types the result; @payloadRef types the request. Both gate
84
88
  // the helper: the entity must declare voRequest/voResponse field.object
@@ -111,7 +115,8 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
111
115
 
112
116
  // Derive the parse format from the prompt's @format attr.
113
117
  // "xml" → Format.XML; absent or any other value → Format.JSON.
114
- const promptFormat = prompt.ownAttr(TEMPLATE_ATTR_FORMAT);
118
+ // ADR-0039: resolving — a prompt may inherit @format via extends.
119
+ const promptFormat = prompt.attr(TEMPLATE_ATTR_FORMAT);
115
120
  const formatLiteral = typeof promptFormat === "string" && promptFormat.toLowerCase() === "xml"
116
121
  ? "Format.XML"
117
122
  : "Format.JSON";
@@ -124,7 +129,8 @@ export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts):
124
129
 
125
130
  // A renderable prompt (carries @textRef) gets an additional call<Entity> helper
126
131
  // that renders the prompt text, calls the LLM, then parses + persists a trace row.
127
- const textRef = prompt.ownAttr(TEMPLATE_ATTR_TEXT_REF);
132
+ // ADR-0039: resolving — a prompt may inherit @textRef via extends.
133
+ const textRef = prompt.attr(TEMPLATE_ATTR_TEXT_REF);
128
134
  const renderable = typeof textRef === "string";
129
135
  // Same @format attr, two intentionally different shapes: extract() takes the
130
136
  // Format enum (formatLiteral, above → Format.XML/Format.JSON), render() takes the
package/src/index.ts CHANGED
@@ -108,6 +108,8 @@ export {
108
108
  renderCreateFn,
109
109
  renderUpdateFn,
110
110
  renderDeleteByIdFn,
111
+ renderReverseFinderFns,
112
+ reverseFksFor,
111
113
  getPkInfo,
112
114
  } from "./templates/queries.js";
113
115