@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
@@ -0,0 +1,231 @@
1
+ // server/typescript/packages/cli/src/commands/deps.ts
2
+ //
3
+ // FR-023 Phase 1a — Task 14 shipped `meta deps sync` (`path` transport) and
4
+ // `meta deps list`; Task 15 shipped `meta deps check` (this file's `runCheck`).
5
+ import { join } from "node:path";
6
+ import {
7
+ DEFAULT_METAOBJECTS_DIR,
8
+ discoverCollectionRoot,
9
+ loadConfig,
10
+ loadMemory,
11
+ resolveCollection,
12
+ type DependencySpec,
13
+ type Lock,
14
+ } from "@metaobjectsdev/sdk";
15
+ import { parseDepsArgs, type DepsFlags } from "../lib/args.js";
16
+ import { log } from "../lib/log.js";
17
+ import { emitStructured, type OutputFormat } from "../lib/format.js";
18
+ import { reportLoadError } from "../lib/load-error.js";
19
+ import { collectionLoadOptions } from "../lib/collection-load-options.js";
20
+ import {
21
+ applySync,
22
+ checkDependencies,
23
+ ERR_DEPENDENCY_UPSTREAM_DRIFT,
24
+ formatCheckLine,
25
+ hash8,
26
+ planSync,
27
+ readLockOrThrow,
28
+ } from "../lib/dependency-sync.js";
29
+
30
+ /** The declared `dependencies` for the config governing `cwd` — read
31
+ * DIRECTLY via `loadConfig`, never through `resolveCollection`/`Collection`:
32
+ * that resolver VERIFIES the snapshot against the lock (`verifySnapshot`)
33
+ * and throws `ERR_DEPENDENCY_SNAPSHOT_STALE` the moment they disagree — the
34
+ * exact condition `meta deps sync` exists to fix. A project with no config
35
+ * at all declares none (`resolveCollection`'s own default: no config ⇒ no
36
+ * dependencies), which `discoverCollectionRoot`'s `hasConfig` already knows
37
+ * without a doomed `loadConfig` call. */
38
+ async function declaredDependencies(cwd: string): Promise<{ configDir: string; specs: readonly DependencySpec[] }> {
39
+ const { dir: configDir, hasConfig } = await discoverCollectionRoot(cwd);
40
+ if (!hasConfig) return { configDir, specs: [] };
41
+ const cfg = await loadConfig(join(configDir, DEFAULT_METAOBJECTS_DIR));
42
+ return { configDir, specs: cfg.dependencies };
43
+ }
44
+
45
+ async function runSync(configDir: string, specs: readonly DependencySpec[], flags: DepsFlags, fmt: OutputFormat): Promise<number> {
46
+ let lock: Lock | undefined;
47
+ try {
48
+ lock = await readLockOrThrow(configDir);
49
+ } catch (err) {
50
+ // Same convention as `depsCommand`'s `declaredDependencies` catch below: a
51
+ // committed project file that fails to parse is a config-class problem,
52
+ // exit 2 — distinct from the exit-1 a `sync` verdict failure gets.
53
+ log.error((err as Error).message);
54
+ return 2;
55
+ }
56
+
57
+ let plan;
58
+ try {
59
+ plan = await planSync(configDir, specs, lock, flags.names);
60
+ } catch (err) {
61
+ log.error((err as Error).message);
62
+ return 1;
63
+ }
64
+
65
+ let applied;
66
+ try {
67
+ applied = await applySync(configDir, plan, { dryRun: flags.dryRun });
68
+ } catch (err) {
69
+ log.error((err as Error).message);
70
+ return 1;
71
+ }
72
+
73
+ if (fmt === "text") {
74
+ for (const w of plan.warnings) log.info(w);
75
+ if (applied.report.length === 0) {
76
+ log.info("meta deps sync: nothing to do — no dependencies declared.");
77
+ } else {
78
+ for (const r of applied.report) log.info(r.line);
79
+ }
80
+ } else {
81
+ emitStructured(
82
+ {
83
+ warnings: plan.warnings,
84
+ dependencies: applied.report.map((r) => ({ name: r.name, action: r.action, message: r.line })),
85
+ },
86
+ fmt,
87
+ );
88
+ }
89
+
90
+ // A dry run wrote nothing to validate — the plan already stands for what
91
+ // WOULD load.
92
+ if (flags.dryRun) return 0;
93
+
94
+ // DESIGN §4.1's closing sentence: load the full collection once, so a sync
95
+ // that produces an unloadable model (a collision the plan missed because it
96
+ // only compares declared node LISTS, a scope that now excludes something
97
+ // load-bearing, …) fails in THIS command with the loader's own error,
98
+ // rather than surfacing on the next unrelated `meta gen`/`verify`.
99
+ let collection;
100
+ try {
101
+ collection = await resolveCollection(configDir);
102
+ } catch (err) {
103
+ log.error(`meta deps sync: the updated dependency set does not resolve: ${(err as Error).message}`);
104
+ return 1;
105
+ }
106
+ try {
107
+ await loadMemory(collection.configDir, collectionLoadOptions(collection));
108
+ } catch (err) {
109
+ reportLoadError(log, "meta deps sync: the updated dependency set does not load", err);
110
+ return 1;
111
+ }
112
+
113
+ return 0;
114
+ }
115
+
116
+ async function runList(configDir: string, fmt: OutputFormat): Promise<number> {
117
+ let lock: Lock | undefined;
118
+ try {
119
+ lock = await readLockOrThrow(configDir);
120
+ } catch (err) {
121
+ log.error((err as Error).message);
122
+ return 2;
123
+ }
124
+ const entries = Object.entries(lock?.dependencies ?? {});
125
+
126
+ if (fmt === "text") {
127
+ if (entries.length === 0) {
128
+ log.info("meta deps list: no dependencies locked — run `meta deps sync`.");
129
+ return 0;
130
+ }
131
+ for (const [name, entry] of entries) {
132
+ log.info(
133
+ `${name} ${entry.version} ${hash8(entry.integrity)} ${entry.nodes.length} node(s) ${entry.packages.join(", ")}`,
134
+ );
135
+ }
136
+ } else {
137
+ emitStructured(
138
+ { dependencies: entries.map(([name, entry]) => ({ name, ...entry })) },
139
+ fmt,
140
+ );
141
+ }
142
+ return 0;
143
+ }
144
+
145
+ /**
146
+ * `meta deps check` (Task 15) — re-resolve each declared dependency exactly
147
+ * as `sync` steps 1-2 do and compare its installed artifact's TRUE hash
148
+ * against what `.metaobjects/deps.lock.json` pinned. Read-only: never touches
149
+ * the lock or the snapshot (that is `sync`'s job). A `drifted` or
150
+ * `unresolved` dependency fails the command — a check that cannot check must
151
+ * not pass.
152
+ */
153
+ async function runCheck(
154
+ configDir: string,
155
+ specs: readonly DependencySpec[],
156
+ fmt: OutputFormat,
157
+ ): Promise<number> {
158
+ let lock: Lock | undefined;
159
+ try {
160
+ lock = await readLockOrThrow(configDir);
161
+ } catch (err) {
162
+ log.error((err as Error).message);
163
+ return 2;
164
+ }
165
+
166
+ if (specs.length === 0) {
167
+ if (fmt === "text") {
168
+ log.info("meta deps check: nothing to check — no dependencies declared.");
169
+ } else {
170
+ emitStructured({ dependencies: [] }, fmt);
171
+ }
172
+ return 0;
173
+ }
174
+
175
+ const results = await checkDependencies(configDir, specs, lock);
176
+ const failing = results.filter((r) => r.status !== "current");
177
+
178
+ if (fmt === "text") {
179
+ for (const r of results) log.info(formatCheckLine(r));
180
+ if (failing.length > 0) {
181
+ log.error(
182
+ `meta deps check: ${failing.length} of ${results.length} dependenc` +
183
+ `${results.length === 1 ? "y" : "ies"} drifted or unresolved (${ERR_DEPENDENCY_UPSTREAM_DRIFT}).`,
184
+ );
185
+ }
186
+ } else {
187
+ emitStructured(
188
+ {
189
+ dependencies: results.map((r) => ({ ...r, line: formatCheckLine(r) })),
190
+ ...(failing.length > 0 ? { code: ERR_DEPENDENCY_UPSTREAM_DRIFT } : {}),
191
+ },
192
+ fmt,
193
+ );
194
+ }
195
+
196
+ return failing.length > 0 ? 1 : 0;
197
+ }
198
+
199
+ export async function depsCommand(args: string[], cwd: string, fmt: OutputFormat): Promise<number> {
200
+ let flags: DepsFlags;
201
+ try {
202
+ flags = parseDepsArgs(args);
203
+ } catch (err) {
204
+ log.error((err as Error).message);
205
+ return 2;
206
+ }
207
+
208
+ let configDir: string;
209
+ let specs: readonly DependencySpec[];
210
+ try {
211
+ ({ configDir, specs } = await declaredDependencies(cwd));
212
+ } catch (err) {
213
+ // Mirrors `resolveCollection`'s own convention for a config.json that
214
+ // EXISTS but fails to load (malformed JSON, a schema violation): exit 2,
215
+ // the same code every other routed command uses for that class of
216
+ // failure (gen.ts, migrate.ts, docs.ts) — distinct from the exit 1 a
217
+ // dependency-resolution failure gets below, because this is a config
218
+ // problem, not a `sync` verdict.
219
+ log.error((err as Error).message);
220
+ return 2;
221
+ }
222
+
223
+ switch (flags.subverb) {
224
+ case "sync":
225
+ return runSync(configDir, specs, flags, fmt);
226
+ case "list":
227
+ return runList(configDir, fmt);
228
+ case "check":
229
+ return runCheck(configDir, specs, fmt);
230
+ }
231
+ }
@@ -11,10 +11,11 @@
11
11
  // output is therefore guaranteed — it is byte-for-byte the same generator the
12
12
  // `meta gen` pipeline runs (gated by the docs conformance fixture).
13
13
 
14
- import { resolve as resolvePath, basename } from "node:path";
14
+ import { resolve as resolvePath, basename, dirname } from "node:path";
15
15
  import { mkdir, writeFile } from "node:fs/promises";
16
16
  import { log } from "../lib/log.js";
17
17
  import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
18
+ import { collectionLoadOptions } from "../lib/collection-load-options.js";
18
19
  import { loadMemory, resolveCollection, resolveConfigDir, type Collection } from "@metaobjectsdev/sdk";
19
20
  import { existsSync } from "node:fs";
20
21
  import { join } from "node:path";
@@ -515,7 +516,7 @@ export async function docsCommand(
515
516
  let root;
516
517
  try {
517
518
  root = await loadMemory(collection.configDir, {
518
- files: collection.files,
519
+ ...collectionLoadOptions(collection),
519
520
  ...configLoadOptions,
520
521
  });
521
522
  } catch (err) {
@@ -860,7 +861,22 @@ async function emitSite(
860
861
  // files: a declared source directory holding no metadata yet would otherwise
861
862
  // vanish from the site's group list entirely, and `sourceDirs` could come back
862
863
  // empty where the pre-branch code always passed `<root>/metaobjects`.
863
- const sourceDirs = [...collection.sourceRoots];
864
+ // `collection.sourceRoots` deliberately EXCLUDES dependency artifacts (sdk's
865
+ // collection.ts: "a dependency's artifact is not a source root"). But
866
+ // `generateSite` below does NOT reuse the already-loaded `collection` root —
867
+ // it runs its own independent `loadModel` over exactly the dirs handed to
868
+ // it. Without the resolved snapshot dirs appended here, a consumer that
869
+ // `extends` an imported abstract or `overlay: true`s an imported node — the
870
+ // two constructs entities.md and abstracts-and-inheritance.md recommend for
871
+ // this exact boundary — hits `ERR_UNRESOLVED_SUPER` / `ERR_OVERLAY_NO_TARGET`
872
+ // in THIS loader even though the same metadata resolves fine through
873
+ // `loadMemory` above. `loadModel` handles a dependency's snapshot dir
874
+ // natively: each holds exactly the one artifact file, which its own
875
+ // `.(json|ya?ml)$` file walk picks up like any other source dir.
876
+ const sourceDirs = [
877
+ ...collection.sourceRoots,
878
+ ...new Set(collection.dependencies.map((d) => dirname(d.artifactPath))),
879
+ ];
864
880
  const seenDirs = new Set(sourceDirs);
865
881
  if (promptsDir !== undefined && !existsSync(promptsDir)) {
866
882
  log.warn(`docs: --prompts dir does not exist: ${promptsDir}`);
@@ -2,6 +2,7 @@ import { relative } from "node:path";
2
2
  import { parseGenArgs } from "../lib/args.js";
3
3
  import { resolveGenConfig } from "../lib/config.js";
4
4
  import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
5
+ import { collectionLoadOptions } from "../lib/collection-load-options.js";
5
6
  import { formatGenResult, formatGenResultToon, type GenFileEntry, type GenFileStatus } from "../lib/output.js";
6
7
  import { formatGenResultJson } from "../lib/output-json.js";
7
8
  import type { OutputFormat } from "../lib/format.js";
@@ -16,6 +17,9 @@ import {
16
17
  import { loadMemory, resolveCollection } from "@metaobjectsdev/sdk";
17
18
  import { runGen, listGenerators } from "@metaobjectsdev/codegen-ts";
18
19
  import type { WriteStatus } from "@metaobjectsdev/codegen-ts";
20
+ import { packageOfResolutionKey } from "@metaobjectsdev/metadata";
21
+ import type { MetaRoot } from "@metaobjectsdev/metadata";
22
+ import type { Collection } from "@metaobjectsdev/sdk";
19
23
  import { reportLoadError } from "../lib/load-error.js";
20
24
 
21
25
  /**
@@ -122,7 +126,7 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
122
126
  let metadata;
123
127
  try {
124
128
  metadata = await loadMemory(genCollection.configDir, {
125
- files: genCollection.files,
129
+ ...collectionLoadOptions(genCollection),
126
130
  ...loadMemoryOptionsFrom(forgeConfig),
127
131
  });
128
132
  } catch (err) {
@@ -130,6 +134,19 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
130
134
  return 2;
131
135
  }
132
136
 
137
+ // FR-023 §11.1 item 2 — refuse a positional that names ONLY objects imported
138
+ // from a dependency and excluded from output by the default exclusion rule.
139
+ // Distinct from the runner's existing "no entities matched" WARNING
140
+ // (runner.ts): that path covers a name matching nothing loaded at all; this
141
+ // one covers a name that matches something real, just not something this
142
+ // project generates. A name matching nothing falls through untouched — the
143
+ // runner's own warning still covers it.
144
+ const importedRefusal = refuseImportedPositional(cliConfig.entities, metadata, genCollection);
145
+ if (importedRefusal !== undefined) {
146
+ log.error(importedRefusal);
147
+ return 2;
148
+ }
149
+
133
150
  let result;
134
151
  try {
135
152
  result = await runGen({
@@ -145,6 +162,10 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
145
162
  // passed: an unconfigured project's predicate admits everything, so this
146
163
  // is a no-op for the common case, not a behavior change.
147
164
  scope: genCollection.inScope,
165
+ // FR-023 §4.3 — the collection's own source files (never a dependency's
166
+ // snapshot artifact), so sharedModelFile() defaults its `files` selection
167
+ // to exactly what this project itself declares.
168
+ sourceFiles: genCollection.ownFiles,
148
169
  ...(cliConfig.entities.length > 0 ? { entityFilter: cliConfig.entities } : {}),
149
170
  });
150
171
  } catch (err) {
@@ -220,6 +241,45 @@ export async function genCommand(args: string[], cwd: string, fmt: OutputFormat
220
241
  return hasFailure ? 1 : 0;
221
242
  }
222
243
 
244
+ /**
245
+ * FR-023 §11.1 item 2 — the error message for `meta gen <Name>` when EVERY
246
+ * loaded object named `<Name>` is imported from a dependency and excluded from
247
+ * output (imported metadata is load-only by default; only the consumer's own
248
+ * `scope.include` naming the package literally opts it in). Returns undefined
249
+ * for every other case, INCLUDING a name matching nothing at all — that stays
250
+ * the runner's existing "no entities matched" warning, unchanged.
251
+ *
252
+ * A bare positional is a NAME, not a fully-qualified one (`cliConfig.entities`
253
+ * is matched against `entity.name` elsewhere in this file the same way), so two
254
+ * packages declaring the same short name are both candidates; the refusal
255
+ * fires only when NONE of them would generate.
256
+ */
257
+ function refuseImportedPositional(
258
+ entityNames: readonly string[],
259
+ metadata: MetaRoot,
260
+ collection: Collection,
261
+ ): string | undefined {
262
+ if (entityNames.length === 0) return undefined;
263
+ const allObjects = metadata.objects();
264
+ for (const name of entityNames) {
265
+ const matches = allObjects.filter((o) => o.name === name);
266
+ if (matches.length === 0) continue; // nothing loaded by this name — the runner's own warning covers it
267
+ const allExcludedImports = matches.every((o) => {
268
+ const fqn = o.resolutionKey();
269
+ return collection.imported(fqn) && !collection.inScope(fqn);
270
+ });
271
+ if (!allExcludedImports) continue;
272
+ const fqn = matches[0]!.resolutionKey();
273
+ const pkg = packageOfResolutionKey(fqn);
274
+ const dependencyName = collection.dependencies.find((d) => d.packages.includes(pkg))?.name ?? pkg;
275
+ return (
276
+ `meta gen: '${name}' (${fqn}) is imported from dependency '${dependencyName}' and is not ` +
277
+ `generated here — add its package to scope.include in .metaobjects/config.json to generate it`
278
+ );
279
+ }
280
+ return undefined;
281
+ }
282
+
223
283
  /**
224
284
  * Run the advisory verify-as-teacher scan (same pass `meta verify` runs): surface
225
285
  * authored source that hand-rolls what the metadata could model. `gen` is the
@@ -1,9 +1,9 @@
1
1
  import { mkdir, writeFile, readFile, readdir, stat, rm } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { basename, dirname } from "node:path";
3
+ import { dirname } from "node:path";
4
4
  import { existsSync as existsSyncWrap, readFileSync as readFileSyncWrap } from "node:fs";
5
5
  import { createRequire } from "node:module";
6
- import { DEFAULT_CONFIG, ConfigSchema, saveConfig, PACKAGE_MANIFEST_FILE, DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR } from "@metaobjectsdev/sdk";
6
+ import { DEFAULT_CONFIG, ConfigSchema, saveConfig, PACKAGE_MANIFEST_FILE, DEFAULT_METADATA_DIR, DEFAULT_METAOBJECTS_DIR, DEPS_DIR, LOCK_FILE } from "@metaobjectsdev/sdk";
7
7
  import {
8
8
  assemble, resolveAgentContextRoot, planScaffold,
9
9
  AGENT_CONTEXT_MANIFEST_PATH, type Manifest, type Stack,
@@ -85,7 +85,8 @@ const METAOBJECTS_GITIGNORE_BODY = `# The codegen merge base. The snapshot BODIE
85
85
  # These ARE meant to be tracked — keep them even if a broad pattern matches.
86
86
  !migrations/
87
87
  !config.json
88
- !package.meta.json
88
+ !${DEPS_DIR}/
89
+ !${LOCK_FILE}
89
90
  `;
90
91
 
91
92
  // A minimal root .gitignore for a fresh project — only written when none exists,
@@ -673,7 +674,6 @@ export async function init(opts: InitOptions): Promise<InitResult> {
673
674
  "metaobjects/meta.common.json",
674
675
  ".metaobjects/config.json",
675
676
  ".metaobjects/.gitignore",
676
- `.metaobjects/${PACKAGE_MANIFEST_FILE}`,
677
677
  );
678
678
  result.created.push(".metaobjects/AGENTS.md", ".metaobjects/CLAUDE.md", ".claude/skills/metaobjects-*", AGENT_CONTEXT_MANIFEST_PATH);
679
679
  for (const name of SCAFFOLDED_GENERATOR_NAMES) result.created.push(`${OWNED_GENERATORS_DIR}/${name}.ts`);
@@ -703,19 +703,15 @@ export async function init(opts: InitOptions): Promise<InitResult> {
703
703
  await writeFile(join(agentDir, ".gitignore"), METAOBJECTS_GITIGNORE_BODY, "utf8");
704
704
  result.created.push(".metaobjects/.gitignore");
705
705
 
706
- // .metaobjects/package.meta.json — scaffold v0.3 package manifest if absent
707
- const manifestPath = join(agentDir, PACKAGE_MANIFEST_FILE);
708
- if (!(await fileExists(manifestPath))) {
709
- const defaultPackageName = basename(opts.cwd);
710
- const manifestBody = {
711
- name: defaultPackageName,
712
- version: "0.1.0",
713
- extends: [] as string[],
714
- };
715
- await writeFile(manifestPath, JSON.stringify(manifestBody, null, 2) + "\n", "utf8");
716
- result.created.push(`.metaobjects/${PACKAGE_MANIFEST_FILE}`);
717
- } else {
718
- result.preserved.push(`.metaobjects/${PACKAGE_MANIFEST_FILE}`);
706
+ // .metaobjects/package.meta.json — the v0.3 prototype manifest is deprecated
707
+ // (nothing reads it; FR-023 metadata dependencies is its replacement), so
708
+ // `meta init` no longer scaffolds it. A pre-existing one is left untouched;
709
+ // just note the deprecation so an existing project sees it.
710
+ const legacyManifestPath = join(agentDir, PACKAGE_MANIFEST_FILE);
711
+ if (await fileExists(legacyManifestPath)) {
712
+ result.warnings.push(
713
+ `note: .metaobjects/${PACKAGE_MANIFEST_FILE} is deprecated (nothing reads it; removed in 2.0) — see docs/features/metadata-dependencies.md`,
714
+ );
719
715
  }
720
716
 
721
717
  await writeAgentContext(opts, result);
@@ -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) {