@metaobjectsdev/cli 0.24.0 → 0.24.2

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 (41) hide show
  1. package/dist/src/commands/docs.d.ts.map +1 -1
  2. package/dist/src/commands/docs.js +13 -13
  3. package/dist/src/commands/docs.js.map +1 -1
  4. package/dist/src/commands/gen.d.ts.map +1 -1
  5. package/dist/src/commands/gen.js +9 -4
  6. package/dist/src/commands/gen.js.map +1 -1
  7. package/dist/src/commands/upgrade.d.ts.map +1 -1
  8. package/dist/src/commands/upgrade.js +55 -25
  9. package/dist/src/commands/upgrade.js.map +1 -1
  10. package/dist/src/commands/verify.d.ts.map +1 -1
  11. package/dist/src/commands/verify.js +72 -12
  12. package/dist/src/commands/verify.js.map +1 -1
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js +20 -8
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/lib/args.d.ts +8 -0
  17. package/dist/src/lib/args.d.ts.map +1 -1
  18. package/dist/src/lib/args.js +2 -0
  19. package/dist/src/lib/args.js.map +1 -1
  20. package/dist/src/lib/load-metaobjects-config.d.ts +31 -0
  21. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  22. package/dist/src/lib/load-metaobjects-config.js +43 -0
  23. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  24. package/dist/src/lib/requirement-check.d.ts +61 -7
  25. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  26. package/dist/src/lib/requirement-check.js +113 -29
  27. package/dist/src/lib/requirement-check.js.map +1 -1
  28. package/dist/src/lib/requirement-lint.d.ts +29 -0
  29. package/dist/src/lib/requirement-lint.d.ts.map +1 -0
  30. package/dist/src/lib/requirement-lint.js +318 -0
  31. package/dist/src/lib/requirement-lint.js.map +1 -0
  32. package/package.json +10 -10
  33. package/src/commands/docs.ts +22 -18
  34. package/src/commands/gen.ts +10 -4
  35. package/src/commands/upgrade.ts +57 -28
  36. package/src/commands/verify.ts +77 -10
  37. package/src/index.ts +20 -8
  38. package/src/lib/args.ts +10 -0
  39. package/src/lib/load-metaobjects-config.ts +45 -0
  40. package/src/lib/requirement-check.ts +150 -32
  41. package/src/lib/requirement-lint.ts +356 -0
@@ -1,6 +1,8 @@
1
1
  // server/typescript/packages/cli/src/commands/upgrade.ts
2
2
  //
3
- // `meta upgrade` — rewrite retired vocabulary in this project's metadata.
3
+ // `meta upgrade` — rewrite what the current loader no longer accepts in this project's
4
+ // metadata: RETIRED vocabulary (`retired-vocabulary.ts`) and ATTRIBUTE CONTRADICTIONS —
5
+ // pairs of live attributes that may not sit on one node (`attr-contradictions.ts`).
4
6
  //
5
7
  // DELIBERATELY NOT `meta migrate`. That command owns DATABASE SCHEMA (ADR-0015) and an
6
8
  // adopter reading `migrate` expects DDL. Overloading it with a metadata rewrite would make
@@ -22,8 +24,8 @@ import { resolveCollection } from "@metaobjectsdev/sdk";
22
24
  import { rewriteDocument } from "@metaobjectsdev/metadata";
23
25
  import { log } from "../lib/log.js";
24
26
 
25
- /** Authoring formats the rewriter cannot edit see `vocabulary-rewrite.ts` for why. */
26
- const UNREWRITABLE_EXTENSIONS = new Set([".yaml", ".yml"]);
27
+ /** YAML authoring (ADR-0006). Rewritten by the `yaml`-backed arm, loaded on demand below. */
28
+ const YAML_EXTENSIONS = new Set([".yaml", ".yml"]);
27
29
 
28
30
  interface UpgradeFlags {
29
31
  apply: boolean;
@@ -56,8 +58,11 @@ export async function upgradeCommand(args: string[], cwd: string): Promise<numbe
56
58
  if ((err as Error).message === "__help__") {
57
59
  log.info(
58
60
  "meta upgrade [<project>] [--to <version>] [--apply]\n\n" +
59
- " Rewrites retired metadata vocabulary. Previews by default; --apply writes.\n" +
60
- " Retirements needing a human decision are REFUSED and listed with their guide.",
61
+ " Rewrites metadata the current loader no longer accepts, in JSON and YAML alike:\n" +
62
+ " retired vocabulary, and pairs of live attributes that may no longer sit together.\n" +
63
+ " Previews by default; --apply writes.\n" +
64
+ " Changes needing a human decision are REFUSED and listed with their guide.\n\n" +
65
+ " Exit: 0 clean · 1 refusals remain · 2 bad usage · 3 some files could not be read.",
61
66
  );
62
67
  return 0;
63
68
  }
@@ -78,23 +83,38 @@ export async function upgradeCommand(args: string[], cwd: string): Promise<numbe
78
83
  let totalChanges = 0;
79
84
  let totalRefusals = 0;
80
85
  let filesChanged = 0;
81
- const skipped: string[] = [];
86
+ let checked = 0;
87
+ // Files we could not READ AT ALL. Distinct from "checked and clean" in every report and in
88
+ // the exit code — conflating them is the whole of #339.
89
+ const notChecked: string[] = [];
90
+
91
+ // The YAML arm carries the `yaml` package, so it lives behind its own subpath and is
92
+ // loaded only when the estate actually contains YAML. Importing it eagerly would pull a
93
+ // Node-only dependency into every `meta` invocation.
94
+ const hasYaml = files.some((f) => YAML_EXTENSIONS.has(extname(f).toLowerCase()));
95
+ const rewriteYaml = hasYaml
96
+ ? (await import("@metaobjectsdev/metadata/vocabulary-rewrite-yaml")).rewriteYamlDocument
97
+ : undefined;
98
+
99
+ const opts = flags.maxVersion !== undefined ? { maxVersion: flags.maxVersion } : {};
82
100
 
83
101
  for (const file of files) {
84
102
  const rel = relative(projectRoot, file);
103
+ const before = await readFile(file, "utf8");
85
104
 
86
- // A file we cannot rewrite is NAMED, never passed over. Silently skipping it is the
87
- // failure this command exists to prevent: the adopter runs the documented migration,
88
- // reads "no retired vocabulary found", and ships metadata that does not load.
89
- if (UNREWRITABLE_EXTENSIONS.has(extname(file).toLowerCase())) {
90
- skipped.push(rel);
91
- continue;
105
+ let r;
106
+ if (YAML_EXTENSIONS.has(extname(file).toLowerCase())) {
107
+ const y = rewriteYaml?.(before, opts);
108
+ // A document that does not parse was not examined, and must never be counted as clean.
109
+ if (y === undefined || y.unparseable) {
110
+ notChecked.push(rel);
111
+ continue;
112
+ }
113
+ r = y;
114
+ } else {
115
+ r = rewriteDocument(before, opts);
92
116
  }
93
-
94
- const before = await readFile(file, "utf8");
95
- const r = rewriteDocument(before, {
96
- ...(flags.maxVersion !== undefined ? { maxVersion: flags.maxVersion } : {}),
97
- });
117
+ checked++;
98
118
  if (r.changes.length === 0 && r.refusals.length === 0) continue;
99
119
 
100
120
  log.info(`\n${rel}`);
@@ -115,19 +135,23 @@ export async function upgradeCommand(args: string[], cwd: string): Promise<numbe
115
135
  }
116
136
 
117
137
  log.info("");
118
- if (skipped.length > 0) {
138
+ if (notChecked.length > 0) {
119
139
  log.warn(
120
- `${skipped.length} YAML file(s) cannot be rewritten automatically and were NOT ` +
121
- `checked migrate them by hand:\n ${skipped.join("\n ")}`,
140
+ `${notChecked.length} file(s) could not be parsed and were NOT checked — fix these ` +
141
+ `first, then re-run:\n ${notChecked.join("\n ")}`,
122
142
  );
123
143
  }
124
144
 
145
+ // Every conclusion states how many files it is a conclusion ABOUT. A bare "nothing found"
146
+ // read on its own says the estate is clean, and it is the last line, so it is the one that
147
+ // sticks — on the estate that reported this, it was the opposite of the truth.
148
+ //
149
+ // It says "nothing to rewrite", not "your metadata loads": this command knows about retired
150
+ // vocabulary and attribute contradictions, and nothing else. `meta verify` owns the verdict.
151
+ const scope = `${checked} file(s) checked${notChecked.length > 0 ? `, ${notChecked.length} NOT checked` : ""}`;
152
+
125
153
  if (totalChanges === 0 && totalRefusals === 0) {
126
- log.info(
127
- skipped.length > 0
128
- ? "meta upgrade — no retired vocabulary found in the JSON metadata."
129
- : "meta upgrade — no retired vocabulary found.",
130
- );
154
+ log.info(`meta upgrade — nothing to rewrite (${scope}).`);
131
155
  } else if (totalChanges === 0) {
132
156
  // Refusals only. Reporting "rewrote 0 declarations", or advertising `--apply`, both
133
157
  // promise an action guaranteed to change nothing and bury the fact that the remaining
@@ -143,8 +167,7 @@ export async function upgradeCommand(args: string[], cwd: string): Promise<numbe
143
167
  }
144
168
 
145
169
  // Non-zero while ANY refusal stands, applied or not. A partial upgrade that exited 0 would
146
- // let CI record the migration as done while metadata still fails to load. A file we could
147
- // not read at all counts the same way, for the same reason.
170
+ // let CI record the migration as done while metadata still fails to load.
148
171
  if (totalRefusals > 0) {
149
172
  log.error(
150
173
  `${totalRefusals} declaration(s) need a human decision and were left untouched — ` +
@@ -152,5 +175,11 @@ export async function upgradeCommand(args: string[], cwd: string): Promise<numbe
152
175
  );
153
176
  return 1;
154
177
  }
155
- return skipped.length > 0 ? 1 : 0;
178
+
179
+ // "I could not look" gets its OWN code. It used to share exit 1 with "work remains", so a
180
+ // script could not tell an estate needing decisions from one the tool never opened — and
181
+ // an adopter whose whole estate was skipped got a failure exit next to a message saying
182
+ // nothing was found.
183
+ if (notChecked.length > 0) return 3;
184
+ return 0;
156
185
  }
@@ -15,9 +15,12 @@ import { warnIfManifestIgnored } from "../lib/manifest-ignored-check.js";
15
15
  import { scanSourceForAntiPatterns } from "../lib/anti-patterns.js";
16
16
  import { FileProvider } from "../lib/file-provider.js";
17
17
  import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
18
- import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
18
+ import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
19
19
  import { computeCodegenDrift } from "../lib/codegen-drift.js";
20
- import { checkRequirements, summariseRequirements } from "../lib/requirement-check.js";
20
+ import {
21
+ checkRequirements, summariseRequirements, scanRequirements, type Diagnostic,
22
+ } from "../lib/requirement-check.js";
23
+ import { lintRequirements } from "../lib/requirement-lint.js";
21
24
  import { resolveD1Config, resolveMigrateConfig } from "../lib/config.js";
22
25
  import {
23
26
  buildWranglerExecuteArgs,
@@ -461,12 +464,16 @@ export async function verifyCommand(
461
464
  // rule reaches the semantic cases. FR-038 retires the attribute rather than
462
465
  // narrowing it; the replacement generates the test FROM the requirement, so
463
466
  // the link is structural instead of a string the author picks.
464
- const diags = [...checkRequirements(root)];
467
+ // ONE scan for all three passes. The gate and the summary each used to walk the
468
+ // model AND resolve every @implementedBy claim for themselves — the resolution
469
+ // being the expensive half — and the lint added a third walk on top.
470
+ const scan = scanRequirements(root);
471
+ const diags = [...checkRequirements(root, scan)];
465
472
 
466
473
  // Printed on EVERY run, clean or not — a gate that says nothing when it
467
474
  // passes cannot be told apart from a gate that checked nothing, and the
468
475
  // recorded-gap counts are the whole reason to keep a ledger.
469
- const s = summariseRequirements(root);
476
+ const s = summariseRequirements(root, scan);
470
477
  if (s !== undefined) {
471
478
  const order = [...REQUIREMENT_STATUSES];
472
479
  const parts = order
@@ -493,15 +500,51 @@ export async function verifyCommand(
493
500
  }
494
501
  }
495
502
 
496
- if (diags.length === 0) return 0;
497
503
  const errors = diags.filter((d) => d.severity === "error");
498
504
  const warns = diags.filter((d) => d.severity === "warn");
499
505
  const CAP = 20;
500
- for (const d of errors) log.error(` ${d.code}${d.name !== undefined ? ` [${d.name}]` : ""}: ${d.message}`);
501
- for (const d of warns.slice(0, CAP)) {
502
- log.warn(` ${d.code}${d.name !== undefined ? ` [${d.name}]` : ""}: ${d.message}`);
506
+ const fmt = (d: Diagnostic): string =>
507
+ ` ${d.code}${d.path !== undefined ? ` [${d.path}]` : ""}: ${d.message}`;
508
+ /** Print a capped run of warnings. Capped per SECTION, never across them: a
509
+ * ledger of a few hundred entries can produce hundreds of prose findings, and a
510
+ * shared cap would let the advisory lint push every gate warning off the end. */
511
+ const warnCapped = (ds: readonly Diagnostic[]): void => {
512
+ for (const d of ds.slice(0, CAP)) log.warn(fmt(d));
513
+ if (ds.length > CAP) log.warn(` …and ${ds.length - CAP} more.`);
514
+ };
515
+ for (const d of errors) log.error(fmt(d));
516
+ warnCapped(warns);
517
+
518
+ // -- the authoring lint: its own section, its own cap ----------------------
519
+ // Separate from the gate above because it makes a different claim. The gate
520
+ // says the ledger DISAGREES WITH THE MODEL; the lint says it agrees but
521
+ // records less than its author thinks — a name that is not an address, two
522
+ // slots holding one sentence, prose written where no surface reads it.
523
+ //
524
+ // The separate cap is the load-bearing part. A ledger of a few hundred
525
+ // entries can produce hundreds of prose findings, and under one shared cap
526
+ // those would push every WARN_REQUIREMENT_OBJECT_UNCLAIMED off the end of the
527
+ // list — the lint would silence the gate it was added beside. Nothing here
528
+ // reaches the exit code: every lint finding is a warning by construction.
529
+ // Muted with --no-requirement-lint or META_NO_REQUIREMENT_LINT=1, the same pair
530
+ // its sibling advisory offers. It mutes the ADVISORY half only — the gate above
531
+ // still runs and can still fail the build, which is the point of the split.
532
+ // `s === undefined` means the model declares no requirements at all, which the
533
+ // two passes above have already established: opt-in by declaration, decided
534
+ // without a third walk to rediscover it.
535
+ const lint = s === undefined
536
+ || flags.noRequirementLint
537
+ || process.env.META_NO_REQUIREMENT_LINT === "1"
538
+ ? []
539
+ : lintRequirements(root, scan.addressed);
540
+ if (lint.length > 0) {
541
+ log.warn(
542
+ `meta verify — requirements: ${lint.length} authoring warning(s) ` +
543
+ `(advisory — does not fail the build):`,
544
+ );
545
+ warnCapped(lint);
503
546
  }
504
- if (warns.length > CAP) log.warn(` …and ${warns.length - CAP} more.`);
547
+
505
548
  if (errors.length > 0) {
506
549
  log.error(`meta verify — requirements: ${errors.length} error(s).`);
507
550
  return 1;
@@ -954,9 +997,33 @@ export async function verifyCommand(
954
997
  // question 3) — a `gen` that committed under a narrowed scope and a
955
998
  // `verify --codegen` that regenerates unscoped would disagree about which
956
999
  // files should exist, reporting every out-of-scope entity as drift.
1000
+ //
1001
+ // The same argument governs the SOURCE SET (#340), and it is the reason this
1002
+ // resolves its own collection instead of reusing the outer one: `gen` in a
1003
+ // sub-project generates from that package's own sources, so a `--codegen` gate
1004
+ // that regenerated from the ancestor's wider set would report every file the
1005
+ // ancestor contributes as drift — turning the #340 fix into a broken gate. It is
1006
+ // re-resolved rather than hoisted because `verify`'s subverbs COMPOSE: `--db` and
1007
+ // `--templates` are answering a question about the whole declared collection, and
1008
+ // narrowing the outer `root` would silently change what they check.
1009
+ const genCollection = await resolveGenCollection(collection, genConfigDir);
1010
+ let codegenRoot = root;
1011
+ if (genCollection !== collection) {
1012
+ try {
1013
+ codegenRoot = await loadMemory(genCollection.configDir, {
1014
+ files: genCollection.files,
1015
+ ...configLoadOptions,
1016
+ strict: !flags.lax,
1017
+ });
1018
+ } catch (err) {
1019
+ log.error(`verify --codegen: failed to load this package's metadata: ${(err as Error).message}`);
1020
+ return 2;
1021
+ }
1022
+ }
1023
+
957
1024
  let result;
958
1025
  try {
959
- result = await computeCodegenDrift(forgeConfig, root, genConfigDir, collection.inScope);
1026
+ result = await computeCodegenDrift(forgeConfig, codegenRoot, genConfigDir, genCollection.inScope);
960
1027
  } catch (err) {
961
1028
  log.error(`verify --codegen: regeneration failed: ${(err as Error).message}`);
962
1029
  return 1;
package/src/index.ts CHANGED
@@ -21,7 +21,7 @@ COMMANDS:
21
21
  gen [<entity>...] Codegen TS targets from your declared metadata
22
22
  types [query] Search the metadata vocabulary (types, subtypes, @attrs) by name or description
23
23
  export Flatten loaded metadata to one canonical JSON artifact
24
- docs <metadata> --out <dir> Generate neutral metadata documentation (entity + template pages; --site for HTML site)
24
+ docs [<project-root>] --out <dir> Generate neutral metadata documentation (entity + template pages; --site for HTML site)
25
25
  verify Drift gate — subverbs: --templates / --db / --codegen (bare = --templates)
26
26
  upgrade Rewrite retired metadata vocabulary (previews; --apply writes)
27
27
  prompt-snapshot Snapshot rendered template.* output; --check gates drift
@@ -42,9 +42,11 @@ EXPORT FLAGS:
42
42
  --out <file> Write output to a file (default: stdout)
43
43
 
44
44
  DOCS FLAGS:
45
- <metadata> Project root to resolve metadata from; passing it SCOPES the run (default: cwd)
45
+ [<project-root>] PROJECT ROOT to resolve metadata from the directory that CONTAINS
46
+ your metadata, NOT the metadata directory. Passing it SCOPES the run
47
+ (default: cwd)
46
48
  --out <dir>, -o Output directory for the pages (default: ./docs)
47
- --templates <dir> Project root to resolve adopter templates/ overrides (default: <metadata>)
49
+ --templates <dir> Project root to resolve adopter templates/ overrides (default: <project-root>)
48
50
  --prompts <dir> Extra dir holding prompt .mustache sources for --site (e.g. data/templates/)
49
51
 
50
52
  VERIFY FLAGS (ADR-0021 D2 — explicit subverbs; combine any; exit 1 on ANY drift):
@@ -143,6 +145,7 @@ FLAGS:
143
145
  --remote Target remote D1 instead of local (only with --dialect d1) —
144
146
  the ONLY way to verify the actual deployed D1 database
145
147
  --no-antipatterns Suppress the advisory "hand-rolled what MetaObjects can model" pass
148
+ --no-requirement-lint Suppress the advisory requirement AUTHORING lint (not the gate)
146
149
  --help, -h Print this help
147
150
 
148
151
  A bare 'meta verify' also runs an ADVISORY anti-pattern pass: it scans your authored
@@ -150,6 +153,12 @@ source for hand-rolled aggregates, money-as-float, and CHECK-IN enums and points
150
153
  at the construct that models them (origin.aggregate / field.currency / field.enum).
151
154
  Warnings only — it never fails the build. Opt out with --no-antipatterns or
152
155
  META_NO_ANTIPATTERNS=1.
156
+
157
+ If the project declares requirement.* nodes, verify also prints an ADVISORY authoring
158
+ lint in its own section: names that are not addressable, prose slots holding one
159
+ sentence twice, content written where no surface reads it. Warnings only — it can
160
+ never fail the build. Opt out with --no-requirement-lint or META_NO_REQUIREMENT_LINT=1.
161
+ The requirements GATE itself (dangling refs, link floor, levels) always runs.
153
162
  `,
154
163
  export: `meta export — flatten loaded metadata to one canonical JSON artifact
155
164
 
@@ -163,12 +172,15 @@ FLAGS:
163
172
  docs: `meta docs — generate neutral metadata documentation (entity + template pages)
164
173
 
165
174
  USAGE:
166
- meta docs [<metadata>] [flags]
175
+ meta docs [<project-root>] [flags]
167
176
 
168
177
  FLAGS:
169
- <metadata> Project root to resolve metadata from. Passing it SCOPES the run to
170
- that directory's own sources; no ancestor .metaobjects/config.json is
171
- consulted. Omitted (default), the project is discovered by walking up.
178
+ [<project-root>] PROJECT ROOT to resolve metadata from the directory that CONTAINS
179
+ your metadata, NOT the metadata directory itself. (The Python and C#
180
+ 'docs' positionals mean the metadata dir; this one does not.) Passing
181
+ it SCOPES the run to that directory's own sources; no ancestor
182
+ .metaobjects/config.json is consulted. Omitted (default), the project
183
+ is discovered by walking up.
172
184
  --out <dir>, -o Output directory for the pages (default: ./docs)
173
185
  --model Emit the markdown model surface (entity + template pages)
174
186
  --api Emit the markdown api surface (generated SDK reference)
@@ -177,7 +189,7 @@ FLAGS:
177
189
  --metamodel Document the built-in metamodel vocabulary (no metadata needed)
178
190
  --site Generate the browsable HTML documentation site (<out>/site/)
179
191
  --scaffold-site Copy the site's templates + assets into codegen/docs-site/ to own (theme) them
180
- --templates <dir> Project root to resolve adopter templates/ overrides (default: <metadata>)
192
+ --templates <dir> Project root to resolve adopter templates/ overrides (default: <project-root>)
181
193
  --prompts <dir> Extra dir holding prompt .mustache sources (for --site) when they
182
194
  live outside the metadata sources or templates/ (e.g. data/templates/)
183
195
  --help, -h Print this help
package/src/lib/args.ts CHANGED
@@ -259,6 +259,14 @@ export interface VerifyFlags {
259
259
  anyExplicit: boolean;
260
260
  /** Suppress the advisory anti-pattern (verify-as-teacher) pass. */
261
261
  noAntipatterns: boolean;
262
+ /**
263
+ * Suppress the advisory requirement AUTHORING lint — the second, prose-quality
264
+ * section, never the gate above it. Its own findings argue that a noisy advisory
265
+ * gets switched off wholesale, and a ledger mid-migration can print its capped
266
+ * twenty lines on every run for weeks; a mute for the advisory half is what keeps
267
+ * the half that CAN fail a build switched on. Same shape as --no-antipatterns.
268
+ */
269
+ noRequirementLint: boolean;
262
270
  /**
263
271
  * ADR-0023 strict-attr load opt-OUT (#96). `verify` is strict-by-default — an
264
272
  * undeclared/typo'd own `@attr` fails verify (ERR_UNKNOWN_ATTR). `--lax`
@@ -281,6 +289,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
281
289
  replay: { type: "boolean", default: false },
282
290
  "replay-snapshot": { type: "boolean", default: false },
283
291
  "no-antipatterns": { type: "boolean", default: false },
292
+ "no-requirement-lint": { type: "boolean", default: false },
284
293
  lax: { type: "boolean", default: false },
285
294
  "d1": { type: "string" },
286
295
  "remote": { type: "boolean", default: false },
@@ -330,6 +339,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
330
339
  replaySnapshot,
331
340
  anyExplicit,
332
341
  noAntipatterns: !!values["no-antipatterns"],
342
+ noRequirementLint: !!values["no-requirement-lint"],
333
343
  lax: !!values.lax,
334
344
  d1: values.d1 as string | undefined,
335
345
  remote: !!values.remote,
@@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url";
6
6
  import { randomBytes } from "node:crypto";
7
7
  import { createJiti } from "jiti";
8
8
  import type { MetaDataTypeProvider, MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
9
+ import { resolveCollection, type Collection } from "@metaobjectsdev/sdk";
9
10
 
10
11
  const CONFIG_FILE = "metaobjects.config.ts";
11
12
 
@@ -212,6 +213,50 @@ export function resolveGenConfigDir(startDir: string, fallback: string): string
212
213
  return fallback;
213
214
  }
214
215
 
216
+ /**
217
+ * The collection a TypeScript package GENERATES FROM (#340).
218
+ *
219
+ * #326/#327 established that the two config files answer different questions, and gave
220
+ * `metaobjects.config.ts` its own walk. This is the remaining half of the same split:
221
+ * a sub-project whose TS config sits below the collection root was still LOADING the
222
+ * ancestor's whole source set, so its `src/generated` absorbed metadata belonging to
223
+ * unrelated parts of the repository — one adopter's web app went from 376 files to 831,
224
+ * the surplus being another module's server-side prompt payload DTOs. It fails OPEN
225
+ * (`tsc` passes, tests pass), so the only symptom is a directory that quietly doubled.
226
+ *
227
+ * The rule: an ancestor `.metaobjects/config.json` is the DEFAULT for a package that
228
+ * declares no sources of its own, never an ADDITION to one that does. So when the TS
229
+ * config sits somewhere the collection did not, that directory is re-resolved as a
230
+ * collection in its own right, and it wins if it actually resolves any metadata.
231
+ *
232
+ * It can only ever NARROW, and only in a shape that could not have worked before:
233
+ * - the two directories coincide (every `meta init` project, and every run from a
234
+ * project root) — returns the original, untouched, without a second resolve;
235
+ * - the sub-project declares no sources — the pinned resolve throws
236
+ * `ERR_SOURCE_UNRESOLVED` or comes back empty, and the ancestor stands, so a
237
+ * package that genuinely lives off an ancestor tree keeps working;
238
+ * - the sub-project has its own metadata — it generates from exactly that, which is
239
+ * what it did before source resolution learned to walk upward.
240
+ *
241
+ * Deliberately NOT applied to `.metaobjects/` STATE. Migrations, snapshots and the
242
+ * operational block stay keyed on the discovered collection's directory (#326 settled
243
+ * that); this narrows what is LOADED, and nothing about where state lives.
244
+ */
245
+ export async function resolveGenCollection(
246
+ collection: Collection,
247
+ genConfigDir: string,
248
+ ): Promise<Collection> {
249
+ if (resolve(genConfigDir) === resolve(collection.configDir)) return collection;
250
+ try {
251
+ const pinned = await resolveCollection(genConfigDir, { explicitDir: genConfigDir });
252
+ return pinned.files.length > 0 ? pinned : collection;
253
+ } catch {
254
+ // The sub-project declares nothing resolvable of its own — inherit, exactly as a
255
+ // package with no config always has.
256
+ return collection;
257
+ }
258
+ }
259
+
215
260
  export async function loadMetaobjectsConfig(projectRoot: string): Promise<MetaobjectsGenConfig> {
216
261
  const fullPath = resolve(projectRoot, CONFIG_FILE);
217
262
  if (!existsSync(fullPath)) {