@metaobjectsdev/cli 1.0.13 → 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
@@ -30,10 +30,12 @@ import { scanForRemovedProps, type RemovedPropFinding } from "../lib/removed-pro
30
30
  import { replayRemedy } from "../lib/replay-remedy.js";
31
31
  import { FileProvider, mustacheTagLine } from "../lib/file-provider.js";
32
32
  import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
33
- import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
33
+ import { loadMemoryOptionsFrom, loadMetaobjectsConfig, RemovedNamingConfigError, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
34
34
  import { collectionLoadOptions } from "../lib/collection-load-options.js";
35
35
  import { computeCodegenDrift } from "../lib/codegen-drift.js";
36
- import { computeDocsDrift } from "../lib/docs-drift.js";
36
+ import { hasLegacyDocsBlock, legacyDocsConfigMessage, REMOVED_VERIFY_DOCS } from "../lib/docs-removed.js";
37
+ import { buildAgentSchemaInput } from "../lib/agent-schema.js";
38
+ import { sourceRootsOf } from "./gen.js";
37
39
  import {
38
40
  checkRequirements, summariseRequirements, scanRequirements, type Diagnostic,
39
41
  } from "../lib/requirement-check.js";
@@ -41,8 +43,9 @@ import { lintRequirements } from "../lib/requirement-lint.js";
41
43
  import { lintOverlays } from "../lib/overlay-lint.js";
42
44
  import { lintNodeNames } from "../lib/name-lint.js";
43
45
  import { lintDeprecatedReferences } from "../lib/deprecation-lint.js";
46
+ import { lintDuplicateFields, lintReferenceFields } from "../lib/field-lint.js";
44
47
  import { FileSource } from "@metaobjectsdev/metadata/core";
45
- import { resolveD1Config, resolveMigrateConfig } from "../lib/config.js";
48
+ import { resolveD1Config, resolveMigrateConfig, type ResolvedMigrateConfig } from "../lib/config.js";
46
49
  import {
47
50
  buildWranglerExecuteArgs,
48
51
  defaultWranglerRunner,
@@ -50,7 +53,7 @@ import {
50
53
  type WranglerRunner,
51
54
  } from "../lib/wrangler.js";
52
55
  import type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
53
- import { buildProjectionViews, resolveDocsConfig } from "@metaobjectsdev/codegen-ts";
56
+ import { buildProjectionViews } from "@metaobjectsdev/codegen-ts";
54
57
  import { buildKyselyFromUrl, inferDialect, type Dialect } from "../lib/kysely.js";
55
58
  import { tokensToAllowOptions, describeChange } from "../lib/allow.js";
56
59
  import {
@@ -69,6 +72,7 @@ import {
69
72
  verifyReplay,
70
73
  introspect,
71
74
  diff,
75
+ withoutViewRecreates,
72
76
  readSnapshot,
73
77
  snapshotPath,
74
78
  type SchemaSnapshot,
@@ -80,6 +84,8 @@ import {
80
84
  type D1Binding,
81
85
  type D1Runner,
82
86
  type DriftResult,
87
+ postgresViewReplaceProbe,
88
+ withoutFlywayHistory,
83
89
  } from "@metaobjectsdev/migrate-ts";
84
90
  import {
85
91
  DEFAULT_METAOBJECTS_DIR,
@@ -95,6 +101,7 @@ import {
95
101
  TEMPLATE_SUBTYPE_PROMPT,
96
102
  TEMPLATE_SUBTYPE_OUTPUT,
97
103
  TEMPLATE_ATTR_PAYLOAD_REF,
104
+ refTarget,
98
105
  TEMPLATE_ATTR_TEXT_REF,
99
106
  TEMPLATE_ATTR_REQUIRED_SLOTS,
100
107
  TEMPLATE_ATTR_REQUIRED_TAGS,
@@ -105,6 +112,7 @@ import {
105
112
  TEMPLATE_ATTR_HTML_BODY_REF,
106
113
  TEMPLATE_ATTR_TEXT_BODY_REF,
107
114
  REQUIREMENT_STATUSES,
115
+ physicalNamingOf,
108
116
  } from "@metaobjectsdev/metadata";
109
117
  import { verify, ERR_REQUIRED_SLOT_UNUSED, ERR_PARTIAL_UNRESOLVED } from "@metaobjectsdev/render";
110
118
  import { describeLoadError, reportLoadError } from "../lib/load-error.js";
@@ -119,9 +127,11 @@ const ERR_UNKNOWN_ATTR = "ERR_UNKNOWN_ATTR";
119
127
 
120
128
  /**
121
129
  * A no-flags MigrateFlags, so `resolveMigrateConfig` yields exactly what `meta migrate`
122
- * would use with nothing passed on the command line — config value, else default.
130
+ * would use with nothing passed on the command line — config value, else default. The one
131
+ * migrate flag verify also takes, `--migration-format`, is laid over it per invocation, so
132
+ * the format resolves flag > config > default exactly as it does for `meta migrate`.
123
133
  *
124
- * verify consumes `outDir` (#292) and — for the replay gate ONLY — `dialect`. The #292
134
+ * verify consumes `outDir` (#292), `format` and — for the replay gate ONLY — `dialect`. The #292
125
135
  * restriction that reading anything else "would be reaching into migrate's decisions"
126
136
  * was written about the DRIFT gate, whose dialect comes from the live `--db` URL. The
127
137
  * replay gate has no `--db` at all, and the dialect a committed chain was EMITTED for
@@ -172,6 +182,15 @@ export async function verifyCommand(
172
182
  emitStructured({ error: msg, hint: "run `meta verify --help` for the accepted flags" }, fmt);
173
183
  return 2;
174
184
  }
185
+ // ADR-0034 Amendment 9: documentation is checked by `--codegen` with everything else the
186
+ // generator list declares. The old flag still parses, so it can say where it went. It says
187
+ // so once, as plain text on stderr, like every other removed command: a structured copy on
188
+ // stdout printed the same sentence a second time, escaped (`error: "\`meta verify ...`).
189
+ if (flags.docs) {
190
+ log.error(REMOVED_VERIFY_DOCS);
191
+ return 2;
192
+ }
193
+ const migrateFlags: MigrateFlags = { ...EMPTY_MIGRATE_FLAGS, format: flags.migrationFormat };
175
194
 
176
195
  // ADR-0021 D2 — explicit verify subverbs. Each flag selects one drift mode;
177
196
  // any combination runs each and the overall exit code is the MAX (non-zero on
@@ -182,16 +201,15 @@ export async function verifyCommand(
182
201
  // The schema gate is selected by the presence of --db, or --dialect d1 (#225 —
183
202
  // D1 has no URL connection); that check lives inside runSchemaVerify.
184
203
  const runCodegen = flags.codegen;
185
- const runDocs = flags.docs;
186
- // Task 15 — same shape as --codegen/--docs: selected ONLY by its own flag,
204
+ // Task 15 — same shape as --codegen: selected ONLY by its own flag,
187
205
  // never folded into the bare-verify default (it needs the publisher
188
206
  // reachable, which CI may not have — see the VerifyFlags doc on `deps`).
189
207
  const runDeps = flags.deps;
190
208
  if (!flags.anyExplicit) {
191
209
  say(
192
210
  "meta verify — running --templates (default). Explicit subverbs: " +
193
- "--templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift), " +
194
- "--docs (docs drift), --deps (dependency drift), " +
211
+ "--templates (prompt drift), --db/--dialect d1 (schema drift), " +
212
+ "--codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), " +
195
213
  "--replay/--replay-snapshot (the committed migration chain replays from empty).",
196
214
  );
197
215
  }
@@ -259,7 +277,13 @@ export async function verifyCommand(
259
277
  let forgeConfig: MetaobjectsGenConfig | undefined;
260
278
  try {
261
279
  forgeConfig = await loadMetaobjectsConfig(genConfigDir);
262
- } catch {
280
+ } catch (e) {
281
+ // ADR-0058: the one config problem verify cannot fall back from — the default column
282
+ // rule would report every column of a database built with the old value as drift.
283
+ if (e instanceof RemovedNamingConfigError) {
284
+ log.error(e.message);
285
+ return 2;
286
+ }
263
287
  forgeConfig = undefined;
264
288
  }
265
289
  // Both of the gen config's contributions to the load, together — see
@@ -360,6 +384,8 @@ export async function verifyCommand(
360
384
  // #305 — the deprecated-reference authoring lint. Its own section, same reason.
361
385
  let deprecationSection: AdvisorySection<AdvisoryDiagnosticRow> =
362
386
  skippedSection("the deprecated-reference lint did not run");
387
+ let fieldSection: AdvisorySection<AdvisoryDiagnosticRow> =
388
+ skippedSection("the field lint did not run");
363
389
  // The ledger counts `meta verify` prints on every run. Undefined for a project
364
390
  // declaring no requirement.* node at all (opt-in by declaration) — the payload
365
391
  // then omits the block rather than reporting zeroes that would read as an empty
@@ -376,9 +402,14 @@ export async function verifyCommand(
376
402
  // subverb so ANY kind of drift fails CI. Each gate only runs when its mode is
377
403
  // selected; an unselected gate contributes 0.
378
404
  const templateExit = runTemplates ? runTemplateVerify() : 0;
405
+ // Set when the schema gate could not reach or read the database: a failure, but not
406
+ // drift, and the payload must not report it as drift.
407
+ let schemaRunError: string | undefined;
408
+ // The schema gate's findings, for the structured payload: what differs, not only that
409
+ // something does. Left undefined when the gate did not compare anything.
410
+ let schemaDrift: SchemaDriftSection | undefined;
379
411
  const schemaExit = await runSchemaVerify();
380
412
  const codegenExit = runCodegen ? await runCodegenVerify() : 0;
381
- const docsExit = runDocs ? await runDocsVerify() : 0;
382
413
  const depsExit = runDeps ? await runDepsVerify() : 0;
383
414
  // Requirements have no subverb: `requirement.*` nodes are metadata, so they
384
415
  // are checked on every `meta verify`. Opt-in by DECLARATION — a model with no
@@ -405,6 +436,11 @@ export async function verifyCommand(
405
436
  // warnings ONLY, never changes the exit code.
406
437
  runDeprecationLintAdvisory();
407
438
 
439
+ // A reference identity over a field the object lacks, and a field name declared
440
+ // twice in one children list — both load clean on every port. Runs on every
441
+ // `meta verify`; warnings ONLY, never changes the exit code.
442
+ await runFieldLintAdvisory();
443
+
408
444
  // Advisory verify-as-teacher pass: surface hand-rolled work the metadata could
409
445
  // model. Warnings ONLY — never changes the exit code (bias to under-flagging).
410
446
  // Suppressed with --no-antipatterns or META_NO_ANTIPATTERNS=1 for the rare
@@ -415,7 +451,6 @@ export async function verifyCommand(
415
451
  templateExit,
416
452
  schemaExit,
417
453
  codegenExit,
418
- docsExit,
419
454
  depsExit,
420
455
  requirementExit,
421
456
  replayExit,
@@ -429,7 +464,6 @@ export async function verifyCommand(
429
464
  // failure mode this whole change exists to remove.
430
465
  { gate: "schema", ran: ranSchemaGate, ok: schemaExit === 0 },
431
466
  { gate: "codegen", ran: runCodegen, ok: codegenExit === 0 },
432
- { gate: "docs", ran: runDocs, ok: docsExit === 0 },
433
467
  { gate: "deps", ran: runDeps, ok: depsExit === 0 },
434
468
  { gate: "requirements", ran: true, ok: requirementExit === 0 },
435
469
  { gate: "replay", ran: flags.replay || flags.replaySnapshot, ok: replayExit === 0 },
@@ -455,6 +489,9 @@ export async function verifyCommand(
455
489
  overlays: overlaySection,
456
490
  names: nameSection,
457
491
  deprecations: deprecationSection,
492
+ fields: fieldSection,
493
+ ...(schemaDrift !== undefined ? { schemaDrift } : {}),
494
+ errors: schemaRunError !== undefined ? [{ gate: "schema", error: schemaRunError }] : [],
458
495
  }),
459
496
  fmt,
460
497
  );
@@ -483,7 +520,7 @@ export async function verifyCommand(
483
520
  // Resolve the migrations directory and the chain's dialect through MIGRATE's own
484
521
  // precedence, never a second derivation — verify must not look somewhere migrate
485
522
  // does not write, nor assume a dialect the chain was not emitted for.
486
- const migrateConfig = await resolveMigrateConfig(EMPTY_MIGRATE_FLAGS, projectRoot);
523
+ const migrateConfig = await resolveMigrateConfig(migrateFlags, projectRoot);
487
524
 
488
525
  if (migrateConfig.format === "flyway") {
489
526
  log.error(
@@ -624,11 +661,9 @@ export async function verifyCommand(
624
661
  // passes no `governed` and gets the comparison exactly as it was.
625
662
  let governed: GovernedScope | undefined;
626
663
  if (schemaScope !== undefined) {
627
- const viewStrategy = forgeConfig?.columnNamingStrategy ?? "snake_case";
628
664
  const built = buildExpectedSchemaWithProvenance(root, {
629
665
  dialect,
630
- columnNamingStrategy: viewStrategy,
631
- views: buildProjectionViews(root, { dialect, columnNamingStrategy: viewStrategy }),
666
+ views: buildProjectionViews(root, { dialect }),
632
667
  });
633
668
  governed = scopeExpectedSchema(built, schemaScope, importedOption(collection));
634
669
  }
@@ -671,7 +706,14 @@ export async function verifyCommand(
671
706
  // ONE scan for all three passes. The gate and the summary each used to walk the
672
707
  // model AND resolve every @implementedBy claim for themselves — the resolution
673
708
  // being the expensive half — and the lint added a third walk on top.
674
- const scan = scanRequirements(root, { coverable: collection.inScope });
709
+ // The strict switch (ADR-0057) is decided here, once, and travels on the scan: the
710
+ // flag or META_REQUIRE_IMPLEMENTERS=1 — a flag-or-variable pair, as
711
+ // --no-requirement-lint has below, so a CI job can turn it on without editing a
712
+ // command line it shares with local runs.
713
+ const scan = scanRequirements(root, {
714
+ coverable: collection.inCoverage,
715
+ requireImplementers: flags.requireImplementers || process.env.META_REQUIRE_IMPLEMENTERS === "1",
716
+ });
675
717
  const diags = [...checkRequirements(root, scan)];
676
718
 
677
719
  // Printed on EVERY run, clean or not — a gate that says nothing when it
@@ -852,6 +894,41 @@ export async function verifyCommand(
852
894
  }
853
895
  }
854
896
 
897
+ // -- field authoring lint (advisory) ----------------------------------------
898
+ // Reads the LOADED model for the reference half and the project's OWN raw files
899
+ // for the duplicate half (see field-lint.ts) — a dependency artifact is not the
900
+ // adopter's to edit. Its own section, its own cap, and a skip that carries its
901
+ // reason rather than looking like a clean scan.
902
+ async function runFieldLintAdvisory(): Promise<void> {
903
+ if (flags.noFieldLint) {
904
+ fieldSection = skippedSection("suppressed by --no-field-lint");
905
+ return;
906
+ }
907
+ if (process.env.META_NO_FIELD_LINT === "1") {
908
+ fieldSection = skippedSection("suppressed by META_NO_FIELD_LINT=1");
909
+ return;
910
+ }
911
+ let findings: Diagnostic[];
912
+ try {
913
+ findings = [
914
+ ...lintReferenceFields(root),
915
+ ...(await lintDuplicateFields(collection.ownFiles, (path) => new FileSource(path))),
916
+ ];
917
+ } catch (err) {
918
+ // Never let an advisory scan break verify — and never report it as clean.
919
+ fieldSection = skippedSection(`the field lint failed: ${describeError(err)}`);
920
+ return;
921
+ }
922
+ fieldSection = ranSection(findings.map((d) => toDiagnosticRow(d, "lint")));
923
+ if (findings.length > 0) {
924
+ log.warn(
925
+ `meta verify — fields: ${findings.length} authoring warning(s) ` +
926
+ `(advisory — does not fail the build):`,
927
+ );
928
+ warnCapped(findings.map(formatDiagnostic), flags.limit, { structured });
929
+ }
930
+ }
931
+
855
932
  // -- deprecated-reference authoring lint (advisory) -------------------------
856
933
  // #305 — reads the LOADED model (see deprecation-lint.ts). Its own section,
857
934
  // its own cap, and a skip that carries its reason rather than looking clean.
@@ -942,7 +1019,6 @@ export async function verifyCommand(
942
1019
  if (scanDialect !== "mysql") {
943
1020
  unindexedFks = scanForUnindexedForeignKeys(root, {
944
1021
  dialect: scanDialect,
945
- columnNamingStrategy: forgeConfig?.columnNamingStrategy ?? "snake_case",
946
1022
  skip: collection.imported,
947
1023
  });
948
1024
  }
@@ -1059,7 +1135,7 @@ export async function verifyCommand(
1059
1135
 
1060
1136
  // @payloadRef must resolve to a loaded object.value (drives the field tree
1061
1137
  // every render-engine drift check runs against).
1062
- const fieldTree = derivePayloadFieldTree(root, payloadRef, tmpl.package ?? tmpl.fileDefaultPackage ?? "");
1138
+ const fieldTree = derivePayloadFieldTree(refTarget(tmpl, TEMPLATE_ATTR_PAYLOAD_REF));
1063
1139
  if (fieldTree.length === 0) {
1064
1140
  log.error(
1065
1141
  `[${tmpl.name}] (${tmpl.subType}) ${ERR_PARTIAL_UNRESOLVED}: ` +
@@ -1180,11 +1256,9 @@ export async function verifyCommand(
1180
1256
  // consults it.
1181
1257
  const scopeMismatch = migrateScopeMismatch(collection, () => {
1182
1258
  const dialect: Dialect = usingD1 ? "d1" : (flags.dialect ?? inferDialect(flags.db as string));
1183
- const viewStrategy = forgeConfig?.columnNamingStrategy ?? "snake_case";
1184
1259
  return buildExpectedSchemaWithProvenance(root, {
1185
1260
  dialect,
1186
- columnNamingStrategy: viewStrategy,
1187
- views: buildProjectionViews(root, { dialect, columnNamingStrategy: viewStrategy }),
1261
+ views: buildProjectionViews(root, { dialect }),
1188
1262
  }).provenance;
1189
1263
  });
1190
1264
  if (scopeMismatch !== undefined) {
@@ -1225,16 +1299,21 @@ export async function verifyCommand(
1225
1299
  try {
1226
1300
  kysely = await buildKyselyFromUrl(flags.db as string, flags.dialect as Dialect | undefined);
1227
1301
  } catch (err) {
1228
- log.error(`verify: ${describeError(err)}`);
1302
+ schemaRunError = describeError(err);
1303
+ log.error(`verify: ${schemaRunError}`);
1229
1304
  return 1;
1230
1305
  }
1231
1306
 
1232
1307
  try {
1233
1308
  const allow = tokensToAllowOptions(flags.allow);
1309
+ // Resolve the migrate config once for this whole path: `tryLoadConfig` is uncached,
1310
+ // and the flyway filter below and checkCommittedSnapshot's migrations-dir read want
1311
+ // the same value — resolving twice would read and parse `.metaobjects/config.json`
1312
+ // twice per `meta verify --db`.
1313
+ const migrateConfig = await resolveMigrateConfig(migrateFlags, projectRoot);
1234
1314
  // Expected views from the single view-SQL source (codegen-ts), so view-body
1235
1315
  // drift is detected against the live DB.
1236
- const viewStrategy = forgeConfig?.columnNamingStrategy ?? "snake_case";
1237
- const expectedViews = buildProjectionViews(root, { dialect: kysely.dialect, columnNamingStrategy: viewStrategy });
1316
+ const expectedViews = buildProjectionViews(root, { dialect: kysely.dialect });
1238
1317
  let driftResult;
1239
1318
  let actual: SchemaSnapshot;
1240
1319
  try {
@@ -1242,22 +1321,29 @@ export async function verifyCommand(
1242
1321
  // `actual` this drift comparison uses, and re-introspecting for it would both
1243
1322
  // cost a second round trip and open a window where the two could disagree.
1244
1323
  actual = await introspect(kysely.db, kysely.dialect);
1324
+ // Flyway owns its history table; it is no drift under the flyway format, as in `meta migrate`.
1325
+ if (migrateConfig.format === "flyway") {
1326
+ actual = withoutFlywayHistory(actual);
1327
+ }
1245
1328
  driftResult = await computeDriftFromActual(actual, kysely.dialect, root, {
1246
1329
  allow,
1247
1330
  views: expectedViews,
1331
+ // The same question `meta migrate` asks, so the report names the change it plans.
1332
+ ...(kysely.dialect === "postgres" ? { viewReplaceProbe: postgresViewReplaceProbe(kysely.db) } : {}),
1248
1333
  ...(schemaScope !== undefined ? { inScope: schemaScope } : {}),
1249
1334
  // The import exclusion reaches scopeExpectedSchema through this option bag —
1250
1335
  // there is no direct call on this path to attach it to.
1251
1336
  ...importedOption(collection),
1252
1337
  });
1253
1338
  } catch (err) {
1254
- log.error(`verify: failed to introspect ${kysely.displayUrl}: ${describeError(err)}`);
1339
+ schemaRunError = `failed to introspect ${kysely.displayUrl}: ${describeError(err)}`;
1340
+ log.error(`verify: ${schemaRunError}`);
1255
1341
  return 1;
1256
1342
  }
1257
1343
 
1258
1344
  const snapshotDrift =
1259
1345
  driftResult.changes.length === 0
1260
- ? await checkCommittedSnapshot(actual, kysely.dialect, kysely.displayUrl, driftResult)
1346
+ ? await checkCommittedSnapshot(actual, kysely.dialect, kysely.displayUrl, driftResult, migrateConfig)
1261
1347
  : [];
1262
1348
 
1263
1349
  return reportSchemaDrift(driftResult, [...ledgerDrift, ...snapshotDrift], kysely.displayUrl);
@@ -1327,8 +1413,7 @@ export async function verifyCommand(
1327
1413
  }
1328
1414
 
1329
1415
  const allow = tokensToAllowOptions(flags.allow);
1330
- const viewStrategy = forgeConfig?.columnNamingStrategy ?? "snake_case";
1331
- const expectedViews = buildProjectionViews(root, { dialect: "d1", columnNamingStrategy: viewStrategy });
1416
+ const expectedViews = buildProjectionViews(root, { dialect: "d1" });
1332
1417
  let driftResult;
1333
1418
  try {
1334
1419
  driftResult = await computeDriftFromActual(actual, "d1", root, {
@@ -1382,13 +1467,15 @@ export async function verifyCommand(
1382
1467
  dialect: Dialect,
1383
1468
  displayUrl: string,
1384
1469
  governed: GovernedScope,
1470
+ // Resolved by the caller and threaded through — see the `verify --db` branch: the same
1471
+ // run's flyway filter reads this config too, and `tryLoadConfig` does not cache.
1472
+ migrateConfig: ResolvedMigrateConfig,
1385
1473
  ): Promise<string[]> {
1386
1474
  if (dialect === "d1") return []; // d1 keeps migrations Wrangler-native; no offline snapshot
1387
- // Resolve the migrations dir through migrate's OWN precedence (flag > config >
1388
- // default) rather than re-deriving it, so verify can never look somewhere migrate
1389
- // does not write. Only `outDir` is consumed; the rest of the resolved config is
1390
- // migrate's business.
1391
- const migrateConfig = await resolveMigrateConfig(EMPTY_MIGRATE_FLAGS, projectRoot);
1475
+ // The migrations dir comes from migrate's OWN precedence (flag > config > default)
1476
+ // rather than a re-derivation here, so verify can never look somewhere migrate does
1477
+ // not write. Only `outDir` and `format` are consumed; the rest of the resolved config
1478
+ // is migrate's business.
1392
1479
  const dir = resolvePath(projectRoot, migrateConfig.outDir);
1393
1480
  let snapshot: SchemaSnapshot | null;
1394
1481
  try {
@@ -1397,6 +1484,7 @@ export async function verifyCommand(
1397
1484
  return [];
1398
1485
  }
1399
1486
  if (snapshot === null) return [];
1487
+ if (migrateConfig.format === "flyway") snapshot = withoutFlywayHistory(snapshot);
1400
1488
 
1401
1489
  // Out-of-scope objects leave BOTH sides of this comparison, and the schema pin
1402
1490
  // comes from the scope decision the DRIFT comparison already made — one door
@@ -1421,7 +1509,9 @@ export async function verifyCommand(
1421
1509
  // MERGED with the out-of-scope set, and a second key would silently drop that half.
1422
1510
  dialect,
1423
1511
  });
1424
- if (result.changes.length === 0) return [];
1512
+ // A view recreated only around a table change matches the snapshot (D3) — not a difference.
1513
+ const changes = withoutViewRecreates(result.changes);
1514
+ if (changes.length === 0) return [];
1425
1515
 
1426
1516
  return [
1427
1517
  // `meta migrate --from-db` is NOT the repair: it writes a snapshot only when it has
@@ -1431,10 +1521,10 @@ export async function verifyCommand(
1431
1521
  // been told everything is in sync. `baseline --from-db` rewrites it unconditionally,
1432
1522
  // which is the whole point of the subcommand.
1433
1523
  `the committed schema snapshot disagrees with ${displayUrl} ` +
1434
- `(${result.changes.length} difference(s)) — the next 'meta migrate' would emit DDL from it ` +
1524
+ `(${changes.length} difference(s)) — the next 'meta migrate' would emit DDL from it ` +
1435
1525
  `and fail at apply. Re-derive it with ` +
1436
1526
  `'meta migrate baseline --from-db --db <url> --dialect ${dialect}'.`,
1437
- ...summarizeDrift(result.changes),
1527
+ ...summarizeDrift(changes),
1438
1528
  ];
1439
1529
  }
1440
1530
 
@@ -1458,6 +1548,7 @@ export async function verifyCommand(
1458
1548
  }
1459
1549
 
1460
1550
  const changes = driftResult.changes;
1551
+ schemaDrift = { changes: changes.map(toSchemaDriftRow), findings: ledgerDrift };
1461
1552
  if (changes.length === 0 && ledgerDrift.length === 0) {
1462
1553
  say(`meta verify — schema in sync with ${displayUrl}.`);
1463
1554
  return 0;
@@ -1517,13 +1608,28 @@ export async function verifyCommand(
1517
1608
  // and the loader's own remedies. It printed a bare message plus a hand-rolled
1518
1609
  // suggestions read — the half-true rule this file's sibling comment warns about.
1519
1610
  reportLoadError(log, "verify --codegen: failed to load this package's metadata", err);
1520
- return 2;
1611
+ return 1;
1521
1612
  }
1522
1613
  }
1523
1614
 
1615
+ // A 1.0 `docs` block configured a command that no longer exists; regenerating without it
1616
+ // would check no documentation and pass. Refused before anything is written, as `meta gen`
1617
+ // refuses it.
1618
+ if (hasLegacyDocsBlock(forgeConfig)) {
1619
+ log.error(legacyDocsConfigMessage("meta verify"));
1620
+ return 2;
1621
+ }
1622
+ const config = forgeConfig;
1524
1623
  let result;
1525
1624
  try {
1526
- result = await computeCodegenDrift(forgeConfig, codegenRoot, genConfigDir, genCollection.inScope);
1625
+ result = await computeCodegenDrift(config, codegenRoot, genConfigDir, genCollection.inScope, genCollection.ownFiles, {
1626
+ sourceRoots: sourceRootsOf(genCollection),
1627
+ agentSchema: () => buildAgentSchemaInput(
1628
+ codegenRoot,
1629
+ config.dialect,
1630
+ (m) => log.warn(`verify --codegen: [agent-docs] ${m}`),
1631
+ ),
1632
+ });
1527
1633
  } catch (err) {
1528
1634
  log.error(`verify --codegen: regeneration failed: ${describeError(err)}`);
1529
1635
  return 1;
@@ -1539,6 +1645,17 @@ export async function verifyCommand(
1539
1645
  // contribution is current" and nothing else. Reported on every run; fails the build
1540
1646
  // only under --forbid-hand-edits.
1541
1647
  const handEditExit = reportHandEdits(result.handEdited);
1648
+ // Say how much documentation was NOT checked, on the pass and the fail line alike, so a
1649
+ // gate that compared two pages of 589 cannot read as one that compared all of them.
1650
+ if (result.docsIgnored > 0) {
1651
+ say(`meta verify — ${result.docsIgnored} generated documentation page(s) are git-ignored and were not checked.`);
1652
+ }
1653
+ for (const dir of result.docsIgnoredDirs) {
1654
+ say(`meta verify — ${dir}: the repository git-ignores every documentation page generated there, so they were not compared.`);
1655
+ }
1656
+ if (result.docsIgnoreUnavailable !== undefined) {
1657
+ say(`meta verify — git could not say which documentation pages are ignored (${result.docsIgnoreUnavailable}), so every page was checked.`);
1658
+ }
1542
1659
 
1543
1660
  if (result.clean) {
1544
1661
  // Says what it CHECKED, not more. Since 0.24.3 this gate asks whether each file's
@@ -1558,9 +1675,18 @@ export async function verifyCommand(
1558
1675
  // it MERGES the edit and exits 0 — so prescribing it there was the unbreakable loop
1559
1676
  // the hand-edit exemption exists to end, reintroduced for the one file the exemption
1560
1677
  // deliberately excludes. Both lines print when the drift set holds both kinds.
1561
- if (result.driftedFiles.length > result.handEditedNames.length) {
1678
+ if (result.driftedFiles.length > result.handEditedNames.length + result.editedOrphans.length) {
1562
1679
  log.error("Run 'meta gen' to regenerate, then commit the result.");
1563
1680
  }
1681
+ // A hand-edited orphan is the other kind `meta gen` cannot fix: it deletes an untouched
1682
+ // file no generator emits any more, and refuses an edited one.
1683
+ if (result.editedOrphans.length > 0) {
1684
+ log.error(
1685
+ "For the file(s) above that no generator emits any more and that were edited by hand, " +
1686
+ "'meta gen' will NOT delete them. Delete each one, or move your edits into a file of " +
1687
+ "your own first.",
1688
+ );
1689
+ }
1564
1690
  if (result.handEditedNames.length > 0) {
1565
1691
  log.error(
1566
1692
  "For the names artifact(s) above, 'meta gen' will NOT help — it preserves a hand " +
@@ -1588,90 +1714,6 @@ export async function verifyCommand(
1588
1714
  return 0;
1589
1715
  }
1590
1716
 
1591
- // -- docs drift -------------------------------------------------------------
1592
- // Gated on --docs. Runs `meta docs` into a temp dir and diffs the committed docs
1593
- // tree. See lib/docs-drift.ts for the deliberate differences from --codegen: a byte
1594
- // difference IS drift here; a committed file a regen would not emit is reported ONLY
1595
- // under `agent/` and only when it opens with the @generated marker (`docs.outDir` is
1596
- // otherwise full of files MetaObjects did not write); and a page the project
1597
- // GIT-IGNORES is exempt, because `docs.outDir` is a directory rather than a namespace
1598
- // MetaObjects owns.
1599
- //
1600
- // It needs a config for the same reason `--codegen` does: `docs.outDir` says which
1601
- // tree to compare against, and the `agent` surface will not even materialise
1602
- // without one.
1603
- async function runDocsVerify(): Promise<number> {
1604
- if (forgeConfig === undefined) {
1605
- log.error(
1606
- "verify --docs: no metaobjects.config.ts found (or it is invalid) — " +
1607
- "cannot locate the committed docs tree to diff against. " +
1608
- "Run 'meta init' to scaffold one, or run without --docs.",
1609
- );
1610
- return 2;
1611
- }
1612
-
1613
- // Resolved through the SAME resolver `meta docs` uses, so a project that moved its
1614
- // docs elsewhere is checked where its docs actually are. `outputLayout` is the
1615
- // documented fallback for `docs.layout`.
1616
- const docsCfg = resolveDocsConfig(forgeConfig.docs, {}, forgeConfig.outputLayout ?? "flat");
1617
- let result;
1618
- try {
1619
- result = await computeDocsDrift({
1620
- projectRoot: genConfigDir,
1621
- docsDir: resolvePath(genConfigDir, docsCfg.outDir),
1622
- cwd: genConfigDir,
1623
- });
1624
- } catch (err) {
1625
- log.error(`verify --docs: regeneration failed: ${describeError(err)}`);
1626
- return 1;
1627
- }
1628
-
1629
- if (result.error !== undefined) {
1630
- log.error(result.error);
1631
- return 2;
1632
- }
1633
-
1634
- // What the gate did NOT check, on BOTH the passing and the failing line. A gate
1635
- // that quietly compared two of 589 pages and reported "no drift" would be
1636
- // indistinguishable from one that compared everything.
1637
- const notChecked =
1638
- result.ignoreReason !== undefined
1639
- ? `; .gitignore not consulted — ${result.ignoreReason} — so every page a fresh run emits was expected to be committed`
1640
- : result.ignored > 0
1641
- ? `; ${result.ignored} git-ignored page(s) not checked`
1642
- : "";
1643
-
1644
- if (result.clean) {
1645
- // The denominator is what was actually COMPARED, so the passing line and the
1646
- // failing line below count the same set. A gate whose two halves divide by
1647
- // different numbers is how `verify --templates` came to report seven templates
1648
- // vanishing between a red run and a green one.
1649
- say(
1650
- `meta verify — ${result.checked} docs page(s) match a fresh 'meta docs' ` +
1651
- `(no docs drift${notChecked}).`,
1652
- );
1653
- return 0;
1654
- }
1655
-
1656
- log.error(
1657
- `meta verify — docs drift (${result.driftedFiles.length} of ${result.checked} page(s) ` +
1658
- `differ from a fresh 'meta docs'${notChecked}):`,
1659
- );
1660
- for (const line of result.lines) log.error(` ${line}`);
1661
- log.error("Run 'meta docs' to regenerate, then commit the result.");
1662
- // The one-command path to green, named at the point of conviction — a page reported
1663
- // as uncommitted is the one case where regenerating is NOT the answer.
1664
- if (result.lines.some((l) => l.startsWith("+ "))) {
1665
- log.error(
1666
- result.ignoreReason === undefined
1667
- ? "A page you deliberately do not commit is exempt once it is git-ignored."
1668
- : `.gitignore was not consulted (${result.ignoreReason}), so a page you ` +
1669
- `deliberately do not commit is reported above.`,
1670
- );
1671
- }
1672
- return 1;
1673
- }
1674
-
1675
1717
  // -- dependency drift (Task 15, FR-023) --------------------------------------
1676
1718
  // Gated on --deps. Re-resolves each declared dependency exactly as `meta deps
1677
1719
  // check` does — never part of the bare-verify default (it needs the publisher
@@ -1765,6 +1807,28 @@ function summarizeDrift(changes: Change[]): string[] {
1765
1807
  });
1766
1808
  }
1767
1809
 
1810
+ /** One schema difference in the structured payload — the same text the stderr summary prints. */
1811
+ interface SchemaDriftRow {
1812
+ kind: Change["kind"];
1813
+ object: string;
1814
+ detail: string;
1815
+ }
1816
+
1817
+ /**
1818
+ * The schema gate's findings in the structured payload. An adopter whose `verify --db` failed
1819
+ * on views could not tell from `--format json` what differed (D3): the payload carried the
1820
+ * verdict only. `changes` is the metadata↔database comparison; `findings` are the
1821
+ * migration-ledger and committed-snapshot lines, which are text by construction.
1822
+ */
1823
+ interface SchemaDriftSection {
1824
+ changes: SchemaDriftRow[];
1825
+ findings: string[];
1826
+ }
1827
+
1828
+ function toSchemaDriftRow(c: Change): SchemaDriftRow {
1829
+ return { kind: c.kind, object: DRIFT_PRESENTATION[c.kind].noun, detail: describeChange(c) };
1830
+ }
1831
+
1768
1832
  // ---------------------------------------------------------------------------
1769
1833
  // structured output (--format toon|json)
1770
1834
  // ---------------------------------------------------------------------------
@@ -1819,7 +1883,6 @@ const GATE_SELECTOR: Record<string, string> = {
1819
1883
  templates: "--templates",
1820
1884
  schema: "--db <url>",
1821
1885
  codegen: "--codegen",
1822
- docs: "--docs",
1823
1886
  deps: "--deps",
1824
1887
  replay: "--replay",
1825
1888
  };
@@ -1890,14 +1953,26 @@ function buildVerifyPayload(input: {
1890
1953
  overlays: AdvisorySection<AdvisoryDiagnosticRow>;
1891
1954
  names: AdvisorySection<AdvisoryDiagnosticRow>;
1892
1955
  deprecations: AdvisorySection<AdvisoryDiagnosticRow>;
1956
+ fields: AdvisorySection<AdvisoryDiagnosticRow>;
1957
+ /** What the schema gate found, when it compared anything. */
1958
+ schemaDrift?: SchemaDriftSection;
1959
+ /** Gates that could not run at all (an unreachable database) — failures, not drift. */
1960
+ errors?: readonly { gate: string; error: string }[];
1893
1961
  }): Record<string, unknown> {
1894
1962
  const ran = input.gates.filter((g) => g.ran);
1895
1963
  const failed = ran.filter((g) => !g.ok);
1896
- const parts: string[] = [
1897
- failed.length === 0
1898
- ? `${ran.length} gate(s) ran, all clean`
1899
- : `${failed.length} of ${ran.length} gate(s) failed (${failed.map((g) => g.gate).join(", ")})`,
1900
- ];
1964
+ const errors = input.errors ?? [];
1965
+ const drifted = failed.filter((g) => !errors.some((e) => e.gate === g.gate));
1966
+ const parts: string[] = failed.length === 0
1967
+ ? [`${ran.length} gate(s) ran, all clean`]
1968
+ : [
1969
+ ...(drifted.length > 0
1970
+ ? [`${drifted.length} of ${ran.length} gate(s) failed (${drifted.map((g) => g.gate).join(", ")})`]
1971
+ : []),
1972
+ ...(errors.length > 0
1973
+ ? [`${errors.length} gate(s) could not run (${errors.map((e) => e.gate).join(", ")})`]
1974
+ : []),
1975
+ ];
1901
1976
  if (input.antiPatterns.status === "ran" && input.antiPatterns.total > 0) {
1902
1977
  // "advisory", not "anti-pattern": the section also carries unindexed foreign keys, a
1903
1978
  // missing baseUrl and the other non-scanner rows — `help` breaks the count down by kind.
@@ -1915,9 +1990,18 @@ function buildVerifyPayload(input: {
1915
1990
  if (input.deprecations.status === "ran" && input.deprecations.total > 0) {
1916
1991
  parts.push(`${input.deprecations.total} deprecated-reference finding(s)`);
1917
1992
  }
1993
+ if (input.fields.status === "ran" && input.fields.total > 0) {
1994
+ parts.push(`${input.fields.total} field authoring finding(s)`);
1995
+ }
1918
1996
 
1919
- const help: string[] = [];
1920
- if (failed.length > 0) {
1997
+ const help: string[] = errors.map((e) =>
1998
+ `the ${e.gate} gate could not run, which is not drift: ${e.error} — fix the connection and re-run`);
1999
+ if (drifted.some((g) => g.gate === "schema") && input.schemaDrift !== undefined) {
2000
+ help.push(
2001
+ `the schema gate's differences are in schemaDrift — changes[] (metadata vs database, one row per change) and findings[] (migration ledger and committed snapshot)`,
2002
+ );
2003
+ }
2004
+ if (drifted.some((g) => g.gate !== "schema")) {
1921
2005
  help.push(
1922
2006
  `the failing gate's drift DETAIL is printed as text on stderr — this payload carries the verdict only`,
1923
2007
  );
@@ -1940,19 +2024,26 @@ function buildVerifyPayload(input: {
1940
2024
  `${input.deprecations.total} reference(s) to a deprecated node — see deprecations.rows[]; each names the deprecated target and, where declared, its replacement`,
1941
2025
  );
1942
2026
  }
2027
+ if (input.fields.total > 0) {
2028
+ help.push(
2029
+ `${input.fields.total} field authoring finding(s) — see fields.rows[]; each names a reference identity listing a field its object lacks, or a field name declared twice in one children list`,
2030
+ );
2031
+ }
1943
2032
  if (
1944
2033
  failed.length === 0 &&
1945
2034
  input.antiPatterns.total === 0 &&
1946
2035
  input.requirements.total === 0 &&
1947
2036
  input.overlays.total === 0 &&
1948
2037
  input.names.total === 0 &&
1949
- input.deprecations.total === 0
2038
+ input.deprecations.total === 0 &&
2039
+ input.fields.total === 0
1950
2040
  ) {
1951
2041
  help.push("no drift and nothing advisory to answer — nothing to do");
1952
2042
  }
1953
2043
 
1954
2044
  return {
1955
2045
  verify: input.gates,
2046
+ ...(errors.length > 0 ? { errors } : {}),
1956
2047
  exitCode: input.exitCode,
1957
2048
  summary: parts.join("; "),
1958
2049
  help,
@@ -1961,12 +2052,14 @@ function buildVerifyPayload(input: {
1961
2052
  overlays: input.overlays,
1962
2053
  names: input.names,
1963
2054
  deprecations: input.deprecations,
2055
+ fields: input.fields,
1964
2056
  ...(input.requirementCounts !== undefined ? { requirementCounts: input.requirementCounts } : {}),
2057
+ ...(input.schemaDrift !== undefined ? { schemaDrift: input.schemaDrift } : {}),
1965
2058
  // The honest boundary. Everything named here is REACHABLE — it is printed as
1966
2059
  // text on stderr — but it is not in this document, and a reader must not have
1967
2060
  // to discover that by its absence.
1968
2061
  notRepresented: [
1969
- "per-gate drift detail (which template variable drifted, which schema change, which generated file differs, which migration failed to replay) — printed as text on stderr; this payload carries each gate's pass/fail verdict",
2062
+ "per-gate drift detail for every gate but schema (which template variable drifted, which generated file differs, which migration failed to replay) — printed as text on stderr; this payload carries each gate's pass/fail verdict, and the schema gate's differences in schemaDrift",
1970
2063
  "the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr",
1971
2064
  ],
1972
2065
  };