@metaobjectsdev/cli 1.0.1 → 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 (73) 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 +34 -33
  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/allow.d.ts.map +1 -1
  31. package/dist/src/lib/allow.js +7 -1
  32. package/dist/src/lib/allow.js.map +1 -1
  33. package/dist/src/lib/args.d.ts +57 -1
  34. package/dist/src/lib/args.d.ts.map +1 -1
  35. package/dist/src/lib/args.js +39 -2
  36. package/dist/src/lib/args.js.map +1 -1
  37. package/dist/src/lib/collection-load-options.d.ts +13 -0
  38. package/dist/src/lib/collection-load-options.d.ts.map +1 -0
  39. package/dist/src/lib/collection-load-options.js +19 -0
  40. package/dist/src/lib/collection-load-options.js.map +1 -0
  41. package/dist/src/lib/dependency-sync.d.ts +170 -0
  42. package/dist/src/lib/dependency-sync.d.ts.map +1 -0
  43. package/dist/src/lib/dependency-sync.js +457 -0
  44. package/dist/src/lib/dependency-sync.js.map +1 -0
  45. package/dist/src/lib/migrate-scope.d.ts +32 -1
  46. package/dist/src/lib/migrate-scope.d.ts.map +1 -1
  47. package/dist/src/lib/migrate-scope.js +51 -3
  48. package/dist/src/lib/migrate-scope.js.map +1 -1
  49. package/dist/src/lib/overlay-lint.d.ts +37 -0
  50. package/dist/src/lib/overlay-lint.d.ts.map +1 -0
  51. package/dist/src/lib/overlay-lint.js +102 -0
  52. package/dist/src/lib/overlay-lint.js.map +1 -0
  53. package/dist/src/lib/requirement-check.d.ts +9 -1
  54. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  55. package/dist/src/lib/requirement-check.js +23 -7
  56. package/dist/src/lib/requirement-check.js.map +1 -1
  57. package/package.json +10 -10
  58. package/src/commands/deps.ts +231 -0
  59. package/src/commands/docs.ts +19 -3
  60. package/src/commands/gen.ts +61 -1
  61. package/src/commands/init.ts +13 -17
  62. package/src/commands/migrate.ts +36 -29
  63. package/src/commands/prompt-snapshot.ts +2 -1
  64. package/src/commands/verify.ts +174 -23
  65. package/src/index.ts +62 -2
  66. package/src/lib/agent-context-staleness.ts +7 -1
  67. package/src/lib/allow.ts +7 -1
  68. package/src/lib/args.ts +89 -3
  69. package/src/lib/collection-load-options.ts +36 -0
  70. package/src/lib/dependency-sync.ts +656 -0
  71. package/src/lib/migrate-scope.ts +59 -4
  72. package/src/lib/overlay-lint.ts +129 -0
  73. package/src/lib/requirement-check.ts +33 -7
@@ -10,7 +10,7 @@
10
10
  // or a refusal that drifted between them would be drift the user reads.
11
11
 
12
12
  import type { Collection } from "@metaobjectsdev/sdk";
13
- import type { SchemaProvenance } from "@metaobjectsdev/migrate-ts";
13
+ import type { ObjectScopePredicate, SchemaProvenance } from "@metaobjectsdev/migrate-ts";
14
14
 
15
15
  /**
16
16
  * Say what a declared `migrate.scope` left out, for `migrate` and `verify --db`
@@ -32,6 +32,56 @@ export function outOfScopeNote(command: string, names: readonly string[]): strin
32
32
  );
33
33
  }
34
34
 
35
+ /**
36
+ * Say what a DEPENDENCY owns, for `migrate` and `verify --db` alike (FR-023).
37
+ *
38
+ * A dependency's nodes are loaded so this project's own model can resolve against
39
+ * them, and are governed by nobody here. That is not the same sentence as
40
+ * `outOfScopeNote`'s: an excluded import is usually not the result of anything the
41
+ * author wrote — a project with no `migrate.scope` at all still excludes its imports —
42
+ * so reporting it as "outside migrate.scope" would name a key that does not exist.
43
+ * It also carries the opt-in, which is the whole remedy: name the package.
44
+ */
45
+ export function dependencyNote(command: string, names: readonly string[]): string {
46
+ return (
47
+ `meta ${command} — ${names.length} object(s) from dependencies not governed here ` +
48
+ `(name the package in migrate.scope to own them): ${names.join(", ")}`
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Every exclusion a run owes the reader, each object named exactly ONCE.
54
+ *
55
+ * `outOfScope` is the full suppression set and `fromDependencies` a subset of it, so
56
+ * reporting both as-is would print an imported table twice under two different
57
+ * explanations. The partition happens here, once, rather than in each command.
58
+ */
59
+ export function exclusionNotes(
60
+ command: string,
61
+ outOfScope: readonly string[],
62
+ fromDependencies: readonly string[],
63
+ ): string[] {
64
+ const notes: string[] = [];
65
+ const imported = new Set(fromDependencies);
66
+ const declared = outOfScope.filter((name) => !imported.has(name));
67
+ if (declared.length > 0) notes.push(outOfScopeNote(command, declared));
68
+ if (fromDependencies.length > 0) notes.push(dependencyNote(command, fromDependencies));
69
+ return notes;
70
+ }
71
+
72
+ /**
73
+ * The import predicate a schema run threads into migrate-ts, or NOTHING.
74
+ *
75
+ * Nothing is the load-bearing half. A project with no dependencies must reach
76
+ * `scopeExpectedSchema` exactly as it always did — passing a predicate that happens to
77
+ * answer `false` for everything is not the same thing, because supplying one at all
78
+ * leaves the untouched same-object path and changes `declaredSchemas` from absent to
79
+ * derived. One helper so all five call sites make that decision identically.
80
+ */
81
+ export function importedOption(collection: Collection): { imported?: ObjectScopePredicate } {
82
+ return collection.dependencies.length > 0 ? { imported: collection.imported } : {};
83
+ }
84
+
35
85
  /** How many loaded FQNs to name in the refusal below — enough to show the shape
36
86
  * an author's patterns have to match, short enough to stay readable. */
37
87
  const EXAMPLE_FQN_CAP = 3;
@@ -70,8 +120,13 @@ export function migrateScopeMismatch(
70
120
  */
71
121
  provenance: () => SchemaProvenance,
72
122
  ): string | undefined {
73
- const { inMigrateScope, migrateScopePatterns } = collection;
74
- if (inMigrateScope === undefined) return undefined;
123
+ // The DECLARED `migrate.scope` alone, never the composed `inMigrateScope` (FR-023).
124
+ // The composed predicate also excludes imports, so a consumer whose only
125
+ // table-declaring objects come from a dependency — and who declared no scope at all
126
+ // — would be refused here, citing patterns that do not exist. There is nothing wrong
127
+ // with that project: its imports are excluded by design and it governs no tables yet.
128
+ const { declaredMigrateScope, migrateScopePatterns } = collection;
129
+ if (declaredMigrateScope === undefined) return undefined;
75
130
 
76
131
  // The declaring FQNs of every table and view the UNSCOPED model contributes —
77
132
  // the same provenance `scopeExpectedSchema` decides scope on, so the refusal
@@ -87,7 +142,7 @@ export function migrateScopeMismatch(
87
142
  // nothing for a pattern to govern, scoped or not, and an empty schema has its
88
143
  // own (much louder) failure modes downstream.
89
144
  if (fqns.length === 0) return undefined;
90
- if (fqns.some(inMigrateScope)) return undefined;
145
+ if (fqns.some(declaredMigrateScope)) return undefined;
91
146
 
92
147
  const patterns = JSON.stringify(migrateScopePatterns ?? []);
93
148
  const examples = fqns.slice(0, EXAMPLE_FQN_CAP).join(", ");
@@ -0,0 +1,129 @@
1
+ // server/typescript/packages/cli/src/lib/overlay-lint.ts
2
+ //
3
+ // `meta verify` — the overlay AUTHORING lint (FR-023 §11.1 item 4).
4
+ //
5
+ // The parser's own merge rule (parser-core.ts, "Default: no operator →
6
+ // silently reuse existing or create new") means a top-level redeclaration of
7
+ // an existing node works TODAY whether or not it carries `overlay: true` —
8
+ // as long as the target still exists under the same (type, resolutionKey).
9
+ // That is exactly the trap: a consumer who redeclares an imported node
10
+ // without the flag gets today's silent merge for free, and gets a silent NEW
11
+ // OBJECT the day the upstream node is renamed or removed — no error, just a
12
+ // second, disconnected node under the same name. `overlay: true` turns that
13
+ // into a loud `ERR_OVERLAY_NO_TARGET` instead.
14
+ //
15
+ // This lint does not change that parser behavior (nor could it — it reads
16
+ // raw file content, never the parser's merge decisions). It tells the AUTHOR:
17
+ // every top-level (type, resolutionKey) declared in two or more of the
18
+ // collection's files where more than one declaration lacks the flag. Exactly
19
+ // one unflagged declaration is the base and is fine; every additional
20
+ // unflagged one is a finding.
21
+ //
22
+ // Advisory only, like its sibling in requirement-lint.ts: never fails
23
+ // `meta verify`, never changes load behavior. Deliberately NO severity
24
+ // constant to flip — see requirement-lint.ts's header for why that promise
25
+ // would be false here too.
26
+
27
+ import { declaredTopLevelKeys, type DeclaredTopLevelKey, type MetaDataSource } from "@metaobjectsdev/metadata";
28
+ import type { Collection } from "@metaobjectsdev/sdk";
29
+ import { relPosix } from "./rel-posix.js";
30
+ import type { Diagnostic } from "./requirement-check.js";
31
+
32
+ export const WARN_OVERLAY_IMPLICIT = "WARN_OVERLAY_IMPLICIT";
33
+
34
+ /**
35
+ * Resolve one collection file path to the `MetaDataSource` `lintOverlays`
36
+ * reads it through. Injected so the lint never touches the filesystem
37
+ * directly: the production wiring in `verify.ts` backs this with
38
+ * `FileSource` (from `@metaobjectsdev/metadata/core`); a test can supply an
39
+ * in-memory source instead.
40
+ */
41
+ export type ReadSource = (path: string) => MetaDataSource;
42
+
43
+ function warn(code: string, message: string): Diagnostic {
44
+ return { severity: "warn", code, message };
45
+ }
46
+
47
+ /** One file's declaration of a given (type, resolutionKey). */
48
+ interface Declaration {
49
+ readonly file: string;
50
+ readonly overlay: boolean;
51
+ }
52
+
53
+ /**
54
+ * Lint the collection's files for unflagged cross-file redeclarations.
55
+ *
56
+ * Reads every file in `collection.files` — dependency artifacts first, since
57
+ * `Collection.files` already orders them that way and they are the BASES a
58
+ * consumer's own declarations amend — and structurally scans each with
59
+ * {@link declaredTopLevelKeys} (never the loaded/merged model: the merged
60
+ * tree has already lost which FILE contributed which declaration).
61
+ *
62
+ * Groups by (type, resolutionKey) — mirroring the parser's own merge lookup,
63
+ * which matches on type AND resolutionKey (parser-core.ts) — and reports
64
+ * every unflagged declaration after the first one for a key with 2+
65
+ * declarations. The first unflagged declaration is the base.
66
+ *
67
+ * `file` in each finding's message is the collection-relative path
68
+ * (`relPosix`), or the dependency's `dep:<name>/<artifact>` id when the
69
+ * declaration came from an imported artifact.
70
+ *
71
+ * Unreadable or unparsable files are skipped — the loader itself reports
72
+ * those (as a load error / `meta verify`'s own failure path); duplicating
73
+ * that diagnosis here would just be a second, worse-informed version of the
74
+ * same message.
75
+ */
76
+ export async function lintOverlays(
77
+ collection: Collection,
78
+ readSource: ReadSource,
79
+ ): Promise<Diagnostic[]> {
80
+ // type -> resolutionKey -> every declaration seen for it, in file order.
81
+ const byType = new Map<string, Map<string, Declaration[]>>();
82
+
83
+ for (const path of collection.files) {
84
+ let declared: ReadonlyArray<DeclaredTopLevelKey>;
85
+ try {
86
+ const source = readSource(path);
87
+ const content = await source.read();
88
+ declared = await declaredTopLevelKeys(content, source.format);
89
+ } catch {
90
+ continue;
91
+ }
92
+ const file = collection.fileIds.get(path) ?? relPosix(collection.configDir, path);
93
+ for (const decl of declared) {
94
+ let byKey = byType.get(decl.type);
95
+ if (byKey === undefined) {
96
+ byKey = new Map<string, Declaration[]>();
97
+ byType.set(decl.type, byKey);
98
+ }
99
+ const list = byKey.get(decl.key) ?? [];
100
+ list.push({ file, overlay: decl.overlay });
101
+ byKey.set(decl.key, list);
102
+ }
103
+ }
104
+
105
+ const diags: Diagnostic[] = [];
106
+ for (const byKey of byType.values()) {
107
+ for (const [fqn, declarations] of byKey) {
108
+ if (declarations.length < 2) continue;
109
+ let baseSeen = false;
110
+ for (const decl of declarations) {
111
+ if (decl.overlay) continue;
112
+ if (!baseSeen) {
113
+ // The first unflagged declaration is the base — nothing to report.
114
+ baseSeen = true;
115
+ continue;
116
+ }
117
+ diags.push(
118
+ warn(
119
+ WARN_OVERLAY_IMPLICIT,
120
+ `${fqn} is redeclared in ${decl.file} without overlay: true — add the flag ` +
121
+ `so a removed or renamed target fails loudly (ERR_OVERLAY_NO_TARGET) instead ` +
122
+ `of silently becoming a new object`,
123
+ ),
124
+ );
125
+ }
126
+ }
127
+ }
128
+ return diags;
129
+ }
@@ -249,11 +249,25 @@ export function collectRequirements(root: MetaData): MetaRequirement[] {
249
249
  export interface RequirementScan {
250
250
  readonly addressed: readonly AddressedRequirement[];
251
251
  readonly claimedObjects: ReadonlySet<string>;
252
+ /** FR-023 — narrows which entities `coverableEntities` counts. Undefined
253
+ * means "every non-abstract entity is coverable", exactly as before this
254
+ * option existed: a project with no dependencies gets no change at all.
255
+ * Threaded through so BOTH `coverableEntities` call sites (the gate and the
256
+ * summary) inherit the same narrowing — see `coverableEntities`. */
257
+ readonly coverable?: (fqn: string) => boolean;
252
258
  }
253
259
 
254
- export function scanRequirements(root: MetaData): RequirementScan {
260
+ export function scanRequirements(
261
+ root: MetaData,
262
+ opts?: { coverable?: (fqn: string) => boolean },
263
+ ): RequirementScan {
255
264
  const addressed = collectAddressedRequirements(root);
256
- return { addressed, claimedObjects: claimedObjectKeys(root, addressed.map((r) => r.node)) };
265
+ return {
266
+ addressed,
267
+ claimedObjects: claimedObjectKeys(root, addressed.map((r) => r.node)),
268
+ // `exactOptionalPropertyTypes` — an omitted key, never an explicit `undefined`.
269
+ ...(opts?.coverable !== undefined ? { coverable: opts.coverable } : {}),
270
+ };
257
271
  }
258
272
 
259
273
  /**
@@ -307,10 +321,22 @@ function claimedObjectKeys(root: MetaData, reqs: MetaRequirement[]): Set<string>
307
321
  * An ABSTRACT entity is shape, not data: there is no table and no rows, so
308
322
  * demanding a capability claim for it is the same category error as demanding
309
323
  * one for an object.value. It is exempt for the same reason.
324
+ *
325
+ * FR-023: an entity loaded from a dependency's synced snapshot is load-only —
326
+ * the publisher owns it, and this project never declared it — unless the
327
+ * project's own `scope.include` names the dependency's package literally
328
+ * (`Collection.inScope`, threaded in as `coverable`). Demanding a capability
329
+ * claim for an import nobody here opted into owning would report a project as
330
+ * failing to claim entities it does not own. `coverable` is applied INSIDE
331
+ * this function, once, so every call site inherits it — the alternative
332
+ * (filtering at a call site) is exactly the shape of bug this task exists to
333
+ * avoid: the other call site would keep counting imports.
310
334
  */
311
- function coverableEntities(root: MetaData): MetaData[] {
335
+ function coverableEntities(root: MetaData, coverable?: (fqn: string) => boolean): MetaData[] {
312
336
  return root.children().filter(
313
- (n) => n.type === TYPE_OBJECT && n.subType === OBJECT_SUBTYPE_ENTITY && !n.isAbstract,
337
+ (n) =>
338
+ n.type === TYPE_OBJECT && n.subType === OBJECT_SUBTYPE_ENTITY && !n.isAbstract &&
339
+ (coverable === undefined || coverable(n.resolutionKey())),
314
340
  );
315
341
  }
316
342
 
@@ -324,7 +350,7 @@ function coverableEntities(root: MetaData): MetaData[] {
324
350
  */
325
351
  export function checkRequirements(root: MetaData, scan: RequirementScan = scanRequirements(root)): Diagnostic[] {
326
352
  const out: Diagnostic[] = [];
327
- const { addressed, claimedObjects } = scan;
353
+ const { addressed, claimedObjects, coverable } = scan;
328
354
  if (addressed.length === 0) return out; // opt-in by declaration — no requirements, nothing to say
329
355
 
330
356
  for (const { node: req, path: reqPath } of addressed) {
@@ -538,7 +564,7 @@ export function checkRequirements(root: MetaData, scan: RequirementScan = scanRe
538
564
  //
539
565
  // So a green run means "every entity is claimed by something", not "every node is
540
566
  // described". The stronger reading would be false.
541
- for (const ent of coverableEntities(root)) {
567
+ for (const ent of coverableEntities(root, coverable)) {
542
568
  const key = ent.resolutionKey();
543
569
  if (!claimedObjects.has(key)) {
544
570
  out.push({
@@ -596,7 +622,7 @@ export function summariseRequirements(
596
622
  // summary cannot disagree with the diagnostics printed beneath it — previously
597
623
  // the same helper, now literally the same result.
598
624
  const claimed = scan.claimedObjects;
599
- for (const ent of coverableEntities(root)) {
625
+ for (const ent of coverableEntities(root, scan.coverable)) {
600
626
  summary.entitiesTotal++;
601
627
  if (claimed.has(ent.resolutionKey())) summary.entitiesClaimed++;
602
628
  }