@metaobjectsdev/codegen-ts 1.0.0-rc.3 → 1.0.0-rc.5

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 (138) hide show
  1. package/dist/column-mapper.d.ts +12 -0
  2. package/dist/column-mapper.d.ts.map +1 -1
  3. package/dist/column-mapper.js +16 -0
  4. package/dist/column-mapper.js.map +1 -1
  5. package/dist/generator-registry.d.ts.map +1 -1
  6. package/dist/generator-registry.js +1 -0
  7. package/dist/generator-registry.js.map +1 -1
  8. package/dist/generators/agent-ui-page.d.ts.map +1 -1
  9. package/dist/generators/agent-ui-page.js +11 -9
  10. package/dist/generators/agent-ui-page.js.map +1 -1
  11. package/dist/generators/api-model.d.ts +2 -1
  12. package/dist/generators/api-model.d.ts.map +1 -1
  13. package/dist/generators/api-model.js +12 -6
  14. package/dist/generators/api-model.js.map +1 -1
  15. package/dist/generators/barrel.d.ts.map +1 -1
  16. package/dist/generators/barrel.js +2 -1
  17. package/dist/generators/barrel.js.map +1 -1
  18. package/dist/generators/callable-file.d.ts.map +1 -1
  19. package/dist/generators/callable-file.js +2 -1
  20. package/dist/generators/callable-file.js.map +1 -1
  21. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  22. package/dist/generators/docs-data-builder.js +6 -5
  23. package/dist/generators/docs-data-builder.js.map +1 -1
  24. package/dist/generators/entity-file.d.ts.map +1 -1
  25. package/dist/generators/entity-file.js +2 -1
  26. package/dist/generators/entity-file.js.map +1 -1
  27. package/dist/generators/names-file.d.ts +22 -1
  28. package/dist/generators/names-file.d.ts.map +1 -1
  29. package/dist/generators/names-file.js +12 -18
  30. package/dist/generators/names-file.js.map +1 -1
  31. package/dist/generators/queries-file.d.ts.map +1 -1
  32. package/dist/generators/queries-file.js +2 -1
  33. package/dist/generators/queries-file.js.map +1 -1
  34. package/dist/generators/render-helper-file.js +1 -1
  35. package/dist/generators/render-helper-file.js.map +1 -1
  36. package/dist/generators/requirements-markdown.d.ts.map +1 -1
  37. package/dist/generators/requirements-markdown.js +26 -1
  38. package/dist/generators/requirements-markdown.js.map +1 -1
  39. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  40. package/dist/generators/routes-file-hono.js +2 -1
  41. package/dist/generators/routes-file-hono.js.map +1 -1
  42. package/dist/generators/routes-file.d.ts.map +1 -1
  43. package/dist/generators/routes-file.js +2 -1
  44. package/dist/generators/routes-file.js.map +1 -1
  45. package/dist/index.d.ts +6 -5
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +10 -5
  48. package/dist/index.js.map +1 -1
  49. package/dist/names.d.ts.map +1 -1
  50. package/dist/names.js +5 -4
  51. package/dist/names.js.map +1 -1
  52. package/dist/overwrite-policy.d.ts +9 -7
  53. package/dist/overwrite-policy.d.ts.map +1 -1
  54. package/dist/overwrite-policy.js +22 -0
  55. package/dist/overwrite-policy.js.map +1 -1
  56. package/dist/runner.d.ts +24 -0
  57. package/dist/runner.d.ts.map +1 -1
  58. package/dist/runner.js +96 -8
  59. package/dist/runner.js.map +1 -1
  60. package/dist/templates/callable-file.d.ts.map +1 -1
  61. package/dist/templates/callable-file.js +2 -1
  62. package/dist/templates/callable-file.js.map +1 -1
  63. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  64. package/dist/templates/drizzle-schema.js +39 -14
  65. package/dist/templates/drizzle-schema.js.map +1 -1
  66. package/dist/templates/entity-file.d.ts.map +1 -1
  67. package/dist/templates/entity-file.js +4 -2
  68. package/dist/templates/entity-file.js.map +1 -1
  69. package/dist/templates/filter-allowlist.d.ts.map +1 -1
  70. package/dist/templates/filter-allowlist.js +10 -6
  71. package/dist/templates/filter-allowlist.js.map +1 -1
  72. package/dist/templates/filter-shared.d.ts +42 -1
  73. package/dist/templates/filter-shared.d.ts.map +1 -1
  74. package/dist/templates/filter-shared.js +68 -5
  75. package/dist/templates/filter-shared.js.map +1 -1
  76. package/dist/templates/inferred-types.d.ts.map +1 -1
  77. package/dist/templates/inferred-types.js +9 -6
  78. package/dist/templates/inferred-types.js.map +1 -1
  79. package/dist/templates/projection-decl.d.ts.map +1 -1
  80. package/dist/templates/projection-decl.js +4 -2
  81. package/dist/templates/projection-decl.js.map +1 -1
  82. package/dist/templates/queries-file.d.ts.map +1 -1
  83. package/dist/templates/queries-file.js +6 -5
  84. package/dist/templates/queries-file.js.map +1 -1
  85. package/dist/templates/relations-block.d.ts.map +1 -1
  86. package/dist/templates/relations-block.js +2 -1
  87. package/dist/templates/relations-block.js.map +1 -1
  88. package/dist/templates/render-helper.d.ts +12 -1
  89. package/dist/templates/render-helper.d.ts.map +1 -1
  90. package/dist/templates/render-helper.js +30 -7
  91. package/dist/templates/render-helper.js.map +1 -1
  92. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  93. package/dist/templates/routes-file-hono.js +2 -1
  94. package/dist/templates/routes-file-hono.js.map +1 -1
  95. package/dist/templates/routes-file.d.ts.map +1 -1
  96. package/dist/templates/routes-file.js +18 -10
  97. package/dist/templates/routes-file.js.map +1 -1
  98. package/dist/templates/zod-validators.d.ts.map +1 -1
  99. package/dist/templates/zod-validators.js +7 -4
  100. package/dist/templates/zod-validators.js.map +1 -1
  101. package/package.json +6 -6
  102. package/src/column-mapper.ts +17 -0
  103. package/src/generator-registry.ts +1 -0
  104. package/src/generators/agent-ui-page.ts +10 -10
  105. package/src/generators/api-model.ts +14 -7
  106. package/src/generators/barrel.ts +2 -1
  107. package/src/generators/callable-file.ts +2 -1
  108. package/src/generators/docs-data-builder.ts +6 -5
  109. package/src/generators/entity-file.ts +2 -1
  110. package/src/generators/names-file.ts +33 -4
  111. package/src/generators/queries-file.ts +2 -1
  112. package/src/generators/render-helper-file.ts +1 -1
  113. package/src/generators/requirements-markdown.ts +28 -1
  114. package/src/generators/routes-file-hono.ts +2 -1
  115. package/src/generators/routes-file.ts +2 -1
  116. package/src/index.ts +17 -5
  117. package/src/names.ts +5 -4
  118. package/src/overwrite-policy.ts +19 -9
  119. package/src/reference/barrel.ts +2 -1
  120. package/src/reference/entity.ts +2 -2
  121. package/src/reference/names.ts +26 -4
  122. package/src/reference/queries.ts +3 -3
  123. package/src/reference/routes-hono.ts +2 -1
  124. package/src/reference/routes.ts +2 -1
  125. package/src/runner.ts +112 -7
  126. package/src/templates/callable-file.ts +2 -1
  127. package/src/templates/drizzle-schema.ts +43 -13
  128. package/src/templates/entity-file.ts +4 -2
  129. package/src/templates/filter-allowlist.ts +9 -6
  130. package/src/templates/filter-shared.ts +91 -6
  131. package/src/templates/inferred-types.ts +9 -6
  132. package/src/templates/projection-decl.ts +4 -2
  133. package/src/templates/queries-file.ts +6 -5
  134. package/src/templates/relations-block.ts +2 -1
  135. package/src/templates/render-helper.ts +31 -6
  136. package/src/templates/routes-file-hono.ts +2 -1
  137. package/src/templates/routes-file.ts +18 -10
  138. package/src/templates/zod-validators.ts +7 -4
@@ -45,20 +45,30 @@ import { spawnSync } from "node:child_process";
45
45
  import { tmpdir } from "node:os";
46
46
  import { createHash, randomBytes } from "node:crypto";
47
47
 
48
- export type WriteStatus =
49
- | "new"
50
- | "unchanged"
51
- | "overwrite"
52
- | "merged"
53
- | "conflict"
54
- | "refused"
55
- | "skipped"
48
+ /**
49
+ * Every outcome a write decision can have. An `as const` array rather than a bare
50
+ * union so a consumer that MAPS these — the CLI's reporting layer does — can be
51
+ * gated against the full set instead of against a second list of its own. The CLI
52
+ * silently folded "overwrite" into "new" for exactly as long as no test could
53
+ * enumerate what it was folding.
54
+ */
55
+ export const WRITE_STATUSES = [
56
+ "new",
57
+ "unchanged",
58
+ "overwrite",
59
+ "merged",
60
+ "conflict",
61
+ "refused",
62
+ "skipped",
56
63
  /** FR-038 §8 — deleted because it was generated by a previous run, is no longer
57
64
  * generated, and was never edited by hand. Reported as a file outcome rather
58
65
  * than a warning because a deletion is exactly as consequential as a write,
59
66
  * and a run summary that lists writes but hides deletions is how a silent
60
67
  * deletion happens. */
61
- | "removed";
68
+ "removed",
69
+ ] as const;
70
+
71
+ export type WriteStatus = (typeof WRITE_STATUSES)[number];
62
72
 
63
73
  /**
64
74
  * "overwrite" — default; three-way merge if .gen-state exists, else write-if-
@@ -26,6 +26,7 @@ import {
26
26
  barrelModuleSpecifier,
27
27
  formatTs,
28
28
  GENERATED_HEADER,
29
+ effectivePackage,
29
30
  } from "@metaobjectsdev/codegen-ts";
30
31
 
31
32
  interface BarrelEntry {
@@ -63,7 +64,7 @@ export const barrel = function barrel(opts?: BarrelOpts): Generator {
63
64
  path: "index.ts",
64
65
  content: await formatTs(
65
66
  renderBarrel(
66
- entities.map((e) => ({ name: rc.valueObjectEmittedName(e), package: e.package })),
67
+ entities.map((e) => ({ name: rc.valueObjectEmittedName(e), package: effectivePackage(e) })),
67
68
  rc.extStyle,
68
69
  rc.selfTarget,
69
70
  rc.entityModuleTarget,
@@ -32,7 +32,7 @@
32
32
  // primitives below, or (with a globally-installed / linked CLI, where the project and
33
33
  // the CLI resolve ts-poet to different physical copies) every section renders
34
34
  // standalone with its own duplicate import header.
35
- import { joinCode, type Code } from "@metaobjectsdev/codegen-ts";
35
+ import { joinCode, type Code, effectivePackage } from "@metaobjectsdev/codegen-ts";
36
36
  import type { MetaObject } from "@metaobjectsdev/metadata";
37
37
  import {
38
38
  perEntity,
@@ -164,7 +164,7 @@ export const entityFile = function entityFile(opts?: EntityFileOpts): Generator
164
164
  return [];
165
165
  }
166
166
  return {
167
- path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity.package, `${entity.name}.ts`),
167
+ path: entityOutputPath(ctx.config.outputLayout ?? "flat", effectivePackage(entity), `${entity.name}.ts`),
168
168
  content: await formatTs(renderEntity(entity, ctx.renderContext, { allowlists })),
169
169
  };
170
170
  });
@@ -39,11 +39,26 @@ import {
39
39
  type EmittedFile,
40
40
  type GenContext,
41
41
  type Generator,
42
+ effectivePackage,
42
43
  } from "@metaobjectsdev/codegen-ts";
43
44
  import type { MetaObject } from "@metaobjectsdev/metadata";
44
45
 
45
- export function namesFile(): Generator {
46
- return {
46
+ export interface NamesFileOpts {
47
+ /**
48
+ * Narrow which objects get a names artifact. ANDed with the generator's own
49
+ * gates, so it can only narrow — the abstract bases a MATCHED object extends
50
+ * are still emitted, because the matched object's artifact `extends` them.
51
+ *
52
+ * A multi-package model is why this exists: one metadata tree can drive several
53
+ * consumers, and only some of its objects belong to the tier being generated.
54
+ */
55
+ filter?: (entity: MetaObject) => boolean;
56
+ /** Named output target, as on every other generator. */
57
+ target?: string;
58
+ }
59
+
60
+ export function namesFile(opts?: NamesFileOpts): Generator {
61
+ const generator: Generator = {
47
62
  name: "names",
48
63
  // §A6 — the marker the runner aggregates into ResolvedGenConfig.includeNames, so the
49
64
  // entity generator can tell whether this artifact will exist. Exactly the mechanism
@@ -63,13 +78,13 @@ export function namesFile(): Generator {
63
78
  // its entity sits at <pkg>/<Entity>.ts — an unresolvable import, and a hard
64
79
  // conflicting-duplicate-path failure as soon as two packages declare a
65
80
  // same-bare-named entity.
66
- entityOutputPath(layout, obj.package, `${obj.name}.names.ts`);
81
+ entityOutputPath(layout, effectivePackage(obj), `${obj.name}.names.ts`);
67
82
 
68
83
  const superSpecifierFor = (obj: MetaObject): string | undefined => {
69
84
  const sup = namesArtifactSuperOf(obj);
70
85
  return sup === undefined
71
86
  ? undefined
72
- : crossEntitySpecifier(layout, obj.package, sup.package, `${sup.name}.names`, extStyle);
87
+ : crossEntitySpecifier(layout, effectivePackage(obj), effectivePackage(sup), `${sup.name}.names`, extStyle);
73
88
  };
74
89
 
75
90
  const out: EmittedFile[] = [];
@@ -122,4 +137,11 @@ export function namesFile(): Generator {
122
137
  return out;
123
138
  },
124
139
  };
140
+ if (opts?.filter) {
141
+ generator.filter = opts.filter;
142
+ }
143
+ if (opts?.target) {
144
+ generator.target = opts.target;
145
+ }
146
+ return generator;
125
147
  }
@@ -30,7 +30,7 @@
30
30
  // primitives below, or (with a globally-installed / linked CLI, where the project and
31
31
  // the CLI resolve ts-poet to different physical copies) every section renders
32
32
  // standalone with its own duplicate import header.
33
- import { code, joinCode, type Code } from "@metaobjectsdev/codegen-ts";
33
+ import { code, joinCode, type Code, effectivePackage } from "@metaobjectsdev/codegen-ts";
34
34
  import type { MetaObject } from "@metaobjectsdev/metadata";
35
35
  import {
36
36
  perEntity,
@@ -71,7 +71,7 @@ function renderQueries(obj: MetaObject, ctx: RenderContext): string {
71
71
  const entityFileName = entityModuleSpecifier(
72
72
  ctx.selfTarget,
73
73
  ctx.entityModuleTarget,
74
- obj.package,
74
+ effectivePackage(obj),
75
75
  entityName,
76
76
  ctx.extStyle,
77
77
  );
@@ -162,7 +162,7 @@ export const queriesFile = function queriesFile(opts?: QueriesFileOpts): Generat
162
162
  throw new Error("queries-file: renderContext is required (provided by runGen)");
163
163
  }
164
164
  return {
165
- path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity.package, `${entity.name}.queries.ts`),
165
+ path: entityOutputPath(ctx.config.outputLayout ?? "flat", effectivePackage(entity), `${entity.name}.queries.ts`),
166
166
  content: await formatTs(renderQueries(entity, ctx.renderContext)),
167
167
  };
168
168
  }),
@@ -38,6 +38,7 @@ import {
38
38
  hasAnyRdbSource,
39
39
  formatTs,
40
40
  entityOutputPath,
41
+ effectivePackage,
41
42
  } from "@metaobjectsdev/codegen-ts";
42
43
 
43
44
  export interface RoutesFileHonoOpts {
@@ -100,7 +101,7 @@ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts)
100
101
  return {
101
102
  path: entityOutputPath(
102
103
  ctx.config.outputLayout ?? "flat",
103
- entity.package,
104
+ effectivePackage(entity),
104
105
  `${entity.name}.routes.hono.ts`,
105
106
  ),
106
107
  content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
@@ -46,6 +46,7 @@ import {
46
46
  hasAnyRdbSource,
47
47
  formatTs,
48
48
  entityOutputPath,
49
+ effectivePackage,
49
50
  } from "@metaobjectsdev/codegen-ts";
50
51
 
51
52
  export interface RoutesFileOpts {
@@ -79,7 +80,7 @@ export const routesFile = function routesFile(opts?: RoutesFileOpts): Generator
79
80
  throw new Error("routes-file: renderContext is required (provided by runGen)");
80
81
  }
81
82
  return {
82
- path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity.package, `${entity.name}.routes.ts`),
83
+ path: entityOutputPath(ctx.config.outputLayout ?? "flat", effectivePackage(entity), `${entity.name}.routes.ts`),
83
84
  content: await formatTs(renderRoutesFile(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
84
85
  };
85
86
  }),
package/src/runner.ts CHANGED
@@ -19,6 +19,7 @@ import { buildRelationMap } from "./relation-resolver.js";
19
19
  import { makeRenderContext } from "./render-context.js";
20
20
  import { sweepOrphans, type OrphanJob } from "./orphan-sweep.js";
21
21
  import { refusedOrphanMessage } from "./reconcile-orphans.js";
22
+ import { effectivePackage } from "./docs-paths.js";
22
23
  import {
23
24
  decideAndWrite,
24
25
  previewWriteStatus,
@@ -140,6 +141,20 @@ function orderable(v: string | undefined): [number, number, number] | undefined
140
141
  return m === null ? undefined : [Number(m[1]), Number(m[2]), Number(m[3])];
141
142
  }
142
143
 
144
+ /** True when `a` sorts strictly before `b` under `[major, minor, patch]` order. The
145
+ * shared comparison the two "did we just cross a release boundary" predicates below
146
+ * reduce to once both sides are `orderable()` — their SILENCE rules (what makes a
147
+ * boundary crossing worth noting at all) stay separate; only this pure ordering is
148
+ * common between them. */
149
+ function versionBefore(a: readonly [number, number, number], b: readonly [number, number, number]): boolean {
150
+ for (let i = 0; i < 3; i++) {
151
+ const ai = a[i] as number;
152
+ const bi = b[i] as number;
153
+ if (ai !== bi) return ai < bi;
154
+ }
155
+ return false;
156
+ }
157
+
143
158
  /**
144
159
  * True exactly on the FIRST gen after upgrading past the release that moved the API base
145
160
  * URL out of the entity descriptor, and only for a project that actually had a prefix to
@@ -166,12 +181,49 @@ export function shouldNoteBaseUrlMove(
166
181
  const was = orderable(recordedEngine);
167
182
  const move = orderable(moveVersion);
168
183
  if (was === undefined || move === undefined) return false;
169
- for (let i = 0; i < 3; i++) {
170
- const a = was[i] as number;
171
- const b = move[i] as number;
172
- if (a !== b) return a < b;
173
- }
174
- return false;
184
+ return versionBefore(was, move);
185
+ }
186
+
187
+ /** The release in which `<Entity>Names` became the way a physical name is spelled once. */
188
+ const NAMES_ARTIFACT_VERSION = "1.0.0";
189
+
190
+ /**
191
+ * True exactly on the FIRST gen after crossing into the release that made
192
+ * `<Entity>Names` the doctrine, for a project that has DB-backed objects and has not
193
+ * wired `namesFile()`. Exported for test.
194
+ *
195
+ * The problem it addresses was reported from three separate adopter estates: on
196
+ * TypeScript `generators: [...]` IS the complete suite — there is no default set to
197
+ * inherit — so an EXISTING project emits no names artifact until the line is added, and
198
+ * `meta gen` said nothing about it. One estate carried ~200 physical names as string
199
+ * literals in hand-written SQL and the whole run contained zero occurrences of the word
200
+ * "names". `meta init` scaffolds the generator, so the gap is only ever for projects that
201
+ * already exist — which is exactly the population that cannot be reached by a scaffold.
202
+ *
203
+ * Keyed on the #232 engine stamp for the same reason `shouldNoteBaseUrlMove` is: whether
204
+ * an adopter has DECIDED against the artifact is not something `meta gen` can see, so a
205
+ * condition-only trigger would nag for ever with no way to satisfy it — the cry-wolf
206
+ * failure that got the `timestampMode` warning deleted from this file. Crossing the
207
+ * version boundary is an event that happens once.
208
+ *
209
+ * Silence is the default for an absent or unorderable recorded version, matching
210
+ * `shouldNoteBaseUrlMove`: no gen history means nothing has been generated the old way,
211
+ * and a project generating for the first time under 1.0 is not migrating.
212
+ */
213
+ export function shouldNoteNamesArtifactAbsent(
214
+ namesWired: boolean,
215
+ hasDbBackedObject: boolean,
216
+ recordedEngine: string | undefined,
217
+ sinceVersion: string = NAMES_ARTIFACT_VERSION,
218
+ ): boolean {
219
+ if (namesWired) return false;
220
+ // Nothing with a physical name to spell — an all-value / sourceless model has no
221
+ // artifact to miss, and saying so would be noise rather than a finding.
222
+ if (!hasDbBackedObject) return false;
223
+ const was = orderable(recordedEngine);
224
+ const since = orderable(sinceVersion);
225
+ if (was === undefined || since === undefined) return false;
226
+ return versionBefore(was, since);
175
227
  }
176
228
 
177
229
  export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
@@ -411,7 +463,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
411
463
  const key = o.subType === OBJECT_SUBTYPE_VALUE
412
464
  ? (valueObjectNames.get(o.resolutionKey()) ?? o.name)
413
465
  : o.name;
414
- packageOf.set(key, o.package);
466
+ packageOf.set(key, effectivePackage(o));
415
467
  }
416
468
 
417
469
  // Auto-detect: is the OPT-IN Hono routes generator in the active suite? If so,
@@ -442,6 +494,29 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
442
494
  // prompt-generator-gate.ts. Self-extinguishing; warning only.
443
495
  warnMissingPromptGenerators(root, config.generators, (m) => warnings.push(m));
444
496
 
497
+ // <Entity>Names is opt-in on TypeScript and an existing project gets no signal that
498
+ // it exists. Fires ONCE, on the first gen after crossing the release that made it the
499
+ // doctrine — see shouldNoteNamesArtifactAbsent for why it is keyed on the engine stamp
500
+ // rather than on the condition alone.
501
+ if (
502
+ shouldNoteNamesArtifactAbsent(
503
+ namesTargets.size > 0,
504
+ safeEntities.some((e) => hasAnyRdbSource(e)),
505
+ recordedEngine,
506
+ )
507
+ ) {
508
+ warnings.push(
509
+ `no <Entity>Names artifact: namesFile() is not in this run's generators, so every ` +
510
+ `physical table/column name your hand-written SQL, upserts and fixtures spell is a ` +
511
+ `string literal that nothing checks against the metadata. Add it — ` +
512
+ `import { namesFile } from "@metaobjectsdev/codegen-ts/generators" (or the copy ` +
513
+ `'meta eject names' writes) — then reference <Entity>Names instead of the literals. ` +
514
+ `A typed ORM handle already in a name position is CORRECT and should stay; the ` +
515
+ `constants are for where no handle exists (raw SQL, excluded.<column>). ` +
516
+ `This note appears once.`,
517
+ );
518
+ }
519
+
445
520
  // A retired `@emit*` codegen flag still sitting in the metadata suppresses nothing now.
446
521
  // Named here rather than left to be discovered as a file that reappeared. Scoped to
447
522
  // the run's own entity set (the same set every generator sees), so `meta gen <entity>`
@@ -455,6 +530,10 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
455
530
  // directory their policy's relative paths are measured from. Collected here
456
531
  // because `writeOutDir` is resolved per generator inside this loop.
457
532
  const orphanJobs: OrphanJob[] = [];
533
+ // Generators that ran and produced no file. See the note at the `files.length === 0`
534
+ // check below. A Set because a generator may run once per target.
535
+ const emptyGenerators = new Set<string>();
536
+
458
537
  for (const generator of config.generators) {
459
538
  // ADR-0025: `meta docs` is the single docs door. A `meta gen` config that
460
539
  // still lists a deprecated doc generator is warned + skipped, not run — the
@@ -547,6 +626,17 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
547
626
  let files: EmittedFile[];
548
627
  try {
549
628
  files = await generator.generate(ctx);
629
+ // A generator the author DELIBERATELY WIRED that matched nothing is a fact
630
+ // worth one line. `namesFile()` on a model with no DB-backed object emitted
631
+ // zero files with no line, no count and no warning, and an adopter could not
632
+ // tell that from a run that had done the work. 0.21.4 gave `tanstackGrid()`
633
+ // a self-extinguishing warning for exactly this shape.
634
+ //
635
+ // Collected rather than warned per generator: one aggregated line names every
636
+ // silent generator, so a suite with several inapplicable ones costs one line,
637
+ // not five. Emitting nothing is often CORRECT — a form generator on a model
638
+ // with no forms — so this reports, it never fails.
639
+ if (files.length === 0) emptyGenerators.add(generator.name);
550
640
  } catch (err) {
551
641
  const msg = err instanceof Error ? err.message : String(err);
552
642
  // `cause` preserves the original throw. Without it a `runGen` caller sees a
@@ -579,6 +669,21 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
579
669
  }
580
670
  }
581
671
 
672
+ // One line for every wired generator that matched nothing — see the note at the
673
+ // collection site. Deliberately AFTER the loop so the message is a single line
674
+ // naming all of them, and deliberately a warning: emitting nothing is frequently
675
+ // the correct outcome, and a gate that failed on it would be wrong more often
676
+ // than right.
677
+ if (emptyGenerators.size > 0) {
678
+ const names = [...emptyGenerators].sort();
679
+ warnings.push(
680
+ `${names.length} wired generator(s) matched nothing and wrote no file: ` +
681
+ `${names.join(", ")}. That is often correct — a generator whose metadata this ` +
682
+ `model does not declare has nothing to emit. It is reported because a run that ` +
683
+ `emitted nothing and a run that did the work otherwise look identical.`,
684
+ );
685
+ }
686
+
582
687
  // 5. Write phase.
583
688
  const writes: WriteResult[] = [];
584
689
  const conflicts: WriteResult[] = [];
@@ -35,6 +35,7 @@ import { GENERATED_HEADER } from "../constants.js";
35
35
  import { crossEntitySpecifier } from "../import-path.js";
36
36
  import { resolveObjectNames } from "../names.js";
37
37
  import type { RenderContext } from "../render-context.js";
38
+ import { effectivePackage } from "../docs-paths.js";
38
39
 
39
40
  const CALLABLE_KINDS: ReadonlySet<string> = new Set([
40
41
  SOURCE_KIND_STORED_PROC,
@@ -128,7 +129,7 @@ export function renderCallableFile(entity: MetaObject, ctx?: RenderContext): str
128
129
  const namesImport = namesConst === undefined
129
130
  ? ""
130
131
  : `import { ${namesConst} } from "${crossEntitySpecifier(
131
- ctx!.selfTarget.outputLayout, entity.package, entity.package,
132
+ ctx!.selfTarget.outputLayout, effectivePackage(entity), effectivePackage(entity),
132
133
  `${entity.name}.names`, ctx!.extStyle,
133
134
  )}";\n`;
134
135
  // The identifier must stay an IDENTIFIER. A bare interpolation into drizzle's `sql` tag
@@ -5,7 +5,6 @@
5
5
  import { code, imp, joinCode, type Code } from "ts-poet";
6
6
  import { MetaObject, MetaField, MetaIndex, isMetaObject, stripPackage } from "@metaobjectsdev/metadata";
7
7
  import {
8
- FIELD_SUBTYPE_LONG,
9
8
  IDENTITY_ATTR_FIELDS, IDENTITY_ATTR_GENERATION,
10
9
  IDENTITY_ATTR_EXPR, IDENTITY_ATTR_USING, IDENTITY_ATTR_ORDERS, IDENTITY_ATTR_WHERE,
11
10
  GENERATION_INCREMENT, GENERATION_UUID,
@@ -14,7 +13,10 @@ import {
14
13
  } from "@metaobjectsdev/metadata";
15
14
  import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
16
15
  import { crossEntitySpecifier, valueObjectModuleSpecifier } from "../import-path.js";
17
- import { mapColumnType, type ColumnSpec, type EnumIntCustomType } from "../column-mapper.js";
16
+ import {
17
+ mapColumnType, PG_IDENTITY_CAPABLE_FNS,
18
+ type ColumnSpec, type EnumIntCustomType,
19
+ } from "../column-mapper.js";
18
20
  import { tableNameFromEntity } from "../naming.js";
19
21
  import {
20
22
  namesRef, physicalNameExpr, sourceSchemaExpr, indexNameExpr, columnExpr,
@@ -23,6 +25,7 @@ import { resolveTableSchema } from "@metaobjectsdev/metadata";
23
25
  import { renderRelationsBlock } from "./relations-block.js";
24
26
  import { renderDocsFor } from "./jsdoc.js";
25
27
  import { collectTphSubtypeFields } from "./tph-discriminator.js";
28
+ import { effectivePackage } from "../docs-paths.js";
26
29
 
27
30
  /**
28
31
  * Render the Drizzle table definition for one entity, including:
@@ -102,6 +105,10 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
102
105
  // `isMetaObject`, never `instanceof`: two physical copies of @metaobjectsdev/metadata in
103
106
  // one process give the class object and the instance different identities, so the check
104
107
  // would return false for a real node — silently.
108
+ // `obj` never changes across this function, so its effective package is computed once
109
+ // and reused everywhere below (the `fromPackage` passed to `namesRef` here, and the
110
+ // per-field `renderColumn` calls in both the own-field and TPH-subtype-fold loops).
111
+ const pkg = effectivePackage(obj);
105
112
  const columnNameExpr = (field: MetaField, dbName: string): Code => {
106
113
  if (names === undefined || names.resolved.fields[field.name] !== undefined) {
107
114
  return columnExpr(names, field.name, dbName);
@@ -111,7 +118,7 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
111
118
  // base's), not to the subtype's own package.
112
119
  const ownerNames =
113
120
  owner !== undefined && isMetaObject(owner) && owner !== obj
114
- ? namesRef(owner, ctx, obj.package)
121
+ ? namesRef(owner, ctx, pkg)
115
122
  : undefined;
116
123
  return columnExpr(ownerNames, field.name, dbName);
117
124
  };
@@ -202,7 +209,7 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
202
209
  enumIntTypes.set(spec.enumIntCustomType.fnConstName, spec.enumIntCustomType);
203
210
  }
204
211
  const fieldDocs = renderDocsFor(child);
205
- const columnLine = renderColumn(spec, columnNameExpr(child, spec.dbName), child, ctx, isPk, pkGeneration, fkInfo, isComposite, obj.package, obj.name);
212
+ const columnLine = renderColumn(spec, columnNameExpr(child, spec.dbName), child, ctx, isPk, pkGeneration, fkInfo, isComposite, pkg, obj.name);
206
213
  columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
207
214
  if (spec.checkConstraint !== undefined) checkConstraints.push(checkEntry(child, spec));
208
215
  }
@@ -222,7 +229,7 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
222
229
  }
223
230
  const fieldDocs = renderDocsFor(child);
224
231
  const columnLine = renderColumn(
225
- spec, columnNameExpr(child, spec.dbName), child, ctx, false, undefined, fkMap.get(child.name), isComposite, obj.package, obj.name, true,
232
+ spec, columnNameExpr(child, spec.dbName), child, ctx, false, undefined, fkMap.get(child.name), isComposite, pkg, obj.name, true,
226
233
  );
227
234
  columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
228
235
  // Enum CHECK constraints stay valid under TPH: `NULL IN (...)` is NULL
@@ -532,15 +539,38 @@ function renderColumn(
532
539
  // Composite PKs don't use .primaryKey() per-column; table callback owns it.
533
540
  pkSuffix = isComposite ? "" : ".primaryKey({ autoIncrement: true })";
534
541
  } else {
535
- // Postgres: bigserial for long (8-byte), serial for int (4-byte).
536
- if (field.subType === FIELD_SUBTYPE_LONG) {
537
- const bigserialSym = imp(`bigserial@${spec.importModule}`);
538
- baseCall = code`${bigserialSym}(${dbNameExpr}, { mode: "number" })`;
539
- } else {
540
- const serialSym = imp(`serial@${spec.importModule}`);
541
- baseCall = code`${serialSym}(${dbNameExpr})`;
542
- }
542
+ // Postgres: the column KEEPS the type the column-mapper derived and
543
+ // GAINS the identity clause. It does not get a second spelling.
544
+ //
545
+ // This used to substitute `serial`/`bigserial` for the mapped
546
+ // `integer`/`bigint`, and that was the 1.0 defect wearing another hat:
547
+ // migrate emits `… GENERATED BY DEFAULT AS IDENTITY` (emit/postgres.ts
548
+ // renderColumn) for the same field, and `serial` is NOT that column.
549
+ // `serial` is historical sugar for integer + an owned sequence + a
550
+ // genuine DEFAULT nextval(...); an identity column carries neither a
551
+ // default nor a droppable sequence. So the generated Drizzle schema
552
+ // misrepresented the live PK of every integer-keyed table migrate had
553
+ // created — one question, two doors, two answers, and nothing compared
554
+ // them. migrate owns the DDL (ADR-0015), so codegen is what changes —
555
+ // the same call #293 made when the two named a CHECK constraint
556
+ // differently.
557
+ //
558
+ // The clause is gated on the MAPPED column function rather than on the
559
+ // field subtype, so the two cannot drift apart again: only Drizzle's
560
+ // int family (integer/bigint/smallint, via PgIntColumnBaseBuilder)
561
+ // carries .generatedByDefaultAsIdentity(). A subtype outside that
562
+ // family with @generation:"increment" is a combination Postgres cannot
563
+ // express at all — migrate emits `TEXT GENERATED BY DEFAULT AS
564
+ // IDENTITY` and fails at apply — and the honest thing here is to leave
565
+ // the declared type alone and let that failure be loud, rather than
566
+ // silently substituting an integer column for the declared one.
543
567
  pkSuffix = isComposite ? "" : ".primaryKey()";
568
+ if (
569
+ spec.enumIntCustomType === undefined &&
570
+ PG_IDENTITY_CAPABLE_FNS.has(spec.fnName)
571
+ ) {
572
+ pkSuffix += ".generatedByDefaultAsIdentity()";
573
+ }
544
574
  }
545
575
  } else if (pkGeneration === GENERATION_UUID) {
546
576
  pkSuffix = isComposite
@@ -30,6 +30,7 @@ import { namesRef, namesConstArg, physicalNameExpr, sourceSchemaExpr } from "../
30
30
  import { hasWritableRdbSource } from "../source-detect.js";
31
31
  import { renderValueObjectFile } from "./value-object-file.js";
32
32
  import { isAbstract } from "../instance-artifacts.js";
33
+ import { effectivePackage } from "../docs-paths.js";
33
34
 
34
35
  /**
35
36
  * Render-time options for the entity-file composer.
@@ -139,14 +140,15 @@ export function renderEntityFile(
139
140
  if (writeThrough) {
140
141
  const camel = entity.name.charAt(0).toLowerCase() + entity.name.slice(1);
141
142
  const fields = entity.fields();
143
+ const entityPkg = effectivePackage(entity);
142
144
  // ADR-0044/#228 — resolve a view column's `@objectRef` to the value object's
143
145
  // EMITTED name + module TOGETHER (lock-step), so the read-view artifact imports
144
146
  // `AcmeAlphaNote` from `./AcmeAlphaNote.js` (not a bare `Note` → `./Note.js`)
145
147
  // under a cross-package short-name collision.
146
148
  const voRef = (field: MetaField): { name: string; module: string } => {
147
149
  const ref = field.attr(FIELD_ATTR_OBJECT_REF);
148
- const name = ctx.resolveValueObjectName(typeof ref === "string" ? ref : "", fieldDeclaringPackage(field, entity.package));
149
- const module = valueObjectModuleSpecifier(name, ctx.packageOf, entity.package, ctx.outputLayout, ctx.extStyle);
150
+ const name = ctx.resolveValueObjectName(typeof ref === "string" ? ref : "", fieldDeclaringPackage(field, entityPkg));
151
+ const module = valueObjectModuleSpecifier(name, ctx.packageOf, entityPkg, ctx.outputLayout, ctx.extStyle);
150
152
  return { name, module };
151
153
  };
152
154
  // ONE selection of the replica source: `projectionViewName` names it, and this is the
@@ -2,7 +2,6 @@ import { code, type Code } from "ts-poet";
2
2
  import { MetaField, MetaObject } from "@metaobjectsdev/metadata";
3
3
  import {
4
4
  FIELD_ATTR_FILTERABLE,
5
- FIELD_ATTR_SORTABLE_DEFAULT_ORDER,
6
5
  FIELD_SUBTYPE_BOOLEAN,
7
6
  FIELD_SUBTYPE_INT,
8
7
  FIELD_SUBTYPE_LONG,
@@ -15,7 +14,7 @@ import {
15
14
  FIELD_SUBTYPE_CURRENCY,
16
15
  opsForField,
17
16
  } from "@metaobjectsdev/metadata";
18
- import { sortableFields } from "./filter-shared.js";
17
+ import { sortableFields, declaredSortDefaultOrder } from "./filter-shared.js";
19
18
  import type { RenderContext } from "../render-context.js";
20
19
 
21
20
  const NUMBER_SUBTYPES = new Set<string>([
@@ -108,11 +107,15 @@ export const ${entity.name}SortAllowlist = {} as const satisfies SortAllowlist;
108
107
  }
109
108
  const rows = sortable
110
109
  .map((f) => {
111
- const defaultOrder = f.attr(FIELD_ATTR_SORTABLE_DEFAULT_ORDER) as string | undefined;
110
+ // The DECLARED order only an undeclared field stays `{}` so the runtime's own
111
+ // `?? "asc"` remains the single place the fallback is spelled. Read through the
112
+ // shared resolver, never off the attr here, so this allowlist and the grid tier
113
+ // cannot answer one declaration two ways.
114
+ const defaultOrder = declaredSortDefaultOrder(f);
112
115
  const rule =
113
- defaultOrder === "asc" || defaultOrder === "desc"
114
- ? `{ defaultOrder: ${JSON.stringify(defaultOrder)} as const }`
115
- : `{}`;
116
+ defaultOrder === undefined
117
+ ? `{}`
118
+ : `{ defaultOrder: ${JSON.stringify(defaultOrder)} as const }`;
116
119
  return ` ${f.name}: ${rule}`;
117
120
  })
118
121
  .join(",\n");