@metaobjectsdev/codegen-ts 1.0.0-rc.4 → 1.0.0-rc.6

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 (168) 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/constants.d.ts +10 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +10 -0
  8. package/dist/constants.js.map +1 -1
  9. package/dist/generator-registry.d.ts.map +1 -1
  10. package/dist/generator-registry.js +1 -0
  11. package/dist/generator-registry.js.map +1 -1
  12. package/dist/generator.d.ts +15 -0
  13. package/dist/generator.d.ts.map +1 -1
  14. package/dist/generator.js.map +1 -1
  15. package/dist/generators/agent-docs-file.d.ts.map +1 -1
  16. package/dist/generators/agent-docs-file.js +12 -3
  17. package/dist/generators/agent-docs-file.js.map +1 -1
  18. package/dist/generators/agent-schema-input.d.ts +11 -0
  19. package/dist/generators/agent-schema-input.d.ts.map +1 -1
  20. package/dist/generators/agent-schema-page.d.ts.map +1 -1
  21. package/dist/generators/agent-schema-page.js +13 -1
  22. package/dist/generators/agent-schema-page.js.map +1 -1
  23. package/dist/generators/agent-ui-page.d.ts.map +1 -1
  24. package/dist/generators/agent-ui-page.js +11 -9
  25. package/dist/generators/agent-ui-page.js.map +1 -1
  26. package/dist/generators/api-model.d.ts +2 -1
  27. package/dist/generators/api-model.d.ts.map +1 -1
  28. package/dist/generators/api-model.js +12 -6
  29. package/dist/generators/api-model.js.map +1 -1
  30. package/dist/generators/barrel.d.ts.map +1 -1
  31. package/dist/generators/barrel.js +2 -1
  32. package/dist/generators/barrel.js.map +1 -1
  33. package/dist/generators/callable-file.d.ts.map +1 -1
  34. package/dist/generators/callable-file.js +2 -1
  35. package/dist/generators/callable-file.js.map +1 -1
  36. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  37. package/dist/generators/docs-data-builder.js +6 -5
  38. package/dist/generators/docs-data-builder.js.map +1 -1
  39. package/dist/generators/entity-file.d.ts.map +1 -1
  40. package/dist/generators/entity-file.js +2 -1
  41. package/dist/generators/entity-file.js.map +1 -1
  42. package/dist/generators/names-file.d.ts +22 -1
  43. package/dist/generators/names-file.d.ts.map +1 -1
  44. package/dist/generators/names-file.js +13 -18
  45. package/dist/generators/names-file.js.map +1 -1
  46. package/dist/generators/queries-file.d.ts.map +1 -1
  47. package/dist/generators/queries-file.js +2 -1
  48. package/dist/generators/queries-file.js.map +1 -1
  49. package/dist/generators/render-helper-file.js +1 -1
  50. package/dist/generators/render-helper-file.js.map +1 -1
  51. package/dist/generators/requirements-markdown.d.ts.map +1 -1
  52. package/dist/generators/requirements-markdown.js +26 -1
  53. package/dist/generators/requirements-markdown.js.map +1 -1
  54. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  55. package/dist/generators/routes-file-hono.js +2 -1
  56. package/dist/generators/routes-file-hono.js.map +1 -1
  57. package/dist/generators/routes-file.d.ts.map +1 -1
  58. package/dist/generators/routes-file.js +2 -1
  59. package/dist/generators/routes-file.js.map +1 -1
  60. package/dist/index.d.ts +9 -7
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +16 -7
  63. package/dist/index.js.map +1 -1
  64. package/dist/metaobjects-config.d.ts +10 -0
  65. package/dist/metaobjects-config.d.ts.map +1 -1
  66. package/dist/metaobjects-config.js +14 -1
  67. package/dist/metaobjects-config.js.map +1 -1
  68. package/dist/names.d.ts.map +1 -1
  69. package/dist/names.js +5 -4
  70. package/dist/names.js.map +1 -1
  71. package/dist/overwrite-policy.d.ts +23 -9
  72. package/dist/overwrite-policy.d.ts.map +1 -1
  73. package/dist/overwrite-policy.js +48 -0
  74. package/dist/overwrite-policy.js.map +1 -1
  75. package/dist/runner.d.ts +24 -0
  76. package/dist/runner.d.ts.map +1 -1
  77. package/dist/runner.js +143 -13
  78. package/dist/runner.js.map +1 -1
  79. package/dist/templates/callable-file.d.ts.map +1 -1
  80. package/dist/templates/callable-file.js +2 -1
  81. package/dist/templates/callable-file.js.map +1 -1
  82. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  83. package/dist/templates/drizzle-schema.js +39 -14
  84. package/dist/templates/drizzle-schema.js.map +1 -1
  85. package/dist/templates/entity-file.d.ts.map +1 -1
  86. package/dist/templates/entity-file.js +4 -2
  87. package/dist/templates/entity-file.js.map +1 -1
  88. package/dist/templates/filter-allowlist.d.ts.map +1 -1
  89. package/dist/templates/filter-allowlist.js +10 -6
  90. package/dist/templates/filter-allowlist.js.map +1 -1
  91. package/dist/templates/filter-shared.d.ts +42 -1
  92. package/dist/templates/filter-shared.d.ts.map +1 -1
  93. package/dist/templates/filter-shared.js +68 -5
  94. package/dist/templates/filter-shared.js.map +1 -1
  95. package/dist/templates/inferred-types.d.ts.map +1 -1
  96. package/dist/templates/inferred-types.js +9 -6
  97. package/dist/templates/inferred-types.js.map +1 -1
  98. package/dist/templates/projection-decl.d.ts.map +1 -1
  99. package/dist/templates/projection-decl.js +4 -2
  100. package/dist/templates/projection-decl.js.map +1 -1
  101. package/dist/templates/queries-file.d.ts.map +1 -1
  102. package/dist/templates/queries-file.js +6 -5
  103. package/dist/templates/queries-file.js.map +1 -1
  104. package/dist/templates/relations-block.d.ts.map +1 -1
  105. package/dist/templates/relations-block.js +2 -1
  106. package/dist/templates/relations-block.js.map +1 -1
  107. package/dist/templates/render-helper.d.ts +12 -1
  108. package/dist/templates/render-helper.d.ts.map +1 -1
  109. package/dist/templates/render-helper.js +30 -7
  110. package/dist/templates/render-helper.js.map +1 -1
  111. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  112. package/dist/templates/routes-file-hono.js +2 -1
  113. package/dist/templates/routes-file-hono.js.map +1 -1
  114. package/dist/templates/routes-file.d.ts.map +1 -1
  115. package/dist/templates/routes-file.js +18 -10
  116. package/dist/templates/routes-file.js.map +1 -1
  117. package/dist/templates/zod-validators.d.ts.map +1 -1
  118. package/dist/templates/zod-validators.js +7 -4
  119. package/dist/templates/zod-validators.js.map +1 -1
  120. package/dist/ui-tier-gate.d.ts +44 -0
  121. package/dist/ui-tier-gate.d.ts.map +1 -0
  122. package/dist/ui-tier-gate.js +68 -0
  123. package/dist/ui-tier-gate.js.map +1 -0
  124. package/package.json +6 -6
  125. package/src/column-mapper.ts +17 -0
  126. package/src/constants.ts +11 -0
  127. package/src/generator-registry.ts +1 -0
  128. package/src/generator.ts +15 -0
  129. package/src/generators/agent-docs-file.ts +11 -2
  130. package/src/generators/agent-schema-input.ts +11 -0
  131. package/src/generators/agent-schema-page.ts +14 -1
  132. package/src/generators/agent-ui-page.ts +10 -10
  133. package/src/generators/api-model.ts +14 -7
  134. package/src/generators/barrel.ts +2 -1
  135. package/src/generators/callable-file.ts +2 -1
  136. package/src/generators/docs-data-builder.ts +6 -5
  137. package/src/generators/entity-file.ts +2 -1
  138. package/src/generators/names-file.ts +34 -4
  139. package/src/generators/queries-file.ts +2 -1
  140. package/src/generators/render-helper-file.ts +1 -1
  141. package/src/generators/requirements-markdown.ts +28 -1
  142. package/src/generators/routes-file-hono.ts +2 -1
  143. package/src/generators/routes-file.ts +2 -1
  144. package/src/index.ts +23 -7
  145. package/src/metaobjects-config.ts +20 -1
  146. package/src/names.ts +5 -4
  147. package/src/overwrite-policy.ts +62 -11
  148. package/src/reference/barrel.ts +2 -1
  149. package/src/reference/entity.ts +2 -2
  150. package/src/reference/names.ts +30 -4
  151. package/src/reference/queries.ts +3 -3
  152. package/src/reference/routes-hono.ts +2 -1
  153. package/src/reference/routes.ts +2 -1
  154. package/src/runner.ts +162 -12
  155. package/src/templates/callable-file.ts +2 -1
  156. package/src/templates/drizzle-schema.ts +43 -13
  157. package/src/templates/entity-file.ts +4 -2
  158. package/src/templates/filter-allowlist.ts +9 -6
  159. package/src/templates/filter-shared.ts +91 -6
  160. package/src/templates/inferred-types.ts +9 -6
  161. package/src/templates/projection-decl.ts +4 -2
  162. package/src/templates/queries-file.ts +6 -5
  163. package/src/templates/relations-block.ts +2 -1
  164. package/src/templates/render-helper.ts +31 -6
  165. package/src/templates/routes-file-hono.ts +2 -1
  166. package/src/templates/routes-file.ts +18 -10
  167. package/src/templates/zod-validators.ts +7 -4
  168. package/src/ui-tier-gate.ts +79 -0
@@ -1,8 +1,10 @@
1
1
  import type { Generator, GenContext, EmittedFile } from "../generator.js";
2
2
  import { entityOutputPath, crossEntitySpecifier } from "../import-path.js";
3
+ import { NAMES_FILE_SUFFIX } from "../constants.js";
3
4
  import { renderNamesDecl } from "../templates/names-decl.js";
4
5
  import { namesArtifactSuperOf, resolveObjectNames } from "../names.js";
5
6
  import type { MetaObject } from "@metaobjectsdev/metadata";
7
+ import { effectivePackage } from "../docs-paths.js";
6
8
 
7
9
  /**
8
10
  * §A1/§A2/§A6 — `<Entity>Names`: the physical database names for one object, as constants a
@@ -18,8 +20,29 @@ import type { MetaObject } from "@metaobjectsdev/metadata";
18
20
  * artifact is a MINOR under docs/compatibility-policy.md and adds zero bytes to existing
19
21
  * files, where a flag would move every $table-carrying golden for the same functionality.
20
22
  */
21
- export function namesFile(): Generator {
22
- return {
23
+ export interface NamesFileOpts {
24
+ /**
25
+ * Narrow which objects get a names artifact. ANDed with the generator's own
26
+ * gates, so it can only narrow.
27
+ *
28
+ * A multi-package model is the reason this exists: one metadata tree can drive
29
+ * several consumers (a TypeScript data layer, a wire protocol, a JVM rail), and
30
+ * only some of those objects belong to the tier being generated. Without a
31
+ * filter, wiring this generator emitted an artifact for every persistable object
32
+ * in the tree — including the ones another rail owns — while every sibling
33
+ * generator carried `filter` and narrowed correctly.
34
+ *
35
+ * Note the abstract bases a MATCHED object extends are still emitted, filter or
36
+ * not: the matched object's own artifact `extends` them, so excluding them would
37
+ * emit an artifact whose import does not resolve.
38
+ */
39
+ filter?: (entity: MetaObject) => boolean;
40
+ /** Named output target, as on every other generator. */
41
+ target?: string;
42
+ }
43
+
44
+ export function namesFile(opts?: NamesFileOpts): Generator {
45
+ const generator: Generator = {
23
46
  name: "names",
24
47
  // §A6 — the marker the runner aggregates into ResolvedGenConfig.includeNames, so the
25
48
  // entity generator can tell whether this artifact will exist. Exactly the mechanism
@@ -39,13 +62,13 @@ export function namesFile(): Generator {
39
62
  // its entity sits at <pkg>/<Entity>.ts — an unresolvable import, and a hard
40
63
  // conflicting-duplicate-path failure as soon as two packages declare a
41
64
  // same-bare-named entity.
42
- entityOutputPath(layout, obj.package, `${obj.name}.names.ts`);
65
+ entityOutputPath(layout, effectivePackage(obj), `${obj.name}${NAMES_FILE_SUFFIX}`);
43
66
 
44
67
  const superSpecifierFor = (obj: MetaObject): string | undefined => {
45
68
  const sup = namesArtifactSuperOf(obj);
46
69
  return sup === undefined
47
70
  ? undefined
48
- : crossEntitySpecifier(layout, obj.package, sup.package, `${sup.name}.names`, extStyle);
71
+ : crossEntitySpecifier(layout, effectivePackage(obj), effectivePackage(sup), `${sup.name}.names`, extStyle);
49
72
  };
50
73
 
51
74
  const out: EmittedFile[] = [];
@@ -95,4 +118,11 @@ export function namesFile(): Generator {
95
118
  return out;
96
119
  },
97
120
  };
121
+ if (opts?.filter) {
122
+ generator.filter = opts.filter;
123
+ }
124
+ if (opts?.target) {
125
+ generator.target = opts.target;
126
+ }
127
+ return generator;
98
128
  }
@@ -5,6 +5,7 @@ import { isTphSubtype } from "../templates/zod-validators.js";
5
5
  import { hasAnyRdbSource } from "../source-detect.js";
6
6
  import { formatTs } from "../format.js";
7
7
  import { entityOutputPath } from "../import-path.js";
8
+ import { effectivePackage } from "../docs-paths.js";
8
9
 
9
10
  export interface QueriesFileOpts {
10
11
  filter?: (entity: MetaObject) => boolean;
@@ -40,7 +41,7 @@ export const queriesFile = function queriesFile(opts?: QueriesFileOpts): Generat
40
41
  throw new Error("queries-file: renderContext is required (provided by runGen)");
41
42
  }
42
43
  return {
43
- path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity.package, `${entity.name}.queries.ts`),
44
+ path: entityOutputPath(ctx.config.outputLayout ?? "flat", effectivePackage(entity), `${entity.name}.queries.ts`),
44
45
  content: await formatTs(renderQueriesFile(entity, ctx.renderContext)),
45
46
  };
46
47
  }),
@@ -64,7 +64,7 @@ export const renderHelper = function renderHelper(opts?: RenderHelperOpts): Gene
64
64
  // renderRenderHelper THROWS (fails codegen) on a mustache↔VO drift —
65
65
  // intentionally NOT caught: a drifted template is a build error.
66
66
  path: `${dirPrefix}${t.name}.render.ts`,
67
- content: renderRenderHelper(root, t.name, provider),
67
+ content: renderRenderHelper(root, t.name, provider, ctx.config.extStyle ?? "js"),
68
68
  });
69
69
  }
70
70
  return files;
@@ -47,10 +47,37 @@ function heading(depth: number): string {
47
47
  */
48
48
  function headingLine(r: RequirementRow): string {
49
49
  const title = r.title?.replace(/\s+/g, " ").trim();
50
- const label = title === undefined || title === "" ? "" : ` — ${title}`;
50
+ const label =
51
+ title === undefined || title === "" || titleRestatesLeaf(r.path, title)
52
+ ? ""
53
+ : ` — ${title}`;
51
54
  return `${heading(r.depth)} ${r.path}${label}`;
52
55
  }
53
56
 
57
+ /**
58
+ * True when the title says nothing the path's LEAF SEGMENT does not already say.
59
+ *
60
+ * Measured on a hand-curated 262-entry ledger: **64 headings (24%)** read
61
+ * `## security — Security`, `### security.integrity — Integrity`. That is not bad
62
+ * authoring — `title` is chartered as "a short display label", and for a node called
63
+ * `integrity` the honest label IS "Integrity". The redundancy is in the RENDER, so
64
+ * the render is what changes.
65
+ *
66
+ * Only the LEAF is compared, never the whole path: a node `security.integrity`
67
+ * titled "Data Integrity" keeps its title, and one titled "Integrity" loses it. And
68
+ * the path always survives — dropping the path instead would break the two
69
+ * properties the heading is built on (it is unique by construction, so anchors
70
+ * cannot collide, and every sibling surface names a requirement by it).
71
+ *
72
+ * Comparison is case- and separator-insensitive so `data-integrity` / "Data
73
+ * Integrity" / "data integrity" all count as the same word.
74
+ */
75
+ function titleRestatesLeaf(path: string, title: string): boolean {
76
+ const normalize = (s: string): string => s.toLowerCase().replace(/[^a-z0-9]/g, "");
77
+ const leaf = path.slice(path.lastIndexOf(".") + 1);
78
+ return normalize(leaf) === normalize(title);
79
+ }
80
+
54
81
  /** The one-line fact bar: the attrs that are scannable rather than readable. */
55
82
  function facts(r: RequirementRow): string {
56
83
  const parts: string[] = [`\`${r.subType}\``];
@@ -6,6 +6,7 @@ import { formatTs } from "../format.js";
6
6
  import { entityOutputPath } from "../import-path.js";
7
7
  import { isTphSubtype } from "../templates/zod-validators.js";
8
8
  import { resolveExpose, type ExposeOption } from "../routes-expose.js";
9
+ import { effectivePackage } from "../docs-paths.js";
9
10
 
10
11
  export interface RoutesFileHonoOpts {
11
12
  filter?: (entity: MetaObject) => boolean;
@@ -87,7 +88,7 @@ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts)
87
88
  return {
88
89
  path: entityOutputPath(
89
90
  ctx.config.outputLayout ?? "flat",
90
- entity.package,
91
+ effectivePackage(entity),
91
92
  `${entity.name}.routes.hono.ts`,
92
93
  ),
93
94
  content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
@@ -6,6 +6,7 @@ import { hasAnyRdbSource } from "../source-detect.js";
6
6
  import { formatTs } from "../format.js";
7
7
  import { entityOutputPath } from "../import-path.js";
8
8
  import { resolveExpose, type ExposeOption } from "../routes-expose.js";
9
+ import { effectivePackage } from "../docs-paths.js";
9
10
 
10
11
  export interface RoutesFileOpts {
11
12
  filter?: (entity: MetaObject) => boolean;
@@ -49,7 +50,7 @@ export const routesFile = function routesFile(opts?: RoutesFileOpts): Generator
49
50
  throw new Error("routes-file: renderContext is required (provided by runGen)");
50
51
  }
51
52
  return {
52
- path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity.package, `${entity.name}.routes.ts`),
53
+ path: entityOutputPath(ctx.config.outputLayout ?? "flat", effectivePackage(entity), `${entity.name}.routes.ts`),
53
54
  content: await formatTs(renderRoutesFile(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
54
55
  };
55
56
  }),
package/src/index.ts CHANGED
@@ -8,6 +8,10 @@ export type { RunGenOpts, RunGenResult } from "./runner.js";
8
8
 
9
9
  export type { Generator, GenContext, EmittedFile, GeneratorFactory } from "./generator.js";
10
10
  export { perEntity, perPackage, perModel, oncePerRun } from "./generator.js";
11
+ // The one aggregation of the UI-tier markers. Exported because `meta docs` is the
12
+ // SECOND door onto `agent/ui.md` and must answer "does this run emit a UI tier?"
13
+ // with the same function the gen runner uses, not a second derivation of it.
14
+ export { runEmitsUiTier, runEmitsHonoRoutes, UI_TIER_GENERATOR_NAMES } from "./ui-tier-gate.js";
11
15
 
12
16
  // SP-1 declarative Mustache template-codegen — scope walks, neutral data dict,
13
17
  // output-pattern, and the JSON template-spec the CLI ports reuse.
@@ -42,7 +46,7 @@ export { defineConfig, normalizeConfig, resolveGenerators, resolveDocsConfig } f
42
46
  // The dialect every generator falls back to when a config declares none. Exported because
43
47
  // `meta docs` must apply the SAME default `normalizeConfig` does — a command that resolved
44
48
  // the dialect differently would document a schema the toolchain does not produce.
45
- export { DEFAULT_DIALECT } from "./metaobjects-config.js";
49
+ export { DEFAULT_DIALECT, DEFAULT_DOCS_DIR } from "./metaobjects-config.js";
46
50
  // The runner's own "does this model need a dialect?" guard. `meta docs` must ask it rather
47
51
  // than defaulting, because DEFAULT_DIALECT is inert and a DB project with no dialect is one
48
52
  // `meta gen` REFUSES — documenting it as sqlite states an answer the toolchain never gave.
@@ -50,7 +54,7 @@ export { dbEmittingObjects, missingDialectMessage } from "./db-emitting.js";
50
54
  export { apiLabel } from "./generators/api-label.js";
51
55
 
52
56
  export type { ColumnSpec, DefaultExpr } from "./column-mapper.js";
53
- export { mapColumnType } from "./column-mapper.js";
57
+ export { mapColumnType, PG_IDENTITY_CAPABLE_FNS } from "./column-mapper.js";
54
58
 
55
59
  export type { PkInfo } from "./pk-resolver.js";
56
60
  export { buildPkMap } from "./pk-resolver.js";
@@ -68,7 +72,7 @@ export type {
68
72
  BaselineMode,
69
73
  DecideAndWriteOpts,
70
74
  } from "./overwrite-policy.js";
71
- export { decideAndWrite, GitMissingError } from "./overwrite-policy.js";
75
+ export { decideAndWrite, GitMissingError, WRITE_STATUSES } from "./overwrite-policy.js";
72
76
  // The committed half of `.gen-state`. Exported for `verify --codegen`, which needs
73
77
  // the same "is this still exactly what the generator wrote?" evidence the write path
74
78
  // uses — without it the gate cannot tell a preserved hand edit from stale output.
@@ -78,12 +82,17 @@ export { decideAndWrite, GitMissingError } from "./overwrite-policy.js";
78
82
  export { contentHash, readGeneratedHash, listGeneratedPaths } from "./overwrite-policy.js";
79
83
 
80
84
  export { CodegenError } from "./errors.js";
81
- export { GENERATED_HEADER, DEFAULT_OUT_DIR, RETIRED_CODEGEN_ATTRS, type RetiredCodegenAttr } from "./constants.js";
85
+ export { GENERATED_HEADER, NAMES_FILE_SUFFIX, DEFAULT_OUT_DIR, RETIRED_CODEGEN_ATTRS, type RetiredCodegenAttr } from "./constants.js";
82
86
  export { warnRetiredCodegenAttrs } from "./retired-codegen-attrs.js";
83
87
 
84
88
  export { formatTs } from "./format.js";
85
89
 
86
- export { pluralize, columnNameFromField, tableNameFromEntity, viewNameFromProjection } from "./naming.js";
90
+ export { pluralize, columnNameFromField, tableNameFromEntity, viewNameFromProjection, routesHandlerName } from "./naming.js";
91
+ // routesHandlerName is public for the same reason the routes-expose members below are: the
92
+ // routes reference template tells an adopter retargeting to another HTTP framework to copy
93
+ // `renderRoutesFile`'s body out of the package source, and that body names this. See the
94
+ // owned-composition-imports gate in test/ — it derives the required set from that body rather
95
+ // than trusting this list to stay complete.
87
96
 
88
97
  export { packageToPath, entityOutputPath, crossEntitySpecifier, barrelEntrySpecifier, relativeModuleSpecifier, entityModuleSpecifier, siblingSpecifier, barrelModuleSpecifier } from "./import-path.js";
89
98
  export type { OutputLayout, ResolvedTarget } from "./import-path.js";
@@ -122,7 +131,14 @@ export { isTphSubtype, tphDiscriminatorBase, tphDiscriminatorPin } from "./templ
122
131
  // emitter, say) marks a column sortable by ASKING the server's rule rather than
123
132
  // reimplementing its three branches out of tree. A hand-copied predicate is how the
124
133
  // grid came to offer headers the allowlist rejects (#352/#354).
125
- export { isSortableField, sortableFields } from "./templates/filter-shared.js";
134
+ export {
135
+ isSortableField,
136
+ sortableFields,
137
+ declaredSortDefaultOrder,
138
+ sortDefaultOrder,
139
+ resolveGridDefaultSort,
140
+ } from "./templates/filter-shared.js";
141
+ export type { SortOrder, GridDefaultSort } from "./templates/filter-shared.js";
126
142
 
127
143
  // ADR-0034 reference-template composition helpers. Promoted to the public engine
128
144
  // surface so a COPIED reference generator (src/reference/*.ts → consumer's
@@ -194,7 +210,7 @@ export { renderEntityConstants, resourcePath } from "./templates/entity-constant
194
210
  export { renderQueriesFile } from "./templates/queries-file.js";
195
211
  // #348 — which CRUD verbs a generated routes file mounts. Public because an OWNED
196
212
  // routes generator (ADR-0034) composes the same render call and needs the same option.
197
- export { CRUD_VERBS, resolveExpose, intersectExpose, exposeLine } from "./routes-expose.js";
213
+ export { CRUD_VERBS, TPH_POLYMORPHIC_VERBS, resolveExpose, intersectExpose, exposeLine } from "./routes-expose.js";
198
214
  export type { CrudVerb, ExposeOption } from "./routes-expose.js";
199
215
  export { renderRoutesFile } from "./templates/routes-file.js";
200
216
  export { renderRoutesFileHono } from "./templates/routes-file-hono.js";
@@ -63,6 +63,11 @@ export interface ResolvedGenConfig {
63
63
  * Undefined ⇒ false. Templates read the same fact off `RenderContext.includeNames`,
64
64
  * which the runner sets from this same aggregation. */
65
65
  includeNames?: boolean;
66
+ /** Whether any CLIENT UI generator (form, hook, grid, columns) is active in the
67
+ * run — aggregated by the runner from the suite's `emitsUiTier` markers.
68
+ * `agent/ui.md` reads this to decide whether a UI tier exists to describe;
69
+ * a metadata predicate can only say a UI *could* be generated. Undefined ⇒ false. */
70
+ includeUiTier?: boolean;
66
71
  /**
67
72
  * FR-019 / ADR-0026: the module specifier from which an externally-PROVIDED
68
73
  * shared enum (`@provided: true` on an abstract package-level `field.enum`) is
@@ -289,13 +294,27 @@ export interface ResolvedDocsConfig {
289
294
  /** Merge the config `docs:` block with CLI overrides over documented defaults.
290
295
  * `fallbackLayout` is the project's `outputLayout` so docs default to the same
291
296
  * page placement as codegen when `docs.layout` is unset. */
297
+ /**
298
+ * Where `meta docs` writes when nothing says otherwise. A SUB-directory on purpose: the
299
+ * generated pages are MetaObjects' to own and overwrite, and `docs/` as a whole is not.
300
+ */
301
+ export const DEFAULT_DOCS_DIR = "./docs/generated";
302
+
292
303
  export function resolveDocsConfig(
293
304
  block: DocsConfig | undefined,
294
305
  cli: Partial<ResolvedDocsConfig>,
295
306
  fallbackLayout: OutputLayout,
296
307
  ): ResolvedDocsConfig {
297
308
  return {
298
- outDir: cli.outDir ?? block?.outDir ?? "./docs",
309
+ // F57 NOT "./docs". `docs/` is the human documentation folder in most repos: one
310
+ // estate's held a business-strategy .docx, product screenshots, email drafts and an
311
+ // analytics roadmap, and a bare `meta docs` scattered 29 generated pages through it
312
+ // and wanted to own `docs/README.md`. The gate's own design language is right —
313
+ // "docs.outDir is a directory, not a namespace MetaObjects owns" — and that is
314
+ // precisely the argument for not defaulting into the most-owned directory name in
315
+ // the ecosystem. `meta init` scaffolds this key explicitly so a new project's choice
316
+ // is visible rather than implied.
317
+ outDir: cli.outDir ?? block?.outDir ?? DEFAULT_DOCS_DIR,
299
318
  layout: cli.layout ?? block?.layout ?? fallbackLayout,
300
319
  baseUrl: cli.baseUrl ?? block?.baseUrl ?? "",
301
320
  // `requirements` defaults ON. Safe ONLY because requirementsFile() emits ZERO
package/src/names.ts CHANGED
@@ -26,6 +26,7 @@ import {
26
26
  import { code, imp, type Code } from "ts-poet";
27
27
  import { crossEntitySpecifier } from "./import-path.js";
28
28
  import type { RenderContext } from "./render-context.js";
29
+ import { effectivePackage } from "./docs-paths.js";
29
30
 
30
31
  export interface FieldNames { readonly name: string; readonly column: string; }
31
32
 
@@ -335,7 +336,7 @@ export function resolveObjectNames(
335
336
  const superObj = namesArtifactSuperOf(obj);
336
337
  const superNames: SuperNames | undefined = superObj === undefined
337
338
  ? undefined
338
- : { name: superObj.name, package: superObj.package };
339
+ : { name: superObj.name, package: effectivePackage(superObj) };
339
340
  // Identity of the resolved source NODE, not equality of the resolved strings: a
340
341
  // divergence guard is exactly what this codebase forbids here, and the question being
341
342
  // asked is structural — did this object declare a source, or is it using its parent's?
@@ -438,7 +439,7 @@ export function resolveSuperFragmentNames(
438
439
  ownIndexes: keysOf(obj.ownLookupIndexes()),
439
440
  superNames: superObj === undefined
440
441
  ? undefined
441
- : { name: superObj.name, package: superObj.package },
442
+ : { name: superObj.name, package: effectivePackage(superObj) },
442
443
  inheritsSource: false,
443
444
  };
444
445
  }
@@ -466,7 +467,7 @@ export function resolveSuperFragmentNames(
466
467
  export function namesRef(
467
468
  obj: MetaObject,
468
469
  ctx: RenderContext,
469
- fromPackage: string | undefined = obj.package,
470
+ fromPackage: string | undefined = effectivePackage(obj),
470
471
  ): { readonly resolved: ObjectNames; readonly symbol: Code } | undefined {
471
472
  if (!ctx.includeNames) return undefined;
472
473
  const resolved = resolveObjectNames(obj, ctx.columnNamingStrategy);
@@ -475,7 +476,7 @@ export function namesRef(
475
476
  `${obj.name}Names@${crossEntitySpecifier(
476
477
  ctx.selfTarget.outputLayout,
477
478
  fromPackage,
478
- obj.package,
479
+ effectivePackage(obj),
479
480
  `${obj.name}.names`,
480
481
  ctx.extStyle,
481
482
  )}`,
@@ -45,20 +45,36 @@ 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",
63
+ /** `--baseline=adopt` — the file WOULD have been refused, and instead its current
64
+ * content was recorded as the merge base. Nothing was written. Its own status
65
+ * rather than "unchanged" or "skipped" because the run's whole product is the
66
+ * manifest it just created: a summary that reported these as unchanged would
67
+ * hide the one thing the adopter has to commit. */
68
+ "adopted",
56
69
  /** FR-038 §8 — deleted because it was generated by a previous run, is no longer
57
70
  * generated, and was never edited by hand. Reported as a file outcome rather
58
71
  * than a warning because a deletion is exactly as consequential as a write,
59
72
  * and a run summary that lists writes but hides deletions is how a silent
60
73
  * deletion happens. */
61
- | "removed";
74
+ "removed",
75
+ ] as const;
76
+
77
+ export type WriteStatus = (typeof WRITE_STATUSES)[number];
62
78
 
63
79
  /**
64
80
  * "overwrite" — default; three-way merge if .gen-state exists, else write-if-
@@ -71,8 +87,20 @@ export type MergeStrategy = "overwrite" | "skip-existing";
71
87
  /** "default" — the standard three-way merge flow described in the file header.
72
88
  * "fresh" — opt-in via `meta gen --baseline=fresh`. When .gen-state is absent
73
89
  * but the file exists, OVERWRITE with fresh content and seed .gen-state from
74
- * the fresh content (caveat 3 escape hatch). */
75
- export type BaselineMode = "default" | "fresh";
90
+ * the fresh content (caveat 3 escape hatch).
91
+ * "adopt" opt-in via `meta gen --baseline=adopt`. Same population, opposite
92
+ * half: where the run would REFUSE, record the file's CURRENT content as the
93
+ * merge base and write nothing. It exists because the refusal's leading remedy
94
+ * ("commit .hashes.json") could not be performed by the population it named —
95
+ * nothing writes a manifest until a gen succeeds, and gen refuses until one
96
+ * exists. `adopt` is the run that produces the file to commit.
97
+ *
98
+ * What it is NOT: protection for an edit already inside one of those files. The
99
+ * base becomes the edited text, so base == ours and the next regen's three-way
100
+ * merge takes fresh output wholesale. It protects edits made FROM THEN ON, and
101
+ * it guarantees that establishing the baseline writes nothing — which is what
102
+ * lets the content change land as its own reviewable diff afterwards. */
103
+ export type BaselineMode = "default" | "fresh" | "adopt";
76
104
 
77
105
  export interface DecideAndWriteOpts {
78
106
  strategy?: MergeStrategy;
@@ -458,6 +486,7 @@ type WriteCase =
458
486
  | { kind: "no-snapshot-unchanged" }
459
487
  | { kind: "no-snapshot-pristine-overwrite" }
460
488
  | { kind: "no-snapshot-refused"; hasRecord: boolean }
489
+ | { kind: "no-snapshot-adopt"; currentText: string }
461
490
  | { kind: "snapshot-unchanged" }
462
491
  | { kind: "snapshot-merge-required"; snapshotText: string };
463
492
 
@@ -511,6 +540,18 @@ function classifyWrite(
511
540
 
512
541
  // Either somebody edited it (hash mismatch) or we have no record of writing
513
542
  // it at all (no hash). Both are unprovable, so fail closed.
543
+ //
544
+ // …unless the caller has explicitly adopted what is on disk. `adopt` is placed
545
+ // HERE, at the refusal, rather than at the top of the no-snapshot branch, and
546
+ // that placement is the design: it means adopting can only ever convert a
547
+ // refusal into a recorded baseline. Everything else — unchanged, and the
548
+ // pristine-file overwrite a fresh clone depends on — keeps behaving exactly as
549
+ // it does without the flag, so a project cannot accidentally freeze its
550
+ // regeneration by passing it.
551
+ if ((opts.baseline ?? "default") === "adopt") {
552
+ return { kind: "no-snapshot-adopt", currentText: current };
553
+ }
554
+
514
555
  return {
515
556
  kind: "no-snapshot-refused",
516
557
  hasRecord: readGeneratedHash(genStateDir, relPath) !== undefined,
@@ -560,6 +601,8 @@ export function previewWriteStatus(
560
601
  return "overwrite";
561
602
  case "no-snapshot-refused":
562
603
  return "refused";
604
+ case "no-snapshot-adopt":
605
+ return "adopted";
563
606
  case "snapshot-merge-required":
564
607
  return "overwrite";
565
608
  }
@@ -631,6 +674,14 @@ export function decideAndWrite(
631
674
  "--baseline=fresh to overwrite it and adopt fresh output as the baseline.",
632
675
  };
633
676
 
677
+ case "no-snapshot-adopt":
678
+ // Record what is ON DISK — deliberately `kase.currentText`, never `content`.
679
+ // Recording fresh output would claim we wrote a file we did not, and the next
680
+ // run would then read the adopter's file as edited-since-generated and refuse
681
+ // all over again, which is the loop this mode exists to end.
682
+ advanceSnapshot(genStateDir, relPath, kase.currentText);
683
+ return { path, status: "adopted" };
684
+
634
685
  case "snapshot-unchanged":
635
686
  return { path, status: "unchanged" };
636
687
 
@@ -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
  });
@@ -36,14 +36,30 @@ import {
36
36
  namesArtifactSuperOf,
37
37
  renderNamesDecl,
38
38
  resolveObjectNames,
39
+ NAMES_FILE_SUFFIX,
39
40
  type EmittedFile,
40
41
  type GenContext,
41
42
  type Generator,
43
+ effectivePackage,
42
44
  } from "@metaobjectsdev/codegen-ts";
43
45
  import type { MetaObject } from "@metaobjectsdev/metadata";
44
46
 
45
- export function namesFile(): Generator {
46
- return {
47
+ export interface NamesFileOpts {
48
+ /**
49
+ * Narrow which objects get a names artifact. ANDed with the generator's own
50
+ * gates, so it can only narrow — the abstract bases a MATCHED object extends
51
+ * are still emitted, because the matched object's artifact `extends` them.
52
+ *
53
+ * A multi-package model is why this exists: one metadata tree can drive several
54
+ * consumers, and only some of its objects belong to the tier being generated.
55
+ */
56
+ filter?: (entity: MetaObject) => boolean;
57
+ /** Named output target, as on every other generator. */
58
+ target?: string;
59
+ }
60
+
61
+ export function namesFile(opts?: NamesFileOpts): Generator {
62
+ const generator: Generator = {
47
63
  name: "names",
48
64
  // §A6 — the marker the runner aggregates into ResolvedGenConfig.includeNames, so the
49
65
  // entity generator can tell whether this artifact will exist. Exactly the mechanism
@@ -63,13 +79,16 @@ export function namesFile(): Generator {
63
79
  // its entity sits at <pkg>/<Entity>.ts — an unresolvable import, and a hard
64
80
  // conflicting-duplicate-path failure as soon as two packages declare a
65
81
  // same-bare-named entity.
66
- entityOutputPath(layout, obj.package, `${obj.name}.names.ts`);
82
+ // NAMES_FILE_SUFFIX, not a literal: `meta verify --codegen` treats this artifact as
83
+ // fail-closed (a hand edit in it IS drift, unlike every other generated file) and
84
+ // recognises it BY THIS SUFFIX. Rename it and you opt out of that protection.
85
+ entityOutputPath(layout, effectivePackage(obj), `${obj.name}${NAMES_FILE_SUFFIX}`);
67
86
 
68
87
  const superSpecifierFor = (obj: MetaObject): string | undefined => {
69
88
  const sup = namesArtifactSuperOf(obj);
70
89
  return sup === undefined
71
90
  ? undefined
72
- : crossEntitySpecifier(layout, obj.package, sup.package, `${sup.name}.names`, extStyle);
91
+ : crossEntitySpecifier(layout, effectivePackage(obj), effectivePackage(sup), `${sup.name}.names`, extStyle);
73
92
  };
74
93
 
75
94
  const out: EmittedFile[] = [];
@@ -122,4 +141,11 @@ export function namesFile(): Generator {
122
141
  return out;
123
142
  },
124
143
  };
144
+ if (opts?.filter) {
145
+ generator.filter = opts.filter;
146
+ }
147
+ if (opts?.target) {
148
+ generator.target = opts.target;
149
+ }
150
+ return generator;
125
151
  }
@@ -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
  }),