@metaobjectsdev/cli 1.0.12 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/README.md +32 -22
  2. package/dist/bin/meta.js +14 -2
  3. package/dist/bin/meta.js.map +1 -1
  4. package/dist/src/commands/docs.d.ts +1 -14
  5. package/dist/src/commands/docs.d.ts.map +1 -1
  6. package/dist/src/commands/docs.js +11 -835
  7. package/dist/src/commands/docs.js.map +1 -1
  8. package/dist/src/commands/eject.d.ts +24 -1
  9. package/dist/src/commands/eject.d.ts.map +1 -1
  10. package/dist/src/commands/eject.js +240 -69
  11. package/dist/src/commands/eject.js.map +1 -1
  12. package/dist/src/commands/gen.d.ts +7 -0
  13. package/dist/src/commands/gen.d.ts.map +1 -1
  14. package/dist/src/commands/gen.js +91 -21
  15. package/dist/src/commands/gen.js.map +1 -1
  16. package/dist/src/commands/generator.js +2 -2
  17. package/dist/src/commands/generator.js.map +1 -1
  18. package/dist/src/commands/init.d.ts +7 -0
  19. package/dist/src/commands/init.d.ts.map +1 -1
  20. package/dist/src/commands/init.js +71 -36
  21. package/dist/src/commands/init.js.map +1 -1
  22. package/dist/src/commands/migrate.d.ts +1 -1
  23. package/dist/src/commands/migrate.d.ts.map +1 -1
  24. package/dist/src/commands/migrate.js +224 -125
  25. package/dist/src/commands/migrate.js.map +1 -1
  26. package/dist/src/commands/types.d.ts.map +1 -1
  27. package/dist/src/commands/types.js +16 -2
  28. package/dist/src/commands/types.js.map +1 -1
  29. package/dist/src/commands/upgrade.d.ts.map +1 -1
  30. package/dist/src/commands/upgrade.js +4 -1
  31. package/dist/src/commands/upgrade.js.map +1 -1
  32. package/dist/src/commands/verify.d.ts.map +1 -1
  33. package/dist/src/commands/verify.js +189 -125
  34. package/dist/src/commands/verify.js.map +1 -1
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/index.js +59 -72
  37. package/dist/src/index.js.map +1 -1
  38. package/dist/src/lib/agent-context-staleness.d.ts +8 -1
  39. package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
  40. package/dist/src/lib/agent-context-staleness.js +20 -5
  41. package/dist/src/lib/agent-context-staleness.js.map +1 -1
  42. package/dist/src/lib/agent-schema.d.ts +9 -0
  43. package/dist/src/lib/agent-schema.d.ts.map +1 -0
  44. package/dist/src/lib/agent-schema.js +61 -0
  45. package/dist/src/lib/agent-schema.js.map +1 -0
  46. package/dist/src/lib/allow.d.ts +6 -0
  47. package/dist/src/lib/allow.d.ts.map +1 -1
  48. package/dist/src/lib/allow.js +49 -5
  49. package/dist/src/lib/allow.js.map +1 -1
  50. package/dist/src/lib/anti-patterns.d.ts.map +1 -1
  51. package/dist/src/lib/anti-patterns.js +4 -2
  52. package/dist/src/lib/anti-patterns.js.map +1 -1
  53. package/dist/src/lib/args.d.ts +51 -10
  54. package/dist/src/lib/args.d.ts.map +1 -1
  55. package/dist/src/lib/args.js +44 -18
  56. package/dist/src/lib/args.js.map +1 -1
  57. package/dist/src/lib/catalog-listing.d.ts +21 -1
  58. package/dist/src/lib/catalog-listing.d.ts.map +1 -1
  59. package/dist/src/lib/catalog-listing.js +85 -16
  60. package/dist/src/lib/catalog-listing.js.map +1 -1
  61. package/dist/src/lib/catalog.d.ts +30 -0
  62. package/dist/src/lib/catalog.d.ts.map +1 -1
  63. package/dist/src/lib/catalog.js +40 -5
  64. package/dist/src/lib/catalog.js.map +1 -1
  65. package/dist/src/lib/codegen-drift.d.ts +31 -2
  66. package/dist/src/lib/codegen-drift.d.ts.map +1 -1
  67. package/dist/src/lib/codegen-drift.js +109 -4
  68. package/dist/src/lib/codegen-drift.js.map +1 -1
  69. package/dist/src/lib/collection-load-options.d.ts +1 -1
  70. package/dist/src/lib/collection-load-options.d.ts.map +1 -1
  71. package/dist/src/lib/collection-load-options.js +1 -0
  72. package/dist/src/lib/collection-load-options.js.map +1 -1
  73. package/dist/src/lib/config.d.ts +4 -2
  74. package/dist/src/lib/config.d.ts.map +1 -1
  75. package/dist/src/lib/config.js +4 -2
  76. package/dist/src/lib/config.js.map +1 -1
  77. package/dist/src/lib/decimal-precision-advisory.d.ts +10 -0
  78. package/dist/src/lib/decimal-precision-advisory.d.ts.map +1 -0
  79. package/dist/src/lib/decimal-precision-advisory.js +34 -0
  80. package/dist/src/lib/decimal-precision-advisory.js.map +1 -0
  81. package/dist/src/lib/deprecation-lint.d.ts.map +1 -1
  82. package/dist/src/lib/deprecation-lint.js +17 -26
  83. package/dist/src/lib/deprecation-lint.js.map +1 -1
  84. package/dist/src/lib/docs-removed.d.ts +13 -0
  85. package/dist/src/lib/docs-removed.d.ts.map +1 -0
  86. package/dist/src/lib/docs-removed.js +31 -0
  87. package/dist/src/lib/docs-removed.js.map +1 -0
  88. package/dist/src/lib/docs-site-eject.d.ts +13 -0
  89. package/dist/src/lib/docs-site-eject.d.ts.map +1 -0
  90. package/dist/src/lib/docs-site-eject.js +40 -0
  91. package/dist/src/lib/docs-site-eject.js.map +1 -0
  92. package/dist/src/lib/field-lint.d.ts +25 -0
  93. package/dist/src/lib/field-lint.d.ts.map +1 -0
  94. package/dist/src/lib/field-lint.js +102 -0
  95. package/dist/src/lib/field-lint.js.map +1 -0
  96. package/dist/src/lib/fk-index-advisory.d.ts +1 -2
  97. package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
  98. package/dist/src/lib/fk-index-advisory.js +4 -3
  99. package/dist/src/lib/fk-index-advisory.js.map +1 -1
  100. package/dist/src/lib/format.d.ts +7 -0
  101. package/dist/src/lib/format.d.ts.map +1 -1
  102. package/dist/src/lib/format.js +13 -0
  103. package/dist/src/lib/format.js.map +1 -1
  104. package/dist/src/lib/gen-migrate-advice.d.ts +3 -1
  105. package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
  106. package/dist/src/lib/gen-migrate-advice.js +3 -4
  107. package/dist/src/lib/gen-migrate-advice.js.map +1 -1
  108. package/dist/src/lib/install-set.d.ts +10 -1
  109. package/dist/src/lib/install-set.d.ts.map +1 -1
  110. package/dist/src/lib/install-set.js +20 -2
  111. package/dist/src/lib/install-set.js.map +1 -1
  112. package/dist/src/lib/load-metaobjects-config.d.ts +13 -11
  113. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  114. package/dist/src/lib/load-metaobjects-config.js +53 -75
  115. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  116. package/dist/src/lib/manifest-ignored-check.d.ts +4 -1
  117. package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
  118. package/dist/src/lib/manifest-ignored-check.js +9 -4
  119. package/dist/src/lib/manifest-ignored-check.js.map +1 -1
  120. package/dist/src/lib/output.d.ts +15 -0
  121. package/dist/src/lib/output.d.ts.map +1 -1
  122. package/dist/src/lib/output.js +9 -1
  123. package/dist/src/lib/output.js.map +1 -1
  124. package/dist/src/lib/payload-field-tree.d.ts +4 -5
  125. package/dist/src/lib/payload-field-tree.d.ts.map +1 -1
  126. package/dist/src/lib/payload-field-tree.js +16 -31
  127. package/dist/src/lib/payload-field-tree.js.map +1 -1
  128. package/dist/src/lib/requirement-check.d.ts +7 -0
  129. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  130. package/dist/src/lib/requirement-check.js +14 -7
  131. package/dist/src/lib/requirement-check.js.map +1 -1
  132. package/dist/src/lib/strict-args.d.ts +28 -0
  133. package/dist/src/lib/strict-args.d.ts.map +1 -0
  134. package/dist/src/lib/strict-args.js +49 -0
  135. package/dist/src/lib/strict-args.js.map +1 -0
  136. package/dist/src/lib/template-eject.d.ts +108 -0
  137. package/dist/src/lib/template-eject.d.ts.map +1 -0
  138. package/dist/src/lib/template-eject.js +438 -0
  139. package/dist/src/lib/template-eject.js.map +1 -0
  140. package/dist/src/lib/version.d.ts +9 -0
  141. package/dist/src/lib/version.d.ts.map +1 -1
  142. package/dist/src/lib/version.js +15 -0
  143. package/dist/src/lib/version.js.map +1 -1
  144. package/package.json +10 -10
  145. package/src/commands/docs.ts +11 -952
  146. package/src/commands/eject.ts +318 -73
  147. package/src/commands/gen.ts +94 -22
  148. package/src/commands/generator.ts +2 -2
  149. package/src/commands/init.ts +80 -38
  150. package/src/commands/migrate.ts +248 -117
  151. package/src/commands/types.ts +18 -2
  152. package/src/commands/upgrade.ts +5 -1
  153. package/src/commands/verify.ts +229 -136
  154. package/src/index.ts +59 -72
  155. package/src/lib/agent-context-staleness.ts +20 -5
  156. package/src/lib/agent-schema.ts +74 -0
  157. package/src/lib/allow.ts +57 -6
  158. package/src/lib/anti-patterns.ts +4 -2
  159. package/src/lib/args.ts +83 -28
  160. package/src/lib/catalog-listing.ts +105 -25
  161. package/src/lib/catalog.ts +51 -6
  162. package/src/lib/codegen-drift.ts +138 -3
  163. package/src/lib/collection-load-options.ts +2 -1
  164. package/src/lib/config.ts +5 -3
  165. package/src/lib/decimal-precision-advisory.ts +50 -0
  166. package/src/lib/deprecation-lint.ts +16 -24
  167. package/src/lib/docs-removed.ts +41 -0
  168. package/src/lib/docs-site-eject.ts +48 -0
  169. package/src/lib/field-lint.ts +122 -0
  170. package/src/lib/fk-index-advisory.ts +4 -4
  171. package/src/lib/format.ts +12 -0
  172. package/src/lib/gen-migrate-advice.ts +6 -4
  173. package/src/lib/install-set.ts +20 -1
  174. package/src/lib/load-metaobjects-config.ts +57 -78
  175. package/src/lib/manifest-ignored-check.ts +14 -10
  176. package/src/lib/output.ts +18 -1
  177. package/src/lib/payload-field-tree.ts +18 -35
  178. package/src/lib/requirement-check.ts +21 -6
  179. package/src/lib/strict-args.ts +52 -0
  180. package/src/lib/template-eject.ts +526 -0
  181. package/src/lib/version.ts +18 -0
  182. package/dist/src/lib/docs-drift.d.ts +0 -42
  183. package/dist/src/lib/docs-drift.d.ts.map +0 -1
  184. package/dist/src/lib/docs-drift.js +0 -264
  185. package/dist/src/lib/docs-drift.js.map +0 -1
  186. package/src/lib/docs-drift.ts +0 -312
@@ -42,11 +42,19 @@ import { join, relative, resolve, isAbsolute } from "node:path";
42
42
  import {
43
43
  runGen,
44
44
  contentHash,
45
+ DOCS_TARGET_NAME,
45
46
  readGeneratedHash,
46
47
  listGeneratedPaths,
48
+ orphanVerdict,
47
49
  NAMES_FILE_SUFFIX,
50
+ generatorRegistry,
51
+ normalizeConfig,
52
+ stableGeneratorName,
48
53
  } from "@metaobjectsdev/codegen-ts";
49
- import type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
54
+ import type { MetaobjectsGenConfig, RunGenOpts } from "@metaobjectsdev/codegen-ts";
55
+ import { DEFAULT_TARGET_NAME } from "@metaobjectsdev/codegen-ts/engine";
56
+ import { gitIgnored } from "./git-ignore.js";
57
+ import { toPosix } from "./rel-posix.js";
50
58
  import type { MetaData } from "@metaobjectsdev/metadata";
51
59
 
52
60
  /** Per-target config as carried on MetaobjectsGenConfig (TargetConfig isn't
@@ -68,6 +76,12 @@ export interface CodegenDriftResult {
68
76
  * to end. Two doors asking one question must not answer it twice.
69
77
  */
70
78
  handEditedNames: string[];
79
+ /**
80
+ * The subset of `driftedFiles` that no generator emits any more AND that was edited
81
+ * since `meta gen` wrote it, sorted. `meta gen` deletes an untouched orphan but refuses
82
+ * one of these, so prescribing it would loop: the remedy is to delete the file.
83
+ */
84
+ editedOrphans: string[];
71
85
  /**
72
86
  * Project-relative paths of generated files that carry a PRESERVED hand edit, sorted:
73
87
  * the generated contribution is current (a fresh regen hashes to what we recorded
@@ -78,10 +92,43 @@ export interface CodegenDriftResult {
78
92
  * Never overlaps `driftedFiles`.
79
93
  */
80
94
  handEdited: string[];
95
+ /**
96
+ * Generated documentation pages the repository git-ignores, and so did not check. Only
97
+ * the directories documentation generators write to are asked: a project that generates
98
+ * many pages and commits a few on purpose is checked on the few it chose. Reported on
99
+ * the pass and the fail line so a gate that checked two of 589 pages cannot read as one
100
+ * that checked them all.
101
+ */
102
+ docsIgnored: number;
103
+ /**
104
+ * Documentation directories, project-relative, whose every freshly generated page the
105
+ * repository git-ignores. Not compared at all: a project that generates its documentation
106
+ * without committing it passes, as it did when 1.0's `verify --codegen` never looked at
107
+ * documentation.
108
+ */
109
+ docsIgnoredDirs: string[];
110
+ /** Why git could not be asked which pages are ignored (every page was then checked). */
111
+ docsIgnoreUnavailable?: string;
81
112
  /** Set when the gate could not run (e.g. no outDir to compare against). */
82
113
  error?: string;
83
114
  }
84
115
 
116
+ /**
117
+ * The output directories a documentation generator writes to, absolute. A generator is
118
+ * documentation when its registry row's layer is `docs`; an owned copy keeps its packaged
119
+ * name, so it is recognised too.
120
+ */
121
+ function docsOutDirs(config: MetaobjectsGenConfig, projectRoot: string): Set<string> {
122
+ const dirs = new Set<string>();
123
+ for (const g of normalizeConfig(config).generators) {
124
+ if (generatorRegistry[stableGeneratorName(g, generatorRegistry)]?.layer !== "docs") continue;
125
+ const target = g.target ?? DOCS_TARGET_NAME;
126
+ const outDir = target === DEFAULT_TARGET_NAME ? config.outDir : config.targets?.[target]?.outDir;
127
+ if (outDir !== undefined && outDir.length > 0) dirs.add(resolve(projectRoot, outDir));
128
+ }
129
+ return dirs;
130
+ }
131
+
85
132
  /** Collect the committed outDirs declared by the config (default + per-target),
86
133
  * resolved against `projectRoot` — a relative outDir (the common, portable,
87
134
  * `meta init`-scaffolded shape) must resolve against the project the CLI's
@@ -127,12 +174,20 @@ function listFiles(dir: string): string[] {
127
174
  * never committed because the `meta gen` that produced the committed tree
128
175
  * never emitted it either. Undefined ⇒ everything is in scope (byte-identical
129
176
  * to a project with no `scope` declared).
177
+ * @param sourceFiles the collection's OWN source files, the same list `meta gen`
178
+ * threads onto `ctx.sourceFiles`. `shared-model` loads them standalone to build its
179
+ * artifact, so without them the regen asked it to publish nothing and this gate
180
+ * failed with "no source files to load" on every project that wires it.
181
+ * @param run what `meta gen` also hands the run and this regen must hand it the same way:
182
+ * the source directories (the docs site loads them) and the agent pages' schema builder.
130
183
  */
131
184
  export async function computeCodegenDrift(
132
185
  config: MetaobjectsGenConfig,
133
186
  metadata: MetaData,
134
187
  projectRoot: string,
135
188
  scope?: (fqn: string) => boolean,
189
+ sourceFiles?: readonly string[],
190
+ run?: Pick<RunGenOpts, "sourceRoots" | "agentSchema">,
136
191
  ): Promise<CodegenDriftResult> {
137
192
  const root = isAbsolute(projectRoot) ? projectRoot : resolve(projectRoot);
138
193
 
@@ -148,7 +203,10 @@ export async function computeCodegenDrift(
148
203
  driftedFiles: [],
149
204
  lines: [],
150
205
  handEditedNames: [],
206
+ editedOrphans: [],
151
207
  handEdited: [],
208
+ docsIgnored: 0,
209
+ docsIgnoredDirs: [],
152
210
  error:
153
211
  "verify --codegen: no outDir configured — cannot locate the committed " +
154
212
  "generated output to diff against. Set 'outDir' (and/or per-target " +
@@ -204,35 +262,75 @@ export async function computeCodegenDrift(
204
262
  // Generate fresh into the temp tree. "overwrite" + "fresh" + a temp
205
263
  // gen-state dir guarantees every file is written (no merge/skip), so the
206
264
  // temp tree is the canonical "what gen would produce right now".
207
- await runGen({
265
+ const regen = await runGen({
208
266
  config: tempConfig,
209
267
  metadata,
210
268
  projectRoot: tempRoot,
211
269
  genStateDir: join(tempRoot, ".gen-state"),
212
270
  mergeStrategy: "overwrite",
213
271
  baseline: "fresh",
272
+ // The real project, for a generator that reads its files (`ctx.inputRoot`):
273
+ // `shared-model` takes its default version from the nearest package.json, and an
274
+ // owned generator may read anything. `projectRoot` stays the temp tree, which the
275
+ // owned route and entity generators compute their adapter import against.
276
+ inputRoot: root,
214
277
  ...(scope !== undefined ? { scope } : {}),
278
+ ...(sourceFiles !== undefined ? { sourceFiles } : {}),
279
+ ...(run?.sourceRoots !== undefined ? { sourceRoots: run.sourceRoots } : {}),
280
+ ...(run?.agentSchema !== undefined ? { agentSchema: run.agentSchema } : {}),
215
281
  });
216
282
 
283
+ // Where gen discards hand edits instead of keeping them (handEdits: "overwrite").
284
+ const overwrite = regen.handEditsOverwrite;
285
+
217
286
  // Diff each committed outDir against its temp mirror.
218
287
  const driftedFiles = new Set<string>();
219
288
  const lines: string[] = [];
220
289
  const handEditedNames = new Set<string>();
221
290
  const handEdited = new Set<string>();
291
+ const editedOrphans = new Set<string>();
222
292
 
223
293
  // Whether we have any record of what we wrote here. With records, the orphan
224
294
  // branch below can scope itself to our own output; with none, it cannot tell
225
295
  // our stale output from a stranger's file, so the old verdict stands.
226
296
  const haveWriteRecords = listGeneratedPaths(projectGenStateDir).length > 0;
297
+ const docsDirs = docsOutDirs(config, root);
298
+ let docsIgnored = 0;
299
+ const docsIgnoredDirs: string[] = [];
300
+ let docsIgnoreUnavailable: string | undefined;
227
301
  for (const committed of committedDirs) {
228
302
  const fresh = tempFor(committed);
229
303
  const committedFiles = new Set(listFiles(committed));
230
304
  const freshFiles = new Set(listFiles(fresh));
231
305
  const all = new Set([...committedFiles, ...freshFiles]);
306
+
307
+ // Documentation the project git-ignores is not checked (carried from 1.0's
308
+ // `verify --docs`). Asked of the FRESH set by name, present on disk or not, so the
309
+ // verdict is a property of the repository and not of the machine: a box that ran
310
+ // `meta gen` has every page on disk and a CI runner has the committed few. A TRACKED
311
+ // page is never reported ignored by git, so a committed page is always compared.
312
+ // honourGlobalExcludes: false — a personal ignore file must not change a gate.
313
+ let ignored: ReadonlySet<string> = new Set<string>();
314
+ if (docsDirs.has(committed) && all.size > 0) {
315
+ const prefix = toPosix(relative(root, committed));
316
+ const fromRoot = (rel: string): string => (prefix === "" ? toPosix(rel) : `${prefix}/${toPosix(rel)}`);
317
+ const lookup = gitIgnored(root, [...all].map(fromRoot), { honourGlobalExcludes: false });
318
+ if ("ignored" in lookup) ignored = new Set([...all].map(toPosix).filter((rel) => lookup.ignored.has(fromRoot(rel))));
319
+ else docsIgnoreUnavailable = lookup.unavailable;
320
+ const checked = [...freshFiles].filter((rel) => !ignored.has(toPosix(rel)));
321
+ if (freshFiles.size > 0 && checked.length === 0) {
322
+ docsIgnoredDirs.push(relative(root, committed) || ".");
323
+ continue;
324
+ }
325
+ }
232
326
  for (const rel of [...all].sort()) {
233
327
  const relKey = relative(root, join(committed, rel));
234
328
  const inCommitted = committedFiles.has(rel);
235
329
  const inFresh = freshFiles.has(rel);
330
+ if (ignored.has(toPosix(rel))) {
331
+ docsIgnored++;
332
+ continue;
333
+ }
236
334
  if (inCommitted && !inFresh) {
237
335
  // JURISDICTION, not staleness. A regen not emitting a path means
238
336
  // "stale generated output" only for a path we have ever WRITTEN; for
@@ -247,8 +345,33 @@ export async function computeCodegenDrift(
247
345
  if (haveWriteRecords && readGeneratedHash(projectGenStateDir, relKey) === undefined) {
248
346
  continue;
249
347
  }
348
+ const verdict = haveWriteRecords
349
+ ? orphanVerdict(regen.orphanSweepJobs ?? [], tempRoot, relative(tempRoot, join(fresh, rel)))
350
+ : undefined;
351
+ if (verdict?.kept === true) continue;
250
352
  driftedFiles.add(relKey);
251
- lines.push(`- ${relKey} (committed but regen would not emit it)`);
353
+ // `meta gen` deletes an orphan it can prove untouched and REFUSES one edited
354
+ // since it was written, so the remedy differs by which this is. Asked of the
355
+ // same evidence the sweep asks (the recorded hash), so the two cannot disagree.
356
+ // With no write records at all, neither verdict can be proven, so the line
357
+ // stays the plain one.
358
+ if (!haveWriteRecords) {
359
+ lines.push(`- ${relKey} (committed but regen would not emit it)`);
360
+ } else if (readGeneratedHash(projectGenStateDir, relKey) === contentHash(readFileSync(join(committed, rel), "utf8"))) {
361
+ lines.push(`- ${relKey} (committed but regen would not emit it; 'meta gen' deletes it)`);
362
+ } else if (verdict?.force === true) {
363
+ // The sweep force-deletes an edited orphan here, so gen deletes it.
364
+ lines.push(
365
+ `- ${relKey} (no generator emits it any more, and it was edited by hand; ` +
366
+ `a force or handEdits: "overwrite" orphan policy owns it, so 'meta gen' deletes it)`,
367
+ );
368
+ } else {
369
+ editedOrphans.add(relKey);
370
+ lines.push(
371
+ `- ${relKey} (no generator emits it any more, and it was edited by hand: ` +
372
+ `delete it, or move your edits into a file of your own and then delete it)`,
373
+ );
374
+ }
252
375
  } else if (!inCommitted && inFresh) {
253
376
  driftedFiles.add(relKey);
254
377
  lines.push(`+ ${relKey} (regen would emit it; not committed — run 'meta gen')`);
@@ -298,6 +421,14 @@ export async function computeCodegenDrift(
298
421
  // nothing is proven, so the old byte verdict stands.
299
422
  driftedFiles.add(relKey);
300
423
  lines.push(`~ ${relKey} (committed content differs from a fresh regen)`);
424
+ } else if (overwrite?.paths.has(join(fresh, rel)) === true) {
425
+ // Its generator says handEdits: "overwrite": `meta gen` replaces the edit
426
+ // with fresh output, so it is drift that gen fixes, not a kept edit.
427
+ driftedFiles.add(relKey);
428
+ lines.push(
429
+ `~ ${relKey} (edited by hand; its generator sets handEdits: "overwrite", so ` +
430
+ `'meta gen' replaces it with fresh output)`,
431
+ );
301
432
  } else {
302
433
  // The generated contribution is current and the bytes still differ: a
303
434
  // hand edit `meta gen` preserved. Not drift, but never silent either.
@@ -314,7 +445,11 @@ export async function computeCodegenDrift(
314
445
  driftedFiles: sorted,
315
446
  lines,
316
447
  handEditedNames: [...handEditedNames].sort(),
448
+ editedOrphans: [...editedOrphans].sort(),
317
449
  handEdited: [...handEdited].sort(),
450
+ docsIgnored,
451
+ docsIgnoredDirs,
452
+ ...(docsIgnoreUnavailable !== undefined ? { docsIgnoreUnavailable } : {}),
318
453
  };
319
454
  } finally {
320
455
  rmSync(tempRoot, { recursive: true, force: true });
@@ -26,7 +26,7 @@ import type { Collection, LoadMemoryOptions } from "@metaobjectsdev/sdk";
26
26
  */
27
27
  export function collectionLoadOptions(
28
28
  collection: Collection,
29
- ): Required<Pick<LoadMemoryOptions, "files" | "fileIds" | "importedPackages" | "importedNodes" | "libraries">> {
29
+ ): Required<Pick<LoadMemoryOptions, "files" | "fileIds" | "importedPackages" | "importedNodes" | "libraries" | "physicalNaming">> {
30
30
  return {
31
31
  files: collection.files,
32
32
  fileIds: collection.fileIds,
@@ -38,5 +38,6 @@ export function collectionLoadOptions(
38
38
  // threaded by `loadMemoryOptionsFrom`, a SECOND helper, and #333 is on record as the
39
39
  // bug where one of the two reached every command and the other reached none.
40
40
  libraries: collection.libraries,
41
+ physicalNaming: collection.physicalNaming,
41
42
  };
42
43
  }
package/src/lib/config.ts CHANGED
@@ -83,16 +83,18 @@ async function tryLoadConfig(metaRoot: string): Promise<Config | undefined> {
83
83
  // ---------------------------------------------------------------------------
84
84
 
85
85
  /**
86
- * The migrations directory and dialect `meta migrate` would use with no flags — what
87
- * `meta gen` needs to word its "now migrate" next step and find the committed snapshot.
86
+ * The migrations directory, dialect and format `meta migrate` would use with no flags — what
87
+ * `meta gen` needs to word its "now migrate" next step and read the committed snapshot the
88
+ * way `meta migrate` reads it.
88
89
  */
89
90
  export async function resolveMigrateDefaults(
90
91
  metaRoot: string,
91
- ): Promise<{ outDir: string; dialect: Dialect | undefined }> {
92
+ ): Promise<{ outDir: string; dialect: Dialect | undefined; format: MigrateFormat }> {
92
93
  const config = await tryLoadConfig(metaRoot);
93
94
  return {
94
95
  outDir: config?.migrate?.outDir ?? MIGRATE_DEFAULTS.outDir,
95
96
  dialect: config?.migrate?.dialect ?? MIGRATE_DEFAULTS.dialect,
97
+ format: config?.migrate?.format ?? MIGRATE_DEFAULTS.format,
96
98
  };
97
99
  }
98
100
 
@@ -0,0 +1,50 @@
1
+ // ADR-0019 Amendment 1, W4 — the SQLite and D1 tier has no decimal type. A text value with
2
+ // NUMERIC affinity is stored as an INTEGER or REAL, keeping only its first 15 significant
3
+ // digits, so a `field.decimal` declaring `@precision` above 15 is stored lossily.
4
+ // Advisory, never a gate: `meta migrate` warns once per field, on those dialects only.
5
+
6
+ import {
7
+ FIELD_ATTR_PRECISION,
8
+ FIELD_SUBTYPE_DECIMAL,
9
+ OBJECT_SUBTYPE_ENTITY,
10
+ TYPE_FIELD,
11
+ TYPE_OBJECT,
12
+ type MetaData,
13
+ } from "@metaobjectsdev/metadata";
14
+
15
+ /** Significant digits a SQLite or D1 NUMERIC column keeps exactly. */
16
+ export const SQLITE_DECIMAL_EXACT_DIGITS = 15;
17
+
18
+ const LOSSY_DECIMAL_DIALECTS: readonly string[] = ["sqlite", "d1"];
19
+
20
+ export interface DecimalPrecisionOptions {
21
+ /** Skip an entity by FQN — imported (dependency) metadata is not the adopter's to fix. */
22
+ skip?: (fqn: string) => boolean;
23
+ }
24
+
25
+ /** One message per `field.decimal` with `@precision` above 15, or [] on any other dialect. */
26
+ export function decimalPrecisionWarnings(
27
+ root: MetaData,
28
+ dialect: string,
29
+ opts: DecimalPrecisionOptions = {},
30
+ ): string[] {
31
+ if (!LOSSY_DECIMAL_DIALECTS.includes(dialect)) return [];
32
+ const out: string[] = [];
33
+ for (const obj of root.children()) {
34
+ if (obj.type !== TYPE_OBJECT || obj.subType !== OBJECT_SUBTYPE_ENTITY || obj.isAbstract) continue;
35
+ const fqn = obj.resolutionKey();
36
+ if (opts.skip?.(fqn) === true) continue;
37
+ // ADR-0039: resolving — fields (and their @precision) inherited via extends count.
38
+ for (const f of obj.children()) {
39
+ if (f.type !== TYPE_FIELD || f.subType !== FIELD_SUBTYPE_DECIMAL) continue;
40
+ const precision = f.attr(FIELD_ATTR_PRECISION);
41
+ if (typeof precision !== "number" || precision <= SQLITE_DECIMAL_EXACT_DIGITS) continue;
42
+ out.push(
43
+ `${fqn}.${f.name} is a decimal with @precision ${precision}, but ${dialect} keeps only ` +
44
+ `${SQLITE_DECIMAL_EXACT_DIGITS} significant digits of a decimal (it stores NUMERIC as an integer or ` +
45
+ `a double). Digits beyond the ${SQLITE_DECIMAL_EXACT_DIGITS}th are lost; use Postgres for exact wide decimals.`,
46
+ );
47
+ }
48
+ }
49
+ return out;
50
+ }
@@ -50,6 +50,7 @@ import {
50
50
  ORIGIN_AGGREGATE_ATTR_OF,
51
51
  isMetaObject,
52
52
  resolveObjectRef,
53
+ refTarget,
53
54
  type MetaData,
54
55
  type MetaObject,
55
56
  } from "@metaobjectsdev/metadata";
@@ -166,12 +167,11 @@ function findHop(entity: MetaObject, hopName: string): MetaData | undefined {
166
167
  );
167
168
  }
168
169
 
169
- /** The entity a resolved hop (relationship or identity.reference) points at, raw. */
170
- function hopTargetRef(hop: MetaData): string | undefined {
171
- const v = hop.type === TYPE_RELATIONSHIP
172
- ? hop.attr(RELATIONSHIP_ATTR_OBJECT_REF)
173
- : hop.attr(IDENTITY_REFERENCE_ATTR_REFERENCES);
174
- return typeof v === "string" && v !== "" ? headOf(v) : undefined;
170
+ /** The entity a resolved hop (relationship or identity.reference) points at, as the
171
+ * loader bound it (ADR-0059 R7: resolved where the hop's reference was declared). */
172
+ function hopTarget(hop: MetaData): MetaObject | undefined {
173
+ const target = refTarget(hop, hop.type === TYPE_RELATIONSHIP ? RELATIONSHIP_ATTR_OBJECT_REF : IDENTITY_REFERENCE_ATTR_REFERENCES);
174
+ return target !== undefined && isMetaObject(target) ? target : undefined;
175
175
  }
176
176
 
177
177
  /**
@@ -187,18 +187,15 @@ function hopTargetRef(hop: MetaData): string | undefined {
187
187
  * directory takes.
188
188
  */
189
189
  function checkEntityFieldPath(
190
- root: MetaData,
191
190
  referrer: MetaData,
192
191
  referrerPath: string,
193
192
  verb: string,
194
193
  raw: string,
195
- referrerPkg: string,
194
+ entity: MetaObject | undefined,
196
195
  out: Diagnostic[],
197
196
  ): void {
198
- const head = headOf(raw);
199
- const entity = resolveEntityRef(root, head, referrerPkg);
200
197
  if (entity === undefined) return;
201
- const fieldName = raw.slice(head.length + 1);
198
+ const fieldName = raw.slice(headOf(raw).length + 1);
202
199
  const field = fieldName !== "" ? entity.fields().find((f) => f.name === fieldName) : undefined;
203
200
  if (field !== undefined && considerTarget(referrer, referrerPath, verb, field, out)) {
204
201
  return; // the field itself is the deprecated thing — don't ALSO warn about its (fine) entity.
@@ -232,9 +229,7 @@ function checkViaPath(
232
229
  const hop = findHop(current, hopName);
233
230
  if (hop === undefined) return;
234
231
  considerTarget(origin, referrerPath, verb, hop, out);
235
- const targetRef = hopTargetRef(hop);
236
- if (targetRef === undefined) return;
237
- const next = resolveEntityRef(root, targetRef, packageOf(current));
232
+ const next = hopTarget(hop);
238
233
  if (next === undefined) return;
239
234
  considerTarget(origin, referrerPath, verb, next, out);
240
235
  current = next;
@@ -252,11 +247,11 @@ function checkOrigin(root: MetaData, origin: MetaData, referrerPath: string, out
252
247
  // same stance every origin accessor in meta-origin.ts takes).
253
248
  const from = origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_FROM);
254
249
  if (typeof from === "string" && from !== "") {
255
- checkEntityFieldPath(root, origin, referrerPath, `${subtypeLabel} @from references`, from, referrerPkg, out);
250
+ checkEntityFieldPath(origin, referrerPath, `${subtypeLabel} @from references`, from, resolveEntityRef(root, headOf(from), referrerPkg), out);
256
251
  }
257
252
  const of_ = origin.ownAttr(ORIGIN_AGGREGATE_ATTR_OF);
258
253
  if (typeof of_ === "string" && of_ !== "") {
259
- checkEntityFieldPath(root, origin, referrerPath, `${subtypeLabel} @of references`, of_, referrerPkg, out);
254
+ checkEntityFieldPath(origin, referrerPath, `${subtypeLabel} @of references`, of_, resolveEntityRef(root, headOf(of_), referrerPkg), out);
260
255
  }
261
256
  const via = origin.ownAttr(ORIGIN_PASSTHROUGH_ATTR_VIA);
262
257
  if (typeof via === "string" && via !== "") {
@@ -273,12 +268,9 @@ function checkNode(root: MetaData, node: MetaData, path: string, out: Diagnostic
273
268
  // @objectRef — relationship.* (its target object) and field.object/value (a
274
269
  // value-typed field's own objectRef). Same attr name on both (naming-refs.ts).
275
270
  if (node.type === TYPE_RELATIONSHIP || node.type === TYPE_FIELD) {
276
- const ref = node.attr(node.type === TYPE_RELATIONSHIP ? RELATIONSHIP_ATTR_OBJECT_REF : FIELD_ATTR_OBJECT_REF);
277
- if (typeof ref === "string" && ref !== "") {
278
- const owner = owningObject(node);
279
- const target = resolveEntityRef(root, ref, owner !== undefined ? packageOf(owner) : "");
280
- if (target !== undefined) considerTarget(node, path, "references (@objectRef)", target, out);
281
- }
271
+ // ADR-0059 R7: the target the loader bound, resolved where @objectRef was declared.
272
+ const target = refTarget(node, node.type === TYPE_RELATIONSHIP ? RELATIONSHIP_ATTR_OBJECT_REF : FIELD_ATTR_OBJECT_REF);
273
+ if (target !== undefined && isMetaObject(target)) considerTarget(node, path, "references (@objectRef)", target, out);
282
274
  }
283
275
 
284
276
  // @references — identity.reference's FK target. May be bare ("Entity",
@@ -290,8 +282,8 @@ function checkNode(root: MetaData, node: MetaData, path: string, out: Diagnostic
290
282
  if (node.type === TYPE_IDENTITY && node.subType === IDENTITY_SUBTYPE_REFERENCE) {
291
283
  const raw = node.attr(IDENTITY_REFERENCE_ATTR_REFERENCES);
292
284
  if (typeof raw === "string" && raw !== "") {
293
- const owner = owningObject(node);
294
- checkEntityFieldPath(root, node, path, "references (@references)", raw, owner !== undefined ? packageOf(owner) : "", out);
285
+ const target = refTarget(node, IDENTITY_REFERENCE_ATTR_REFERENCES);
286
+ checkEntityFieldPath(node, path, "references (@references)", raw, target !== undefined && isMetaObject(target) ? target : undefined, out);
295
287
  }
296
288
  }
297
289
 
@@ -0,0 +1,41 @@
1
+ // ADR-0034 Amendment 9 — one way to generate. Documentation is written by generators the
2
+ // project lists in `generators`, and `meta gen` runs them. The docs command, `meta verify
3
+ // --docs` and the `docs` config block are gone, and each says so in the amendment's fixed
4
+ // words, pointing at the migration section an agent can apply by hand.
5
+ import { guideUrl } from "@metaobjectsdev/metadata/release-docs";
6
+
7
+ /** The migration guide every message below points at, and the section for this port. */
8
+ export const DOCS_MIGRATION_GUIDE = guideUrl("docs/features/migrations/docs-are-generators.md");
9
+ const SECTION = `section "TypeScript"`;
10
+ /** Where a TypeScript project declares a docs generator. */
11
+ const WHERE = "in `generators` in metaobjects.config.ts";
12
+
13
+ /** What `meta docs` prints before it exits 2. */
14
+ export const REMOVED_DOCS_COMMAND =
15
+ "`meta docs` was removed in 1.1.0. `meta gen` writes documentation now: declare each docs " +
16
+ `generator ${WHERE}. See ${DOCS_MIGRATION_GUIDE}, ${SECTION}.`;
17
+
18
+ /** What `meta verify --docs` prints before it exits 2. */
19
+ export const REMOVED_VERIFY_DOCS =
20
+ "`meta verify --docs` was removed in 1.1.0. `meta verify --codegen` checks documentation now, " +
21
+ `with everything else \`generators\` declares. See ${DOCS_MIGRATION_GUIDE}, ${SECTION}.`;
22
+
23
+ /** What a gen run prints, and fails on, when there is no config to read the list from. */
24
+ export const NO_CONFIG_MESSAGE =
25
+ "meta gen: no metaobjects.config.ts. Nothing is generated until a config declares it, " +
26
+ "documentation included; `meta init` writes one. A 1.0 project that ran `meta docs` with no " +
27
+ `config: see ${DOCS_MIGRATION_GUIDE}, ${SECTION}.`;
28
+
29
+ /** True when a loaded config still carries the 1.0 `docs` block. */
30
+ export function hasLegacyDocsBlock(config: object): boolean {
31
+ return Object.hasOwn(config, "docs");
32
+ }
33
+
34
+ /** What `command` prints, and fails on, before it writes anything, for a 1.0 `docs` block. */
35
+ export function legacyDocsConfigMessage(command: string): string {
36
+ return (
37
+ `${command}: found 1.0 docs configuration (the docs block in metaobjects.config.ts). ` +
38
+ "Nothing reads it any more, so this run would write no documentation. Declare each docs " +
39
+ `generator ${WHERE}. See ${DOCS_MIGRATION_GUIDE}, ${SECTION}.`
40
+ );
41
+ }
@@ -0,0 +1,48 @@
1
+ // `meta eject docs-site` (ADR-0034 Amendment 9): copy the HTML site's templates and assets
2
+ // into `codegen/docs-site/{templates,assets}`, where `docsSiteFile()` reads them in place of
3
+ // the bundled ones. The site's BUILDER stays packaged (it assembles the page data the
4
+ // templates read; Amendment 8 ruling 3); what an adopter changes is the look, and that is
5
+ // these files. It was `meta docs --scaffold-site` until 1.1.0, and writes the same files to
6
+ // the same place.
7
+
8
+ import { existsSync } from "node:fs";
9
+ import { mkdir, writeFile } from "node:fs/promises";
10
+ import { join } from "node:path";
11
+ import { OWNED_DOCS_SITE_DIR } from "@metaobjectsdev/codegen-ts";
12
+ import { readSiteFile, SITE_ASSET_NAMES, SITE_TEMPLATE_NAMES } from "@metaobjectsdev/docs-site";
13
+
14
+ /** The name `meta eject` takes for the site theme. A catalog entry, but not a generator copy. */
15
+ export const DOCS_SITE_NAME = "docs-site";
16
+
17
+ export interface DocsSiteFileResult {
18
+ /** Project-relative path. */
19
+ path: string;
20
+ status: "created" | "preserved" | "replaced";
21
+ }
22
+
23
+ /**
24
+ * Write each template and asset that is missing. A file already there is kept unless
25
+ * `force`: a re-run never clobbers a theme the adopter has edited.
26
+ */
27
+ export async function ejectDocsSite(projectRoot: string, force: boolean): Promise<DocsSiteFileResult[]> {
28
+ const out: DocsSiteFileResult[] = [];
29
+ const sets: Array<{ dir: string; names: readonly string[]; kind: "template" | "asset" }> = [
30
+ { dir: `${OWNED_DOCS_SITE_DIR}/templates`, names: SITE_TEMPLATE_NAMES, kind: "template" },
31
+ { dir: `${OWNED_DOCS_SITE_DIR}/assets`, names: SITE_ASSET_NAMES, kind: "asset" },
32
+ ];
33
+ for (const { dir, names, kind } of sets) {
34
+ await mkdir(join(projectRoot, dir), { recursive: true });
35
+ for (const name of names) {
36
+ const path = `${dir}/${name}`;
37
+ const abs = join(projectRoot, path);
38
+ const existed = existsSync(abs);
39
+ if (existed && !force) {
40
+ out.push({ path, status: "preserved" });
41
+ continue;
42
+ }
43
+ await writeFile(abs, readSiteFile(kind, name), "utf8");
44
+ out.push({ path, status: existed ? "replaced" : "created" });
45
+ }
46
+ }
47
+ return out;
48
+ }
@@ -0,0 +1,122 @@
1
+ // `meta verify` — the field AUTHORING lint.
2
+ //
3
+ // Two metadata mistakes about an object's FIELDS load with no error on every port:
4
+ //
5
+ // 1. An `identity.reference` whose `@fields` names a field the object does not have.
6
+ // The loader resolves `@references` (the target) and never looks at `@fields`, so a
7
+ // typo there produces a foreign key over a column nothing declares.
8
+ // 2. Two `field.*` children with the same `name` in one object's `children` list. The
9
+ // later declaration is folded into the first, and one of a different subtype is
10
+ // dropped, both silently.
11
+ //
12
+ // WHY THESE ARE WARNINGS AND NOT LOAD ERRORS. docs/compatibility-policy.md does not allow
13
+ // a new load error for metadata that loads today — name-lint.ts's header has the same
14
+ // ruling for whitespace names. So, like every sibling lint in this directory:
15
+ //
16
+ // EVERY FINDING IS A WARNING, BY CONSTRUCTION. Nothing here reaches the exit code.
17
+ //
18
+ // THE TWO HALVES READ DIFFERENT THINGS, and have to. The reference half reads the LOADED
19
+ // model, because "does this object have that field" is a question about the EFFECTIVE
20
+ // field set — inherited through `extends` and merged from overlay files. The duplicate
21
+ // half reads the RAW DOCUMENTS, because the merge has already erased the duplicate from
22
+ // the model by the time anything can ask (overlay-lint.ts reads files for the same reason).
23
+ //
24
+ // The codes, the message text and the fixtures are shared with the C#, Java and Python
25
+ // CLIs: fixtures/field-lint-conformance/.
26
+
27
+ import {
28
+ IDENTITY_ATTR_FIELDS,
29
+ IDENTITY_SUBTYPE_REFERENCE,
30
+ TYPE_FIELD,
31
+ TYPE_IDENTITY,
32
+ TYPE_OBJECT,
33
+ type MetaData,
34
+ type MetaDataSource,
35
+ } from "@metaobjectsdev/metadata";
36
+ import { declaredDuplicateFields, type DeclaredDuplicateField } from "@metaobjectsdev/metadata/core";
37
+ import type { Diagnostic } from "./requirement-check.js";
38
+
39
+ /** An `identity.reference` lists a field its object does not have. */
40
+ export const WARN_REFERENCE_FIELD_NOT_FOUND = "WARN_REFERENCE_FIELD_NOT_FOUND";
41
+ /** One `children` list declares the same field name more than once. */
42
+ export const WARN_DUPLICATE_FIELD_NAME = "WARN_DUPLICATE_FIELD_NAME";
43
+
44
+ /** Resolve one metadata file path to the source the duplicate scan reads it through. */
45
+ export type ReadSource = (path: string) => MetaDataSource;
46
+
47
+ function warn(path: string, code: string, message: string): Diagnostic {
48
+ return { severity: "warn", code, path, message };
49
+ }
50
+
51
+ /**
52
+ * Report every `identity.reference` whose `@fields` names a field its object lacks.
53
+ * Returns `[]` for a model where every listed field exists.
54
+ */
55
+ export function lintReferenceFields(root: MetaData): Diagnostic[] {
56
+ const out: Diagnostic[] = [];
57
+ // OWN-ONLY (ADR-0039 sanctioned case): a root has no super, and its own children are
58
+ // the declared objects.
59
+ for (const object of root.ownChildren()) {
60
+ if (object.type !== TYPE_OBJECT) continue;
61
+ const address = object.resolutionKey();
62
+ // RESOLVING: the effective field set — a field inherited through `extends` or added
63
+ // by an overlay file is a field the object has.
64
+ const fields = new Set(object.children().filter((c) => c.type === TYPE_FIELD).map((c) => c.name));
65
+ // OWN-ONLY (ADR-0039 sanctioned case): report each DECLARATION once, on the object
66
+ // that declares it. The resolving `children()` would repeat an inherited reference
67
+ // on every subtype.
68
+ for (const identity of object.ownChildren()) {
69
+ if (identity.type !== TYPE_IDENTITY || identity.subType !== IDENTITY_SUBTYPE_REFERENCE) continue;
70
+ // RESOLVING: `@fields` may itself be inherited through the identity's `extends`.
71
+ const listed = identity.attr(IDENTITY_ATTR_FIELDS);
72
+ const names = Array.isArray(listed) ? listed : typeof listed === "string" ? [listed] : [];
73
+ for (const name of names) {
74
+ if (typeof name !== "string" || fields.has(name)) continue;
75
+ out.push(warn(`${address}.${identity.name}`, WARN_REFERENCE_FIELD_NOT_FOUND, referenceFieldMessage(identity.name, name, address)));
76
+ }
77
+ }
78
+ }
79
+ return out;
80
+ }
81
+
82
+ function referenceFieldMessage(identity: string, field: string, object: string): string {
83
+ return (
84
+ `identity.reference ${JSON.stringify(identity)} lists ${JSON.stringify(field)} in @fields, but ` +
85
+ `${object} has no field of that name, inherited and overlaid fields included. Nothing checks ` +
86
+ `this at load, so the reference is built on a field that does not exist. Rename the entry to ` +
87
+ `an existing field, or declare the field.`
88
+ );
89
+ }
90
+
91
+ /** One raw-scan finding as a diagnostic. Exported so the scan and its text are testable
92
+ * without a filesystem. */
93
+ export function duplicateFieldDiagnostic(dup: DeclaredDuplicateField): Diagnostic {
94
+ return warn(
95
+ `${dup.object}.${dup.field}`,
96
+ WARN_DUPLICATE_FIELD_NAME,
97
+ `${dup.object} declares the field ${JSON.stringify(dup.field)} ${dup.count} times in one children ` +
98
+ `list. Nothing reports this at load, and only the first declaration is certain to take effect. ` +
99
+ `Remove or rename the duplicate.`,
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Report every field name declared more than once in one object's `children` list,
105
+ * scanning each of `files` as raw content.
106
+ *
107
+ * Unreadable or unparsable files are skipped — the loader reports those itself.
108
+ */
109
+ export async function lintDuplicateFields(files: readonly string[], readSource: ReadSource): Promise<Diagnostic[]> {
110
+ const out: Diagnostic[] = [];
111
+ for (const path of files) {
112
+ let declared: ReadonlyArray<DeclaredDuplicateField>;
113
+ try {
114
+ const source = readSource(path);
115
+ declared = declaredDuplicateFields(await source.read(), source.format);
116
+ } catch {
117
+ continue;
118
+ }
119
+ out.push(...declared.map(duplicateFieldDiagnostic));
120
+ }
121
+ return out;
122
+ }