@metaobjectsdev/cli 1.0.2 → 1.0.3

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 (69) hide show
  1. package/README.md +1 -1
  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/gen.d.ts.map +1 -1
  10. package/dist/src/commands/gen.js +54 -1
  11. package/dist/src/commands/gen.js.map +1 -1
  12. package/dist/src/commands/init.d.ts.map +1 -1
  13. package/dist/src/commands/init.js +12 -18
  14. package/dist/src/commands/init.js.map +1 -1
  15. package/dist/src/commands/migrate.d.ts.map +1 -1
  16. package/dist/src/commands/migrate.js +24 -19
  17. package/dist/src/commands/migrate.js.map +1 -1
  18. package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
  19. package/dist/src/commands/prompt-snapshot.js +2 -1
  20. package/dist/src/commands/prompt-snapshot.js.map +1 -1
  21. package/dist/src/commands/verify.d.ts.map +1 -1
  22. package/dist/src/commands/verify.js +154 -23
  23. package/dist/src/commands/verify.js.map +1 -1
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/index.js +62 -2
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
  28. package/dist/src/lib/agent-context-staleness.js +7 -1
  29. package/dist/src/lib/agent-context-staleness.js.map +1 -1
  30. package/dist/src/lib/args.d.ts +57 -1
  31. package/dist/src/lib/args.d.ts.map +1 -1
  32. package/dist/src/lib/args.js +39 -2
  33. package/dist/src/lib/args.js.map +1 -1
  34. package/dist/src/lib/collection-load-options.d.ts +13 -0
  35. package/dist/src/lib/collection-load-options.d.ts.map +1 -0
  36. package/dist/src/lib/collection-load-options.js +19 -0
  37. package/dist/src/lib/collection-load-options.js.map +1 -0
  38. package/dist/src/lib/dependency-sync.d.ts +170 -0
  39. package/dist/src/lib/dependency-sync.d.ts.map +1 -0
  40. package/dist/src/lib/dependency-sync.js +457 -0
  41. package/dist/src/lib/dependency-sync.js.map +1 -0
  42. package/dist/src/lib/migrate-scope.d.ts +32 -1
  43. package/dist/src/lib/migrate-scope.d.ts.map +1 -1
  44. package/dist/src/lib/migrate-scope.js +51 -3
  45. package/dist/src/lib/migrate-scope.js.map +1 -1
  46. package/dist/src/lib/overlay-lint.d.ts +37 -0
  47. package/dist/src/lib/overlay-lint.d.ts.map +1 -0
  48. package/dist/src/lib/overlay-lint.js +102 -0
  49. package/dist/src/lib/overlay-lint.js.map +1 -0
  50. package/dist/src/lib/requirement-check.d.ts +9 -1
  51. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  52. package/dist/src/lib/requirement-check.js +23 -7
  53. package/dist/src/lib/requirement-check.js.map +1 -1
  54. package/package.json +10 -10
  55. package/src/commands/deps.ts +231 -0
  56. package/src/commands/docs.ts +19 -3
  57. package/src/commands/gen.ts +61 -1
  58. package/src/commands/init.ts +13 -17
  59. package/src/commands/migrate.ts +26 -16
  60. package/src/commands/prompt-snapshot.ts +2 -1
  61. package/src/commands/verify.ts +174 -23
  62. package/src/index.ts +62 -2
  63. package/src/lib/agent-context-staleness.ts +7 -1
  64. package/src/lib/args.ts +89 -3
  65. package/src/lib/collection-load-options.ts +36 -0
  66. package/src/lib/dependency-sync.ts +656 -0
  67. package/src/lib/migrate-scope.ts +59 -4
  68. package/src/lib/overlay-lint.ts +129 -0
  69. package/src/lib/requirement-check.ts +33 -7
@@ -24,12 +24,15 @@ import { replayRemedy } from "../lib/replay-remedy.js";
24
24
  import { FileProvider } from "../lib/file-provider.js";
25
25
  import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
26
26
  import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
27
+ import { collectionLoadOptions } from "../lib/collection-load-options.js";
27
28
  import { computeCodegenDrift } from "../lib/codegen-drift.js";
28
29
  import { computeDocsDrift } from "../lib/docs-drift.js";
29
30
  import {
30
31
  checkRequirements, summariseRequirements, scanRequirements, type Diagnostic,
31
32
  } from "../lib/requirement-check.js";
32
33
  import { lintRequirements } from "../lib/requirement-lint.js";
34
+ import { lintOverlays } from "../lib/overlay-lint.js";
35
+ import { FileSource } from "@metaobjectsdev/metadata/core";
33
36
  import { resolveD1Config, resolveMigrateConfig } from "../lib/config.js";
34
37
  import {
35
38
  buildWranglerExecuteArgs,
@@ -69,8 +72,15 @@ import {
69
72
  type D1Runner,
70
73
  type DriftResult,
71
74
  } from "@metaobjectsdev/migrate-ts";
72
- import { loadMemory, resolveCollection } from "@metaobjectsdev/sdk";
73
- import { migrateScopeMismatch, outOfScopeNote } from "../lib/migrate-scope.js";
75
+ import {
76
+ DEFAULT_METAOBJECTS_DIR,
77
+ loadConfig,
78
+ loadMemory,
79
+ resolveCollection,
80
+ type DependencySpec,
81
+ } from "@metaobjectsdev/sdk";
82
+ import { checkDependencies, ERR_DEPENDENCY_UPSTREAM_DRIFT, formatCheckLine, readLockOrThrow } from "../lib/dependency-sync.js";
83
+ import { exclusionNotes, importedOption, migrateScopeMismatch } from "../lib/migrate-scope.js";
74
84
  import {
75
85
  TYPE_TEMPLATE,
76
86
  TEMPLATE_SUBTYPE_PROMPT,
@@ -163,11 +173,15 @@ export async function verifyCommand(
163
173
  // D1 has no URL connection); that check lives inside runSchemaVerify.
164
174
  const runCodegen = flags.codegen;
165
175
  const runDocs = flags.docs;
176
+ // Task 15 — same shape as --codegen/--docs: selected ONLY by its own flag,
177
+ // never folded into the bare-verify default (it needs the publisher
178
+ // reachable, which CI may not have — see the VerifyFlags doc on `deps`).
179
+ const runDeps = flags.deps;
166
180
  if (!flags.anyExplicit) {
167
181
  say(
168
182
  "meta verify — running --templates (default). Explicit subverbs: " +
169
183
  "--templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift), " +
170
- "--docs (docs drift), " +
184
+ "--docs (docs drift), --deps (dependency drift), " +
171
185
  "--replay/--replay-snapshot (the committed migration chain replays from empty).",
172
186
  );
173
187
  }
@@ -248,7 +262,7 @@ export async function verifyCommand(
248
262
  let root: Awaited<ReturnType<typeof loadMemory>>;
249
263
  try {
250
264
  root = await loadMemory(collection.configDir, {
251
- files: collection.files,
265
+ ...collectionLoadOptions(collection),
252
266
  ...configLoadOptions,
253
267
  strict: !flags.lax,
254
268
  });
@@ -314,7 +328,7 @@ export async function verifyCommand(
314
328
  const promptsDir = join(projectRoot, flags.prompts ?? DEFAULT_PROMPTS_DIR);
315
329
  const provider = new FileProvider(promptsDir);
316
330
 
317
- // The two advisory sections, captured as the gates run so the structured payload
331
+ // The advisory sections, captured as the gates run so the structured payload
318
332
  // can carry them IN FULL. They were previously formatted straight to stderr and
319
333
  // existed nowhere else, which is why 96% of a 239-finding report was unreachable
320
334
  // by any flag, env var or format.
@@ -322,6 +336,12 @@ export async function verifyCommand(
322
336
  skippedSection("the requirement pass did not run");
323
337
  let antiPatternSection: AdvisorySection<AdvisoryFindingRow> =
324
338
  skippedSection("the advisory anti-pattern pass did not run");
339
+ // FR-023 §11.1 item 4 (Task 17) — the overlay authoring lint. Its own section
340
+ // (never folded into `requirements`, which is about a different kind of node
341
+ // entirely) so the structured payload names what ran and what didn't the same
342
+ // way every other advisory pass does.
343
+ let overlaySection: AdvisorySection<AdvisoryDiagnosticRow> =
344
+ skippedSection("the overlay lint did not run");
325
345
  // The ledger counts `meta verify` prints on every run. Undefined for a project
326
346
  // declaring no requirement.* node at all (opt-in by declaration) — the payload
327
347
  // then omits the block rather than reporting zeroes that would read as an empty
@@ -341,6 +361,7 @@ export async function verifyCommand(
341
361
  const schemaExit = await runSchemaVerify();
342
362
  const codegenExit = runCodegen ? await runCodegenVerify() : 0;
343
363
  const docsExit = runDocs ? await runDocsVerify() : 0;
364
+ const depsExit = runDeps ? await runDepsVerify() : 0;
344
365
  // Requirements have no subverb: `requirement.*` nodes are metadata, so they
345
366
  // are checked on every `meta verify`. Opt-in by DECLARATION — a model with no
346
367
  // requirement nodes is silent, not in drift.
@@ -351,6 +372,12 @@ export async function verifyCommand(
351
372
  // would parse cleanly and do nothing at all.
352
373
  const replayExit = flags.replay || flags.replaySnapshot ? await runReplayVerify() : 0;
353
374
 
375
+ // FR-023 §11.1 item 4 (Task 17) — the overlay authoring lint. Runs on every
376
+ // `meta verify`, not gated on any subverb (an unflagged cross-file
377
+ // redeclaration is a risk regardless of which drift gates were selected).
378
+ // Warnings ONLY — never changes the exit code.
379
+ await runOverlayLintAdvisory();
380
+
354
381
  // Advisory verify-as-teacher pass: surface hand-rolled work the metadata could
355
382
  // model. Warnings ONLY — never changes the exit code (bias to under-flagging).
356
383
  // Suppressed with --no-antipatterns or META_NO_ANTIPATTERNS=1 for the rare
@@ -362,6 +389,7 @@ export async function verifyCommand(
362
389
  schemaExit,
363
390
  codegenExit,
364
391
  docsExit,
392
+ depsExit,
365
393
  requirementExit,
366
394
  replayExit,
367
395
  );
@@ -378,6 +406,7 @@ export async function verifyCommand(
378
406
  { gate: "schema", ran: ranSchemaGate, ok: schemaExit === 0 },
379
407
  { gate: "codegen", ran: runCodegen, ok: codegenExit === 0 },
380
408
  { gate: "docs", ran: runDocs, ok: docsExit === 0 },
409
+ { gate: "deps", ran: runDeps, ok: depsExit === 0 },
381
410
  { gate: "requirements", ran: true, ok: requirementExit === 0 },
382
411
  { gate: "replay", ran: flags.replay || flags.replaySnapshot, ok: replayExit === 0 },
383
412
  ],
@@ -385,6 +414,7 @@ export async function verifyCommand(
385
414
  requirements: requirementSection,
386
415
  requirementCounts,
387
416
  antiPatterns: antiPatternSection,
417
+ overlays: overlaySection,
388
418
  }),
389
419
  fmt,
390
420
  );
@@ -553,7 +583,7 @@ export async function verifyCommand(
553
583
  columnNamingStrategy: viewStrategy,
554
584
  views: buildProjectionViews(root, { dialect, columnNamingStrategy: viewStrategy }),
555
585
  });
556
- governed = scopeExpectedSchema(built, schemaScope);
586
+ governed = scopeExpectedSchema(built, schemaScope, importedOption(collection));
557
587
  }
558
588
 
559
589
  // `verifyReplay` calls `applyPending` itself. That is NOT a second replay: the
@@ -594,7 +624,7 @@ export async function verifyCommand(
594
624
  // ONE scan for all three passes. The gate and the summary each used to walk the
595
625
  // model AND resolve every @implementedBy claim for themselves — the resolution
596
626
  // being the expensive half — and the lint added a third walk on top.
597
- const scan = scanRequirements(root);
627
+ const scan = scanRequirements(root, { coverable: collection.inScope });
598
628
  const diags = [...checkRequirements(root, scan)];
599
629
 
600
630
  // Printed on EVERY run, clean or not — a gate that says nothing when it
@@ -617,7 +647,14 @@ export async function verifyCommand(
617
647
  `meta verify — requirements: ${s.total} entries (${s.functional} functional, ` +
618
648
  `${s.architectural} architectural) — ${parts.join(", ")}; ` +
619
649
  `${s.entitiesClaimed}/${s.entitiesTotal} entities claimed, ` +
620
- `counted over ${collection.files.length} metadata file(s).`,
650
+ `counted over ${collection.files.length} metadata file(s)` +
651
+ // FR-023 — only when there ARE dependencies. A project that declares none
652
+ // must print the sentence it printed before dependencies existed, to the
653
+ // byte: "0 from dependencies" is noise on every existing project, and this
654
+ // line is a SURFACE the no-dependency guarantee covers like any other.
655
+ (collection.dependencies.length > 0
656
+ ? `, ${collection.dependencies.length} from dependencies.`
657
+ : `.`),
621
658
  );
622
659
  if (s.undecided > 0) {
623
660
  say(
@@ -645,18 +682,13 @@ export async function verifyCommand(
645
682
 
646
683
  const errors = diags.filter((d) => d.severity === "error");
647
684
  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
685
  // Capped per SECTION, never across them: a ledger of a few hundred entries can
654
686
  // produce hundreds of prose findings, and a shared budget would let the advisory
655
687
  // lint push every gate warning off the end. The cap VALUE is now one shared
656
688
  // constant (`--limit`), so raising it cannot miss a section — errors stay
657
689
  // uncapped, as they always were.
658
- for (const d of errors) log.error(fmtDiag(d));
659
- warnCapped(warns.map(fmtDiag), flags.limit, { structured });
690
+ for (const d of errors) log.error(formatDiagnostic(d));
691
+ warnCapped(warns.map(formatDiagnostic), flags.limit, { structured });
660
692
 
661
693
  // -- the authoring lint: its own section, its own cap ----------------------
662
694
  // Separate from the gate above because it makes a different claim. The gate
@@ -685,7 +717,7 @@ export async function verifyCommand(
685
717
  `meta verify — requirements: ${lint.length} authoring warning(s) ` +
686
718
  `(advisory — does not fail the build):`,
687
719
  );
688
- warnCapped(lint.map(fmtDiag), flags.limit, { structured });
720
+ warnCapped(lint.map(formatDiagnostic), flags.limit, { structured });
689
721
  }
690
722
 
691
723
  // Everything this pass found, uncapped, for the structured payload — gate
@@ -703,6 +735,44 @@ export async function verifyCommand(
703
735
  return 0;
704
736
  }
705
737
 
738
+ // -- overlay authoring lint (FR-023 §11.1 item 4, Task 17) ------------------
739
+ // Its own section, its own cap — same discipline as the requirement lint
740
+ // above: a noisy section must not push another section's findings off the
741
+ // end of a capped run. This lint has NO gate half at all (no
742
+ // "ledger disagrees with the model" claim to make), so nothing here can
743
+ // ever reach the exit code — records its result either way, same as the
744
+ // anti-pattern pass below.
745
+ //
746
+ // Reads raw file content (via `lintOverlays` + `declaredTopLevelKeys`), never
747
+ // the loaded/merged model: the merge has already lost which FILE contributed
748
+ // which declaration, and that is exactly what this lint needs to name.
749
+ async function runOverlayLintAdvisory(): Promise<void> {
750
+ if (flags.noOverlayLint) {
751
+ overlaySection = skippedSection("suppressed by --no-overlay-lint");
752
+ return;
753
+ }
754
+ if (process.env.META_NO_OVERLAY_LINT === "1") {
755
+ overlaySection = skippedSection("suppressed by META_NO_OVERLAY_LINT=1");
756
+ return;
757
+ }
758
+ let findings: Diagnostic[];
759
+ try {
760
+ findings = await lintOverlays(collection, (path) => new FileSource(path));
761
+ } catch (err) {
762
+ // Never let an advisory scan break verify — and never report it as clean.
763
+ overlaySection = skippedSection(`the overlay lint failed: ${(err as Error).message}`);
764
+ return;
765
+ }
766
+ overlaySection = ranSection(findings.map((d) => toDiagnosticRow(d, "lint")));
767
+ if (findings.length > 0) {
768
+ log.warn(
769
+ `meta verify — overlays: ${findings.length} unflagged cross-file redeclaration(s) ` +
770
+ `(advisory — does not fail the build):`,
771
+ );
772
+ warnCapped(findings.map(formatDiagnostic), flags.limit, { structured });
773
+ }
774
+ }
775
+
706
776
  // -- verify-as-teacher (advisory) ------------------------------------------
707
777
  // Records its result EITHER WAY — a skip carries its reason rather than looking
708
778
  // like a clean scan. Warnings only; nothing here reaches the exit code (the
@@ -994,6 +1064,9 @@ export async function verifyCommand(
994
1064
  allow,
995
1065
  views: expectedViews,
996
1066
  ...(schemaScope !== undefined ? { inScope: schemaScope } : {}),
1067
+ // The import exclusion reaches scopeExpectedSchema through this option bag —
1068
+ // there is no direct call on this path to attach it to.
1069
+ ...importedOption(collection),
997
1070
  });
998
1071
  } catch (err) {
999
1072
  log.error(`verify: failed to introspect ${kysely.displayUrl}: ${(err as Error).message}`);
@@ -1080,6 +1153,8 @@ export async function verifyCommand(
1080
1153
  allow,
1081
1154
  views: expectedViews,
1082
1155
  ...(schemaScope !== undefined ? { inScope: schemaScope } : {}),
1156
+ // Same option bag on the D1 path, for the same reason.
1157
+ ...importedOption(collection),
1083
1158
  });
1084
1159
  } catch (err) {
1085
1160
  log.error(`verify: ${(err as Error).message}`);
@@ -1192,11 +1267,12 @@ export async function verifyCommand(
1192
1267
  );
1193
1268
  }
1194
1269
 
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));
1270
+ // Same reasoning for the per-command scope and for imported metadata: an object
1271
+ // excluded either way was NOT checked, and silence would misreport it as
1272
+ // checked-and-clean. Shared wording with `meta migrate` — one declaration, one
1273
+ // sentence about it — and each object named once (`exclusionNotes`).
1274
+ for (const note of exclusionNotes("verify", driftResult.outOfScope, driftResult.importedOutOfScope ?? [])) {
1275
+ say(note);
1200
1276
  }
1201
1277
 
1202
1278
  const changes = driftResult.changes;
@@ -1249,7 +1325,7 @@ export async function verifyCommand(
1249
1325
  if (genCollection !== collection) {
1250
1326
  try {
1251
1327
  codegenRoot = await loadMemory(genCollection.configDir, {
1252
- files: genCollection.files,
1328
+ ...collectionLoadOptions(genCollection),
1253
1329
  ...configLoadOptions,
1254
1330
  strict: !flags.lax,
1255
1331
  });
@@ -1390,6 +1466,55 @@ export async function verifyCommand(
1390
1466
  }
1391
1467
  return 1;
1392
1468
  }
1469
+
1470
+ // -- dependency drift (Task 15, FR-023) --------------------------------------
1471
+ // Gated on --deps. Re-resolves each declared dependency exactly as `meta deps
1472
+ // check` does — never part of the bare-verify default (it needs the publisher
1473
+ // reachable, which CI may not have).
1474
+ async function runDepsVerify(): Promise<number> {
1475
+ // Read RAW declared specs (never `collection.dependencies`, which is the
1476
+ // already-`ResolvedDependency[]` the LOCK produced — it carries no
1477
+ // transport info, so it cannot be re-resolved). A project with no
1478
+ // config.json at all declares no dependencies.
1479
+ let depSpecs: readonly DependencySpec[] = [];
1480
+ try {
1481
+ const cfg = await loadConfig(join(collection.configDir, DEFAULT_METAOBJECTS_DIR));
1482
+ depSpecs = cfg.dependencies;
1483
+ } catch {
1484
+ depSpecs = [];
1485
+ }
1486
+
1487
+ if (depSpecs.length === 0) {
1488
+ say("verify --deps: no dependencies declared — nothing to check.");
1489
+ return 0;
1490
+ }
1491
+
1492
+ let lock: Awaited<ReturnType<typeof readLockOrThrow>>;
1493
+ try {
1494
+ lock = await readLockOrThrow(collection.configDir);
1495
+ } catch (err) {
1496
+ log.error(`verify --deps: ${(err as Error).message}`);
1497
+ return 2;
1498
+ }
1499
+
1500
+ const results = await checkDependencies(collection.configDir, depSpecs, lock);
1501
+ const failing = results.filter((r) => r.status !== "current");
1502
+
1503
+ for (const r of results) {
1504
+ if (r.status === "current") say(formatCheckLine(r));
1505
+ else log.error(formatCheckLine(r));
1506
+ }
1507
+
1508
+ if (failing.length > 0) {
1509
+ log.error(
1510
+ `verify --deps: ${failing.length} of ${results.length} dependenc` +
1511
+ `${results.length === 1 ? "y" : "ies"} drifted or unresolved (${ERR_DEPENDENCY_UPSTREAM_DRIFT}).`,
1512
+ );
1513
+ return 1;
1514
+ }
1515
+ say("verify --deps: every dependency's installed artifact matches the lock.");
1516
+ return 0;
1517
+ }
1393
1518
  }
1394
1519
 
1395
1520
  /**
@@ -1460,6 +1585,17 @@ interface VerifyGateRow {
1460
1585
  ok: boolean;
1461
1586
  }
1462
1587
 
1588
+ /**
1589
+ * Format one `Diagnostic` as a printed TEXT line. Named `formatDiagnostic`, not
1590
+ * `fmt`: `fmt` is this command's OUTPUT FORMAT parameter, and a shadow of it
1591
+ * anywhere near this code is how a structured run quietly reverts to text.
1592
+ * Shared by the requirement gate/lint AND the overlay lint — every advisory or
1593
+ * gate pass that speaks in `Diagnostic` prints it identically.
1594
+ */
1595
+ function formatDiagnostic(d: Diagnostic): string {
1596
+ return ` ${d.code}${d.path !== undefined ? ` [${d.path}]` : ""}: ${d.message}`;
1597
+ }
1598
+
1463
1599
  /** Project a requirement diagnostic into a payload row. */
1464
1600
  function toDiagnosticRow(d: Diagnostic, source: "gate" | "lint"): AdvisoryDiagnosticRow {
1465
1601
  return {
@@ -1491,6 +1627,7 @@ function buildVerifyPayload(input: {
1491
1627
  requirements: AdvisorySection<AdvisoryDiagnosticRow>;
1492
1628
  requirementCounts: RequirementCounts | undefined;
1493
1629
  antiPatterns: AdvisorySection<AdvisoryFindingRow>;
1630
+ overlays: AdvisorySection<AdvisoryDiagnosticRow>;
1494
1631
  }): Record<string, unknown> {
1495
1632
  const ran = input.gates.filter((g) => g.ran);
1496
1633
  const failed = ran.filter((g) => !g.ok);
@@ -1505,6 +1642,9 @@ function buildVerifyPayload(input: {
1505
1642
  if (input.requirements.total > 0) {
1506
1643
  parts.push(`${input.requirements.total} requirement diagnostic(s)`);
1507
1644
  }
1645
+ if (input.overlays.status === "ran" && input.overlays.total > 0) {
1646
+ parts.push(`${input.overlays.total} overlay authoring finding(s)`);
1647
+ }
1508
1648
 
1509
1649
  const help: string[] = [];
1510
1650
  if (failed.length > 0) {
@@ -1517,7 +1657,17 @@ function buildVerifyPayload(input: {
1517
1657
  `${input.antiPatterns.total} authored site(s) hand-roll what MetaObjects can model — see antiPatterns.rows[] and run \`meta types <construct>\``,
1518
1658
  );
1519
1659
  }
1520
- if (failed.length === 0 && input.antiPatterns.total === 0 && input.requirements.total === 0) {
1660
+ if (input.overlays.total > 0) {
1661
+ help.push(
1662
+ `${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`,
1663
+ );
1664
+ }
1665
+ if (
1666
+ failed.length === 0 &&
1667
+ input.antiPatterns.total === 0 &&
1668
+ input.requirements.total === 0 &&
1669
+ input.overlays.total === 0
1670
+ ) {
1521
1671
  help.push("no drift and nothing advisory to answer — nothing to do");
1522
1672
  }
1523
1673
 
@@ -1528,6 +1678,7 @@ function buildVerifyPayload(input: {
1528
1678
  help,
1529
1679
  antiPatterns: input.antiPatterns,
1530
1680
  requirements: input.requirements,
1681
+ overlays: input.overlays,
1531
1682
  ...(input.requirementCounts !== undefined ? { requirementCounts: input.requirementCounts } : {}),
1532
1683
  // The honest boundary. Everything named here is REACHABLE — it is printed as
1533
1684
  // text on stderr — but it is not in this document, and a reader must not have
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ const VERSION = cliVersion();
17
17
  * human text. It is named ONCE and used by both the warning below and the help
18
18
  * text above, so the two cannot drift apart.
19
19
  */
20
- const FORMAT_AWARE_COMMANDS: readonly string[] = ["gen", "verify", "migrate", "types"];
20
+ const FORMAT_AWARE_COMMANDS: readonly string[] = ["gen", "verify", "migrate", "types", "deps"];
21
21
 
22
22
  const HELP_TEXT = `meta — MetaObjects CLI (v${VERSION})
23
23
 
@@ -32,10 +32,13 @@ COMMANDS:
32
32
  gen [<entity>...] Codegen TS targets from your declared metadata
33
33
  eject <generator> Copy a reference generator into codegen/generators/ to own it (any time after init)
34
34
  eject --list List every ejectable generator name, grouped by package
35
+ deps sync [<name>...] Resolve declared dependencies (path transport): sync the committed
36
+ snapshot + sha256 lock (.metaobjects/deps/, .metaobjects/deps.lock.json)
37
+ deps list One line per locked dependency: name, version, hash, node/package summary
35
38
  types [query] Search the metadata vocabulary (types, subtypes, @attrs) by name or description
36
39
  export Flatten loaded metadata to one canonical JSON artifact
37
40
  docs [<project-root>] --out <dir> Generate neutral metadata documentation (entity + template pages; --site for HTML site)
38
- verify Drift gate — subverbs: --templates / --db / --codegen / --docs (bare = --templates)
41
+ verify Drift gate — subverbs: --templates / --db / --codegen / --docs / --deps (bare = --templates)
39
42
  upgrade Rewrite retired metadata vocabulary (previews; --apply writes)
40
43
  prompt-snapshot Snapshot rendered template.* output; --check gates drift
41
44
  migrate Diff metadata vs live DB; emit migration SQL files
@@ -94,6 +97,10 @@ VERIFY FLAGS (ADR-0021 D2 — explicit subverbs; combine any; exit 1 on ANY drif
94
97
  run would emit; never reports an extra file, because docs.outDir
95
98
  holds hand-written documentation MetaObjects did not write.
96
99
  Needs metaobjects.config.ts; exit 2 if absent.
100
+ --deps Dependency drift — re-resolve each declared dependency and compare
101
+ its installed artifact's hash against .metaobjects/deps.lock.json
102
+ (the same check 'meta deps check' runs). Never part of the bare-verify
103
+ default — it needs the publisher reachable, which CI may not have.
97
104
  --db <url> Schema drift — live DB URL enables the schema-drift gate.
98
105
  Supports: file:, libsql:, postgres:, postgresql:. Omit to skip.
99
106
  D1 has no URL — use --dialect d1 / --d1 <binding> instead.
@@ -108,6 +115,8 @@ VERIFY FLAGS (ADR-0021 D2 — explicit subverbs; combine any; exit 1 on ANY drif
108
115
  --no-antipatterns Suppress the advisory "you hand-rolled what MetaObjects can
109
116
  model" pass (aggregate/currency/enum hints; warnings only)
110
117
  --no-requirement-lint Suppress the advisory requirement AUTHORING lint (not the gate)
118
+ --no-overlay-lint Suppress the advisory overlay-redeclaration AUTHORING lint
119
+ (never a gate — this lint can't fail the build)
111
120
  --limit <n|all> How many advisory lines TEXT output prints per section before
112
121
  it truncates (default 20). Never applies to --format
113
122
  toon/json, which carry every finding and every diagnostic.
@@ -191,6 +200,38 @@ the same operation for ANY generator — one you skipped at init time, a UI-tier
191
200
  generator like form/hooks/grid, or one a package gains later. It prints the
192
201
  import line to paste into metaobjects.config.ts, and it never overwrites a
193
202
  file you already own unless you pass --force.
203
+ `,
204
+ deps: `meta deps — sync a declared metadata dependency's committed snapshot
205
+
206
+ USAGE:
207
+ meta deps sync [<name>...] Resolve each declared dependency (path transport only in
208
+ this release — npm/python refuse by name), validate its
209
+ manifest + artifact, copy it into .metaobjects/deps/<name>/,
210
+ and pin it in .metaobjects/deps.lock.json. Naming one or more
211
+ <name>s syncs only those; bare 'sync' syncs everything
212
+ declared. A dependency removed from config is PRUNED from
213
+ the lock and its snapshot dir deleted, every run.
214
+ meta deps list One line per locked dependency: name, version, hash8,
215
+ node count, packages.
216
+ meta deps check Re-resolve each declared dependency (path transport, same
217
+ as sync steps 1-2) and compare its INSTALLED artifact's hash
218
+ against .metaobjects/deps.lock.json. Read-only — never
219
+ touches the lock or the snapshot. Exits 1 if any dependency
220
+ has drifted or cannot be resolved (ERR_DEPENDENCY_UPSTREAM_DRIFT).
221
+
222
+ FLAGS:
223
+ --dry-run Plan and report; write nothing (sync only)
224
+ --format <toon|json|text> Output format (global flag; default toon off-TTY)
225
+ --help, -h Print this help
226
+
227
+ Only the "path" transport resolves in this release — a "npm"/"python" dependency spec is
228
+ valid config (reserved for a future toolchain) but 'meta deps sync' refuses it by name:
229
+ "transport \`npm\` is not supported by this toolchain yet; use \`path\`".
230
+
231
+ The lock is the ONLY thing 'meta deps sync' writes to besides the snapshot directory —
232
+ sync never touches your own metadata files. 'meta deps check' compares the lock against
233
+ what is installed right now (the same check 'meta verify --deps' runs); 'meta deps sync'
234
+ only ever compares against what config DECLARES.
194
235
  `,
195
236
  verify: `meta verify — drift gate (templates / DB schema / codegen / migration replay)
196
237
 
@@ -210,6 +251,12 @@ FLAGS:
210
251
  a namespace MetaObjects owns); the count of exempt pages is
211
252
  reported either way, and a run where every page is ignored is
212
253
  refused rather than reported clean.
254
+ --deps Dependency drift — re-resolve each declared dependency exactly as
255
+ 'meta deps check' does and compare its installed artifact's hash
256
+ against .metaobjects/deps.lock.json. Exits 1 if any dependency has
257
+ drifted or cannot be resolved (ERR_DEPENDENCY_UPSTREAM_DRIFT). NEVER
258
+ part of the bare-verify default — it needs the publisher reachable,
259
+ which CI may not have.
213
260
  --db <url> Schema drift — live DB URL enables the schema-drift gate.
214
261
  Supports: file:, libsql:, postgres:, postgresql:
215
262
  D1 has no URL — use --dialect d1 / --d1 <binding> instead.
@@ -237,6 +284,8 @@ FLAGS:
237
284
  migrating a model onto a registered provider.
238
285
  --no-antipatterns Suppress the advisory "hand-rolled what MetaObjects can model" pass
239
286
  --no-requirement-lint Suppress the advisory requirement AUTHORING lint (not the gate)
287
+ --no-overlay-lint Suppress the advisory overlay-redeclaration AUTHORING lint —
288
+ never a gate; this lint can't fail the build
240
289
  --limit <n|all> Advisory lines TEXT output prints PER SECTION before truncating
241
290
  (default 20; per-section so the authoring lint can never push
242
291
  the gate's own warnings off the end)
@@ -258,6 +307,13 @@ lint in its own section: names that are not addressable, prose slots holding one
258
307
  sentence twice, content written where no surface reads it. Warnings only — it can
259
308
  never fail the build. Opt out with --no-requirement-lint or META_NO_REQUIREMENT_LINT=1.
260
309
  The requirements GATE itself (dangling refs, link floor, levels) always runs.
310
+
311
+ verify also prints an ADVISORY overlay authoring lint, in its own section: a
312
+ top-level declaration redeclared in two or more files where more than one
313
+ redeclaration lacks 'overlay: true' — today's default merge rule reuses it
314
+ silently, but the same unflagged redeclaration silently becomes a NEW object
315
+ the day the target is renamed or removed upstream. Warnings only — it can
316
+ never fail the build. Opt out with --no-overlay-lint or META_NO_OVERLAY_LINT=1.
261
317
  `,
262
318
  export: `meta export — flatten loaded metadata to one canonical JSON artifact
263
319
 
@@ -530,6 +586,10 @@ export async function run(argv: string[]): Promise<number> {
530
586
  const { ejectCommand } = await import("./commands/eject.js");
531
587
  return ejectCommand(rest, cwd);
532
588
  }
589
+ case "deps": {
590
+ const { depsCommand } = await import("./commands/deps.js");
591
+ return depsCommand(rest, cwd, fmt);
592
+ }
533
593
  case "export": {
534
594
  const { exportCommand } = await import("./commands/export.js");
535
595
  return exportCommand(rest, cwd);
@@ -1,6 +1,12 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { AGENT_CONTEXT_MANIFEST_PATH, agentContextStaleness, type Manifest } from "@metaobjectsdev/sdk";
3
+ // The agent-context subpath, not the root barrel: `@metaobjectsdev/sdk` exports TWO
4
+ // types named `Manifest` — this one (the scaffolded agent context) via `export *`,
5
+ // and FR-023's `metaobjects.pkg.json` explicitly. An explicit export shadows a
6
+ // star one, so the root barrel now hands out the dependency manifest under this
7
+ // name. `init.ts` already imports these from the subpath; this was the last site
8
+ // reading them from the root.
9
+ import { AGENT_CONTEXT_MANIFEST_PATH, agentContextStaleness, type Manifest } from "@metaobjectsdev/sdk/agent-context";
4
10
  import { cliVersion } from "./version.js";
5
11
  import { log } from "./log.js";
6
12