@metaobjectsdev/cli 1.0.12 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +32 -22
  2. package/dist/bin/meta.js +14 -2
  3. package/dist/bin/meta.js.map +1 -1
  4. package/dist/src/commands/docs.d.ts +1 -14
  5. package/dist/src/commands/docs.d.ts.map +1 -1
  6. package/dist/src/commands/docs.js +11 -835
  7. package/dist/src/commands/docs.js.map +1 -1
  8. package/dist/src/commands/eject.d.ts +24 -1
  9. package/dist/src/commands/eject.d.ts.map +1 -1
  10. package/dist/src/commands/eject.js +240 -69
  11. package/dist/src/commands/eject.js.map +1 -1
  12. package/dist/src/commands/gen.d.ts +7 -0
  13. package/dist/src/commands/gen.d.ts.map +1 -1
  14. package/dist/src/commands/gen.js +91 -21
  15. package/dist/src/commands/gen.js.map +1 -1
  16. package/dist/src/commands/generator.js +2 -2
  17. package/dist/src/commands/generator.js.map +1 -1
  18. package/dist/src/commands/init.d.ts +7 -0
  19. package/dist/src/commands/init.d.ts.map +1 -1
  20. package/dist/src/commands/init.js +71 -36
  21. package/dist/src/commands/init.js.map +1 -1
  22. package/dist/src/commands/migrate.d.ts +1 -1
  23. package/dist/src/commands/migrate.d.ts.map +1 -1
  24. package/dist/src/commands/migrate.js +224 -125
  25. package/dist/src/commands/migrate.js.map +1 -1
  26. package/dist/src/commands/types.d.ts.map +1 -1
  27. package/dist/src/commands/types.js +16 -2
  28. package/dist/src/commands/types.js.map +1 -1
  29. package/dist/src/commands/upgrade.d.ts.map +1 -1
  30. package/dist/src/commands/upgrade.js +4 -1
  31. package/dist/src/commands/upgrade.js.map +1 -1
  32. package/dist/src/commands/verify.d.ts.map +1 -1
  33. package/dist/src/commands/verify.js +189 -125
  34. package/dist/src/commands/verify.js.map +1 -1
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/index.js +59 -72
  37. package/dist/src/index.js.map +1 -1
  38. package/dist/src/lib/agent-context-staleness.d.ts +8 -1
  39. package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
  40. package/dist/src/lib/agent-context-staleness.js +20 -5
  41. package/dist/src/lib/agent-context-staleness.js.map +1 -1
  42. package/dist/src/lib/agent-schema.d.ts +9 -0
  43. package/dist/src/lib/agent-schema.d.ts.map +1 -0
  44. package/dist/src/lib/agent-schema.js +61 -0
  45. package/dist/src/lib/agent-schema.js.map +1 -0
  46. package/dist/src/lib/allow.d.ts +6 -0
  47. package/dist/src/lib/allow.d.ts.map +1 -1
  48. package/dist/src/lib/allow.js +49 -5
  49. package/dist/src/lib/allow.js.map +1 -1
  50. package/dist/src/lib/anti-patterns.d.ts.map +1 -1
  51. package/dist/src/lib/anti-patterns.js +4 -2
  52. package/dist/src/lib/anti-patterns.js.map +1 -1
  53. package/dist/src/lib/args.d.ts +51 -10
  54. package/dist/src/lib/args.d.ts.map +1 -1
  55. package/dist/src/lib/args.js +44 -18
  56. package/dist/src/lib/args.js.map +1 -1
  57. package/dist/src/lib/catalog-listing.d.ts +21 -1
  58. package/dist/src/lib/catalog-listing.d.ts.map +1 -1
  59. package/dist/src/lib/catalog-listing.js +85 -16
  60. package/dist/src/lib/catalog-listing.js.map +1 -1
  61. package/dist/src/lib/catalog.d.ts +30 -0
  62. package/dist/src/lib/catalog.d.ts.map +1 -1
  63. package/dist/src/lib/catalog.js +40 -5
  64. package/dist/src/lib/catalog.js.map +1 -1
  65. package/dist/src/lib/codegen-drift.d.ts +31 -2
  66. package/dist/src/lib/codegen-drift.d.ts.map +1 -1
  67. package/dist/src/lib/codegen-drift.js +109 -4
  68. package/dist/src/lib/codegen-drift.js.map +1 -1
  69. package/dist/src/lib/collection-load-options.d.ts +1 -1
  70. package/dist/src/lib/collection-load-options.d.ts.map +1 -1
  71. package/dist/src/lib/collection-load-options.js +1 -0
  72. package/dist/src/lib/collection-load-options.js.map +1 -1
  73. package/dist/src/lib/config.d.ts +4 -2
  74. package/dist/src/lib/config.d.ts.map +1 -1
  75. package/dist/src/lib/config.js +4 -2
  76. package/dist/src/lib/config.js.map +1 -1
  77. package/dist/src/lib/decimal-precision-advisory.d.ts +10 -0
  78. package/dist/src/lib/decimal-precision-advisory.d.ts.map +1 -0
  79. package/dist/src/lib/decimal-precision-advisory.js +34 -0
  80. package/dist/src/lib/decimal-precision-advisory.js.map +1 -0
  81. package/dist/src/lib/deprecation-lint.d.ts.map +1 -1
  82. package/dist/src/lib/deprecation-lint.js +17 -26
  83. package/dist/src/lib/deprecation-lint.js.map +1 -1
  84. package/dist/src/lib/docs-removed.d.ts +13 -0
  85. package/dist/src/lib/docs-removed.d.ts.map +1 -0
  86. package/dist/src/lib/docs-removed.js +31 -0
  87. package/dist/src/lib/docs-removed.js.map +1 -0
  88. package/dist/src/lib/docs-site-eject.d.ts +13 -0
  89. package/dist/src/lib/docs-site-eject.d.ts.map +1 -0
  90. package/dist/src/lib/docs-site-eject.js +40 -0
  91. package/dist/src/lib/docs-site-eject.js.map +1 -0
  92. package/dist/src/lib/field-lint.d.ts +25 -0
  93. package/dist/src/lib/field-lint.d.ts.map +1 -0
  94. package/dist/src/lib/field-lint.js +102 -0
  95. package/dist/src/lib/field-lint.js.map +1 -0
  96. package/dist/src/lib/fk-index-advisory.d.ts +1 -2
  97. package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
  98. package/dist/src/lib/fk-index-advisory.js +4 -3
  99. package/dist/src/lib/fk-index-advisory.js.map +1 -1
  100. package/dist/src/lib/format.d.ts +7 -0
  101. package/dist/src/lib/format.d.ts.map +1 -1
  102. package/dist/src/lib/format.js +13 -0
  103. package/dist/src/lib/format.js.map +1 -1
  104. package/dist/src/lib/gen-migrate-advice.d.ts +3 -1
  105. package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
  106. package/dist/src/lib/gen-migrate-advice.js +3 -4
  107. package/dist/src/lib/gen-migrate-advice.js.map +1 -1
  108. package/dist/src/lib/install-set.d.ts +10 -1
  109. package/dist/src/lib/install-set.d.ts.map +1 -1
  110. package/dist/src/lib/install-set.js +20 -2
  111. package/dist/src/lib/install-set.js.map +1 -1
  112. package/dist/src/lib/load-metaobjects-config.d.ts +13 -11
  113. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  114. package/dist/src/lib/load-metaobjects-config.js +53 -75
  115. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  116. package/dist/src/lib/manifest-ignored-check.d.ts +4 -1
  117. package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
  118. package/dist/src/lib/manifest-ignored-check.js +9 -4
  119. package/dist/src/lib/manifest-ignored-check.js.map +1 -1
  120. package/dist/src/lib/output.d.ts +15 -0
  121. package/dist/src/lib/output.d.ts.map +1 -1
  122. package/dist/src/lib/output.js +9 -1
  123. package/dist/src/lib/output.js.map +1 -1
  124. package/dist/src/lib/payload-field-tree.d.ts +4 -5
  125. package/dist/src/lib/payload-field-tree.d.ts.map +1 -1
  126. package/dist/src/lib/payload-field-tree.js +16 -31
  127. package/dist/src/lib/payload-field-tree.js.map +1 -1
  128. package/dist/src/lib/requirement-check.d.ts +7 -0
  129. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  130. package/dist/src/lib/requirement-check.js +14 -7
  131. package/dist/src/lib/requirement-check.js.map +1 -1
  132. package/dist/src/lib/strict-args.d.ts +28 -0
  133. package/dist/src/lib/strict-args.d.ts.map +1 -0
  134. package/dist/src/lib/strict-args.js +49 -0
  135. package/dist/src/lib/strict-args.js.map +1 -0
  136. package/dist/src/lib/template-eject.d.ts +108 -0
  137. package/dist/src/lib/template-eject.d.ts.map +1 -0
  138. package/dist/src/lib/template-eject.js +438 -0
  139. package/dist/src/lib/template-eject.js.map +1 -0
  140. package/dist/src/lib/version.d.ts +9 -0
  141. package/dist/src/lib/version.d.ts.map +1 -1
  142. package/dist/src/lib/version.js +15 -0
  143. package/dist/src/lib/version.js.map +1 -1
  144. package/package.json +10 -10
  145. package/src/commands/docs.ts +11 -952
  146. package/src/commands/eject.ts +318 -73
  147. package/src/commands/gen.ts +94 -22
  148. package/src/commands/generator.ts +2 -2
  149. package/src/commands/init.ts +80 -38
  150. package/src/commands/migrate.ts +248 -117
  151. package/src/commands/types.ts +18 -2
  152. package/src/commands/upgrade.ts +5 -1
  153. package/src/commands/verify.ts +229 -136
  154. package/src/index.ts +59 -72
  155. package/src/lib/agent-context-staleness.ts +20 -5
  156. package/src/lib/agent-schema.ts +74 -0
  157. package/src/lib/allow.ts +57 -6
  158. package/src/lib/anti-patterns.ts +4 -2
  159. package/src/lib/args.ts +83 -28
  160. package/src/lib/catalog-listing.ts +105 -25
  161. package/src/lib/catalog.ts +51 -6
  162. package/src/lib/codegen-drift.ts +138 -3
  163. package/src/lib/collection-load-options.ts +2 -1
  164. package/src/lib/config.ts +5 -3
  165. package/src/lib/decimal-precision-advisory.ts +50 -0
  166. package/src/lib/deprecation-lint.ts +16 -24
  167. package/src/lib/docs-removed.ts +41 -0
  168. package/src/lib/docs-site-eject.ts +48 -0
  169. package/src/lib/field-lint.ts +122 -0
  170. package/src/lib/fk-index-advisory.ts +4 -4
  171. package/src/lib/format.ts +12 -0
  172. package/src/lib/gen-migrate-advice.ts +6 -4
  173. package/src/lib/install-set.ts +20 -1
  174. package/src/lib/load-metaobjects-config.ts +57 -78
  175. package/src/lib/manifest-ignored-check.ts +14 -10
  176. package/src/lib/output.ts +18 -1
  177. package/src/lib/payload-field-tree.ts +18 -35
  178. package/src/lib/requirement-check.ts +21 -6
  179. package/src/lib/strict-args.ts +52 -0
  180. package/src/lib/template-eject.ts +526 -0
  181. package/src/lib/version.ts +18 -0
  182. package/dist/src/lib/docs-drift.d.ts +0 -42
  183. package/dist/src/lib/docs-drift.d.ts.map +0 -1
  184. package/dist/src/lib/docs-drift.js +0 -264
  185. package/dist/src/lib/docs-drift.js.map +0 -1
  186. package/src/lib/docs-drift.ts +0 -312
@@ -21,7 +21,7 @@ import {
21
21
  import {
22
22
  isMetaObject,
23
23
  resolveColumnName,
24
- type ColumnNamingStrategy,
24
+ physicalNamingOf,
25
25
  type MetaData,
26
26
  } from "@metaobjectsdev/metadata";
27
27
  import {
@@ -41,7 +41,6 @@ export interface UnindexedFkFinding {
41
41
 
42
42
  export interface UnindexedFkOptions {
43
43
  dialect: Dialect;
44
- columnNamingStrategy: ColumnNamingStrategy;
45
44
  /** Skip an entity by FQN — imported (dependency) metadata is not the adopter's to index. */
46
45
  skip?: (fqn: string) => boolean;
47
46
  }
@@ -65,8 +64,9 @@ function pascal(s: string): string {
65
64
  export function scanForUnindexedForeignKeys(root: MetaData, opts: UnindexedFkOptions): UnindexedFkFinding[] {
66
65
  const { snapshot, provenance } = buildExpectedSchemaWithProvenance(root, {
67
66
  dialect: opts.dialect,
68
- columnNamingStrategy: opts.columnNamingStrategy,
69
67
  });
68
+ // ADR-0058: the same column rule the expected schema just used, from the loaded root.
69
+ const columnNamingStrategy = physicalNamingOf(root).columns;
70
70
  // ADR-0039: effective children — resolve rather than rely on root being unextended.
71
71
  const objectsByFqn = new Map(
72
72
  root.children().filter(isMetaObject).map((o) => [o.resolutionKey(), o] as const),
@@ -90,7 +90,7 @@ export function scanForUnindexedForeignKeys(root: MetaData, opts: UnindexedFkOpt
90
90
  const ref = entity.referenceIdentities().find((r) => {
91
91
  const cols = r.fields.map((f) => {
92
92
  const field = entity.fields().find((x) => x.name === f);
93
- return field !== undefined ? resolveColumnName(field, opts.columnNamingStrategy) : f;
93
+ return field !== undefined ? resolveColumnName(field, columnNamingStrategy) : f;
94
94
  });
95
95
  return cols.length === fk.columns.length && cols.every((c, i) => c === fk.columns[i]);
96
96
  });
package/src/lib/format.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { encode } from "@toon-format/toon";
2
+ import { log } from "./log.js";
2
3
 
3
4
  export type OutputFormat = "toon" | "json" | "text";
4
5
 
@@ -35,3 +36,14 @@ export function emitStructured(payload: unknown, fmt: OutputFormat): void {
35
36
  if (fmt === "json") console.log(JSON.stringify(payload, null, 2));
36
37
  else if (fmt === "toon") console.log(toonEncode(payload));
37
38
  }
39
+
40
+ /**
41
+ * One narration line, on the other side of the split {@link emitStructured} states:
42
+ * stdout in text format, stderr in a structured run, so a document never has a
43
+ * sentence in front of it. Every command that prints prose alongside a document
44
+ * routes it through here.
45
+ */
46
+ export function narrate(fmt: OutputFormat, line: string): void {
47
+ if (fmt === "text") log.info(line);
48
+ else log.warn(line);
49
+ }
@@ -26,6 +26,7 @@ import {
26
26
  parseWranglerConfig,
27
27
  readSnapshot,
28
28
  snapshotPath,
29
+ withoutFlywayHistory,
29
30
  type Change,
30
31
  type D1Binding,
31
32
  } from "@metaobjectsdev/migrate-ts";
@@ -43,10 +44,12 @@ export interface GenMigrateAdviceInput {
43
44
  dialect: "sqlite" | "postgres" | "mysql" | undefined;
44
45
  /** `.metaobjects/config.json` `migrate.dialect`, when set (may say `d1`). */
45
46
  migrateDialect: "sqlite" | "postgres" | "d1" | undefined;
46
- columnNamingStrategy: "snake_case" | "literal" | "kebab-case" | undefined;
47
47
  projectRoot: string;
48
48
  /** The migrations directory `meta migrate` uses (holds the committed snapshot). */
49
49
  migrateOutDir: string;
50
+ /** `.metaobjects/config.json` `migrate.format`. Under `flyway` the snapshot is read without
51
+ * Flyway's history table, as `meta migrate` reads it. */
52
+ migrateFormat: "default" | "flyway";
50
53
  /** Paths (any form) of the files this run wrote, merged or removed. */
51
54
  changedFiles: readonly string[];
52
55
  }
@@ -63,7 +66,6 @@ export async function genMigrateAdvice(input: GenMigrateAdviceInput): Promise<st
63
66
  try {
64
67
  built = buildExpectedSchemaWithProvenance(input.metadata, {
65
68
  dialect: schemaDialect,
66
- ...(input.columnNamingStrategy !== undefined ? { columnNamingStrategy: input.columnNamingStrategy } : {}),
67
69
  });
68
70
  } catch {
69
71
  // A model `meta migrate` would refuse (it says why); gen is not the place to report it.
@@ -75,7 +77,8 @@ export async function genMigrateAdvice(input: GenMigrateAdviceInput): Promise<st
75
77
  const snapPath = snapshotPath(resolvePath(input.projectRoot, input.migrateOutDir), schemaDialect);
76
78
  const snapshot = existsSync(snapPath) ? await readSnapshot(snapPath) : null;
77
79
  if (snapshot !== null) {
78
- const actual = { ...snapshot, views: [] };
80
+ const committed = input.migrateFormat === "flyway" ? withoutFlywayHistory(snapshot) : snapshot;
81
+ const actual = { ...committed, views: [] };
79
82
  try {
80
83
  const d = await diff({ expected, actual, dialect: schemaDialect });
81
84
  if (d.changes.every((c) => VIEW_CHANGE_KINDS.has(c.kind))) return undefined;
@@ -107,7 +110,6 @@ function mysqlAdvice(input: GenMigrateAdviceInput): string | undefined {
107
110
  try {
108
111
  provenance = buildExpectedSchemaWithProvenance(input.metadata, {
109
112
  dialect: "postgres",
110
- ...(input.columnNamingStrategy !== undefined ? { columnNamingStrategy: input.columnNamingStrategy } : {}),
111
113
  }).provenance;
112
114
  } catch {
113
115
  return undefined;
@@ -38,6 +38,19 @@ import type { PackageManifest } from "./package-manifest.js";
38
38
  * never gated — `.` is the one subpath every package exports.
39
39
  */
40
40
  export function peerRangesOf(packageName: string): Record<string, string> {
41
+ return manifestRangesOf(packageName, "peerDependencies");
42
+ }
43
+
44
+ /**
45
+ * The ranges a package declares for its own dependencies, or `{}` if they cannot be read.
46
+ * What a copied template module imports from outside the engine is read from the package it
47
+ * was copied from — never restated here, for the reason `peerRangesOf` gives.
48
+ */
49
+ export function dependencyRangesOf(packageName: string): Record<string, string> {
50
+ return manifestRangesOf(packageName, "dependencies");
51
+ }
52
+
53
+ function manifestRangesOf(packageName: string, field: "peerDependencies" | "dependencies"): Record<string, string> {
41
54
  try {
42
55
  const req = createRequire(import.meta.url);
43
56
  let dir = dirname(req.resolve(packageName));
@@ -50,7 +63,7 @@ export function peerRangesOf(packageName: string): Record<string, string> {
50
63
  };
51
64
  // Guard against stopping at a nested manifest that is not the package itself.
52
65
  if (manifest.name === packageName) {
53
- return (manifest.peerDependencies ?? {}) as Record<string, string>;
66
+ return (manifest[field] ?? {}) as Record<string, string>;
54
67
  }
55
68
  }
56
69
  const up = dirname(dir);
@@ -125,6 +138,9 @@ export function installSetFor(
125
138
  * project, and what that copy imports. Their output no longer imports the runtime package,
126
139
  * so it is not installed on their account; the copy's own imports are, instead. */
127
140
  ownedRuntime?: OwnedRuntimeInstall,
141
+ /** Third-party packages the copied template modules import, with the range the codegen
142
+ * package that ships them declares. Build-time, so dev. */
143
+ templateThirdParty: ReadonlyMap<string, string | undefined> = new Map(),
128
144
  ): InstallSet {
129
145
  const version = cliVersion();
130
146
  const dev = new Map<string, string | undefined>();
@@ -157,6 +173,9 @@ export function installSetFor(
157
173
  for (const p of templatePackages) {
158
174
  if (!runtime.has(p)) addPackage(dev, p, `^${version}`);
159
175
  }
176
+ for (const [name, range] of templateThirdParty) {
177
+ if (!runtime.has(name)) addPackage(dev, name, range);
178
+ }
160
179
 
161
180
  const devList = specs(dev);
162
181
  const runtimeList = specs(runtime);
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
2
+ import { physicalNamingGuideUrl } from "@metaobjectsdev/metadata/release-docs";
2
3
  import { readFile, writeFile } from "node:fs/promises";
3
4
  import { createRequire } from "node:module";
4
5
  import { dirname, join, resolve } from "node:path";
@@ -7,6 +8,7 @@ import { randomBytes } from "node:crypto";
7
8
  import { createJiti } from "jiti";
8
9
  import type { MetaDataTypeProvider, MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
9
10
  import { resolveCollection, type Collection } from "@metaobjectsdev/sdk";
11
+ import { physicalNamingRemovedMessage } from "@metaobjectsdev/metadata";
10
12
 
11
13
  const CONFIG_FILE = "metaobjects.config.ts";
12
14
 
@@ -132,88 +134,53 @@ function resolveCliPkg(specifier: string): string {
132
134
  * jiti's alias map on non-Bun runtimes).
133
135
  */
134
136
  /**
135
- * 1.0 removed `entityFile` / `queriesFile` / `routesFile` / `barrel` from
136
- * `@metaobjectsdev/codegen-ts/generators` (ADR-0035 A3; ADR-0034 scaffold-and-own). Without
137
- * this check the removal surfaces as a bare runtime TypeError from the transpiled config —
138
- * `(0, _index2.entityFile) is not a function` — which names neither MetaObjects, nor the
139
- * removal, nor the one-command remedy. A breaking change an adopter WILL hit deserves a
140
- * diagnostic, and this is the only place that can give one: the subpath still exists (it is
141
- * the supported home of the generators with no ownable copy), so nothing upstream of here
142
- * can tell a correct import from a removed one.
137
+ * Generator factories `@metaobjectsdev/codegen-ts/generators` used to export and no longer
138
+ * does, each with what an adopter does instead.
143
139
  *
144
- * It reads the config SOURCE because that is the arm that can name the FILE and the exact
145
- * import. `removedGeneratorRuntimeError` below is the second arm, and an adopter estate is
146
- * why there are two: the import does not have to be in the config. This project's config
147
- * imported an OWNED generator — `./codegen/generators/entityFileTyped`, the shape `meta
148
- * eject` itself produces — and THAT file carried the stale
149
- * `import { entityFile } from "@metaobjectsdev/codegen-ts/generators"`. The source scan saw
150
- * a clean config and the adopter got exactly the bare TypeError this check exists to
151
- * prevent: `(0 , _generators.entityFile) is not a function`, exit 2, no file, no remedy.
140
+ * Without this, a removal an adopter WILL hit surfaces as whatever the runtime says about a
141
+ * missing export: `(0 , _generators.mermaidErDiagram) is not a function` from a transpiled
142
+ * config, or `does not provide an export named 'mermaidErDiagram'` from native ESM. Neither
143
+ * names MetaObjects, the removal or the remedy. This is the only place that can: the
144
+ * subpath still exists and serves every other factory, so nothing upstream of here can tell
145
+ * a correct import from a removed one.
146
+ *
147
+ * The diagnostic is recovered from the THROW, in `removedGeneratorRuntimeError`, so it
148
+ * answers for a live import only, wherever that import is: the config itself or a module
149
+ * the config imports (an owned generator under `codegen/generators/`, typically).
150
+ *
151
+ * `mermaidErDiagram` (removed in 1.1) wrote the ER diagram to a file of its own. It was the
152
+ * older home of a diagram the `docs` generator (`docsFile()`) draws.
152
153
  */
153
- const REMOVED_GENERATOR_EXPORTS = ["entityFile", "queriesFile", "routesFile", "barrel"] as const;
154
+ const REMOVED_GENERATOR_EXPORTS: Readonly<Record<string, string>> = {
155
+ mermaidErDiagram:
156
+ "The `docs` generator (`docsFile()`) draws the ER diagram, for the whole model on its " +
157
+ "overview page (README.md) and around each entity on that entity's page.\n" +
158
+ " Fix: delete the import and the `mermaidErDiagram()` entry; list `docsFile()` instead, " +
159
+ "with a `docs` target, and run `meta gen`. " +
160
+ "The file it wrote (`docs/model.md` under your output directory, unless you moved it " +
161
+ "with `outFile`) is no longer generated; delete that too.",
162
+ };
154
163
  const GENERATORS_SUBPATH = "@metaobjectsdev/codegen-ts/generators";
155
164
 
156
- export function removedGeneratorImportError(source: string): string | undefined {
157
- const re = new RegExp(
158
- `import\\s*(?:type\\s+)?\\{([^}]*)\\}\\s*from\\s*['"]${GENERATORS_SUBPATH.replace(/[/\\]/g, "\\$&")}['"]`,
159
- "g",
160
- );
161
- const hits = new Set<string>();
162
- for (const m of source.matchAll(re)) {
163
- for (const raw of (m[1] ?? "").split(",")) {
164
- const name = raw.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim();
165
- if (name && (REMOVED_GENERATOR_EXPORTS as readonly string[]).includes(name)) hits.add(name);
166
- }
167
- }
168
- if (hits.size === 0) return undefined;
169
- const names = [...hits].sort();
170
- const templateOf = (n: string) => n.replace(/File$/, "");
171
- // One command per name: `meta eject` takes at most one generator name.
172
- const commands = names.map((n) => ` meta eject ${templateOf(n)}`).join("\n");
173
- const example = names[0] ?? "";
174
- return (
175
- `metaobjects.config.ts imports ${names.join(", ")} from "${GENERATORS_SUBPATH}". ` +
176
- `1.0 REMOVED ${names.length === 1 ? "that export" : "those exports"} (ADR-0034 scaffold-and-own): ` +
177
- `these generators are yours to own, so the package no longer ships them as an import.\n` +
178
- ` Fix — copy ${names.length === 1 ? "it" : "each of them"} into your repo:\n${commands}\n` +
179
- ` then import the copies, e.g. ` +
180
- `import { ${example} } from "./codegen/generators/${templateOf(example)}";\n` +
181
- ` The subpath itself is fine — promptRender, outputParser, routesFileHono and the rest ` +
182
- `still live there. Only ${names.length === 1 ? "this one" : `these ${names.length}`} moved.\n` +
183
- ` Guide: docs/features/migrations/0.x-to-1.0.md §11`
184
- );
185
- }
186
-
187
165
  /**
188
- * The same diagnostic, recovered from the THROW instead of the source — for the case the
189
- * source scan structurally cannot see: a removed export imported by a module the config
190
- * imports, rather than by the config itself.
191
- *
192
- * It cannot name the file (the TypeError carries no reference back to the import that
193
- * produced it — that limitation is real, it is just not a reason to say nothing), so it
194
- * names what it does know: the symbol, the removal, and the one-command remedy. Matching is
195
- * on the SYMBOL, not the message shape, because the shape is the transpiler's: Bun says
196
- * `(0 , _generators.entityFile) is not a function` and Node ESM says
197
- * `entityFile is not a function`.
166
+ * The diagnostic for a throw that names a removed factory; undefined for any other throw.
167
+ * It cannot name the file (the error carries no reference back to the import that produced
168
+ * it), so it says where to look instead. Matching is on the SYMBOL and on the fact of a
169
+ * missing export, not on one message shape: the shape is the runtime's, and an adopter's
170
+ * runtime is not ours to choose.
198
171
  */
199
172
  export function removedGeneratorRuntimeError(err: unknown): string | undefined {
200
173
  const message = err instanceof Error ? err.message : String(err ?? "");
201
- if (!/is not a function/.test(message)) return undefined;
202
- const hit = REMOVED_GENERATOR_EXPORTS.find((n) => new RegExp(`\\b${n}\\b`).test(message));
174
+ if (!/is not a function|does not provide an export named|[Ee]xport named .* not found/.test(message)) {
175
+ return undefined;
176
+ }
177
+ const hit = Object.keys(REMOVED_GENERATOR_EXPORTS).find((n) => new RegExp(`\\b${n}\\b`).test(message));
203
178
  if (hit === undefined) return undefined;
204
- const template = hit.replace(/File$/, "");
205
179
  return (
206
- `${hit}() is not a function — 1.0 REMOVED it from "${GENERATORS_SUBPATH}" ` +
207
- `(ADR-0034 scaffold-and-own): these generators are yours to own, so the package no ` +
208
- `longer ships them as an import. Your metaobjects.config.ts does not import it, so ` +
209
- `the import is in a module the config imports — most often an OWNED generator under ` +
210
- `codegen/generators/, which is exactly what 'meta eject' writes.\n` +
211
- ` Fix — copy it into your repo:\n meta eject ${template}\n` +
212
- ` then change that module's import to: ` +
213
- `import { ${hit} } from "./codegen/generators/${template}";\n` +
214
- ` The subpath itself is fine — promptRender, outputParser, routesFileHono and the ` +
215
- `rest still live there.\n` +
216
- ` Guide: docs/features/migrations/0.x-to-1.0.md §11`
180
+ `${hit} is no longer exported from "${GENERATORS_SUBPATH}": 1.1 removed that generator. ` +
181
+ `The import is in your metaobjects.config.ts or in a module the config imports, most ` +
182
+ `often one of your own under codegen/generators/. ` +
183
+ `${REMOVED_GENERATOR_EXPORTS[hit] ?? ""}`
217
184
  );
218
185
  }
219
186
 
@@ -342,6 +309,14 @@ export async function resolveGenCollection(
342
309
  }
343
310
  }
344
311
 
312
+ /**
313
+ * A `metaobjects.config.ts` that still sets column naming (ADR-0058). Its own class because
314
+ * the commands that tolerate a missing or broken config (`meta migrate` falls back to
315
+ * defaults) must NOT tolerate this one: falling back to the default rule would propose
316
+ * renaming every column of a database built with the old value.
317
+ */
318
+ export class RemovedNamingConfigError extends Error {}
319
+
345
320
  export async function loadMetaobjectsConfig(projectRoot: string): Promise<MetaobjectsGenConfig> {
346
321
  const fullPath = resolve(projectRoot, CONFIG_FILE);
347
322
  if (!existsSync(fullPath)) {
@@ -450,8 +425,6 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
450
425
  // a belt-and-suspenders fallback for runtimes where jiti's transformer IS
451
426
  // active, but under Bun the pre-processed source is the operative fix.
452
427
  const original = await readFile(fullPath, "utf8");
453
- const removed = removedGeneratorImportError(original);
454
- if (removed !== undefined) throw new Error(removed);
455
428
  const processed = rewriteImportSpecifiers(original, aliasMap);
456
429
 
457
430
  // When any specifiers were rewritten, write the modified source to a temp
@@ -501,10 +474,6 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
501
474
  try {
502
475
  raw = (await jiti.import(loadPath)) as MetaobjectsGenConfig | { default: MetaobjectsGenConfig };
503
476
  } catch (err) {
504
- // The second arm of the ADR-0034 diagnostic: a removed export imported by a module the
505
- // config imports rather than by the config itself. Rethrown as the migration message,
506
- // because what reaches the adopter otherwise is `(0 , _generators.entityFile) is not a
507
- // function` — the exact unactionable failure the source scan above exists to prevent.
508
477
  const runtime = removedGeneratorRuntimeError(err);
509
478
  if (runtime !== undefined) throw new Error(runtime);
510
479
  throw err;
@@ -531,6 +500,16 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
531
500
  `verbatim — the tokens are unchanged.`,
532
501
  );
533
502
  }
503
+ // ADR-0058 — column naming moved to `naming` in .metaobjects/config.json, the one
504
+ // setting every port reads. Refused, never dropped: the old value may be the one that
505
+ // matches this project's database, and dropping it would rename every column.
506
+ const removedNaming = (cfg as unknown as Record<string, unknown>).columnNamingStrategy;
507
+ if (removedNaming !== undefined) {
508
+ throw new RemovedNamingConfigError(
509
+ `metaobjects.config.ts at ${fullPath}: ` +
510
+ physicalNamingRemovedMessage("columnNamingStrategy in metaobjects.config.ts", String(removedNaming), physicalNamingGuideUrl()),
511
+ );
512
+ }
534
513
  return cfg;
535
514
  } finally {
536
515
  // Restoring is safe: Babel's installer self-neuters after the first call, so
@@ -30,15 +30,18 @@ export const HASH_MANIFEST_REL = join(".metaobjects", ".gen-state", ".hashes.jso
30
30
  * Silent when: it is tracked correctly, the project is not a git repository, git is
31
31
  * unavailable, or no generated output has ever been produced (nothing to protect yet).
32
32
  * Never throws, never blocks.
33
+ *
34
+ * Returns the line it printed, so a command with a structured result can carry it
35
+ * there too; undefined when it said nothing.
33
36
  */
34
- export function warnIfManifestIgnored(cwd: string): void {
37
+ export function warnIfManifestIgnored(cwd: string): string | undefined {
35
38
  // Nothing generated yet ⇒ nothing at risk ⇒ nothing worth saying.
36
- if (!existsSync(join(cwd, HASH_MANIFEST_REL))) return;
39
+ if (!existsSync(join(cwd, HASH_MANIFEST_REL))) return undefined;
37
40
 
38
41
  // honourGlobalExcludes: TRUE here, and that is the opposite call to the docs gate.
39
42
  // The question this warning asks is "will this file reach another machine?", and a
40
43
  // per-user ignore file answers it just as truly as a committed .gitignore does.
41
- if (isGitIgnored(cwd, HASH_MANIFEST_REL, { honourGlobalExcludes: true }) !== true) return;
44
+ if (isGitIgnored(cwd, HASH_MANIFEST_REL, { honourGlobalExcludes: true }) !== true) return undefined;
42
45
 
43
46
  // WHERE the rule actually lives, asked of git rather than assumed. This used to name
44
47
  // `.metaobjects/.gitignore` and the literal pattern `.gen-state/`, which is only one of
@@ -56,12 +59,13 @@ export function warnIfManifestIgnored(cwd: string): void {
56
59
  : `Find the rule with 'git check-ignore -v ${HASH_MANIFEST_REL}' and replace it with ` +
57
60
  `a '/*' form plus a '!' negation for the manifest`;
58
61
 
59
- log.warn(
62
+ const msg =
60
63
  `${HASH_MANIFEST_REL} is git-ignored, so it never reaches another machine — ` +
61
- `on a fresh clone or CI runner 'meta gen' cannot tell your hand edits from its ` +
62
- `own stale output, and will refuse to overwrite rather than guess. ${where} ` +
63
- `(the glob matters — git will not descend into an excluded directory, so a negation ` +
64
- `INSIDE an excluded directory can never apply), then commit the manifest. ` +
65
- `If you deliberately do not commit generated output, ignore this.`,
66
- );
64
+ `on a fresh clone or CI runner 'meta gen' cannot tell your hand edits from its ` +
65
+ `own stale output, and will refuse to overwrite rather than guess. ${where} ` +
66
+ `(the glob matters — git will not descend into an excluded directory, so a negation ` +
67
+ `INSIDE an excluded directory can never apply), then commit the manifest. ` +
68
+ `If you deliberately do not commit generated output, ignore this.`;
69
+ log.warn(msg);
70
+ return msg;
67
71
  }
package/src/lib/output.ts CHANGED
@@ -203,6 +203,13 @@ export interface MigrateResultShape {
203
203
  * caller sees the risk before applying.
204
204
  */
205
205
  warnings?: string[];
206
+ /**
207
+ * Explanations of planned changes that are not what they look like — today, views the
208
+ * migration drops and recreates only because it alters a table they read (D3).
209
+ */
210
+ notes?: string[];
211
+ /** A dry run's SQL. Text format prints it as a preview; JSON/toon carry it here. */
212
+ sql?: { up: string; down: string };
206
213
  }
207
214
 
208
215
  export function formatMigrateResult(result: MigrateResultShape, _opts: FormatOptions): string {
@@ -219,6 +226,12 @@ export function formatMigrateResult(result: MigrateResultShape, _opts: FormatOpt
219
226
  lines.push(` Changes: ${summary}`, "");
220
227
  }
221
228
 
229
+ const notes = result.notes ?? [];
230
+ if (notes.length > 0) {
231
+ for (const n of notes) lines.push(` Note: ${n}`);
232
+ lines.push("");
233
+ }
234
+
222
235
  if (result.blocked.length > 0) {
223
236
  const anyRename = result.blocked.some((b) => b.renameFlag !== undefined);
224
237
  lines.push(anyRename ? " Blocked (re-run with the flag shown):" : " Blocked (re-run with --allow):");
@@ -292,7 +305,7 @@ export function genResultToData(result: GenResultShape): {
292
305
  const help = result.files.length !== 0
293
306
  ? ["typecheck the generated code with `npx tsc`", ...(result.migrateAdvice !== undefined ? [result.migrateAdvice] : [])]
294
307
  : noGenerators
295
- ? ["add generators to the `generators: []` array in metaobjects.config.ts — `meta eject --list` names every generator you can own, and `meta init` scaffolds the usual set"]
308
+ ? ["add generators to the `generators: []` array in metaobjects.config.ts — `meta gen --list` names each one with the import that runs it as shipped, and `meta eject <name>` copies one into your repo to change what it emits"]
296
309
  : ["author entities in this project's metadata sources then re-run `meta gen`"];
297
310
  // An absent section is STATED, never omitted: a reader must be able to tell
298
311
  // "the scan found nothing" from "the scan never ran".
@@ -329,6 +342,8 @@ export function migrateResultToData(result: MigrateResultShape): {
329
342
  summary: string;
330
343
  help: string[];
331
344
  warnings?: string[];
345
+ notes?: string[];
346
+ sql?: { up: string; down: string };
332
347
  } {
333
348
  const changeEntries = Object.entries(result.changeCounts).filter(([, v]) => v > 0);
334
349
  const changes = changeEntries.map(([kind, count]) => ({ kind, count }));
@@ -395,6 +410,8 @@ export function migrateResultToData(result: MigrateResultShape): {
395
410
  changes, written: result.writtenPaths, summary, help,
396
411
  // Only when present, so a run with no hazard keeps its existing shape.
397
412
  ...(warnings.length > 0 ? { warnings } : {}),
413
+ ...((result.notes ?? []).length > 0 ? { notes: result.notes } : {}),
414
+ ...(result.sql !== undefined ? { sql: result.sql } : {}),
398
415
  };
399
416
  }
400
417
 
@@ -7,54 +7,37 @@
7
7
 
8
8
  import {
9
9
  type MetaData,
10
- TYPE_FIELD,
11
10
  FIELD_SUBTYPE_OBJECT,
12
11
  FIELD_ATTR_OBJECT_REF,
13
- resolveObjectRef,
12
+ isMetaField,
13
+ refTarget,
14
14
  } from "@metaobjectsdev/metadata";
15
15
  import type { PayloadField } from "@metaobjectsdev/render";
16
16
 
17
- function findObject(root: MetaData, name: string, referrerPkg: string): MetaData | undefined {
18
- // ADR-0039: effective children — resolve rather than rely on root being unextended.
19
- // ADR-0042: package-local — an FQN @objectRef binds the exact package; a bare ref
20
- // binds the referrer's package FIRST (else root-level), never a same-named object.value
21
- // in another package. The SAME resolver render-helper.ts's findObject uses (must not drift).
22
- return resolveObjectRef(root, name, referrerPkg).node;
23
- }
24
-
25
17
  /**
26
18
  * Walk an `object.value` view-object into a render `PayloadField[]`. Object-ref
27
- * fields recurse into their referenced view-object; a `seen` set guards against
28
- * a (pathological) reference cycle. `referrerPkg` (ADR-0042) is the package of
29
- * the node that declares `voName` (the template for the root call; the parent VO
30
- * for a nested recursion), so a bare ref resolves package-locally.
19
+ * fields recurse into the view-object the loader bound for their `@objectRef`
20
+ * (ADR-0059 R7); a `seen` set guards against a (pathological) reference cycle.
21
+ * Returns `[]` when `vo` is undefined (the ref did not resolve).
31
22
  */
32
23
  export function derivePayloadFieldTree(
33
- root: MetaData,
34
- voName: string,
35
- referrerPkg = "",
36
- seen: ReadonlySet<string> = new Set(),
24
+ vo: MetaData | undefined,
25
+ seen: ReadonlySet<MetaData> = new Set(),
37
26
  ): PayloadField[] {
38
- if (seen.has(voName)) return [];
39
- const vo = findObject(root, voName, referrerPkg);
40
- if (!vo) return [];
41
- // A nested @objectRef resolves in the FIELD's own declaring package (fallback
42
- // below), which differs from this VO's when the field is inherited via extends
43
- // from an abstract VO in another package (the bare ref was authored there).
44
- const voPkg = vo.package ?? vo.fileDefaultPackage ?? "";
45
- const nextSeen = new Set(seen).add(voName);
27
+ if (vo === undefined || seen.has(vo)) return [];
28
+ const nextSeen = new Set(seen).add(vo);
46
29
  const fields: PayloadField[] = [];
47
- for (const f of vo.children().filter((c) => c.type === TYPE_FIELD)) {
48
- if (f.subType === FIELD_SUBTYPE_OBJECT) {
30
+ for (const f of vo.children().filter(isMetaField)) {
31
+ // ADR-0039: effective array-ness — `isArray` may be inherited via extends. Carried only
32
+ // when true, so verify can tell a list of objects from a single object and a list of
33
+ // scalars from a scalar.
34
+ const arrayness = f.resolvedIsArray() ? { isArray: true } : {};
35
+ if (f.subType === FIELD_SUBTYPE_OBJECT && typeof f.attr(FIELD_ATTR_OBJECT_REF) === "string") {
49
36
  // ADR-0039: effective attr — @objectRef may be inherited via extends.
50
- const ref = f.attr(FIELD_ATTR_OBJECT_REF);
51
- if (typeof ref === "string") {
52
- const fieldPkg = f.parent?.package ?? f.parent?.fileDefaultPackage ?? voPkg;
53
- fields.push({ name: f.name, fields: derivePayloadFieldTree(root, ref, fieldPkg, nextSeen) });
54
- continue;
55
- }
37
+ fields.push({ name: f.name, fields: derivePayloadFieldTree(refTarget(f, FIELD_ATTR_OBJECT_REF), nextSeen), ...arrayness });
38
+ continue;
56
39
  }
57
- fields.push({ name: f.name });
40
+ fields.push({ name: f.name, ...arrayness });
58
41
  }
59
42
  return fields;
60
43
  }
@@ -36,6 +36,7 @@ import {
36
36
  // One shared resolver (FR-038): codegen's requirement-test fan-out needs the same
37
37
  // ADR-0042 package-local binding, so these moved to @metaobjectsdev/metadata rather
38
38
  // than being reimplemented there.
39
+ effectiveRequirementPackage,
39
40
  resolveClaimTarget,
40
41
  resolveMember,
41
42
  didYouMeanHint,
@@ -233,7 +234,7 @@ export function resolveRequirementRef(
233
234
  ): MetaRequirement | undefined {
234
235
  const keyed = new Map<string, MetaRequirement>();
235
236
  for (const { node, path } of addressed) {
236
- const pkg = node.package ?? node.fileDefaultPackage ?? "";
237
+ const pkg = effectiveRequirementPackage(node);
237
238
  if (pkg !== "") keyed.set(`${pkg}::${path}`, node);
238
239
  // Bare path is registered too, so a single-package ledger can reference
239
240
  // without repeating its own package on every line.
@@ -279,6 +280,11 @@ export interface RequirementScan {
279
280
  * printed a ratio the gate had not enforced would be a measurement nobody could
280
281
  * reconcile with the diagnostics beneath it. */
281
282
  readonly measureCoverage: boolean;
283
+ /** ADR-0057 — the strict switch. When true, the functional EXISTENCE finding
284
+ * (`WARN_REQUIREMENT_NOTHING_IMPLEMENTS`) is reported at severity `error`; the code,
285
+ * path and message do not change, and no other diagnostic is touched. Carried on
286
+ * the scan, like `measureCoverage`, so a caller decides it once per run. */
287
+ readonly requireImplementers: boolean;
282
288
  }
283
289
 
284
290
  /**
@@ -306,7 +312,7 @@ export interface RequirementScan {
306
312
  function projectAuthoredRequirements(addressed: readonly AddressedRequirement[]): boolean {
307
313
  const libPkgs = libraryPackages();
308
314
  return addressed.some((r) => {
309
- const pkg = r.node.package ?? r.node.fileDefaultPackage ?? "";
315
+ const pkg = effectiveRequirementPackage(r.node);
310
316
  return !libPkgs.has(pkg);
311
317
  });
312
318
  }
@@ -321,6 +327,8 @@ export function scanRequirements(
321
327
  * by its own ledger" is exactly what is being asserted — so the derivation would
322
328
  * switch the check off precisely where it is the point. */
323
329
  measureCoverage?: boolean;
330
+ /** Raise WARN_REQUIREMENT_NOTHING_IMPLEMENTS to severity "error". Default false. */
331
+ requireImplementers?: boolean;
324
332
  },
325
333
  ): RequirementScan {
326
334
  const addressed = collectAddressedRequirements(root);
@@ -328,6 +336,7 @@ export function scanRequirements(
328
336
  addressed,
329
337
  claimedObjects: claimedObjectKeys(root, addressed.map((r) => r.node)),
330
338
  measureCoverage: opts?.measureCoverage ?? projectAuthoredRequirements(addressed),
339
+ requireImplementers: opts?.requireImplementers ?? false,
331
340
  // `exactOptionalPropertyTypes` — an omitted key, never an explicit `undefined`.
332
341
  ...(opts?.coverable !== undefined ? { coverable: opts.coverable } : {}),
333
342
  };
@@ -353,7 +362,7 @@ function claimedObjectKeys(root: MetaData, reqs: MetaRequirement[]): Set<string>
353
362
  // referrerPkg is the requirement's own effective package, so a bare ref
354
363
  // binds package-locally under the ADR-0042 contract — the loader's own
355
364
  // resolver, never a parallel name scan (#228).
356
- const referrerPkg = req.package ?? req.fileDefaultPackage ?? "";
365
+ const referrerPkg = effectiveRequirementPackage(req);
357
366
  for (const ref of req.implementedBy()) {
358
367
  const { owner, path } = splitMemberRef(ref);
359
368
  const node = resolveClaimTarget(root, owner, referrerPkg);
@@ -471,7 +480,7 @@ export function checkRequirements(root: MetaData, scan: RequirementScan = scanRe
471
480
  // referrerPkg is the requirement's own effective package, so a bare ref
472
481
  // binds package-locally under the ADR-0042 contract — the loader's own
473
482
  // resolver, never a parallel name scan (#228).
474
- const referrerPkg = req.package ?? req.fileDefaultPackage ?? "";
483
+ const referrerPkg = effectiveRequirementPackage(req);
475
484
  const node = resolveClaimTarget(root, owner, referrerPkg);
476
485
  const isObjectRef = path.length === 0;
477
486
 
@@ -536,7 +545,7 @@ export function checkRequirements(root: MetaData, scan: RequirementScan = scanRe
536
545
  // requirement's own effective package, exactly as @implementedBy does.
537
546
  const superseded = req.supersededBy();
538
547
  if (superseded !== undefined) {
539
- const referrerPkg = req.package ?? req.fileDefaultPackage ?? "";
548
+ const referrerPkg = effectiveRequirementPackage(req);
540
549
  if (resolveRequirementRef(addressed, superseded, referrerPkg) === undefined) {
541
550
  out.push({
542
551
  severity: "error", code: ERR_REQUIREMENT_DANGLING_REF, path: reqPath,
@@ -593,9 +602,15 @@ export function checkRequirements(root: MetaData, scan: RequirementScan = scanRe
593
602
  // tree. So the question is not "does this node claim anything" but "does
594
603
  // anything in this subtree claim anything". A live L1 whose entire subtree
595
604
  // is empty is a capability declared and built by nobody.
605
+ //
606
+ // The strict switch (ADR-0057, `meta verify --require-implementers`) raises the
607
+ // SEVERITY for a project whose ledger has caught up with its links. The code
608
+ // keeps its `WARN_` name on purpose: it identifies the finding, and one finding
609
+ // under two codes would split every suppression and corpus case that keys on it.
596
610
  if (!architectural && live && !subtreeClaimsAnything(req)) {
597
611
  out.push({
598
- severity: "warn", code: WARN_REQUIREMENT_NOTHING_IMPLEMENTS, path: reqPath,
612
+ severity: scan.requireImplementers ? "error" : "warn",
613
+ code: WARN_REQUIREMENT_NOTHING_IMPLEMENTS, path: reqPath,
599
614
  message: `is '${String(status)}' but neither it nor anything nested under it names an ` +
600
615
  `implementing node. A functional requirement's check is existence — a subtree that claims ` +
601
616
  `nothing is a capability nobody built.`,