@metaobjectsdev/cli 1.0.13 → 1.1.1-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -22,26 +22,29 @@ import { scanForRemovedProps } from "../lib/removed-prop-advisory.js";
22
22
  import { replayRemedy } from "../lib/replay-remedy.js";
23
23
  import { FileProvider, mustacheTagLine } from "../lib/file-provider.js";
24
24
  import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
25
- import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
25
+ import { loadMemoryOptionsFrom, loadMetaobjectsConfig, RemovedNamingConfigError, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
26
26
  import { collectionLoadOptions } from "../lib/collection-load-options.js";
27
27
  import { computeCodegenDrift } from "../lib/codegen-drift.js";
28
- import { computeDocsDrift } from "../lib/docs-drift.js";
28
+ import { hasLegacyDocsBlock, legacyDocsConfigMessage, REMOVED_VERIFY_DOCS } from "../lib/docs-removed.js";
29
+ import { buildAgentSchemaInput } from "../lib/agent-schema.js";
30
+ import { sourceRootsOf } from "./gen.js";
29
31
  import { checkRequirements, summariseRequirements, scanRequirements, } from "../lib/requirement-check.js";
30
32
  import { lintRequirements } from "../lib/requirement-lint.js";
31
33
  import { lintOverlays } from "../lib/overlay-lint.js";
32
34
  import { lintNodeNames } from "../lib/name-lint.js";
33
35
  import { lintDeprecatedReferences } from "../lib/deprecation-lint.js";
36
+ import { lintDuplicateFields, lintReferenceFields } from "../lib/field-lint.js";
34
37
  import { FileSource } from "@metaobjectsdev/metadata/core";
35
38
  import { resolveD1Config, resolveMigrateConfig } from "../lib/config.js";
36
39
  import { buildWranglerExecuteArgs, defaultWranglerRunner, isWranglerLocalD1StatePath, } from "../lib/wrangler.js";
37
- import { buildProjectionViews, resolveDocsConfig } from "@metaobjectsdev/codegen-ts";
40
+ import { buildProjectionViews } from "@metaobjectsdev/codegen-ts";
38
41
  import { buildKyselyFromUrl, inferDialect } from "../lib/kysely.js";
39
42
  import { tokensToAllowOptions, describeChange } from "../lib/allow.js";
40
- import { computeDrift, computeDriftFromActual, collectUnmanagedNames, excludeFromSnapshot, scopedDiffInputs, scopeExpectedSchema, buildExpectedSchemaWithProvenance, applyPending, openReplayEngine, verifyReplay, introspect, diff, readSnapshot, snapshotPath, introspectD1, findWranglerConfig, parseWranglerConfig, resolveD1Binding, } from "@metaobjectsdev/migrate-ts";
43
+ import { computeDrift, computeDriftFromActual, collectUnmanagedNames, excludeFromSnapshot, scopedDiffInputs, scopeExpectedSchema, buildExpectedSchemaWithProvenance, applyPending, openReplayEngine, verifyReplay, introspect, diff, withoutViewRecreates, readSnapshot, snapshotPath, introspectD1, findWranglerConfig, parseWranglerConfig, resolveD1Binding, postgresViewReplaceProbe, withoutFlywayHistory, } from "@metaobjectsdev/migrate-ts";
41
44
  import { DEFAULT_METAOBJECTS_DIR, loadConfig, loadMemory, resolveCollection, } from "@metaobjectsdev/sdk";
42
45
  import { checkDependencies, ERR_DEPENDENCY_UPSTREAM_DRIFT, formatCheckLine, readLockOrThrow } from "../lib/dependency-sync.js";
43
46
  import { exclusionNotes, importedOption, migrateScopeMismatch } from "../lib/migrate-scope.js";
44
- import { TYPE_TEMPLATE, TEMPLATE_SUBTYPE_PROMPT, TEMPLATE_SUBTYPE_OUTPUT, TEMPLATE_ATTR_PAYLOAD_REF, TEMPLATE_ATTR_TEXT_REF, TEMPLATE_ATTR_REQUIRED_SLOTS, TEMPLATE_ATTR_REQUIRED_TAGS, TEMPLATE_ATTR_KIND, TEMPLATE_KIND_EMAIL, TEMPLATE_KIND_DEFAULT, TEMPLATE_ATTR_SUBJECT_REF, TEMPLATE_ATTR_HTML_BODY_REF, TEMPLATE_ATTR_TEXT_BODY_REF, REQUIREMENT_STATUSES, } from "@metaobjectsdev/metadata";
47
+ import { TYPE_TEMPLATE, TEMPLATE_SUBTYPE_PROMPT, TEMPLATE_SUBTYPE_OUTPUT, TEMPLATE_ATTR_PAYLOAD_REF, refTarget, TEMPLATE_ATTR_TEXT_REF, TEMPLATE_ATTR_REQUIRED_SLOTS, TEMPLATE_ATTR_REQUIRED_TAGS, TEMPLATE_ATTR_KIND, TEMPLATE_KIND_EMAIL, TEMPLATE_KIND_DEFAULT, TEMPLATE_ATTR_SUBJECT_REF, TEMPLATE_ATTR_HTML_BODY_REF, TEMPLATE_ATTR_TEXT_BODY_REF, REQUIREMENT_STATUSES, physicalNamingOf, } from "@metaobjectsdev/metadata";
45
48
  import { verify, ERR_REQUIRED_SLOT_UNUSED, ERR_PARTIAL_UNRESOLVED } from "@metaobjectsdev/render";
46
49
  import { describeLoadError, reportLoadError } from "../lib/load-error.js";
47
50
  import { describeError } from "../lib/error-text.js";
@@ -52,9 +55,11 @@ const DEFAULT_PROMPTS_DIR = "prompts";
52
55
  const ERR_UNKNOWN_ATTR = "ERR_UNKNOWN_ATTR";
53
56
  /**
54
57
  * A no-flags MigrateFlags, so `resolveMigrateConfig` yields exactly what `meta migrate`
55
- * would use with nothing passed on the command line — config value, else default.
58
+ * would use with nothing passed on the command line — config value, else default. The one
59
+ * migrate flag verify also takes, `--migration-format`, is laid over it per invocation, so
60
+ * the format resolves flag > config > default exactly as it does for `meta migrate`.
56
61
  *
57
- * verify consumes `outDir` (#292) and — for the replay gate ONLY — `dialect`. The #292
62
+ * verify consumes `outDir` (#292), `format` and — for the replay gate ONLY — `dialect`. The #292
58
63
  * restriction that reading anything else "would be reaching into migrate's decisions"
59
64
  * was written about the DRIFT gate, whose dialect comes from the live `--db` URL. The
60
65
  * replay gate has no `--db` at all, and the dialect a committed chain was EMITTED for
@@ -106,6 +111,15 @@ fmt = "text") {
106
111
  emitStructured({ error: msg, hint: "run `meta verify --help` for the accepted flags" }, fmt);
107
112
  return 2;
108
113
  }
114
+ // ADR-0034 Amendment 9: documentation is checked by `--codegen` with everything else the
115
+ // generator list declares. The old flag still parses, so it can say where it went. It says
116
+ // so once, as plain text on stderr, like every other removed command: a structured copy on
117
+ // stdout printed the same sentence a second time, escaped (`error: "\`meta verify ...`).
118
+ if (flags.docs) {
119
+ log.error(REMOVED_VERIFY_DOCS);
120
+ return 2;
121
+ }
122
+ const migrateFlags = { ...EMPTY_MIGRATE_FLAGS, format: flags.migrationFormat };
109
123
  // ADR-0021 D2 — explicit verify subverbs. Each flag selects one drift mode;
110
124
  // any combination runs each and the overall exit code is the MAX (non-zero on
111
125
  // any drift). A bare `verify` (no explicit subverb) keeps its documented
@@ -115,15 +129,14 @@ fmt = "text") {
115
129
  // The schema gate is selected by the presence of --db, or --dialect d1 (#225 —
116
130
  // D1 has no URL connection); that check lives inside runSchemaVerify.
117
131
  const runCodegen = flags.codegen;
118
- const runDocs = flags.docs;
119
- // Task 15 — same shape as --codegen/--docs: selected ONLY by its own flag,
132
+ // Task 15 — same shape as --codegen: selected ONLY by its own flag,
120
133
  // never folded into the bare-verify default (it needs the publisher
121
134
  // reachable, which CI may not have — see the VerifyFlags doc on `deps`).
122
135
  const runDeps = flags.deps;
123
136
  if (!flags.anyExplicit) {
124
137
  say("meta verify — running --templates (default). Explicit subverbs: " +
125
- "--templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift), " +
126
- "--docs (docs drift), --deps (dependency drift), " +
138
+ "--templates (prompt drift), --db/--dialect d1 (schema drift), " +
139
+ "--codegen (drift of everything `generators` declares, documentation included), --deps (dependency drift), " +
127
140
  "--replay/--replay-snapshot (the committed migration chain replays from empty).");
128
141
  }
129
142
  // Where the metadata lives is `resolveCollection`'s decision, not a hardcoded
@@ -187,7 +200,13 @@ fmt = "text") {
187
200
  try {
188
201
  forgeConfig = await loadMetaobjectsConfig(genConfigDir);
189
202
  }
190
- catch {
203
+ catch (e) {
204
+ // ADR-0058: the one config problem verify cannot fall back from — the default column
205
+ // rule would report every column of a database built with the old value as drift.
206
+ if (e instanceof RemovedNamingConfigError) {
207
+ log.error(e.message);
208
+ return 2;
209
+ }
191
210
  forgeConfig = undefined;
192
211
  }
193
212
  // Both of the gen config's contributions to the load, together — see
@@ -281,6 +300,7 @@ fmt = "text") {
281
300
  let nameSection = skippedSection("the name lint did not run");
282
301
  // #305 — the deprecated-reference authoring lint. Its own section, same reason.
283
302
  let deprecationSection = skippedSection("the deprecated-reference lint did not run");
303
+ let fieldSection = skippedSection("the field lint did not run");
284
304
  // The ledger counts `meta verify` prints on every run. Undefined for a project
285
305
  // declaring no requirement.* node at all (opt-in by declaration) — the payload
286
306
  // then omits the block rather than reporting zeroes that would read as an empty
@@ -295,9 +315,14 @@ fmt = "text") {
295
315
  // subverb so ANY kind of drift fails CI. Each gate only runs when its mode is
296
316
  // selected; an unselected gate contributes 0.
297
317
  const templateExit = runTemplates ? runTemplateVerify() : 0;
318
+ // Set when the schema gate could not reach or read the database: a failure, but not
319
+ // drift, and the payload must not report it as drift.
320
+ let schemaRunError;
321
+ // The schema gate's findings, for the structured payload: what differs, not only that
322
+ // something does. Left undefined when the gate did not compare anything.
323
+ let schemaDrift;
298
324
  const schemaExit = await runSchemaVerify();
299
325
  const codegenExit = runCodegen ? await runCodegenVerify() : 0;
300
- const docsExit = runDocs ? await runDocsVerify() : 0;
301
326
  const depsExit = runDeps ? await runDepsVerify() : 0;
302
327
  // Requirements have no subverb: `requirement.*` nodes are metadata, so they
303
328
  // are checked on every `meta verify`. Opt-in by DECLARATION — a model with no
@@ -320,12 +345,16 @@ fmt = "text") {
320
345
  // @from/@of/@via) on a `deprecated` node. Runs on every `meta verify`;
321
346
  // warnings ONLY, never changes the exit code.
322
347
  runDeprecationLintAdvisory();
348
+ // A reference identity over a field the object lacks, and a field name declared
349
+ // twice in one children list — both load clean on every port. Runs on every
350
+ // `meta verify`; warnings ONLY, never changes the exit code.
351
+ await runFieldLintAdvisory();
323
352
  // Advisory verify-as-teacher pass: surface hand-rolled work the metadata could
324
353
  // model. Warnings ONLY — never changes the exit code (bias to under-flagging).
325
354
  // Suppressed with --no-antipatterns or META_NO_ANTIPATTERNS=1 for the rare
326
355
  // noisy project (both opt-outs work on `meta verify` and `meta gen`).
327
356
  await runAntiPatternAdvisory();
328
- const exitCode = Math.max(templateExit, schemaExit, codegenExit, docsExit, depsExit, requirementExit, replayExit);
357
+ const exitCode = Math.max(templateExit, schemaExit, codegenExit, depsExit, requirementExit, replayExit);
329
358
  const gates = [
330
359
  { gate: "templates", ran: runTemplates, ok: templateExit === 0 },
331
360
  // The schema gate decides internally whether it is selected (--db, or
@@ -334,7 +363,6 @@ fmt = "text") {
334
363
  // failure mode this whole change exists to remove.
335
364
  { gate: "schema", ran: ranSchemaGate, ok: schemaExit === 0 },
336
365
  { gate: "codegen", ran: runCodegen, ok: codegenExit === 0 },
337
- { gate: "docs", ran: runDocs, ok: docsExit === 0 },
338
366
  { gate: "deps", ran: runDeps, ok: depsExit === 0 },
339
367
  { gate: "requirements", ran: true, ok: requirementExit === 0 },
340
368
  { gate: "replay", ran: flags.replay || flags.replaySnapshot, ok: replayExit === 0 },
@@ -358,6 +386,9 @@ fmt = "text") {
358
386
  overlays: overlaySection,
359
387
  names: nameSection,
360
388
  deprecations: deprecationSection,
389
+ fields: fieldSection,
390
+ ...(schemaDrift !== undefined ? { schemaDrift } : {}),
391
+ errors: schemaRunError !== undefined ? [{ gate: "schema", error: schemaRunError }] : [],
361
392
  }), fmt);
362
393
  }
363
394
  return exitCode;
@@ -382,7 +413,7 @@ fmt = "text") {
382
413
  // Resolve the migrations directory and the chain's dialect through MIGRATE's own
383
414
  // precedence, never a second derivation — verify must not look somewhere migrate
384
415
  // does not write, nor assume a dialect the chain was not emitted for.
385
- const migrateConfig = await resolveMigrateConfig(EMPTY_MIGRATE_FLAGS, projectRoot);
416
+ const migrateConfig = await resolveMigrateConfig(migrateFlags, projectRoot);
386
417
  if (migrateConfig.format === "flyway") {
387
418
  log.error(`meta verify --replay is not supported with --migration-format flyway — run 'flyway migrate' against a scratch database to replay`);
388
419
  return 2;
@@ -507,11 +538,9 @@ fmt = "text") {
507
538
  // passes no `governed` and gets the comparison exactly as it was.
508
539
  let governed;
509
540
  if (schemaScope !== undefined) {
510
- const viewStrategy = forgeConfig?.columnNamingStrategy ?? "snake_case";
511
541
  const built = buildExpectedSchemaWithProvenance(root, {
512
542
  dialect,
513
- columnNamingStrategy: viewStrategy,
514
- views: buildProjectionViews(root, { dialect, columnNamingStrategy: viewStrategy }),
543
+ views: buildProjectionViews(root, { dialect }),
515
544
  });
516
545
  governed = scopeExpectedSchema(built, schemaScope, importedOption(collection));
517
546
  }
@@ -550,7 +579,14 @@ fmt = "text") {
550
579
  // ONE scan for all three passes. The gate and the summary each used to walk the
551
580
  // model AND resolve every @implementedBy claim for themselves — the resolution
552
581
  // being the expensive half — and the lint added a third walk on top.
553
- const scan = scanRequirements(root, { coverable: collection.inScope });
582
+ // The strict switch (ADR-0057) is decided here, once, and travels on the scan: the
583
+ // flag or META_REQUIRE_IMPLEMENTERS=1 — a flag-or-variable pair, as
584
+ // --no-requirement-lint has below, so a CI job can turn it on without editing a
585
+ // command line it shares with local runs.
586
+ const scan = scanRequirements(root, {
587
+ coverable: collection.inCoverage,
588
+ requireImplementers: flags.requireImplementers || process.env.META_REQUIRE_IMPLEMENTERS === "1",
589
+ });
554
590
  const diags = [...checkRequirements(root, scan)];
555
591
  // Printed on EVERY run, clean or not — a gate that says nothing when it
556
592
  // passes cannot be told apart from a gate that checked nothing, and the
@@ -715,6 +751,39 @@ fmt = "text") {
715
751
  warnCapped(findings.map(formatDiagnostic), flags.limit, { structured });
716
752
  }
717
753
  }
754
+ // -- field authoring lint (advisory) ----------------------------------------
755
+ // Reads the LOADED model for the reference half and the project's OWN raw files
756
+ // for the duplicate half (see field-lint.ts) — a dependency artifact is not the
757
+ // adopter's to edit. Its own section, its own cap, and a skip that carries its
758
+ // reason rather than looking like a clean scan.
759
+ async function runFieldLintAdvisory() {
760
+ if (flags.noFieldLint) {
761
+ fieldSection = skippedSection("suppressed by --no-field-lint");
762
+ return;
763
+ }
764
+ if (process.env.META_NO_FIELD_LINT === "1") {
765
+ fieldSection = skippedSection("suppressed by META_NO_FIELD_LINT=1");
766
+ return;
767
+ }
768
+ let findings;
769
+ try {
770
+ findings = [
771
+ ...lintReferenceFields(root),
772
+ ...(await lintDuplicateFields(collection.ownFiles, (path) => new FileSource(path))),
773
+ ];
774
+ }
775
+ catch (err) {
776
+ // Never let an advisory scan break verify — and never report it as clean.
777
+ fieldSection = skippedSection(`the field lint failed: ${describeError(err)}`);
778
+ return;
779
+ }
780
+ fieldSection = ranSection(findings.map((d) => toDiagnosticRow(d, "lint")));
781
+ if (findings.length > 0) {
782
+ log.warn(`meta verify — fields: ${findings.length} authoring warning(s) ` +
783
+ `(advisory — does not fail the build):`);
784
+ warnCapped(findings.map(formatDiagnostic), flags.limit, { structured });
785
+ }
786
+ }
718
787
  // -- deprecated-reference authoring lint (advisory) -------------------------
719
788
  // #305 — reads the LOADED model (see deprecation-lint.ts). Its own section,
720
789
  // its own cap, and a skip that carries its reason rather than looking clean.
@@ -802,7 +871,6 @@ fmt = "text") {
802
871
  if (scanDialect !== "mysql") {
803
872
  unindexedFks = scanForUnindexedForeignKeys(root, {
804
873
  dialect: scanDialect,
805
- columnNamingStrategy: forgeConfig?.columnNamingStrategy ?? "snake_case",
806
874
  skip: collection.imported,
807
875
  });
808
876
  }
@@ -904,7 +972,7 @@ fmt = "text") {
904
972
  continue;
905
973
  // @payloadRef must resolve to a loaded object.value (drives the field tree
906
974
  // every render-engine drift check runs against).
907
- const fieldTree = derivePayloadFieldTree(root, payloadRef, tmpl.package ?? tmpl.fileDefaultPackage ?? "");
975
+ const fieldTree = derivePayloadFieldTree(refTarget(tmpl, TEMPLATE_ATTR_PAYLOAD_REF));
908
976
  if (fieldTree.length === 0) {
909
977
  log.error(`[${tmpl.name}] (${tmpl.subType}) ${ERR_PARTIAL_UNRESOLVED}: ` +
910
978
  `@payloadRef "${payloadRef}" did not resolve to a loaded object.value`);
@@ -1023,11 +1091,9 @@ fmt = "text") {
1023
1091
  // consults it.
1024
1092
  const scopeMismatch = migrateScopeMismatch(collection, () => {
1025
1093
  const dialect = usingD1 ? "d1" : (flags.dialect ?? inferDialect(flags.db));
1026
- const viewStrategy = forgeConfig?.columnNamingStrategy ?? "snake_case";
1027
1094
  return buildExpectedSchemaWithProvenance(root, {
1028
1095
  dialect,
1029
- columnNamingStrategy: viewStrategy,
1030
- views: buildProjectionViews(root, { dialect, columnNamingStrategy: viewStrategy }),
1096
+ views: buildProjectionViews(root, { dialect }),
1031
1097
  }).provenance;
1032
1098
  });
1033
1099
  if (scopeMismatch !== undefined) {
@@ -1063,15 +1129,20 @@ fmt = "text") {
1063
1129
  kysely = await buildKyselyFromUrl(flags.db, flags.dialect);
1064
1130
  }
1065
1131
  catch (err) {
1066
- log.error(`verify: ${describeError(err)}`);
1132
+ schemaRunError = describeError(err);
1133
+ log.error(`verify: ${schemaRunError}`);
1067
1134
  return 1;
1068
1135
  }
1069
1136
  try {
1070
1137
  const allow = tokensToAllowOptions(flags.allow);
1138
+ // Resolve the migrate config once for this whole path: `tryLoadConfig` is uncached,
1139
+ // and the flyway filter below and checkCommittedSnapshot's migrations-dir read want
1140
+ // the same value — resolving twice would read and parse `.metaobjects/config.json`
1141
+ // twice per `meta verify --db`.
1142
+ const migrateConfig = await resolveMigrateConfig(migrateFlags, projectRoot);
1071
1143
  // Expected views from the single view-SQL source (codegen-ts), so view-body
1072
1144
  // drift is detected against the live DB.
1073
- const viewStrategy = forgeConfig?.columnNamingStrategy ?? "snake_case";
1074
- const expectedViews = buildProjectionViews(root, { dialect: kysely.dialect, columnNamingStrategy: viewStrategy });
1145
+ const expectedViews = buildProjectionViews(root, { dialect: kysely.dialect });
1075
1146
  let driftResult;
1076
1147
  let actual;
1077
1148
  try {
@@ -1079,9 +1150,15 @@ fmt = "text") {
1079
1150
  // `actual` this drift comparison uses, and re-introspecting for it would both
1080
1151
  // cost a second round trip and open a window where the two could disagree.
1081
1152
  actual = await introspect(kysely.db, kysely.dialect);
1153
+ // Flyway owns its history table; it is no drift under the flyway format, as in `meta migrate`.
1154
+ if (migrateConfig.format === "flyway") {
1155
+ actual = withoutFlywayHistory(actual);
1156
+ }
1082
1157
  driftResult = await computeDriftFromActual(actual, kysely.dialect, root, {
1083
1158
  allow,
1084
1159
  views: expectedViews,
1160
+ // The same question `meta migrate` asks, so the report names the change it plans.
1161
+ ...(kysely.dialect === "postgres" ? { viewReplaceProbe: postgresViewReplaceProbe(kysely.db) } : {}),
1085
1162
  ...(schemaScope !== undefined ? { inScope: schemaScope } : {}),
1086
1163
  // The import exclusion reaches scopeExpectedSchema through this option bag —
1087
1164
  // there is no direct call on this path to attach it to.
@@ -1089,11 +1166,12 @@ fmt = "text") {
1089
1166
  });
1090
1167
  }
1091
1168
  catch (err) {
1092
- log.error(`verify: failed to introspect ${kysely.displayUrl}: ${describeError(err)}`);
1169
+ schemaRunError = `failed to introspect ${kysely.displayUrl}: ${describeError(err)}`;
1170
+ log.error(`verify: ${schemaRunError}`);
1093
1171
  return 1;
1094
1172
  }
1095
1173
  const snapshotDrift = driftResult.changes.length === 0
1096
- ? await checkCommittedSnapshot(actual, kysely.dialect, kysely.displayUrl, driftResult)
1174
+ ? await checkCommittedSnapshot(actual, kysely.dialect, kysely.displayUrl, driftResult, migrateConfig)
1097
1175
  : [];
1098
1176
  return reportSchemaDrift(driftResult, [...ledgerDrift, ...snapshotDrift], kysely.displayUrl);
1099
1177
  }
@@ -1160,8 +1238,7 @@ fmt = "text") {
1160
1238
  return 1;
1161
1239
  }
1162
1240
  const allow = tokensToAllowOptions(flags.allow);
1163
- const viewStrategy = forgeConfig?.columnNamingStrategy ?? "snake_case";
1164
- const expectedViews = buildProjectionViews(root, { dialect: "d1", columnNamingStrategy: viewStrategy });
1241
+ const expectedViews = buildProjectionViews(root, { dialect: "d1" });
1165
1242
  let driftResult;
1166
1243
  try {
1167
1244
  driftResult = await computeDriftFromActual(actual, "d1", root, {
@@ -1209,14 +1286,16 @@ fmt = "text") {
1209
1286
  // Fails OPEN when there is no snapshot on disk (a project that has never generated one
1210
1287
  // offline is not in an error state) and when the file cannot be read or parsed (that is
1211
1288
  // migrate's error to raise, with its own message, not a drift verdict).
1212
- async function checkCommittedSnapshot(actual, dialect, displayUrl, governed) {
1289
+ async function checkCommittedSnapshot(actual, dialect, displayUrl, governed,
1290
+ // Resolved by the caller and threaded through — see the `verify --db` branch: the same
1291
+ // run's flyway filter reads this config too, and `tryLoadConfig` does not cache.
1292
+ migrateConfig) {
1213
1293
  if (dialect === "d1")
1214
1294
  return []; // d1 keeps migrations Wrangler-native; no offline snapshot
1215
- // Resolve the migrations dir through migrate's OWN precedence (flag > config >
1216
- // default) rather than re-deriving it, so verify can never look somewhere migrate
1217
- // does not write. Only `outDir` is consumed; the rest of the resolved config is
1218
- // migrate's business.
1219
- const migrateConfig = await resolveMigrateConfig(EMPTY_MIGRATE_FLAGS, projectRoot);
1295
+ // The migrations dir comes from migrate's OWN precedence (flag > config > default)
1296
+ // rather than a re-derivation here, so verify can never look somewhere migrate does
1297
+ // not write. Only `outDir` and `format` are consumed; the rest of the resolved config
1298
+ // is migrate's business.
1220
1299
  const dir = resolvePath(projectRoot, migrateConfig.outDir);
1221
1300
  let snapshot;
1222
1301
  try {
@@ -1227,6 +1306,8 @@ fmt = "text") {
1227
1306
  }
1228
1307
  if (snapshot === null)
1229
1308
  return [];
1309
+ if (migrateConfig.format === "flyway")
1310
+ snapshot = withoutFlywayHistory(snapshot);
1230
1311
  // Out-of-scope objects leave BOTH sides of this comparison, and the schema pin
1231
1312
  // comes from the scope decision the DRIFT comparison already made — one door
1232
1313
  // (migrate-ts's `excludeFromSnapshot` + `scopedDiffInputs`), not a fifth
@@ -1250,7 +1331,9 @@ fmt = "text") {
1250
1331
  // MERGED with the out-of-scope set, and a second key would silently drop that half.
1251
1332
  dialect,
1252
1333
  });
1253
- if (result.changes.length === 0)
1334
+ // A view recreated only around a table change matches the snapshot (D3) — not a difference.
1335
+ const changes = withoutViewRecreates(result.changes);
1336
+ if (changes.length === 0)
1254
1337
  return [];
1255
1338
  return [
1256
1339
  // `meta migrate --from-db` is NOT the repair: it writes a snapshot only when it has
@@ -1260,10 +1343,10 @@ fmt = "text") {
1260
1343
  // been told everything is in sync. `baseline --from-db` rewrites it unconditionally,
1261
1344
  // which is the whole point of the subcommand.
1262
1345
  `the committed schema snapshot disagrees with ${displayUrl} ` +
1263
- `(${result.changes.length} difference(s)) — the next 'meta migrate' would emit DDL from it ` +
1346
+ `(${changes.length} difference(s)) — the next 'meta migrate' would emit DDL from it ` +
1264
1347
  `and fail at apply. Re-derive it with ` +
1265
1348
  `'meta migrate baseline --from-db --db <url> --dialect ${dialect}'.`,
1266
- ...summarizeDrift(result.changes),
1349
+ ...summarizeDrift(changes),
1267
1350
  ];
1268
1351
  }
1269
1352
  function reportSchemaDrift(driftResult, ledgerDrift, displayUrl) {
@@ -1282,6 +1365,7 @@ fmt = "text") {
1282
1365
  say(note);
1283
1366
  }
1284
1367
  const changes = driftResult.changes;
1368
+ schemaDrift = { changes: changes.map(toSchemaDriftRow), findings: ledgerDrift };
1285
1369
  if (changes.length === 0 && ledgerDrift.length === 0) {
1286
1370
  say(`meta verify — schema in sync with ${displayUrl}.`);
1287
1371
  return 0;
@@ -1339,12 +1423,23 @@ fmt = "text") {
1339
1423
  // and the loader's own remedies. It printed a bare message plus a hand-rolled
1340
1424
  // suggestions read — the half-true rule this file's sibling comment warns about.
1341
1425
  reportLoadError(log, "verify --codegen: failed to load this package's metadata", err);
1342
- return 2;
1426
+ return 1;
1343
1427
  }
1344
1428
  }
1429
+ // A 1.0 `docs` block configured a command that no longer exists; regenerating without it
1430
+ // would check no documentation and pass. Refused before anything is written, as `meta gen`
1431
+ // refuses it.
1432
+ if (hasLegacyDocsBlock(forgeConfig)) {
1433
+ log.error(legacyDocsConfigMessage("meta verify"));
1434
+ return 2;
1435
+ }
1436
+ const config = forgeConfig;
1345
1437
  let result;
1346
1438
  try {
1347
- result = await computeCodegenDrift(forgeConfig, codegenRoot, genConfigDir, genCollection.inScope);
1439
+ result = await computeCodegenDrift(config, codegenRoot, genConfigDir, genCollection.inScope, genCollection.ownFiles, {
1440
+ sourceRoots: sourceRootsOf(genCollection),
1441
+ agentSchema: () => buildAgentSchemaInput(codegenRoot, config.dialect, (m) => log.warn(`verify --codegen: [agent-docs] ${m}`)),
1442
+ });
1348
1443
  }
1349
1444
  catch (err) {
1350
1445
  log.error(`verify --codegen: regeneration failed: ${describeError(err)}`);
@@ -1359,6 +1454,17 @@ fmt = "text") {
1359
1454
  // contribution is current" and nothing else. Reported on every run; fails the build
1360
1455
  // only under --forbid-hand-edits.
1361
1456
  const handEditExit = reportHandEdits(result.handEdited);
1457
+ // Say how much documentation was NOT checked, on the pass and the fail line alike, so a
1458
+ // gate that compared two pages of 589 cannot read as one that compared all of them.
1459
+ if (result.docsIgnored > 0) {
1460
+ say(`meta verify — ${result.docsIgnored} generated documentation page(s) are git-ignored and were not checked.`);
1461
+ }
1462
+ for (const dir of result.docsIgnoredDirs) {
1463
+ say(`meta verify — ${dir}: the repository git-ignores every documentation page generated there, so they were not compared.`);
1464
+ }
1465
+ if (result.docsIgnoreUnavailable !== undefined) {
1466
+ say(`meta verify — git could not say which documentation pages are ignored (${result.docsIgnoreUnavailable}), so every page was checked.`);
1467
+ }
1362
1468
  if (result.clean) {
1363
1469
  // Says what it CHECKED, not more. Since 0.24.3 this gate asks whether each file's
1364
1470
  // GENERATED contribution is current — a hand edit `meta gen` preserves is exempt —
@@ -1375,9 +1481,16 @@ fmt = "text") {
1375
1481
  // it MERGES the edit and exits 0 — so prescribing it there was the unbreakable loop
1376
1482
  // the hand-edit exemption exists to end, reintroduced for the one file the exemption
1377
1483
  // deliberately excludes. Both lines print when the drift set holds both kinds.
1378
- if (result.driftedFiles.length > result.handEditedNames.length) {
1484
+ if (result.driftedFiles.length > result.handEditedNames.length + result.editedOrphans.length) {
1379
1485
  log.error("Run 'meta gen' to regenerate, then commit the result.");
1380
1486
  }
1487
+ // A hand-edited orphan is the other kind `meta gen` cannot fix: it deletes an untouched
1488
+ // file no generator emits any more, and refuses an edited one.
1489
+ if (result.editedOrphans.length > 0) {
1490
+ log.error("For the file(s) above that no generator emits any more and that were edited by hand, " +
1491
+ "'meta gen' will NOT delete them. Delete each one, or move your edits into a file of " +
1492
+ "your own first.");
1493
+ }
1381
1494
  if (result.handEditedNames.length > 0) {
1382
1495
  log.error("For the names artifact(s) above, 'meta gen' will NOT help — it preserves a hand " +
1383
1496
  "edit by design. Revert the edit (or delete the file and re-run 'meta gen' to " +
@@ -1404,77 +1517,6 @@ fmt = "text") {
1404
1517
  log.warn(line);
1405
1518
  return 0;
1406
1519
  }
1407
- // -- docs drift -------------------------------------------------------------
1408
- // Gated on --docs. Runs `meta docs` into a temp dir and diffs the committed docs
1409
- // tree. See lib/docs-drift.ts for the deliberate differences from --codegen: a byte
1410
- // difference IS drift here; a committed file a regen would not emit is reported ONLY
1411
- // under `agent/` and only when it opens with the @generated marker (`docs.outDir` is
1412
- // otherwise full of files MetaObjects did not write); and a page the project
1413
- // GIT-IGNORES is exempt, because `docs.outDir` is a directory rather than a namespace
1414
- // MetaObjects owns.
1415
- //
1416
- // It needs a config for the same reason `--codegen` does: `docs.outDir` says which
1417
- // tree to compare against, and the `agent` surface will not even materialise
1418
- // without one.
1419
- async function runDocsVerify() {
1420
- if (forgeConfig === undefined) {
1421
- log.error("verify --docs: no metaobjects.config.ts found (or it is invalid) — " +
1422
- "cannot locate the committed docs tree to diff against. " +
1423
- "Run 'meta init' to scaffold one, or run without --docs.");
1424
- return 2;
1425
- }
1426
- // Resolved through the SAME resolver `meta docs` uses, so a project that moved its
1427
- // docs elsewhere is checked where its docs actually are. `outputLayout` is the
1428
- // documented fallback for `docs.layout`.
1429
- const docsCfg = resolveDocsConfig(forgeConfig.docs, {}, forgeConfig.outputLayout ?? "flat");
1430
- let result;
1431
- try {
1432
- result = await computeDocsDrift({
1433
- projectRoot: genConfigDir,
1434
- docsDir: resolvePath(genConfigDir, docsCfg.outDir),
1435
- cwd: genConfigDir,
1436
- });
1437
- }
1438
- catch (err) {
1439
- log.error(`verify --docs: regeneration failed: ${describeError(err)}`);
1440
- return 1;
1441
- }
1442
- if (result.error !== undefined) {
1443
- log.error(result.error);
1444
- return 2;
1445
- }
1446
- // What the gate did NOT check, on BOTH the passing and the failing line. A gate
1447
- // that quietly compared two of 589 pages and reported "no drift" would be
1448
- // indistinguishable from one that compared everything.
1449
- const notChecked = result.ignoreReason !== undefined
1450
- ? `; .gitignore not consulted — ${result.ignoreReason} — so every page a fresh run emits was expected to be committed`
1451
- : result.ignored > 0
1452
- ? `; ${result.ignored} git-ignored page(s) not checked`
1453
- : "";
1454
- if (result.clean) {
1455
- // The denominator is what was actually COMPARED, so the passing line and the
1456
- // failing line below count the same set. A gate whose two halves divide by
1457
- // different numbers is how `verify --templates` came to report seven templates
1458
- // vanishing between a red run and a green one.
1459
- say(`meta verify — ${result.checked} docs page(s) match a fresh 'meta docs' ` +
1460
- `(no docs drift${notChecked}).`);
1461
- return 0;
1462
- }
1463
- log.error(`meta verify — docs drift (${result.driftedFiles.length} of ${result.checked} page(s) ` +
1464
- `differ from a fresh 'meta docs'${notChecked}):`);
1465
- for (const line of result.lines)
1466
- log.error(` ${line}`);
1467
- log.error("Run 'meta docs' to regenerate, then commit the result.");
1468
- // The one-command path to green, named at the point of conviction — a page reported
1469
- // as uncommitted is the one case where regenerating is NOT the answer.
1470
- if (result.lines.some((l) => l.startsWith("+ "))) {
1471
- log.error(result.ignoreReason === undefined
1472
- ? "A page you deliberately do not commit is exempt once it is git-ignored."
1473
- : `.gitignore was not consulted (${result.ignoreReason}), so a page you ` +
1474
- `deliberately do not commit is reported above.`);
1475
- }
1476
- return 1;
1477
- }
1478
1520
  // -- dependency drift (Task 15, FR-023) --------------------------------------
1479
1521
  // Gated on --deps. Re-resolves each declared dependency exactly as `meta deps
1480
1522
  // check` does — never part of the bare-verify default (it needs the publisher
@@ -1561,6 +1603,9 @@ function summarizeDrift(changes) {
1561
1603
  return `${p.glyph} ${p.noun} ${describeChange(c)}`;
1562
1604
  });
1563
1605
  }
1606
+ function toSchemaDriftRow(c) {
1607
+ return { kind: c.kind, object: DRIFT_PRESENTATION[c.kind].noun, detail: describeChange(c) };
1608
+ }
1564
1609
  /**
1565
1610
  * The hand-edit report for `verify --codegen`: a header with the count, one indented line
1566
1611
  * per path, then how to see an edit. Exported for its test.
@@ -1589,7 +1634,6 @@ const GATE_SELECTOR = {
1589
1634
  templates: "--templates",
1590
1635
  schema: "--db <url>",
1591
1636
  codegen: "--codegen",
1592
- docs: "--docs",
1593
1637
  deps: "--deps",
1594
1638
  replay: "--replay",
1595
1639
  };
@@ -1648,11 +1692,18 @@ function toDiagnosticRow(d, source) {
1648
1692
  function buildVerifyPayload(input) {
1649
1693
  const ran = input.gates.filter((g) => g.ran);
1650
1694
  const failed = ran.filter((g) => !g.ok);
1651
- const parts = [
1652
- failed.length === 0
1653
- ? `${ran.length} gate(s) ran, all clean`
1654
- : `${failed.length} of ${ran.length} gate(s) failed (${failed.map((g) => g.gate).join(", ")})`,
1655
- ];
1695
+ const errors = input.errors ?? [];
1696
+ const drifted = failed.filter((g) => !errors.some((e) => e.gate === g.gate));
1697
+ const parts = failed.length === 0
1698
+ ? [`${ran.length} gate(s) ran, all clean`]
1699
+ : [
1700
+ ...(drifted.length > 0
1701
+ ? [`${drifted.length} of ${ran.length} gate(s) failed (${drifted.map((g) => g.gate).join(", ")})`]
1702
+ : []),
1703
+ ...(errors.length > 0
1704
+ ? [`${errors.length} gate(s) could not run (${errors.map((e) => e.gate).join(", ")})`]
1705
+ : []),
1706
+ ];
1656
1707
  if (input.antiPatterns.status === "ran" && input.antiPatterns.total > 0) {
1657
1708
  // "advisory", not "anti-pattern": the section also carries unindexed foreign keys, a
1658
1709
  // missing baseUrl and the other non-scanner rows — `help` breaks the count down by kind.
@@ -1670,8 +1721,14 @@ function buildVerifyPayload(input) {
1670
1721
  if (input.deprecations.status === "ran" && input.deprecations.total > 0) {
1671
1722
  parts.push(`${input.deprecations.total} deprecated-reference finding(s)`);
1672
1723
  }
1673
- const help = [];
1674
- if (failed.length > 0) {
1724
+ if (input.fields.status === "ran" && input.fields.total > 0) {
1725
+ parts.push(`${input.fields.total} field authoring finding(s)`);
1726
+ }
1727
+ const help = errors.map((e) => `the ${e.gate} gate could not run, which is not drift: ${e.error} — fix the connection and re-run`);
1728
+ if (drifted.some((g) => g.gate === "schema") && input.schemaDrift !== undefined) {
1729
+ help.push(`the schema gate's differences are in schemaDrift — changes[] (metadata vs database, one row per change) and findings[] (migration ledger and committed snapshot)`);
1730
+ }
1731
+ if (drifted.some((g) => g.gate !== "schema")) {
1675
1732
  help.push(`the failing gate's drift DETAIL is printed as text on stderr — this payload carries the verdict only`);
1676
1733
  }
1677
1734
  if (input.antiPatterns.total > 0) {
@@ -1686,16 +1743,21 @@ function buildVerifyPayload(input) {
1686
1743
  if (input.deprecations.total > 0) {
1687
1744
  help.push(`${input.deprecations.total} reference(s) to a deprecated node — see deprecations.rows[]; each names the deprecated target and, where declared, its replacement`);
1688
1745
  }
1746
+ if (input.fields.total > 0) {
1747
+ help.push(`${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`);
1748
+ }
1689
1749
  if (failed.length === 0 &&
1690
1750
  input.antiPatterns.total === 0 &&
1691
1751
  input.requirements.total === 0 &&
1692
1752
  input.overlays.total === 0 &&
1693
1753
  input.names.total === 0 &&
1694
- input.deprecations.total === 0) {
1754
+ input.deprecations.total === 0 &&
1755
+ input.fields.total === 0) {
1695
1756
  help.push("no drift and nothing advisory to answer — nothing to do");
1696
1757
  }
1697
1758
  return {
1698
1759
  verify: input.gates,
1760
+ ...(errors.length > 0 ? { errors } : {}),
1699
1761
  exitCode: input.exitCode,
1700
1762
  summary: parts.join("; "),
1701
1763
  help,
@@ -1704,12 +1766,14 @@ function buildVerifyPayload(input) {
1704
1766
  overlays: input.overlays,
1705
1767
  names: input.names,
1706
1768
  deprecations: input.deprecations,
1769
+ fields: input.fields,
1707
1770
  ...(input.requirementCounts !== undefined ? { requirementCounts: input.requirementCounts } : {}),
1771
+ ...(input.schemaDrift !== undefined ? { schemaDrift: input.schemaDrift } : {}),
1708
1772
  // The honest boundary. Everything named here is REACHABLE — it is printed as
1709
1773
  // text on stderr — but it is not in this document, and a reader must not have
1710
1774
  // to discover that by its absence.
1711
1775
  notRepresented: [
1712
- "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",
1776
+ "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",
1713
1777
  "the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr",
1714
1778
  ],
1715
1779
  };