@metaobjectsdev/cli 1.0.2 → 1.0.4-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 (115) hide show
  1. package/README.md +13 -5
  2. package/dist/src/commands/deps.d.ts +3 -0
  3. package/dist/src/commands/deps.d.ts.map +1 -0
  4. package/dist/src/commands/deps.js +200 -0
  5. package/dist/src/commands/deps.js.map +1 -0
  6. package/dist/src/commands/docs.d.ts.map +1 -1
  7. package/dist/src/commands/docs.js +19 -3
  8. package/dist/src/commands/docs.js.map +1 -1
  9. package/dist/src/commands/eject.d.ts +2 -1
  10. package/dist/src/commands/eject.d.ts.map +1 -1
  11. package/dist/src/commands/eject.js +235 -75
  12. package/dist/src/commands/eject.js.map +1 -1
  13. package/dist/src/commands/gen.d.ts.map +1 -1
  14. package/dist/src/commands/gen.js +173 -30
  15. package/dist/src/commands/gen.js.map +1 -1
  16. package/dist/src/commands/init.d.ts +5 -9
  17. package/dist/src/commands/init.d.ts.map +1 -1
  18. package/dist/src/commands/init.js +92 -402
  19. package/dist/src/commands/init.js.map +1 -1
  20. package/dist/src/commands/migrate.d.ts.map +1 -1
  21. package/dist/src/commands/migrate.js +24 -19
  22. package/dist/src/commands/migrate.js.map +1 -1
  23. package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
  24. package/dist/src/commands/prompt-snapshot.js +2 -1
  25. package/dist/src/commands/prompt-snapshot.js.map +1 -1
  26. package/dist/src/commands/verify.d.ts.map +1 -1
  27. package/dist/src/commands/verify.js +186 -29
  28. package/dist/src/commands/verify.js.map +1 -1
  29. package/dist/src/index.d.ts.map +1 -1
  30. package/dist/src/index.js +97 -15
  31. package/dist/src/index.js.map +1 -1
  32. package/dist/src/lib/advisory.d.ts +6 -0
  33. package/dist/src/lib/advisory.d.ts.map +1 -1
  34. package/dist/src/lib/advisory.js +10 -0
  35. package/dist/src/lib/advisory.js.map +1 -1
  36. package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
  37. package/dist/src/lib/agent-context-staleness.js +7 -1
  38. package/dist/src/lib/agent-context-staleness.js.map +1 -1
  39. package/dist/src/lib/args.d.ts +75 -3
  40. package/dist/src/lib/args.d.ts.map +1 -1
  41. package/dist/src/lib/args.js +54 -5
  42. package/dist/src/lib/args.js.map +1 -1
  43. package/dist/src/lib/catalog-listing.d.ts +92 -0
  44. package/dist/src/lib/catalog-listing.d.ts.map +1 -0
  45. package/dist/src/lib/catalog-listing.js +268 -0
  46. package/dist/src/lib/catalog-listing.js.map +1 -0
  47. package/dist/src/lib/catalog.d.ts +41 -0
  48. package/dist/src/lib/catalog.d.ts.map +1 -0
  49. package/dist/src/lib/catalog.js +89 -0
  50. package/dist/src/lib/catalog.js.map +1 -0
  51. package/dist/src/lib/collection-load-options.d.ts +13 -0
  52. package/dist/src/lib/collection-load-options.d.ts.map +1 -0
  53. package/dist/src/lib/collection-load-options.js +25 -0
  54. package/dist/src/lib/collection-load-options.js.map +1 -0
  55. package/dist/src/lib/dependency-sync.d.ts +170 -0
  56. package/dist/src/lib/dependency-sync.d.ts.map +1 -0
  57. package/dist/src/lib/dependency-sync.js +457 -0
  58. package/dist/src/lib/dependency-sync.js.map +1 -0
  59. package/dist/src/lib/install-set.d.ts +31 -0
  60. package/dist/src/lib/install-set.d.ts.map +1 -0
  61. package/dist/src/lib/install-set.js +103 -0
  62. package/dist/src/lib/install-set.js.map +1 -0
  63. package/dist/src/lib/library-eject.d.ts +79 -0
  64. package/dist/src/lib/library-eject.d.ts.map +1 -0
  65. package/dist/src/lib/library-eject.js +241 -0
  66. package/dist/src/lib/library-eject.js.map +1 -0
  67. package/dist/src/lib/library-listing.d.ts +63 -0
  68. package/dist/src/lib/library-listing.d.ts.map +1 -0
  69. package/dist/src/lib/library-listing.js +178 -0
  70. package/dist/src/lib/library-listing.js.map +1 -0
  71. package/dist/src/lib/library-prefix-advisory.d.ts +17 -0
  72. package/dist/src/lib/library-prefix-advisory.d.ts.map +1 -0
  73. package/dist/src/lib/library-prefix-advisory.js +67 -0
  74. package/dist/src/lib/library-prefix-advisory.js.map +1 -0
  75. package/dist/src/lib/load-metaobjects-config.d.ts +1 -2
  76. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  77. package/dist/src/lib/load-metaobjects-config.js +11 -17
  78. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  79. package/dist/src/lib/migrate-scope.d.ts +32 -1
  80. package/dist/src/lib/migrate-scope.d.ts.map +1 -1
  81. package/dist/src/lib/migrate-scope.js +51 -3
  82. package/dist/src/lib/migrate-scope.js.map +1 -1
  83. package/dist/src/lib/overlay-lint.d.ts +37 -0
  84. package/dist/src/lib/overlay-lint.d.ts.map +1 -0
  85. package/dist/src/lib/overlay-lint.js +102 -0
  86. package/dist/src/lib/overlay-lint.js.map +1 -0
  87. package/dist/src/lib/requirement-check.d.ts +26 -3
  88. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  89. package/dist/src/lib/requirement-check.js +71 -13
  90. package/dist/src/lib/requirement-check.js.map +1 -1
  91. package/package.json +10 -10
  92. package/src/commands/deps.ts +231 -0
  93. package/src/commands/docs.ts +19 -3
  94. package/src/commands/eject.ts +304 -90
  95. package/src/commands/gen.ts +189 -29
  96. package/src/commands/init.ts +95 -407
  97. package/src/commands/migrate.ts +26 -16
  98. package/src/commands/prompt-snapshot.ts +2 -1
  99. package/src/commands/verify.ts +213 -30
  100. package/src/index.ts +97 -15
  101. package/src/lib/advisory.ts +13 -0
  102. package/src/lib/agent-context-staleness.ts +7 -1
  103. package/src/lib/args.ts +118 -8
  104. package/src/lib/catalog-listing.ts +369 -0
  105. package/src/lib/catalog.ts +107 -0
  106. package/src/lib/collection-load-options.ts +42 -0
  107. package/src/lib/dependency-sync.ts +656 -0
  108. package/src/lib/install-set.ts +116 -0
  109. package/src/lib/library-eject.ts +298 -0
  110. package/src/lib/library-listing.ts +258 -0
  111. package/src/lib/library-prefix-advisory.ts +78 -0
  112. package/src/lib/load-metaobjects-config.ts +15 -21
  113. package/src/lib/migrate-scope.ts +59 -4
  114. package/src/lib/overlay-lint.ts +129 -0
  115. package/src/lib/requirement-check.ts +100 -14
@@ -13,7 +13,8 @@ import { buildKyselyFromUrl, redactUrl } from "../lib/kysely.js";
13
13
  import { log } from "../lib/log.js";
14
14
  import { loadMemory, resolveCollection, resolveConfigDir, type Collection } from "@metaobjectsdev/sdk";
15
15
  import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
16
- import { migrateScopeMismatch, outOfScopeNote } from "../lib/migrate-scope.js";
16
+ import { collectionLoadOptions } from "../lib/collection-load-options.js";
17
+ import { exclusionNotes, importedOption, migrateScopeMismatch } from "../lib/migrate-scope.js";
17
18
  import {
18
19
  allowOptionFor,
19
20
  buildExpectedSchemaWithProvenance,
@@ -201,7 +202,8 @@ function warnIfLedgerRelocated(cwd: string, resolvedOutDir: string): void {
201
202
  }
202
203
 
203
204
  /**
204
- * Report what a declared `migrate.scope` left out (wording: `outOfScopeNote`).
205
+ * Report what this run did not govern — what a declared `migrate.scope` left out, and
206
+ * what a DEPENDENCY owns (wording: `exclusionNotes`).
205
207
  *
206
208
  * STDOUT in text format, STDERR otherwise. `--format json` / `--format toon` put a
207
209
  * single machine-readable document on stdout, and a prose line ahead of it breaks
@@ -210,11 +212,15 @@ function warnIfLedgerRelocated(cwd: string, resolvedOutDir: string): void {
210
212
  * (`resolveFormat`): suppressing it outright would silence the note for every
211
213
  * piped and CI run, which is most of them.
212
214
  */
213
- function logOutOfScope(names: readonly string[], fmt: OutputFormat): void {
214
- if (names.length === 0) return;
215
- const msg = outOfScopeNote("migrate", names);
216
- if (fmt === "text") log.info(msg);
217
- else log.warn(msg);
215
+ function logOutOfScope(
216
+ names: readonly string[],
217
+ fromDependencies: readonly string[],
218
+ fmt: OutputFormat,
219
+ ): void {
220
+ for (const msg of exclusionNotes("migrate", names, fromDependencies)) {
221
+ if (fmt === "text") log.info(msg);
222
+ else log.warn(msg);
223
+ }
218
224
  }
219
225
 
220
226
  function emitStructuredError(error: string, hint: string, fmt: OutputFormat): void {
@@ -629,7 +635,7 @@ export async function migrateCommand(
629
635
  let metadata;
630
636
  try {
631
637
  metadata = await loadMemory(collection.configDir, {
632
- files: collection.files,
638
+ ...collectionLoadOptions(collection),
633
639
  ...postgresLoadOptions,
634
640
  });
635
641
  } catch (err) {
@@ -679,9 +685,9 @@ export async function migrateCommand(
679
685
  // leave the expected schema here and are suppressed on the actual side below —
680
686
  // dropping them from `expected` ALONE would propose DROP TABLE for every one of
681
687
  // them that exists in the database.
682
- const scoped = scopeExpectedSchema(built, collection.inMigrateScope);
688
+ const scoped = scopeExpectedSchema(built, collection.inMigrateScope, importedOption(collection));
683
689
  const expected = scoped.snapshot;
684
- logOutOfScope(scoped.outOfScope, fmt);
690
+ logOutOfScope(scoped.outOfScope, scoped.importedOutOfScope ?? [], fmt);
685
691
  let actual;
686
692
  try {
687
693
  actual = await introspect(kysely.db, kysely.dialect);
@@ -1057,7 +1063,7 @@ export async function runBaseline(
1057
1063
  try {
1058
1064
  const collection = await resolveCollection(metaRoot);
1059
1065
  metadata = await loadMemory(collection.configDir, {
1060
- files: collection.files,
1066
+ ...collectionLoadOptions(collection),
1061
1067
  ...baselineLoadOptions,
1062
1068
  });
1063
1069
  } catch (err) {
@@ -1196,7 +1202,7 @@ export async function runOfflineGenerate(
1196
1202
  try {
1197
1203
  collection = await resolveCollection(metaRoot);
1198
1204
  metadata = await loadMemory(collection.configDir, {
1199
- files: collection.files,
1205
+ ...collectionLoadOptions(collection),
1200
1206
  ...offlineLoadOptions,
1201
1207
  });
1202
1208
  } catch (err) {
@@ -1256,6 +1262,10 @@ export async function runOfflineGenerate(
1256
1262
  views: offlineViews,
1257
1263
  // Per-command scope — narrows BOTH sides of the offline diff (see planOffline).
1258
1264
  ...(offlineScope !== undefined ? { inScope: offlineScope } : {}),
1265
+ // ...and the import exclusion, which planOffline threads into the SAME call.
1266
+ // Passing it only at the direct call sites would leave the offline pipeline —
1267
+ // the default `meta migrate` — performing no import exclusion at all.
1268
+ ...importedOption(collection),
1259
1269
  allow: tokensToAllowOptions(config.allow),
1260
1270
  onAmbiguous: async (a) => {
1261
1271
  collectedAmbiguous.push(a);
@@ -1280,7 +1290,7 @@ export async function runOfflineGenerate(
1280
1290
  // entries); `expected` is the governed side the emitter renders against. Equal
1281
1291
  // for an unscoped run.
1282
1292
  const { diff: diffResult, nextSnapshot, expected: governedExpected } = plan;
1283
- logOutOfScope(plan.outOfScope, fmt);
1293
+ logOutOfScope(plan.outOfScope, plan.importedOutOfScope ?? [], fmt);
1284
1294
 
1285
1295
  if (diffResult.blocked.length > 0) {
1286
1296
  log.error(`migrate: ${diffResult.blocked.length} destructive change(s) blocked; re-run with --allow <tokens>`);
@@ -1461,7 +1471,7 @@ async function runD1Migrate(
1461
1471
  let metadata;
1462
1472
  try {
1463
1473
  metadata = await loadMemory(collection.configDir, {
1464
- files: collection.files,
1474
+ ...collectionLoadOptions(collection),
1465
1475
  ...d1LoadOptions,
1466
1476
  });
1467
1477
  } catch (err) {
@@ -1482,9 +1492,9 @@ async function runD1Migrate(
1482
1492
  const scopeRc = refuseScopeMismatch(collection, () => built.provenance, fmt);
1483
1493
  if (scopeRc !== undefined) return scopeRc;
1484
1494
  // Per-command scope — both-sided, exactly as on the Kysely path above.
1485
- const scoped = scopeExpectedSchema(built, collection.inMigrateScope);
1495
+ const scoped = scopeExpectedSchema(built, collection.inMigrateScope, importedOption(collection));
1486
1496
  const expected = scoped.snapshot;
1487
- logOutOfScope(scoped.outOfScope, fmt);
1497
+ logOutOfScope(scoped.outOfScope, scoped.importedOutOfScope ?? [], fmt);
1488
1498
  let actual;
1489
1499
  try {
1490
1500
  actual = await introspectD1({
@@ -16,6 +16,7 @@ import { FileProvider } from "../lib/file-provider.js";
16
16
  import { snapshotPaths, unifiedDiff } from "../lib/snapshot.js";
17
17
  import { reportLoadError } from "../lib/load-error.js";
18
18
  import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
19
+ import { collectionLoadOptions } from "../lib/collection-load-options.js";
19
20
  import { loadMemory, resolveCollection } from "@metaobjectsdev/sdk";
20
21
  import { TYPE_TEMPLATE, TEMPLATE_ATTR_TEXT_REF, TEMPLATE_ATTR_FORMAT } from "@metaobjectsdev/metadata";
21
22
  import { render, ESCAPERS, type RenderFormat } from "@metaobjectsdev/render";
@@ -75,7 +76,7 @@ export async function promptSnapshotCommand(args: string[], cwd: string): Promis
75
76
  let root;
76
77
  try {
77
78
  root = await loadMemory(collection.configDir, {
78
- files: collection.files,
79
+ ...collectionLoadOptions(collection),
79
80
  ...configLoadOptions,
80
81
  });
81
82
  } catch (err) {
@@ -14,7 +14,9 @@ import { emitStructured, type OutputFormat } from "../lib/format.js";
14
14
  import {
15
15
  antiPatternRows, missingBaseUrlRows, removedPropRows, ranSection, skippedSection, warnCapped,
16
16
  type AdvisoryDiagnosticRow, type AdvisoryFindingRow, type AdvisorySection,
17
+ libraryPrefixRows,
17
18
  } from "../lib/advisory.js";
19
+ import { scanForUnprovenancedLibraryPrefix } from "../lib/library-prefix-advisory.js";
18
20
  import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
19
21
  import { warnIfManifestIgnored } from "../lib/manifest-ignored-check.js";
20
22
  import { scanSourceForAntiPatterns } from "../lib/anti-patterns.js";
@@ -24,12 +26,15 @@ import { replayRemedy } from "../lib/replay-remedy.js";
24
26
  import { FileProvider } from "../lib/file-provider.js";
25
27
  import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
26
28
  import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
29
+ import { collectionLoadOptions } from "../lib/collection-load-options.js";
27
30
  import { computeCodegenDrift } from "../lib/codegen-drift.js";
28
31
  import { computeDocsDrift } from "../lib/docs-drift.js";
29
32
  import {
30
33
  checkRequirements, summariseRequirements, scanRequirements, type Diagnostic,
31
34
  } from "../lib/requirement-check.js";
32
35
  import { lintRequirements } from "../lib/requirement-lint.js";
36
+ import { lintOverlays } from "../lib/overlay-lint.js";
37
+ import { FileSource } from "@metaobjectsdev/metadata/core";
33
38
  import { resolveD1Config, resolveMigrateConfig } from "../lib/config.js";
34
39
  import {
35
40
  buildWranglerExecuteArgs,
@@ -69,8 +74,15 @@ import {
69
74
  type D1Runner,
70
75
  type DriftResult,
71
76
  } from "@metaobjectsdev/migrate-ts";
72
- import { loadMemory, resolveCollection } from "@metaobjectsdev/sdk";
73
- import { migrateScopeMismatch, outOfScopeNote } from "../lib/migrate-scope.js";
77
+ import {
78
+ DEFAULT_METAOBJECTS_DIR,
79
+ loadConfig,
80
+ loadMemory,
81
+ resolveCollection,
82
+ type DependencySpec,
83
+ } from "@metaobjectsdev/sdk";
84
+ import { checkDependencies, ERR_DEPENDENCY_UPSTREAM_DRIFT, formatCheckLine, readLockOrThrow } from "../lib/dependency-sync.js";
85
+ import { exclusionNotes, importedOption, migrateScopeMismatch } from "../lib/migrate-scope.js";
74
86
  import {
75
87
  TYPE_TEMPLATE,
76
88
  TEMPLATE_SUBTYPE_PROMPT,
@@ -163,11 +175,15 @@ export async function verifyCommand(
163
175
  // D1 has no URL connection); that check lives inside runSchemaVerify.
164
176
  const runCodegen = flags.codegen;
165
177
  const runDocs = flags.docs;
178
+ // Task 15 — same shape as --codegen/--docs: selected ONLY by its own flag,
179
+ // never folded into the bare-verify default (it needs the publisher
180
+ // reachable, which CI may not have — see the VerifyFlags doc on `deps`).
181
+ const runDeps = flags.deps;
166
182
  if (!flags.anyExplicit) {
167
183
  say(
168
184
  "meta verify — running --templates (default). Explicit subverbs: " +
169
185
  "--templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift), " +
170
- "--docs (docs drift), " +
186
+ "--docs (docs drift), --deps (dependency drift), " +
171
187
  "--replay/--replay-snapshot (the committed migration chain replays from empty).",
172
188
  );
173
189
  }
@@ -248,7 +264,7 @@ export async function verifyCommand(
248
264
  let root: Awaited<ReturnType<typeof loadMemory>>;
249
265
  try {
250
266
  root = await loadMemory(collection.configDir, {
251
- files: collection.files,
267
+ ...collectionLoadOptions(collection),
252
268
  ...configLoadOptions,
253
269
  strict: !flags.lax,
254
270
  });
@@ -314,7 +330,7 @@ export async function verifyCommand(
314
330
  const promptsDir = join(projectRoot, flags.prompts ?? DEFAULT_PROMPTS_DIR);
315
331
  const provider = new FileProvider(promptsDir);
316
332
 
317
- // The two advisory sections, captured as the gates run so the structured payload
333
+ // The advisory sections, captured as the gates run so the structured payload
318
334
  // can carry them IN FULL. They were previously formatted straight to stderr and
319
335
  // existed nowhere else, which is why 96% of a 239-finding report was unreachable
320
336
  // by any flag, env var or format.
@@ -322,6 +338,12 @@ export async function verifyCommand(
322
338
  skippedSection("the requirement pass did not run");
323
339
  let antiPatternSection: AdvisorySection<AdvisoryFindingRow> =
324
340
  skippedSection("the advisory anti-pattern pass did not run");
341
+ // FR-023 §11.1 item 4 (Task 17) — the overlay authoring lint. Its own section
342
+ // (never folded into `requirements`, which is about a different kind of node
343
+ // entirely) so the structured payload names what ran and what didn't the same
344
+ // way every other advisory pass does.
345
+ let overlaySection: AdvisorySection<AdvisoryDiagnosticRow> =
346
+ skippedSection("the overlay lint did not run");
325
347
  // The ledger counts `meta verify` prints on every run. Undefined for a project
326
348
  // declaring no requirement.* node at all (opt-in by declaration) — the payload
327
349
  // then omits the block rather than reporting zeroes that would read as an empty
@@ -341,6 +363,7 @@ export async function verifyCommand(
341
363
  const schemaExit = await runSchemaVerify();
342
364
  const codegenExit = runCodegen ? await runCodegenVerify() : 0;
343
365
  const docsExit = runDocs ? await runDocsVerify() : 0;
366
+ const depsExit = runDeps ? await runDepsVerify() : 0;
344
367
  // Requirements have no subverb: `requirement.*` nodes are metadata, so they
345
368
  // are checked on every `meta verify`. Opt-in by DECLARATION — a model with no
346
369
  // requirement nodes is silent, not in drift.
@@ -351,17 +374,24 @@ export async function verifyCommand(
351
374
  // would parse cleanly and do nothing at all.
352
375
  const replayExit = flags.replay || flags.replaySnapshot ? await runReplayVerify() : 0;
353
376
 
377
+ // FR-023 §11.1 item 4 (Task 17) — the overlay authoring lint. Runs on every
378
+ // `meta verify`, not gated on any subverb (an unflagged cross-file
379
+ // redeclaration is a risk regardless of which drift gates were selected).
380
+ // Warnings ONLY — never changes the exit code.
381
+ await runOverlayLintAdvisory();
382
+
354
383
  // Advisory verify-as-teacher pass: surface hand-rolled work the metadata could
355
384
  // model. Warnings ONLY — never changes the exit code (bias to under-flagging).
356
385
  // Suppressed with --no-antipatterns or META_NO_ANTIPATTERNS=1 for the rare
357
386
  // noisy project (both opt-outs work on `meta verify` and `meta gen`).
358
- runAntiPatternAdvisory();
387
+ await runAntiPatternAdvisory();
359
388
 
360
389
  const exitCode = Math.max(
361
390
  templateExit,
362
391
  schemaExit,
363
392
  codegenExit,
364
393
  docsExit,
394
+ depsExit,
365
395
  requirementExit,
366
396
  replayExit,
367
397
  );
@@ -378,6 +408,7 @@ export async function verifyCommand(
378
408
  { gate: "schema", ran: ranSchemaGate, ok: schemaExit === 0 },
379
409
  { gate: "codegen", ran: runCodegen, ok: codegenExit === 0 },
380
410
  { gate: "docs", ran: runDocs, ok: docsExit === 0 },
411
+ { gate: "deps", ran: runDeps, ok: depsExit === 0 },
381
412
  { gate: "requirements", ran: true, ok: requirementExit === 0 },
382
413
  { gate: "replay", ran: flags.replay || flags.replaySnapshot, ok: replayExit === 0 },
383
414
  ],
@@ -385,6 +416,7 @@ export async function verifyCommand(
385
416
  requirements: requirementSection,
386
417
  requirementCounts,
387
418
  antiPatterns: antiPatternSection,
419
+ overlays: overlaySection,
388
420
  }),
389
421
  fmt,
390
422
  );
@@ -553,7 +585,7 @@ export async function verifyCommand(
553
585
  columnNamingStrategy: viewStrategy,
554
586
  views: buildProjectionViews(root, { dialect, columnNamingStrategy: viewStrategy }),
555
587
  });
556
- governed = scopeExpectedSchema(built, schemaScope);
588
+ governed = scopeExpectedSchema(built, schemaScope, importedOption(collection));
557
589
  }
558
590
 
559
591
  // `verifyReplay` calls `applyPending` itself. That is NOT a second replay: the
@@ -594,7 +626,7 @@ export async function verifyCommand(
594
626
  // ONE scan for all three passes. The gate and the summary each used to walk the
595
627
  // model AND resolve every @implementedBy claim for themselves — the resolution
596
628
  // being the expensive half — and the lint added a third walk on top.
597
- const scan = scanRequirements(root);
629
+ const scan = scanRequirements(root, { coverable: collection.inScope });
598
630
  const diags = [...checkRequirements(root, scan)];
599
631
 
600
632
  // Printed on EVERY run, clean or not — a gate that says nothing when it
@@ -616,8 +648,20 @@ export async function verifyCommand(
616
648
  say(
617
649
  `meta verify — requirements: ${s.total} entries (${s.functional} functional, ` +
618
650
  `${s.architectural} architectural) — ${parts.join(", ")}; ` +
619
- `${s.entitiesClaimed}/${s.entitiesTotal} entities claimed, ` +
620
- `counted over ${collection.files.length} metadata file(s).`,
651
+ // FR-043 §5.4 — a project whose every requirement came from a shipped library
652
+ // is not measured, and says so rather than printing a ratio. Silence would be
653
+ // worse than either: a missing clause reads as a clean coverage result.
654
+ (s.entitiesTotal === undefined
655
+ ? `coverage: not measured (no project-authored requirements).`
656
+ : `${s.entitiesClaimed}/${s.entitiesTotal} entities claimed, ` +
657
+ `counted over ${collection.files.length} metadata file(s)` +
658
+ // FR-023 — only when there ARE dependencies. A project that declares none
659
+ // must print the sentence it printed before dependencies existed, to the
660
+ // byte: "0 from dependencies" is noise on every existing project, and this
661
+ // line is a SURFACE the no-dependency guarantee covers like any other.
662
+ (collection.dependencies.length > 0
663
+ ? `, ${collection.dependencies.length} from dependencies.`
664
+ : `.`)),
621
665
  );
622
666
  if (s.undecided > 0) {
623
667
  say(
@@ -637,26 +681,25 @@ export async function verifyCommand(
637
681
  .filter((k) => (s.byStatus[k] ?? 0) > 0)
638
682
  .map((k) => ({ status: k, count: s.byStatus[k] ?? 0 })),
639
683
  undecided: s.undecided,
640
- entitiesClaimed: s.entitiesClaimed,
641
- entitiesTotal: s.entitiesTotal,
684
+ // Absent when coverage was not measured (FR-043 §5.4), never zeroed: a reader
685
+ // cannot tell `0/0 claimed` from "not measured", and the two mean opposite
686
+ // things.
687
+ ...(s.entitiesTotal === undefined
688
+ ? {}
689
+ : { entitiesClaimed: s.entitiesClaimed ?? 0, entitiesTotal: s.entitiesTotal }),
642
690
  metadataFiles: collection.files.length,
643
691
  };
644
692
  }
645
693
 
646
694
  const errors = diags.filter((d) => d.severity === "error");
647
695
  const warns = diags.filter((d) => d.severity === "warn");
648
- // Named `fmtDiag`, not `fmt`: `fmt` is this command's OUTPUT FORMAT parameter,
649
- // and a shadow of it inside the one function that must not confuse the two is
650
- // how a structured run quietly reverts to text.
651
- const fmtDiag = (d: Diagnostic): string =>
652
- ` ${d.code}${d.path !== undefined ? ` [${d.path}]` : ""}: ${d.message}`;
653
696
  // Capped per SECTION, never across them: a ledger of a few hundred entries can
654
697
  // produce hundreds of prose findings, and a shared budget would let the advisory
655
698
  // lint push every gate warning off the end. The cap VALUE is now one shared
656
699
  // constant (`--limit`), so raising it cannot miss a section — errors stay
657
700
  // uncapped, as they always were.
658
- for (const d of errors) log.error(fmtDiag(d));
659
- warnCapped(warns.map(fmtDiag), flags.limit, { structured });
701
+ for (const d of errors) log.error(formatDiagnostic(d));
702
+ warnCapped(warns.map(formatDiagnostic), flags.limit, { structured });
660
703
 
661
704
  // -- the authoring lint: its own section, its own cap ----------------------
662
705
  // Separate from the gate above because it makes a different claim. The gate
@@ -685,7 +728,7 @@ export async function verifyCommand(
685
728
  `meta verify — requirements: ${lint.length} authoring warning(s) ` +
686
729
  `(advisory — does not fail the build):`,
687
730
  );
688
- warnCapped(lint.map(fmtDiag), flags.limit, { structured });
731
+ warnCapped(lint.map(formatDiagnostic), flags.limit, { structured });
689
732
  }
690
733
 
691
734
  // Everything this pass found, uncapped, for the structured payload — gate
@@ -703,11 +746,49 @@ export async function verifyCommand(
703
746
  return 0;
704
747
  }
705
748
 
749
+ // -- overlay authoring lint (FR-023 §11.1 item 4, Task 17) ------------------
750
+ // Its own section, its own cap — same discipline as the requirement lint
751
+ // above: a noisy section must not push another section's findings off the
752
+ // end of a capped run. This lint has NO gate half at all (no
753
+ // "ledger disagrees with the model" claim to make), so nothing here can
754
+ // ever reach the exit code — records its result either way, same as the
755
+ // anti-pattern pass below.
756
+ //
757
+ // Reads raw file content (via `lintOverlays` + `declaredTopLevelKeys`), never
758
+ // the loaded/merged model: the merge has already lost which FILE contributed
759
+ // which declaration, and that is exactly what this lint needs to name.
760
+ async function runOverlayLintAdvisory(): Promise<void> {
761
+ if (flags.noOverlayLint) {
762
+ overlaySection = skippedSection("suppressed by --no-overlay-lint");
763
+ return;
764
+ }
765
+ if (process.env.META_NO_OVERLAY_LINT === "1") {
766
+ overlaySection = skippedSection("suppressed by META_NO_OVERLAY_LINT=1");
767
+ return;
768
+ }
769
+ let findings: Diagnostic[];
770
+ try {
771
+ findings = await lintOverlays(collection, (path) => new FileSource(path));
772
+ } catch (err) {
773
+ // Never let an advisory scan break verify — and never report it as clean.
774
+ overlaySection = skippedSection(`the overlay lint failed: ${(err as Error).message}`);
775
+ return;
776
+ }
777
+ overlaySection = ranSection(findings.map((d) => toDiagnosticRow(d, "lint")));
778
+ if (findings.length > 0) {
779
+ log.warn(
780
+ `meta verify — overlays: ${findings.length} unflagged cross-file redeclaration(s) ` +
781
+ `(advisory — does not fail the build):`,
782
+ );
783
+ warnCapped(findings.map(formatDiagnostic), flags.limit, { structured });
784
+ }
785
+ }
786
+
706
787
  // -- verify-as-teacher (advisory) ------------------------------------------
707
788
  // Records its result EITHER WAY — a skip carries its reason rather than looking
708
789
  // like a clean scan. Warnings only; nothing here reaches the exit code (the
709
790
  // scanner's own header: bias to under-flagging, never a non-zero exit).
710
- function runAntiPatternAdvisory(): void {
791
+ async function runAntiPatternAdvisory(): Promise<void> {
711
792
  if (flags.noAntipatterns) {
712
793
  antiPatternSection = skippedSection("suppressed by --no-antipatterns");
713
794
  return;
@@ -746,10 +827,23 @@ export async function verifyCommand(
746
827
  // advisory only — never breaks verify.
747
828
  }
748
829
 
830
+ // FR-043 §3.5 — `metaobjects::` in this project's own metadata with no ejection
831
+ // provenance. Advisory because with the library NOT opted in nothing is broken; the
832
+ // opted-in case is refused at load instead.
833
+ let libraryPrefix: Awaited<ReturnType<typeof scanForUnprovenancedLibraryPrefix>> = [];
834
+ try {
835
+ libraryPrefix = await scanForUnprovenancedLibraryPrefix(
836
+ root, collection.ownFiles, collection.configDir,
837
+ );
838
+ } catch {
839
+ // Same discipline as its siblings: an advisory scan never breaks verify.
840
+ }
841
+
749
842
  antiPatternSection = ranSection([
750
843
  ...antiPatternRows(findings),
751
844
  ...missingBaseUrlRows(baseUrl),
752
845
  ...removedPropRows(removedProps),
846
+ ...libraryPrefixRows(libraryPrefix),
753
847
  ]);
754
848
  if (findings.length > 0) {
755
849
  log.warn(
@@ -772,6 +866,13 @@ export async function verifyCommand(
772
866
  );
773
867
  warnCapped(removedProps.map((f) => ` ${f.message}`), flags.limit, { structured });
774
868
  }
869
+ if (libraryPrefix.length > 0) {
870
+ log.warn(
871
+ `meta verify — ${libraryPrefix.length} node(s) declared under "metaobjects::" with ` +
872
+ `no ejection provenance (advisory — does not fail the build):`,
873
+ );
874
+ warnCapped(libraryPrefix.map((f) => ` ${f.message}`), flags.limit, { structured });
875
+ }
775
876
  }
776
877
 
777
878
  // -- template (prompt / output) drift --------------------------------------
@@ -994,6 +1095,9 @@ export async function verifyCommand(
994
1095
  allow,
995
1096
  views: expectedViews,
996
1097
  ...(schemaScope !== undefined ? { inScope: schemaScope } : {}),
1098
+ // The import exclusion reaches scopeExpectedSchema through this option bag —
1099
+ // there is no direct call on this path to attach it to.
1100
+ ...importedOption(collection),
997
1101
  });
998
1102
  } catch (err) {
999
1103
  log.error(`verify: failed to introspect ${kysely.displayUrl}: ${(err as Error).message}`);
@@ -1080,6 +1184,8 @@ export async function verifyCommand(
1080
1184
  allow,
1081
1185
  views: expectedViews,
1082
1186
  ...(schemaScope !== undefined ? { inScope: schemaScope } : {}),
1187
+ // Same option bag on the D1 path, for the same reason.
1188
+ ...importedOption(collection),
1083
1189
  });
1084
1190
  } catch (err) {
1085
1191
  log.error(`verify: ${(err as Error).message}`);
@@ -1192,11 +1298,12 @@ export async function verifyCommand(
1192
1298
  );
1193
1299
  }
1194
1300
 
1195
- // Same reasoning for the per-command scope: an object `migrate.scope` excluded
1196
- // was NOT checked, and silence would misreport it as checked-and-clean. Shared
1197
- // wording with `meta migrate` — one declaration, one sentence about it.
1198
- if (driftResult.outOfScope.length > 0) {
1199
- say(outOfScopeNote("verify", driftResult.outOfScope));
1301
+ // Same reasoning for the per-command scope and for imported metadata: an object
1302
+ // excluded either way was NOT checked, and silence would misreport it as
1303
+ // checked-and-clean. Shared wording with `meta migrate` — one declaration, one
1304
+ // sentence about it — and each object named once (`exclusionNotes`).
1305
+ for (const note of exclusionNotes("verify", driftResult.outOfScope, driftResult.importedOutOfScope ?? [])) {
1306
+ say(note);
1200
1307
  }
1201
1308
 
1202
1309
  const changes = driftResult.changes;
@@ -1249,7 +1356,7 @@ export async function verifyCommand(
1249
1356
  if (genCollection !== collection) {
1250
1357
  try {
1251
1358
  codegenRoot = await loadMemory(genCollection.configDir, {
1252
- files: genCollection.files,
1359
+ ...collectionLoadOptions(genCollection),
1253
1360
  ...configLoadOptions,
1254
1361
  strict: !flags.lax,
1255
1362
  });
@@ -1390,6 +1497,55 @@ export async function verifyCommand(
1390
1497
  }
1391
1498
  return 1;
1392
1499
  }
1500
+
1501
+ // -- dependency drift (Task 15, FR-023) --------------------------------------
1502
+ // Gated on --deps. Re-resolves each declared dependency exactly as `meta deps
1503
+ // check` does — never part of the bare-verify default (it needs the publisher
1504
+ // reachable, which CI may not have).
1505
+ async function runDepsVerify(): Promise<number> {
1506
+ // Read RAW declared specs (never `collection.dependencies`, which is the
1507
+ // already-`ResolvedDependency[]` the LOCK produced — it carries no
1508
+ // transport info, so it cannot be re-resolved). A project with no
1509
+ // config.json at all declares no dependencies.
1510
+ let depSpecs: readonly DependencySpec[] = [];
1511
+ try {
1512
+ const cfg = await loadConfig(join(collection.configDir, DEFAULT_METAOBJECTS_DIR));
1513
+ depSpecs = cfg.dependencies;
1514
+ } catch {
1515
+ depSpecs = [];
1516
+ }
1517
+
1518
+ if (depSpecs.length === 0) {
1519
+ say("verify --deps: no dependencies declared — nothing to check.");
1520
+ return 0;
1521
+ }
1522
+
1523
+ let lock: Awaited<ReturnType<typeof readLockOrThrow>>;
1524
+ try {
1525
+ lock = await readLockOrThrow(collection.configDir);
1526
+ } catch (err) {
1527
+ log.error(`verify --deps: ${(err as Error).message}`);
1528
+ return 2;
1529
+ }
1530
+
1531
+ const results = await checkDependencies(collection.configDir, depSpecs, lock);
1532
+ const failing = results.filter((r) => r.status !== "current");
1533
+
1534
+ for (const r of results) {
1535
+ if (r.status === "current") say(formatCheckLine(r));
1536
+ else log.error(formatCheckLine(r));
1537
+ }
1538
+
1539
+ if (failing.length > 0) {
1540
+ log.error(
1541
+ `verify --deps: ${failing.length} of ${results.length} dependenc` +
1542
+ `${results.length === 1 ? "y" : "ies"} drifted or unresolved (${ERR_DEPENDENCY_UPSTREAM_DRIFT}).`,
1543
+ );
1544
+ return 1;
1545
+ }
1546
+ say("verify --deps: every dependency's installed artifact matches the lock.");
1547
+ return 0;
1548
+ }
1393
1549
  }
1394
1550
 
1395
1551
  /**
@@ -1447,8 +1603,9 @@ interface RequirementCounts {
1447
1603
  architectural: number;
1448
1604
  byStatus: { status: string; count: number }[];
1449
1605
  undecided: number;
1450
- entitiesClaimed: number;
1451
- entitiesTotal: number;
1606
+ /** Absent when coverage was not measured — FR-043 §5.4. */
1607
+ entitiesClaimed?: number;
1608
+ entitiesTotal?: number;
1452
1609
  /** How many metadata files the two entity counts were taken over. */
1453
1610
  metadataFiles: number;
1454
1611
  }
@@ -1460,6 +1617,17 @@ interface VerifyGateRow {
1460
1617
  ok: boolean;
1461
1618
  }
1462
1619
 
1620
+ /**
1621
+ * Format one `Diagnostic` as a printed TEXT line. Named `formatDiagnostic`, not
1622
+ * `fmt`: `fmt` is this command's OUTPUT FORMAT parameter, and a shadow of it
1623
+ * anywhere near this code is how a structured run quietly reverts to text.
1624
+ * Shared by the requirement gate/lint AND the overlay lint — every advisory or
1625
+ * gate pass that speaks in `Diagnostic` prints it identically.
1626
+ */
1627
+ function formatDiagnostic(d: Diagnostic): string {
1628
+ return ` ${d.code}${d.path !== undefined ? ` [${d.path}]` : ""}: ${d.message}`;
1629
+ }
1630
+
1463
1631
  /** Project a requirement diagnostic into a payload row. */
1464
1632
  function toDiagnosticRow(d: Diagnostic, source: "gate" | "lint"): AdvisoryDiagnosticRow {
1465
1633
  return {
@@ -1491,6 +1659,7 @@ function buildVerifyPayload(input: {
1491
1659
  requirements: AdvisorySection<AdvisoryDiagnosticRow>;
1492
1660
  requirementCounts: RequirementCounts | undefined;
1493
1661
  antiPatterns: AdvisorySection<AdvisoryFindingRow>;
1662
+ overlays: AdvisorySection<AdvisoryDiagnosticRow>;
1494
1663
  }): Record<string, unknown> {
1495
1664
  const ran = input.gates.filter((g) => g.ran);
1496
1665
  const failed = ran.filter((g) => !g.ok);
@@ -1505,6 +1674,9 @@ function buildVerifyPayload(input: {
1505
1674
  if (input.requirements.total > 0) {
1506
1675
  parts.push(`${input.requirements.total} requirement diagnostic(s)`);
1507
1676
  }
1677
+ if (input.overlays.status === "ran" && input.overlays.total > 0) {
1678
+ parts.push(`${input.overlays.total} overlay authoring finding(s)`);
1679
+ }
1508
1680
 
1509
1681
  const help: string[] = [];
1510
1682
  if (failed.length > 0) {
@@ -1517,7 +1689,17 @@ function buildVerifyPayload(input: {
1517
1689
  `${input.antiPatterns.total} authored site(s) hand-roll what MetaObjects can model — see antiPatterns.rows[] and run \`meta types <construct>\``,
1518
1690
  );
1519
1691
  }
1520
- if (failed.length === 0 && input.antiPatterns.total === 0 && input.requirements.total === 0) {
1692
+ if (input.overlays.total > 0) {
1693
+ help.push(
1694
+ `${input.overlays.total} unflagged cross-file redeclaration(s) — see overlays.rows[]; add overlay: true so a renamed or removed target fails loudly instead of silently becoming a new object`,
1695
+ );
1696
+ }
1697
+ if (
1698
+ failed.length === 0 &&
1699
+ input.antiPatterns.total === 0 &&
1700
+ input.requirements.total === 0 &&
1701
+ input.overlays.total === 0
1702
+ ) {
1521
1703
  help.push("no drift and nothing advisory to answer — nothing to do");
1522
1704
  }
1523
1705
 
@@ -1528,6 +1710,7 @@ function buildVerifyPayload(input: {
1528
1710
  help,
1529
1711
  antiPatterns: input.antiPatterns,
1530
1712
  requirements: input.requirements,
1713
+ overlays: input.overlays,
1531
1714
  ...(input.requirementCounts !== undefined ? { requirementCounts: input.requirementCounts } : {}),
1532
1715
  // The honest boundary. Everything named here is REACHABLE — it is printed as
1533
1716
  // text on stderr — but it is not in this document, and a reader must not have