@metaobjectsdev/cli 0.23.2 → 0.24.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 (89) hide show
  1. package/README.md +78 -0
  2. package/dist/src/commands/docs.d.ts.map +1 -1
  3. package/dist/src/commands/docs.js +148 -50
  4. package/dist/src/commands/docs.js.map +1 -1
  5. package/dist/src/commands/export.d.ts.map +1 -1
  6. package/dist/src/commands/export.js +28 -6
  7. package/dist/src/commands/export.js.map +1 -1
  8. package/dist/src/commands/gen.d.ts.map +1 -1
  9. package/dist/src/commands/gen.js +61 -22
  10. package/dist/src/commands/gen.js.map +1 -1
  11. package/dist/src/commands/init.d.ts +8 -0
  12. package/dist/src/commands/init.d.ts.map +1 -1
  13. package/dist/src/commands/init.js +142 -52
  14. package/dist/src/commands/init.js.map +1 -1
  15. package/dist/src/commands/migrate.d.ts +9 -2
  16. package/dist/src/commands/migrate.d.ts.map +1 -1
  17. package/dist/src/commands/migrate.js +399 -73
  18. package/dist/src/commands/migrate.js.map +1 -1
  19. package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
  20. package/dist/src/commands/prompt-snapshot.js +40 -18
  21. package/dist/src/commands/prompt-snapshot.js.map +1 -1
  22. package/dist/src/commands/upgrade.d.ts +2 -0
  23. package/dist/src/commands/upgrade.d.ts.map +1 -0
  24. package/dist/src/commands/upgrade.js +139 -0
  25. package/dist/src/commands/upgrade.js.map +1 -0
  26. package/dist/src/commands/verify.d.ts.map +1 -1
  27. package/dist/src/commands/verify.js +336 -48
  28. package/dist/src/commands/verify.js.map +1 -1
  29. package/dist/src/index.d.ts.map +1 -1
  30. package/dist/src/index.js +46 -15
  31. package/dist/src/index.js.map +1 -1
  32. package/dist/src/lib/allow.d.ts.map +1 -1
  33. package/dist/src/lib/allow.js +4 -0
  34. package/dist/src/lib/allow.js.map +1 -1
  35. package/dist/src/lib/args.d.ts +16 -2
  36. package/dist/src/lib/args.d.ts.map +1 -1
  37. package/dist/src/lib/args.js +19 -3
  38. package/dist/src/lib/args.js.map +1 -1
  39. package/dist/src/lib/codegen-drift.d.ts +8 -1
  40. package/dist/src/lib/codegen-drift.d.ts.map +1 -1
  41. package/dist/src/lib/codegen-drift.js +9 -1
  42. package/dist/src/lib/codegen-drift.js.map +1 -1
  43. package/dist/src/lib/detect-stack.d.ts +1 -1
  44. package/dist/src/lib/detect-stack.d.ts.map +1 -1
  45. package/dist/src/lib/detect-stack.js +22 -37
  46. package/dist/src/lib/detect-stack.js.map +1 -1
  47. package/dist/src/lib/load-metaobjects-config.d.ts +48 -1
  48. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  49. package/dist/src/lib/load-metaobjects-config.js +80 -1
  50. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  51. package/dist/src/lib/manifest-ignored-check.d.ts +12 -0
  52. package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -0
  53. package/dist/src/lib/manifest-ignored-check.js +74 -0
  54. package/dist/src/lib/manifest-ignored-check.js.map +1 -0
  55. package/dist/src/lib/migrate-scope.d.ts +49 -0
  56. package/dist/src/lib/migrate-scope.d.ts.map +1 -0
  57. package/dist/src/lib/migrate-scope.js +92 -0
  58. package/dist/src/lib/migrate-scope.js.map +1 -0
  59. package/dist/src/lib/output.d.ts +1 -1
  60. package/dist/src/lib/output.d.ts.map +1 -1
  61. package/dist/src/lib/output.js +9 -3
  62. package/dist/src/lib/output.js.map +1 -1
  63. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  64. package/dist/src/lib/requirement-check.js +10 -57
  65. package/dist/src/lib/requirement-check.js.map +1 -1
  66. package/package.json +11 -11
  67. package/src/commands/docs.ts +158 -49
  68. package/src/commands/export.ts +30 -6
  69. package/src/commands/gen.ts +62 -22
  70. package/src/commands/init.ts +161 -58
  71. package/src/commands/migrate.ts +423 -75
  72. package/src/commands/prompt-snapshot.ts +43 -18
  73. package/src/commands/upgrade.ts +156 -0
  74. package/src/commands/verify.ts +373 -48
  75. package/src/index.ts +46 -15
  76. package/src/lib/allow.ts +4 -0
  77. package/src/lib/args.ts +35 -4
  78. package/src/lib/codegen-drift.ts +9 -0
  79. package/src/lib/detect-stack.ts +20 -33
  80. package/src/lib/load-metaobjects-config.ts +84 -2
  81. package/src/lib/manifest-ignored-check.ts +75 -0
  82. package/src/lib/migrate-scope.ts +102 -0
  83. package/src/lib/output.ts +8 -4
  84. package/src/lib/requirement-check.ts +10 -58
  85. package/dist/src/lib/verified-by-scan.d.ts +0 -20
  86. package/dist/src/lib/verified-by-scan.d.ts.map +0 -1
  87. package/dist/src/lib/verified-by-scan.js +0 -344
  88. package/dist/src/lib/verified-by-scan.js.map +0 -1
  89. package/src/lib/verified-by-scan.ts +0 -375
@@ -0,0 +1,102 @@
1
+ // What `meta migrate` and `meta verify --db` SAY about a declared `migrate.scope`.
2
+ //
3
+ // The scope itself needs no adapter: `resolveCollection` hands back
4
+ // `inMigrateScope` already in migrate-ts's predicate shape, so the pattern grammar
5
+ // lives in exactly one place (`matchesScope`, @metaobjectsdev/sdk) and `migrate` and
6
+ // `gen` cannot come to disagree about what `acme::platform::**` means.
7
+ //
8
+ // What DOES need one home is the user-facing language, and both commands import it
9
+ // from here: they govern the identical object set from one declaration, so a note
10
+ // or a refusal that drifted between them would be drift the user reads.
11
+
12
+ import type { Collection } from "@metaobjectsdev/sdk";
13
+ import type { SchemaProvenance } from "@metaobjectsdev/migrate-ts";
14
+
15
+ /**
16
+ * Say what a declared `migrate.scope` left out, for `migrate` and `verify --db`
17
+ * alike.
18
+ *
19
+ * An excluded object produces neither a create nor a drop and is neither checked
20
+ * nor reported as drift, so without this line "no changes" and "no changes to the
21
+ * half of the model this run governs" read identically — and an unchecked table is
22
+ * indistinguishable from a checked-and-clean one.
23
+ *
24
+ * One sentence, one definition: the two commands say the same thing about the same
25
+ * declaration, and this is a string a user reads, so drift between two copies of it
26
+ * is drift the user sees.
27
+ */
28
+ export function outOfScopeNote(command: string, names: readonly string[]): string {
29
+ return (
30
+ `meta ${command} — ${names.length} object(s) out-of-scope ` +
31
+ `(outside migrate.scope, governed elsewhere): ${names.join(", ")}`
32
+ );
33
+ }
34
+
35
+ /** How many loaded FQNs to name in the refusal below — enough to show the shape
36
+ * an author's patterns have to match, short enough to stay readable. */
37
+ const EXAMPLE_FQN_CAP = 3;
38
+
39
+ /**
40
+ * The refusal for a `migrate.scope` that matches NOTHING it could govern.
41
+ *
42
+ * A scope matching zero of the objects that declare a table or view can never be
43
+ * what someone meant — it says "every table in this model belongs to somebody
44
+ * else", which is a project with no schema to migrate at all, expressed the hard
45
+ * way. In practice it is a typo'd or stale package pattern, or a scope over a
46
+ * package that holds only value objects and abstracts (shapes that can never
47
+ * contribute a table or view), and it is silent: migrate reports "no changes"
48
+ * while having compared nothing.
49
+ *
50
+ * It is also actively dangerous, which is why this is a refusal and not a warning.
51
+ * An empty expected side is what `diff` reads as "no model, govern the whole
52
+ * database" — the inversion `scopeExpectedSchema`'s `declaredSchemas` closes
53
+ * structurally (migrate-ts `scope.ts`). This is the second lock on the same door:
54
+ * the structural fix stops a wrong scope proposing a destructive change, and this
55
+ * stops the wrong scope going unnoticed in the first place.
56
+ *
57
+ * Returns the message to report, or `undefined` when there is nothing to refuse —
58
+ * no scope declared, or at least one table- or view-declaring object inside it.
59
+ * Callers report it and exit 2 (a configuration error), rather than this throwing,
60
+ * so it reads like every other config failure in these commands.
61
+ */
62
+ export function migrateScopeMismatch(
63
+ collection: Collection,
64
+ /**
65
+ * The UNSCOPED expected schema's provenance (migrate-ts
66
+ * `buildExpectedSchemaWithProvenance`) — qualified table/view name → declaring
67
+ * FQN. Supplied lazily because it is consulted only under a declared scope, so
68
+ * a project with no `migrate.scope` pays nothing for this check and its runs
69
+ * are byte-for-byte what they always were.
70
+ */
71
+ provenance: () => SchemaProvenance,
72
+ ): string | undefined {
73
+ const { inMigrateScope, migrateScopePatterns } = collection;
74
+ if (inMigrateScope === undefined) return undefined;
75
+
76
+ // The declaring FQNs of every table and view the UNSCOPED model contributes —
77
+ // the same provenance `scopeExpectedSchema` decides scope on, so the refusal
78
+ // asks exactly the question the run answers. NOT the loaded object set: that
79
+ // counts value objects and abstracts, which can never declare a table or view
80
+ // (persistability derives from a declared/inherited writable source, never
81
+ // from a subtype — #248), so a scope over only those objects passed this
82
+ // refusal while governing zero tables. And not a fresh walk either: it would
83
+ // have to re-implement the builder's skip rules (abstract, TPH subtype, no
84
+ // writable source, `@unmanaged`) and would drift from them.
85
+ const fqns = [...new Set(provenance().values())];
86
+ // A model that declares no table or view at all is not a scope error: there is
87
+ // nothing for a pattern to govern, scoped or not, and an empty schema has its
88
+ // own (much louder) failure modes downstream.
89
+ if (fqns.length === 0) return undefined;
90
+ if (fqns.some(inMigrateScope)) return undefined;
91
+
92
+ const patterns = JSON.stringify(migrateScopePatterns ?? []);
93
+ const examples = fqns.slice(0, EXAMPLE_FQN_CAP).join(", ");
94
+ const more = fqns.length > EXAMPLE_FQN_CAP ? `, …and ${fqns.length - EXAMPLE_FQN_CAP} more` : "";
95
+ return (
96
+ `migrate.scope matched none of the ${fqns.length} object(s) declaring a table or view, ` +
97
+ `so this run would treat every one of them as another owner's and compare nothing. ` +
98
+ `Patterns: ${patterns}. Declaring a table or view: ${examples}${more}. ` +
99
+ `Fix the patterns in .metaobjects/config.json (migrate.scope), or remove the key to ` +
100
+ `govern everything the model declares.`
101
+ );
102
+ }
package/src/lib/output.ts CHANGED
@@ -14,7 +14,7 @@ export interface FormatOptions {
14
14
  // gen
15
15
  // ---------------------------------------------------------------------------
16
16
 
17
- export type GenFileStatus = "new" | "merged" | "conflict" | "unchanged" | "refused";
17
+ export type GenFileStatus = "new" | "merged" | "conflict" | "unchanged" | "refused" | "removed";
18
18
 
19
19
  export interface GenFileEntry {
20
20
  path: string;
@@ -37,6 +37,7 @@ const GEN_GLYPHS: Record<GenFileStatus, string> = {
37
37
  conflict: "✗",
38
38
  unchanged: "=",
39
39
  refused: "⚠",
40
+ removed: "−",
40
41
  };
41
42
 
42
43
  const GEN_WORDS: Record<GenFileStatus, string> = {
@@ -45,6 +46,7 @@ const GEN_WORDS: Record<GenFileStatus, string> = {
45
46
  conflict: "CONFLICT",
46
47
  unchanged: "UNCHANGED",
47
48
  refused: "REFUSED",
49
+ removed: "REMOVED",
48
50
  };
49
51
 
50
52
  export function formatGenResult(result: GenResultShape, opts: FormatOptions): string {
@@ -74,7 +76,7 @@ export function formatGenResult(result: GenResultShape, opts: FormatOptions): st
74
76
  acc[f.status] = (acc[f.status] ?? 0) + 1;
75
77
  return acc;
76
78
  },
77
- { new: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0 },
79
+ { new: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, removed: 0 },
78
80
  );
79
81
  const parts: string[] = [];
80
82
  if (counts.new > 0) parts.push(`${counts.new} written`);
@@ -82,6 +84,7 @@ export function formatGenResult(result: GenResultShape, opts: FormatOptions): st
82
84
  if (counts.conflict > 0) parts.push(`${counts.conflict} conflict`);
83
85
  if (counts.unchanged > 0) parts.push(`${counts.unchanged} unchanged`);
84
86
  if (counts.refused > 0) parts.push(`${counts.refused} refused`);
87
+ if (counts.removed > 0) parts.push(`${counts.removed} removed`);
85
88
  lines.push("", ` ${parts.join(", ")}`, "");
86
89
 
87
90
  if (result.warnings.length > 0) {
@@ -180,7 +183,7 @@ export function genResultToData(result: GenResultShape): {
180
183
  } {
181
184
  const counts = result.files.reduce<Record<GenFileStatus, number>>(
182
185
  (a, f) => ((a[f.status] = (a[f.status] ?? 0) + 1), a),
183
- { new: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0 },
186
+ { new: 0, merged: 0, conflict: 0, unchanged: 0, refused: 0, removed: 0 },
184
187
  );
185
188
  const parts: string[] = [];
186
189
  if (counts.new) parts.push(`${counts.new} written`);
@@ -188,11 +191,12 @@ export function genResultToData(result: GenResultShape): {
188
191
  if (counts.conflict) parts.push(`${counts.conflict} conflict`);
189
192
  if (counts.unchanged) parts.push(`${counts.unchanged} unchanged`);
190
193
  if (counts.refused) parts.push(`${counts.refused} refused`);
194
+ if (counts.removed) parts.push(`${counts.removed} removed`);
191
195
  const summary = result.files.length === 0
192
196
  ? `no entities to generate in ${result.outDir}`
193
197
  : parts.join(", ");
194
198
  const help = result.files.length === 0
195
- ? ["author entities under metaobjects/ then re-run `meta gen`"]
199
+ ? ["author entities in this project's metadata sources then re-run `meta gen`"]
196
200
  : ["typecheck the generated code with `npx tsc`", "create your database tables with `meta migrate --from-db --db <url> --dialect <sqlite|postgres> --slug init --apply`"];
197
201
  return { gen: result.files.map((f) => ({ file: f.path, status: f.status })), summary, help };
198
202
  }
@@ -12,10 +12,9 @@
12
12
  // VERIFY (conditional) `@implementedBy` resolution, whose SEVERITY DEPENDS
13
13
  // ON `@status`. A loader `references` descriptor
14
14
  // always errors on an unresolved target, and an
15
- // `abandoned` requirement exists precisely to name
16
- // nodes that are gone — declaring it there would make
17
- // the entries carrying the mechanism's only controlled
18
- // evidence fail to load.
15
+ // `planned` requirement names nodes that do not exist
16
+ // YET — declaring it there would make every recorded
17
+ // intention fail to load.
19
18
  //
20
19
  // Two kinds, opposite checks: `functional` fails when NOTHING implements it;
21
20
  // `architectural` fails when something VIOLATES it (v1: an empty claim set on a
@@ -33,7 +32,11 @@ import {
33
32
  REQUIREMENT_LEVEL_MEMBER,
34
33
  REQUIREMENT_DISPOSITION_DEFERRED,
35
34
  REQUIREMENT_STATUSES_REQUIRING_LIVE_NODES,
36
- resolveObjectRef,
35
+ // One shared resolver (FR-038): codegen's requirement-test fan-out needs the same
36
+ // ADR-0042 package-local binding, so these moved to @metaobjectsdev/metadata rather
37
+ // than being reimplemented there.
38
+ resolveClaimTarget,
39
+ resolveMember,
37
40
  didYouMeanHint,
38
41
  type MetaData,
39
42
  type MetaRequirement,
@@ -148,57 +151,6 @@ function subtreeClaimsAnything(req: MetaRequirement): boolean {
148
151
  return false;
149
152
  }
150
153
 
151
- /**
152
- * Resolve the owner segment of an `@implementedBy` reference to the node it names.
153
- *
154
- * OBJECTS FIRST, through the loader's own resolver, so package-local binding stays the
155
- * ADR-0042 contract and never a parallel name scan (#228).
156
- *
157
- * Then ROOT-LEVEL NON-OBJECT nodes — `template.prompt` and its siblings today. The
158
- * attribute is documented as naming "the model nodes realising this requirement", and a
159
- * declared prompt is one: it is the durable artifact a capability like "the game master
160
- * is told what the party can see" actually lives in. Resolving only objects meant the
161
- * prompt estate — the thing whose retirement is hardest to see in a model, since a
162
- * removed prompt leaves no table behind — was the one part of a model that could not
163
- * carry a status. So L4 means "a declared top-level model node", not "an object".
164
- *
165
- * Requirements themselves are excluded: hierarchy is nesting, and a requirement claiming
166
- * a requirement would be a second, contradictory parent mechanism.
167
- */
168
- function resolveClaimTarget(root: MetaData, owner: string, referrerPkg: string): MetaData | undefined {
169
- const { node } = resolveObjectRef(root, owner, referrerPkg);
170
- if (node !== undefined) return node;
171
-
172
- const candidates = root
173
- .children()
174
- .filter((c) => c.type !== TYPE_OBJECT && c.type !== TYPE_REQUIREMENT);
175
-
176
- // A fully-qualified reference binds exactly, like every other FQN in the model.
177
- if (owner.includes(PACKAGE_SEPARATOR)) {
178
- return candidates.find((c) => c.resolutionKey() === owner);
179
- }
180
- // A bare reference prefers the referrer's own package, then a root-level node of that
181
- // bare name. An ambiguous bare name binds NOTHING — same fail-closed rule objects use,
182
- // because silently picking one of two same-named nodes is how a claim ends up pointing
183
- // at the wrong thing without anyone noticing.
184
- const local = referrerPkg === "" ? [] : candidates.filter((c) => c.resolutionKey() === `${referrerPkg}${PACKAGE_SEPARATOR}${owner}`);
185
- if (local.length === 1) return local[0];
186
- // Root-level (unpackaged) only, matching resolveObjectRef's own bare fallback. A bare
187
- // ref must not reach into an arbitrary package just because the name is unique there.
188
- const bare = candidates.filter((c) => c.name === owner && c.resolutionKey() === owner);
189
- return bare.length === 1 ? bare[0] : undefined;
190
- }
191
-
192
- /** Walk dotted member segments by CHILD NAME from an object node. */
193
- function resolveMember(obj: MetaData, path: string[]): MetaData | undefined {
194
- let cur: MetaData | undefined = obj;
195
- for (const seg of path) {
196
- if (cur === undefined) return undefined;
197
- cur = cur.children().find((c) => c.name === seg);
198
- }
199
- return cur;
200
- }
201
-
202
154
  /** Every `requirement.*` node in the tree, at any nesting depth. Hierarchy IS
203
155
  * nesting — an L1 solution contains its L2 segments — so this is a walk, not a
204
156
  * scan of a flat list keyed by a `parent` string. */
@@ -376,8 +328,8 @@ export function checkRequirements(root: MetaData): Diagnostic[] {
376
328
  const resolved = node !== undefined && (isObjectRef || resolveMember(node, path) !== undefined);
377
329
  if (!resolved) {
378
330
  // Severity is CONDITIONAL ON STATUS, and the asymmetry inverts as a pair.
379
- // On abandoned/superseded the nodes are SUPPOSED to be gone — that is the
380
- // entry doing its job, and the reason this check cannot live in the loader.
331
+ // On `planned` the nodes do not exist YET — that is the entry doing its
332
+ // job, and the reason this check cannot live in the loader.
381
333
  if (req.requiresLiveNodes()) {
382
334
  out.push({
383
335
  severity: "error", code: ERR_REQUIREMENT_DANGLING_REF, name: req.name,
@@ -1,20 +0,0 @@
1
- import { type MetaData } from "@metaobjectsdev/metadata";
2
- export declare const ERR_REQUIREMENT_TEST_MISSING = "ERR_REQUIREMENT_TEST_MISSING";
3
- export declare const WARN_REQUIREMENT_TEST_SKIPPED = "WARN_REQUIREMENT_TEST_SKIPPED";
4
- export declare const WARN_REQUIREMENT_TEST_COMMENT_ONLY = "WARN_REQUIREMENT_TEST_COMMENT_ONLY";
5
- export declare const WARN_REQUIREMENT_TEST_UNCLASSIFIED = "WARN_REQUIREMENT_TEST_UNCLASSIFIED";
6
- export interface VerifiedByDiagnostic {
7
- severity: "error" | "warn";
8
- code: string;
9
- name?: string;
10
- message: string;
11
- }
12
- /**
13
- * Check every `@verifiedBy` name against the project's test corpus.
14
- *
15
- * Severity mirrors `@implementedBy`: a broken claim is an ERROR on `live`/`partial`
16
- * and silent on `abandoned`/`superseded`, because a retired requirement naming a
17
- * deleted test is the entry doing its job, not drift.
18
- */
19
- export declare function checkVerifiedBy(root: MetaData, cwd: string, testFiles?: string[]): VerifiedByDiagnostic[];
20
- //# sourceMappingURL=verified-by-scan.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"verified-by-scan.d.ts","sourceRoot":"","sources":["../../../src/lib/verified-by-scan.ts"],"names":[],"mappings":"AA2CA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAElC,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAC3E,eAAO,MAAM,6BAA6B,kCAAkC,CAAC;AAC7E,eAAO,MAAM,kCAAkC,uCAAuC,CAAC;AACvF,eAAO,MAAM,kCAAkC,uCAAuC,CAAC;AAEvF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,OAAO,GAAG,MAAM,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AAwMD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,QAAQ,EACd,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,EAAE,GACnB,oBAAoB,EAAE,CAwGxB"}
@@ -1,344 +0,0 @@
1
- // `meta verify` — the `@verifiedBy` check.
2
- //
3
- // A requirement's `@verifiedBy` names the tests that prove the behaviour. verify
4
- // checks each name EXISTS and is NOT SKIPPED; it never runs them. Running them is
5
- // the test runner's job, and a requirement gate that shelled out to one would be
6
- // slow, ecosystem-specific, and wrong in CI.
7
- //
8
- // Until this existed, `@verifiedBy` was registered vocabulary that nothing read —
9
- // the precise pattern ADR-0007 Amendment 2 and the `@role` shrink exist to punish.
10
- // An attribute the loader accepts and no consumer dispatches on teaches authors
11
- // that declaring is free and means nothing.
12
- //
13
- // PRECISION OVER RECALL, deliberately. The scan matches a name anywhere in the
14
- // test corpus, as a whole word, in any language. That is the most generous
15
- // possible reading, so a "missing" verdict means the name appears in NO test file
16
- // at all — which is a broken claim in any ecosystem. The repo's standing rule for
17
- // drift checks is to bias toward under-flagging, and a nagging gate gets disabled,
18
- // which costs more than the misses.
19
- //
20
- // FAIL-OPEN ON INABILITY. If the project has no test files this scan can see, it
21
- // says NOTHING rather than reporting every name missing. Absence of evidence is
22
- // not evidence of absence, and a monorepo whose tests live outside `--cwd` must
23
- // not be told its requirements are unverified.
24
- //
25
- // WHAT COUNTS AS A TEST FILE IS THE PROJECT'S CALL, NOT OURS. The built-in patterns
26
- // below are a convenience for the ecosystems this repo ports to, and they are a GUESS
27
- // about someone else's repository. They were wrong on a mainstream case from the day
28
- // they shipped: Maven Failsafe names integration tests `FooIT.java`, which matched
29
- // nothing, so a JVM project naming a real integration test got a confident
30
- // "the claim was never true".
31
- //
32
- // Two consequences, both deliberate:
33
- // - `testFiles` (config: `verify.testFiles`) lets a project declare its own
34
- // conventions, unioned with the built-ins. Nothing here can be authoritative
35
- // about a convention we have never seen.
36
- // - the fail-open above is extended from "no test files at all" to the case that
37
- // actually bites: a name we cannot find in the corpus, which IS present in a file
38
- // the corpus definition did not classify. That is our ignorance, not a broken
39
- // claim, and it is reported as such (WARN_REQUIREMENT_TEST_UNCLASSIFIED) rather
40
- // than as an error. The error is reserved for a name that appears NOWHERE.
41
- import { readdirSync, readFileSync, statSync } from "node:fs";
42
- import { join, relative, sep } from "node:path";
43
- import { TYPE_REQUIREMENT, } from "@metaobjectsdev/metadata";
44
- export const ERR_REQUIREMENT_TEST_MISSING = "ERR_REQUIREMENT_TEST_MISSING";
45
- export const WARN_REQUIREMENT_TEST_SKIPPED = "WARN_REQUIREMENT_TEST_SKIPPED";
46
- export const WARN_REQUIREMENT_TEST_COMMENT_ONLY = "WARN_REQUIREMENT_TEST_COMMENT_ONLY";
47
- export const WARN_REQUIREMENT_TEST_UNCLASSIFIED = "WARN_REQUIREMENT_TEST_UNCLASSIFIED";
48
- const IGNORE_SEGMENTS = new Set([
49
- "node_modules", ".git", "dist", "build", "out", ".next", "coverage",
50
- ".metaobjects", "generated", "target", "bin", "obj", "__pycache__", ".venv", "venv",
51
- ]);
52
- /**
53
- * Test files across the five ecosystems this project ports to — a CONVENIENCE DEFAULT,
54
- * never an authority. A project whose conventions differ declares them via
55
- * `verify.testFiles`; see the module header.
56
- *
57
- * The `IT` entries are Maven Failsafe's own defaults (`IT*`, `*IT`, `*ITCase`), which
58
- * is how every JVM project in the wild names an integration test. Their absence is the
59
- * bug that motivated making this list extensible in the first place.
60
- */
61
- const TEST_FILE = new RegExp([
62
- "\\.(?:test|spec)\\.[cm]?[jt]sx?$", // bun / jest / vitest / mocha
63
- "(?:^|[./_-])[Tt]est[^/]*\\.java$", // JUnit — TestFoo.java
64
- "[A-Za-z0-9]Test(?:s)?\\.java$", // JUnit — FooTest.java / FooTests.java
65
- "[A-Za-z0-9]IT(?:Case)?\\.java$", // Failsafe — FooIT.java / FooITCase.java
66
- "^IT[A-Za-z0-9][^/]*\\.java$", // Failsafe — ITFoo.java
67
- "[A-Za-z0-9]Tests?\\.cs$", // xUnit / NUnit
68
- "^test_[^/]*\\.py$", // pytest
69
- "[^/]*_test\\.py$", // pytest, trailing convention
70
- "[A-Za-z0-9]Test(?:s)?\\.kt$", // Kotlin
71
- "[A-Za-z0-9]IT(?:Case)?\\.kt$", // Failsafe under Kotlin — FooIT.kt
72
- ].join("|"));
73
- /** Files worth searching when a name is missing from the corpus, to tell "nowhere" from
74
- * "somewhere I did not classify". Source-ish only; a match in a lockfile proves nothing. */
75
- const SOURCE_FILE = /\.(?:[cm]?[jt]sx?|java|kt|kts|cs|py|rb|go|rs|scala|groovy|feature)$/;
76
- /**
77
- * A glob as permissive as the ones adopters actually write (`**​/*IT.kt`, `*.feature`),
78
- * anchored at the project root and matched against forward-slash relative paths.
79
- *
80
- * Deliberately small: `**` spans separators, `*` does not, `?` is one non-separator
81
- * character. Anything richer belongs to a glob library, and pulling one in for a config
82
- * knob this narrow is not worth the dependency.
83
- */
84
- function globToRegExp(glob) {
85
- let out = "";
86
- for (let i = 0; i < glob.length; i++) {
87
- const c = glob[i];
88
- if (c === "*") {
89
- if (glob[i + 1] === "*") {
90
- // `**/` may match zero segments, so `**/*.feature` matches a root-level file.
91
- if (glob[i + 2] === "/") {
92
- out += "(?:.*/)?";
93
- i += 2;
94
- }
95
- else {
96
- out += ".*";
97
- i += 1;
98
- }
99
- }
100
- else
101
- out += "[^/]*";
102
- continue;
103
- }
104
- if (c === "?") {
105
- out += "[^/]";
106
- continue;
107
- }
108
- out += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
109
- }
110
- return new RegExp(`^${out}$`);
111
- }
112
- /** Markers that a test exists but is disabled, across the same ecosystems. */
113
- const SKIP_MARKER = new RegExp([
114
- "\\b(?:it|test|describe)\\.(?:skip|todo)\\b", // jest/vitest/bun
115
- "\\bx(?:it|test|describe)\\b", // mocha/jasmine
116
- "@Disabled\\b", // JUnit 5
117
- "@Ignore\\b", // JUnit 4 / Kotlin
118
- "@pytest\\.mark\\.skip", // pytest
119
- "\\[Ignore[\\](]", // MSTest / NUnit
120
- "\\bSkip\\s*=", // xUnit [Fact(Skip = "...")]
121
- ].join("|"));
122
- function walk(dir, root, acc, isTestFile, depth = 0) {
123
- if (depth > 12)
124
- return; // pathological trees; the scan is advisory, not exhaustive
125
- let entries;
126
- try {
127
- entries = readdirSync(dir, { withFileTypes: true });
128
- }
129
- catch {
130
- return;
131
- }
132
- for (const e of entries) {
133
- if (e.isDirectory()) {
134
- if (IGNORE_SEGMENTS.has(e.name) || e.name.startsWith("."))
135
- continue;
136
- walk(join(dir, e.name), root, acc, isTestFile, depth + 1);
137
- continue;
138
- }
139
- if (!e.isFile())
140
- continue;
141
- const abs = join(dir, e.name);
142
- const rel = relative(root, abs).split(sep).join("/");
143
- if (!isTestFile(rel, e.name)) {
144
- if (SOURCE_FILE.test(e.name) && acc.unclassified.length < 20_000)
145
- acc.unclassified.push(rel);
146
- continue;
147
- }
148
- try {
149
- if (statSync(abs).size > 512 * 1024)
150
- continue;
151
- acc.byFile.set(rel, readFileSync(abs, "utf8").split("\n"));
152
- acc.files++;
153
- }
154
- catch {
155
- /* unreadable file is not a finding */
156
- }
157
- }
158
- }
159
- /** Does this path or body look like a test the corpus definition simply did not match?
160
- * Deliberately narrow: living under a test directory, or containing an assertion/test
161
- * declaration. Without this, a name occurring anywhere in PRODUCTION source downgrades a
162
- * genuinely broken claim to a warning — the exact failure the comment-only check exists
163
- * to catch. */
164
- const TESTISH_PATH = /(^|\/)(tests?|spec|__tests__|src\/test)(\/|$)/i;
165
- const TESTISH_BODY = /\b(assert\w*|expect|should|@Test|def test_|it\(|test\(|describe\()/;
166
- /** Test by LOCATION or by CONTENT — either is enough. Production source with a matching
167
- * name satisfies neither, which is the case that must stay a hard error. */
168
- function looksLikeTest(rel, lines) {
169
- return TESTISH_PATH.test(rel) || lines.some((l) => TESTISH_BODY.test(l));
170
- }
171
- /**
172
- * Where does this name live, if not in the test corpus?
173
- *
174
- * Only ever called on the miss path, so the cost is paid per BROKEN claim rather than
175
- * per run. Returns the first unclassified source file containing the name, which is
176
- * enough to tell the author which pattern they are missing.
177
- */
178
- function findOutsideCorpus(name, root, files) {
179
- const rx = wordRx(name);
180
- for (const rel of files) {
181
- try {
182
- const abs = join(root, ...rel.split("/"));
183
- if (statSync(abs).size > 512 * 1024)
184
- continue;
185
- const lines = readFileSync(abs, "utf8").split("\n");
186
- for (let i = 0; i < lines.length; i++) {
187
- const line = lines[i] ?? "";
188
- if (rx.test(line) && !isCommentLine(line, rel) && looksLikeTest(rel, lines))
189
- return rel;
190
- }
191
- }
192
- catch {
193
- /* unreadable file is not a finding */
194
- }
195
- }
196
- return undefined;
197
- }
198
- /** Every `requirement.*` node in the tree, at any nesting depth. */
199
- function collect(root) {
200
- const out = [];
201
- const rec = (n) => {
202
- for (const c of n.children()) {
203
- if (c.type === TYPE_REQUIREMENT)
204
- out.push(c);
205
- rec(c);
206
- }
207
- };
208
- rec(root);
209
- return out;
210
- }
211
- /**
212
- * A whole-word match, so `OrderServiceTest` never satisfies a claim naming `Order`.
213
- *
214
- * `_` counts as a SEPARATOR, not a word character: pytest's `def test_OrderServiceTest`
215
- * plainly is the test a claim naming `OrderServiceTest` means, and refusing it would
216
- * emit the confident false error this scan is built to avoid. Camel-case boundaries
217
- * stay strict, which is what actually prevents a short name matching a longer one.
218
- */
219
- /**
220
- * Is this whole line a comment?
221
- *
222
- * WHOLE-LINE ONLY, deliberately. Stripping from the first `//` would truncate a code
223
- * line containing one inside a string — a test titled with a URL is the obvious case —
224
- * and turn a real match into a confident false error, which is the failure this scan is
225
- * built to avoid. A trailing comment after code therefore still counts as code; that
226
- * under-flags, which is the repo's standing bias for drift checks.
227
- *
228
- * `#` is Python-only: in TypeScript it opens a private field, not a comment.
229
- */
230
- function isCommentLine(line, file) {
231
- const t = line.trimStart();
232
- if (t.startsWith("//") || t.startsWith("/*") || t.startsWith("*"))
233
- return true;
234
- return file.endsWith(".py") && t.startsWith("#");
235
- }
236
- function wordRx(name) {
237
- return new RegExp(`(?:^|[^A-Za-z0-9])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9])`);
238
- }
239
- /**
240
- * Check every `@verifiedBy` name against the project's test corpus.
241
- *
242
- * Severity mirrors `@implementedBy`: a broken claim is an ERROR on `live`/`partial`
243
- * and silent on `abandoned`/`superseded`, because a retired requirement naming a
244
- * deleted test is the entry doing its job, not drift.
245
- */
246
- export function checkVerifiedBy(root, cwd, testFiles) {
247
- const reqs = collect(root).filter((r) => r.verifiedBy().length > 0);
248
- if (reqs.length === 0)
249
- return []; // opt-in by declaration
250
- // Project-declared conventions ADD to the built-ins: the failure being fixed is
251
- // under-matching, and a project that names an extra convention is telling us
252
- // something we did not know — not asking us to forget what we did.
253
- const declared = (testFiles ?? []).map(globToRegExp);
254
- const isTestFile = (rel, base) => TEST_FILE.test(base) || declared.some((rx) => rx.test(rel));
255
- const corpus = { files: 0, byFile: new Map(), unclassified: [] };
256
- walk(cwd, cwd, corpus, isTestFile);
257
- if (corpus.files === 0)
258
- return []; // fail open: nothing to judge against
259
- const out = [];
260
- for (const req of reqs) {
261
- for (const test of req.verifiedBy()) {
262
- const rx = wordRx(test);
263
- let foundIn;
264
- let skippedAt;
265
- // #293-adjacent (the `verifiedBy` audit): a name that occurs ONLY in comments
266
- // satisfied this scan, because the match is line-agnostic. That is how a claim
267
- // came to name `mountCrudRoutes`, whose single occurrence in an entire corpus was
268
- // inside a `// via mountCrudRoutes(...)` note. Tracked separately so the name can
269
- // still be reported as found (it is) while saying what it was found in.
270
- let commentOnlyAt;
271
- for (const [file, lines] of corpus.byFile) {
272
- for (let i = 0; i < lines.length; i++) {
273
- if (!rx.test(lines[i] ?? ""))
274
- continue;
275
- if (isCommentLine(lines[i] ?? "", file)) {
276
- commentOnlyAt ??= `${file}:${i + 1}`;
277
- continue;
278
- }
279
- foundIn ??= file;
280
- // a decorator/annotation sits above the declaration it disables
281
- const window = lines.slice(Math.max(0, i - 3), i + 1).join("\n");
282
- if (SKIP_MARKER.test(window))
283
- skippedAt ??= `${file}:${i + 1}`;
284
- }
285
- if (foundIn !== undefined && skippedAt !== undefined)
286
- break;
287
- }
288
- // Found, but only ever in prose. Not an error — the scan's job is to catch a name
289
- // that has gone missing, and this one has not — but a comment proves nothing, so
290
- // the claim is reported rather than silently accepted.
291
- if (foundIn === undefined && commentOnlyAt !== undefined) {
292
- out.push({
293
- severity: "warn",
294
- code: WARN_REQUIREMENT_TEST_COMMENT_ONLY,
295
- name: req.name,
296
- message: `'verifiedBy' names '${test}', which appears only in a comment (${commentOnlyAt}) ` +
297
- `and in no test declaration. A comment proves nothing — name the test that asserts it.`,
298
- });
299
- continue;
300
- }
301
- if (foundIn === undefined) {
302
- if (req.requiresLiveNodes()) {
303
- // Before calling a claim broken, rule out the likelier explanation: that this
304
- // project names its tests in a way the corpus definition does not know. A name
305
- // sitting in an unclassified source file is OUR ignorance, and saying "the claim
306
- // was never true" about it is the tool being confidently wrong.
307
- const elsewhere = findOutsideCorpus(test, cwd, corpus.unclassified);
308
- out.push(elsewhere !== undefined
309
- ? {
310
- severity: "warn",
311
- code: WARN_REQUIREMENT_TEST_UNCLASSIFIED,
312
- name: req.name,
313
- message: `'verifiedBy' names '${test}', which is not in any of the ${corpus.files} ` +
314
- `file(s) recognised as tests, but DOES appear in ${elsewhere}. That file is ` +
315
- `probably a test this scan does not know how to recognise — declare the ` +
316
- `convention in metaobjects.config.ts (verify.testFiles, e.g. ` +
317
- `["**/*IT.kt"]) and this becomes a real check instead of a guess.`,
318
- }
319
- : {
320
- severity: "error",
321
- code: ERR_REQUIREMENT_TEST_MISSING,
322
- name: req.name,
323
- message: `'verifiedBy' names '${test}', which appears in none of the ` +
324
- `${corpus.files} test file(s) found under this project, and in no other ` +
325
- `source file either. Either the test was renamed or removed, or the ` +
326
- `claim was never true.`,
327
- });
328
- }
329
- continue;
330
- }
331
- if (skippedAt !== undefined) {
332
- out.push({
333
- severity: "warn",
334
- code: WARN_REQUIREMENT_TEST_SKIPPED,
335
- name: req.name,
336
- message: `'verifiedBy' names '${test}', but it is disabled at ${skippedAt}. ` +
337
- `A skipped test proves nothing — the requirement reads as verified and is not.`,
338
- });
339
- }
340
- }
341
- }
342
- return out;
343
- }
344
- //# sourceMappingURL=verified-by-scan.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"verified-by-scan.js","sourceRoot":"","sources":["../../../src/lib/verified-by-scan.ts"],"names":[],"mappings":"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"}