@metaobjectsdev/cli 1.0.2 → 1.0.4-rc.1
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.
- package/README.md +13 -5
- package/dist/src/commands/deps.d.ts +3 -0
- package/dist/src/commands/deps.d.ts.map +1 -0
- package/dist/src/commands/deps.js +200 -0
- package/dist/src/commands/deps.js.map +1 -0
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +19 -3
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/eject.d.ts +2 -1
- package/dist/src/commands/eject.d.ts.map +1 -1
- package/dist/src/commands/eject.js +235 -75
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +173 -30
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +5 -9
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +92 -402
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +24 -19
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
- package/dist/src/commands/prompt-snapshot.js +2 -1
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +186 -29
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +97 -15
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/advisory.d.ts +6 -0
- package/dist/src/lib/advisory.d.ts.map +1 -1
- package/dist/src/lib/advisory.js +10 -0
- package/dist/src/lib/advisory.js.map +1 -1
- package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
- package/dist/src/lib/agent-context-staleness.js +7 -1
- package/dist/src/lib/agent-context-staleness.js.map +1 -1
- package/dist/src/lib/args.d.ts +75 -3
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +54 -5
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/catalog-listing.d.ts +92 -0
- package/dist/src/lib/catalog-listing.d.ts.map +1 -0
- package/dist/src/lib/catalog-listing.js +268 -0
- package/dist/src/lib/catalog-listing.js.map +1 -0
- package/dist/src/lib/catalog.d.ts +41 -0
- package/dist/src/lib/catalog.d.ts.map +1 -0
- package/dist/src/lib/catalog.js +89 -0
- package/dist/src/lib/catalog.js.map +1 -0
- package/dist/src/lib/collection-load-options.d.ts +13 -0
- package/dist/src/lib/collection-load-options.d.ts.map +1 -0
- package/dist/src/lib/collection-load-options.js +25 -0
- package/dist/src/lib/collection-load-options.js.map +1 -0
- package/dist/src/lib/dependency-sync.d.ts +170 -0
- package/dist/src/lib/dependency-sync.d.ts.map +1 -0
- package/dist/src/lib/dependency-sync.js +457 -0
- package/dist/src/lib/dependency-sync.js.map +1 -0
- package/dist/src/lib/install-set.d.ts +31 -0
- package/dist/src/lib/install-set.d.ts.map +1 -0
- package/dist/src/lib/install-set.js +103 -0
- package/dist/src/lib/install-set.js.map +1 -0
- package/dist/src/lib/library-eject.d.ts +79 -0
- package/dist/src/lib/library-eject.d.ts.map +1 -0
- package/dist/src/lib/library-eject.js +241 -0
- package/dist/src/lib/library-eject.js.map +1 -0
- package/dist/src/lib/library-listing.d.ts +63 -0
- package/dist/src/lib/library-listing.d.ts.map +1 -0
- package/dist/src/lib/library-listing.js +178 -0
- package/dist/src/lib/library-listing.js.map +1 -0
- package/dist/src/lib/library-prefix-advisory.d.ts +17 -0
- package/dist/src/lib/library-prefix-advisory.d.ts.map +1 -0
- package/dist/src/lib/library-prefix-advisory.js +67 -0
- package/dist/src/lib/library-prefix-advisory.js.map +1 -0
- package/dist/src/lib/load-metaobjects-config.d.ts +1 -2
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +11 -17
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/migrate-scope.d.ts +32 -1
- package/dist/src/lib/migrate-scope.d.ts.map +1 -1
- package/dist/src/lib/migrate-scope.js +51 -3
- package/dist/src/lib/migrate-scope.js.map +1 -1
- package/dist/src/lib/overlay-lint.d.ts +37 -0
- package/dist/src/lib/overlay-lint.d.ts.map +1 -0
- package/dist/src/lib/overlay-lint.js +102 -0
- package/dist/src/lib/overlay-lint.js.map +1 -0
- package/dist/src/lib/requirement-check.d.ts +26 -3
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +71 -13
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/deps.ts +231 -0
- package/src/commands/docs.ts +19 -3
- package/src/commands/eject.ts +304 -90
- package/src/commands/gen.ts +189 -29
- package/src/commands/init.ts +95 -407
- package/src/commands/migrate.ts +26 -16
- package/src/commands/prompt-snapshot.ts +2 -1
- package/src/commands/verify.ts +213 -30
- package/src/index.ts +97 -15
- package/src/lib/advisory.ts +13 -0
- package/src/lib/agent-context-staleness.ts +7 -1
- package/src/lib/args.ts +118 -8
- package/src/lib/catalog-listing.ts +369 -0
- package/src/lib/catalog.ts +107 -0
- package/src/lib/collection-load-options.ts +42 -0
- package/src/lib/dependency-sync.ts +656 -0
- package/src/lib/install-set.ts +116 -0
- package/src/lib/library-eject.ts +298 -0
- package/src/lib/library-listing.ts +258 -0
- package/src/lib/library-prefix-advisory.ts +78 -0
- package/src/lib/load-metaobjects-config.ts +15 -21
- package/src/lib/migrate-scope.ts +59 -4
- package/src/lib/overlay-lint.ts +129 -0
- package/src/lib/requirement-check.ts +100 -14
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// FR-043 §3.5 — `metaobjects::` in an adopter's own metadata, with no ejection provenance.
|
|
2
|
+
//
|
|
3
|
+
// The loader already REFUSES a node in a library's package while that library is opted
|
|
4
|
+
// in (ERR_LIBRARY_PACKAGE_COLLISION / ERR_LIBRARY_PACKAGE_NOT_OWNED). This is the other
|
|
5
|
+
// half, and it is advisory on purpose: with the library NOT opted in there is nothing
|
|
6
|
+
// broken to refuse — the nodes are simply the adopter's, sitting under a prefix this
|
|
7
|
+
// project ships libraries into.
|
|
8
|
+
//
|
|
9
|
+
// Two ways to arrive there, and the advice is the same for both:
|
|
10
|
+
//
|
|
11
|
+
// a hand-copy, rather than `meta eject <library>` — no provenance header, so nothing
|
|
12
|
+
// can tell later where it came from or how far it has drifted;
|
|
13
|
+
// a package名 chosen freely that happens to start `metaobjects::` — which will
|
|
14
|
+
// collide the day a library ships into it.
|
|
15
|
+
//
|
|
16
|
+
// A file carrying the eject marker is exempt: that IS the provenance, and `meta eject
|
|
17
|
+
// --list` reports its staleness.
|
|
18
|
+
import { readFile } from "node:fs/promises";
|
|
19
|
+
import { EJECT_MARKER } from "./library-eject.js";
|
|
20
|
+
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
21
|
+
|
|
22
|
+
/** The prefix every shipped library declares into. */
|
|
23
|
+
export const LIBRARY_PACKAGE_PREFIX = "metaobjects::";
|
|
24
|
+
|
|
25
|
+
export interface LibraryPrefixFinding {
|
|
26
|
+
file: string;
|
|
27
|
+
fqn: string;
|
|
28
|
+
message: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Root nodes under `metaobjects::` whose file carries no ejection provenance.
|
|
33
|
+
*
|
|
34
|
+
* Reads the loaded tree for the nodes and the FILES only for the marker — the marker is
|
|
35
|
+
* `meta eject`'s own bookkeeping, not metadata, so there is nothing in the model to read
|
|
36
|
+
* it from.
|
|
37
|
+
*/
|
|
38
|
+
export async function scanForUnprovenancedLibraryPrefix(
|
|
39
|
+
root: MetaData,
|
|
40
|
+
ownFiles: readonly string[],
|
|
41
|
+
projectRoot: string,
|
|
42
|
+
): Promise<LibraryPrefixFinding[]> {
|
|
43
|
+
const suspect = root
|
|
44
|
+
.children()
|
|
45
|
+
.filter((n) => n.resolutionKey().startsWith(LIBRARY_PACKAGE_PREFIX));
|
|
46
|
+
if (suspect.length === 0) return [];
|
|
47
|
+
|
|
48
|
+
// Which of this project's own files carry the marker — read once, not per node.
|
|
49
|
+
const provenanced = new Set<string>();
|
|
50
|
+
for (const file of ownFiles) {
|
|
51
|
+
try {
|
|
52
|
+
if ((await readFile(file, "utf8")).slice(0, 400).includes(EJECT_MARKER)) {
|
|
53
|
+
provenanced.add(file.slice(projectRoot.length + 1));
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
// unreadable — it contributes no exemption, which is the safe direction
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const out: LibraryPrefixFinding[] = [];
|
|
61
|
+
for (const node of suspect) {
|
|
62
|
+
const files = "files" in node.source ? node.source.files : [];
|
|
63
|
+
// A node the SHIPPED library contributed is not the adopter's — `library:` ids say so.
|
|
64
|
+
if (files.some((f) => f.startsWith("library:"))) continue;
|
|
65
|
+
if (files.some((f) => provenanced.has(f) || [...provenanced].some((p) => p.endsWith(f)))) continue;
|
|
66
|
+
out.push({
|
|
67
|
+
file: files[0] ?? "(unknown)",
|
|
68
|
+
fqn: node.resolutionKey(),
|
|
69
|
+
message:
|
|
70
|
+
`${node.resolutionKey()} is declared under "${LIBRARY_PACKAGE_PREFIX}", which is where ` +
|
|
71
|
+
`MetaObjects ships its libraries, and no file declaring it carries an ejection header. ` +
|
|
72
|
+
`If it was copied from a library, re-copy it with 'meta eject <library>' so its origin ` +
|
|
73
|
+
`and its drift stay legible; if it is your own, move it to a package you own — a later ` +
|
|
74
|
+
`release shipping a node of that name would merge into yours.`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
@@ -249,11 +249,10 @@ function rewriteImportSpecifiers(source: string, aliasMap: Record<string, string
|
|
|
249
249
|
* than set to `undefined`, which is what lets `loadMemory` apply its own defaults.
|
|
250
250
|
*/
|
|
251
251
|
export function loadMemoryOptionsFrom(
|
|
252
|
-
cfg: Pick<MetaobjectsGenConfig, "providers"
|
|
253
|
-
): { providers?: readonly MetaDataTypeProvider[]
|
|
252
|
+
cfg: Pick<MetaobjectsGenConfig, "providers"> | undefined,
|
|
253
|
+
): { providers?: readonly MetaDataTypeProvider[] } {
|
|
254
254
|
return {
|
|
255
255
|
...(cfg?.providers !== undefined ? { providers: cfg.providers } : {}),
|
|
256
|
-
...(cfg?.libraries !== undefined ? { libraries: cfg.libraries } : {}),
|
|
257
256
|
};
|
|
258
257
|
}
|
|
259
258
|
|
|
@@ -518,24 +517,19 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
|
|
|
518
517
|
if (!cfg || typeof cfg !== "object" || !Array.isArray(cfg.generators)) {
|
|
519
518
|
throw new Error(`metaobjects.config.ts at ${fullPath} did not export a valid MetaobjectsGenConfig (missing 'generators' array).`);
|
|
520
519
|
}
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
// into
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
throw new Error(
|
|
535
|
-
`metaobjects.config.ts at ${fullPath}: 'libraries' names unknown package(s) ` +
|
|
536
|
-
`${JSON.stringify(unknown)}; available: ${JSON.stringify(available)}.`,
|
|
537
|
-
);
|
|
538
|
-
}
|
|
520
|
+
// `libraries` used to be validated here. It moved to `.metaobjects/config.json`
|
|
521
|
+
// (FR-043 Amendment 1), and so did its validation — see `assertKnownLibraries`,
|
|
522
|
+
// called from `resolveCollection`'s readers. A config still carrying the old key
|
|
523
|
+
// gets a pointed error rather than silence, because silence is what turns a moved
|
|
524
|
+
// key into "my library stopped loading and nothing said why".
|
|
525
|
+
if ("libraries" in (cfg as unknown as Record<string, unknown>)) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`metaobjects.config.ts at ${fullPath}: 'libraries' moved to ` +
|
|
528
|
+
`.metaobjects/config.json (FR-043). Which designs a project adopts is a fact ` +
|
|
529
|
+
`about the PROJECT, not about how one port generates code from it, and that ` +
|
|
530
|
+
`file is the port-neutral one every port already reads. Move the array across ` +
|
|
531
|
+
`verbatim — the tokens are unchanged.`,
|
|
532
|
+
);
|
|
539
533
|
}
|
|
540
534
|
return cfg;
|
|
541
535
|
} finally {
|
package/src/lib/migrate-scope.ts
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
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(
|
|
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
|
+
}
|
|
@@ -42,6 +42,10 @@ import {
|
|
|
42
42
|
type MetaRequirement,
|
|
43
43
|
type RequirementStatus,
|
|
44
44
|
} from "@metaobjectsdev/metadata";
|
|
45
|
+
// FR-043 §5.4 — the provenance key for coverage activation. A node-only subpath, which
|
|
46
|
+
// is why it is a second import rather than a name on the barrel above: the library
|
|
47
|
+
// module reaches the filesystem and the root barrel stays browser-safe.
|
|
48
|
+
import { libraryPackages } from "@metaobjectsdev/metadata/library";
|
|
45
49
|
|
|
46
50
|
export type Severity = "error" | "warn";
|
|
47
51
|
|
|
@@ -82,8 +86,11 @@ export interface RequirementSummary {
|
|
|
82
86
|
undecided: number;
|
|
83
87
|
/** deferred entries naming no ticket, so nobody will be reminded. */
|
|
84
88
|
deferredUntracked: number;
|
|
85
|
-
|
|
86
|
-
|
|
89
|
+
/** ABSENT when coverage was not measured — FR-043 §5.4. A number here is a
|
|
90
|
+
* ratio the project is held to; absence is the honest reading of "this project
|
|
91
|
+
* has authored no requirement of its own, so it asked to be held to none". */
|
|
92
|
+
entitiesTotal?: number;
|
|
93
|
+
entitiesClaimed?: number;
|
|
87
94
|
}
|
|
88
95
|
|
|
89
96
|
/** Severity of the object-coverage gate. Promotion to `"error"` is a one-line
|
|
@@ -249,11 +256,70 @@ export function collectRequirements(root: MetaData): MetaRequirement[] {
|
|
|
249
256
|
export interface RequirementScan {
|
|
250
257
|
readonly addressed: readonly AddressedRequirement[];
|
|
251
258
|
readonly claimedObjects: ReadonlySet<string>;
|
|
259
|
+
/** FR-023 — narrows which entities `coverableEntities` counts. Undefined
|
|
260
|
+
* means "every non-abstract entity is coverable", exactly as before this
|
|
261
|
+
* option existed: a project with no dependencies gets no change at all.
|
|
262
|
+
* Threaded through so BOTH `coverableEntities` call sites (the gate and the
|
|
263
|
+
* summary) inherit the same narrowing — see `coverableEntities`. */
|
|
264
|
+
readonly coverable?: (fqn: string) => boolean;
|
|
265
|
+
/** FR-043 §5.4 — whether object coverage applies at all on this run. See
|
|
266
|
+
* {@link projectAuthoredRequirements}. Read by BOTH the gate and the summary from
|
|
267
|
+
* this one scan, for the same reason `claimedObjects` is shared: a summary that
|
|
268
|
+
* printed a ratio the gate had not enforced would be a measurement nobody could
|
|
269
|
+
* reconcile with the diagnostics beneath it. */
|
|
270
|
+
readonly measureCoverage: boolean;
|
|
252
271
|
}
|
|
253
272
|
|
|
254
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Did the ADOPTER author any of these requirements?
|
|
275
|
+
*
|
|
276
|
+
* FR-043 §5.4, and the rule exists because `checkRequirements` activates object
|
|
277
|
+
* coverage on the presence of any requirement at all. A library ships its own ledger
|
|
278
|
+
* — `iam` ships eleven entries — so without this, opting into a library would switch
|
|
279
|
+
* the unclaimed-entity gate on across a project that has never written a requirement,
|
|
280
|
+
* and report every entity in it. The gate would then be measuring the LIBRARY's
|
|
281
|
+
* decision to ship a ledger rather than anything the adopter did.
|
|
282
|
+
*
|
|
283
|
+
* Provenance is the library's declared PACKAGE. That is a manifest fact the standalone
|
|
284
|
+
* library gate resolves against the library loaded alone, whereas a node's source id
|
|
285
|
+
* differs between the on-disk dev layout (an absolute path) and the embedded one
|
|
286
|
+
* (`library:<ref>.yaml`) — a rule keyed on source would hold in this repo and stop
|
|
287
|
+
* holding in an installed build.
|
|
288
|
+
*
|
|
289
|
+
* An adopter OVERLAYING a library requirement (§5.5) is deliberately not authoring one:
|
|
290
|
+
* the overlay merges into the library's node, in the library's package, and disagreeing
|
|
291
|
+
* with a shipped verdict is a statement about the library's design rather than about
|
|
292
|
+
* what the adopter's own model is for. Treating it as activation would mean correcting
|
|
293
|
+
* one library entry silently acquired a coverage gate over the whole estate.
|
|
294
|
+
*/
|
|
295
|
+
function projectAuthoredRequirements(addressed: readonly AddressedRequirement[]): boolean {
|
|
296
|
+
const libPkgs = libraryPackages();
|
|
297
|
+
return addressed.some((r) => {
|
|
298
|
+
const pkg = r.node.package ?? r.node.fileDefaultPackage ?? "";
|
|
299
|
+
return !libPkgs.has(pkg);
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function scanRequirements(
|
|
304
|
+
root: MetaData,
|
|
305
|
+
opts?: {
|
|
306
|
+
coverable?: (fqn: string) => boolean;
|
|
307
|
+
/** Force coverage on or off instead of deriving it. The one caller that legitimately
|
|
308
|
+
* knows better is the gate over a SHIPPED library loaded standalone: there the
|
|
309
|
+
* library IS the project under test, and "every entity this library ships is claimed
|
|
310
|
+
* by its own ledger" is exactly what is being asserted — so the derivation would
|
|
311
|
+
* switch the check off precisely where it is the point. */
|
|
312
|
+
measureCoverage?: boolean;
|
|
313
|
+
},
|
|
314
|
+
): RequirementScan {
|
|
255
315
|
const addressed = collectAddressedRequirements(root);
|
|
256
|
-
return {
|
|
316
|
+
return {
|
|
317
|
+
addressed,
|
|
318
|
+
claimedObjects: claimedObjectKeys(root, addressed.map((r) => r.node)),
|
|
319
|
+
measureCoverage: opts?.measureCoverage ?? projectAuthoredRequirements(addressed),
|
|
320
|
+
// `exactOptionalPropertyTypes` — an omitted key, never an explicit `undefined`.
|
|
321
|
+
...(opts?.coverable !== undefined ? { coverable: opts.coverable } : {}),
|
|
322
|
+
};
|
|
257
323
|
}
|
|
258
324
|
|
|
259
325
|
/**
|
|
@@ -307,10 +373,22 @@ function claimedObjectKeys(root: MetaData, reqs: MetaRequirement[]): Set<string>
|
|
|
307
373
|
* An ABSTRACT entity is shape, not data: there is no table and no rows, so
|
|
308
374
|
* demanding a capability claim for it is the same category error as demanding
|
|
309
375
|
* one for an object.value. It is exempt for the same reason.
|
|
376
|
+
*
|
|
377
|
+
* FR-023: an entity loaded from a dependency's synced snapshot is load-only —
|
|
378
|
+
* the publisher owns it, and this project never declared it — unless the
|
|
379
|
+
* project's own `scope.include` names the dependency's package literally
|
|
380
|
+
* (`Collection.inScope`, threaded in as `coverable`). Demanding a capability
|
|
381
|
+
* claim for an import nobody here opted into owning would report a project as
|
|
382
|
+
* failing to claim entities it does not own. `coverable` is applied INSIDE
|
|
383
|
+
* this function, once, so every call site inherits it — the alternative
|
|
384
|
+
* (filtering at a call site) is exactly the shape of bug this task exists to
|
|
385
|
+
* avoid: the other call site would keep counting imports.
|
|
310
386
|
*/
|
|
311
|
-
function coverableEntities(root: MetaData): MetaData[] {
|
|
387
|
+
function coverableEntities(root: MetaData, coverable?: (fqn: string) => boolean): MetaData[] {
|
|
312
388
|
return root.children().filter(
|
|
313
|
-
(n) =>
|
|
389
|
+
(n) =>
|
|
390
|
+
n.type === TYPE_OBJECT && n.subType === OBJECT_SUBTYPE_ENTITY && !n.isAbstract &&
|
|
391
|
+
(coverable === undefined || coverable(n.resolutionKey())),
|
|
314
392
|
);
|
|
315
393
|
}
|
|
316
394
|
|
|
@@ -324,7 +402,7 @@ function coverableEntities(root: MetaData): MetaData[] {
|
|
|
324
402
|
*/
|
|
325
403
|
export function checkRequirements(root: MetaData, scan: RequirementScan = scanRequirements(root)): Diagnostic[] {
|
|
326
404
|
const out: Diagnostic[] = [];
|
|
327
|
-
const { addressed, claimedObjects } = scan;
|
|
405
|
+
const { addressed, claimedObjects, coverable, measureCoverage } = scan;
|
|
328
406
|
if (addressed.length === 0) return out; // opt-in by declaration — no requirements, nothing to say
|
|
329
407
|
|
|
330
408
|
for (const { node: req, path: reqPath } of addressed) {
|
|
@@ -538,7 +616,9 @@ export function checkRequirements(root: MetaData, scan: RequirementScan = scanRe
|
|
|
538
616
|
//
|
|
539
617
|
// So a green run means "every entity is claimed by something", not "every node is
|
|
540
618
|
// described". The stronger reading would be false.
|
|
541
|
-
|
|
619
|
+
// ADOPTER-AUTHORED ONLY. FR-043 §5.4 — see `projectAuthoredRequirements`. A library's
|
|
620
|
+
// ledger is counted and checked, but it cannot volunteer you for coverage.
|
|
621
|
+
for (const ent of measureCoverage ? coverableEntities(root, coverable) : []) {
|
|
542
622
|
const key = ent.resolutionKey();
|
|
543
623
|
if (!claimedObjects.has(key)) {
|
|
544
624
|
out.push({
|
|
@@ -575,8 +655,8 @@ export function summariseRequirements(
|
|
|
575
655
|
byStatus: {},
|
|
576
656
|
undecided: 0,
|
|
577
657
|
deferredUntracked: 0,
|
|
578
|
-
|
|
579
|
-
|
|
658
|
+
// The coverage pair is filled in below only when the run measures coverage —
|
|
659
|
+
// `exactOptionalPropertyTypes`, so the keys are absent rather than undefined.
|
|
580
660
|
};
|
|
581
661
|
|
|
582
662
|
for (const req of reqs) {
|
|
@@ -595,10 +675,16 @@ export function summariseRequirements(
|
|
|
595
675
|
// Both sides of the ratio come from the SAME scan the gate read, so the printed
|
|
596
676
|
// summary cannot disagree with the diagnostics printed beneath it — previously
|
|
597
677
|
// the same helper, now literally the same result.
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
678
|
+
if (scan.measureCoverage) {
|
|
679
|
+
const claimed = scan.claimedObjects;
|
|
680
|
+
let total = 0;
|
|
681
|
+
let claimedCount = 0;
|
|
682
|
+
for (const ent of coverableEntities(root, scan.coverable)) {
|
|
683
|
+
total++;
|
|
684
|
+
if (claimed.has(ent.resolutionKey())) claimedCount++;
|
|
685
|
+
}
|
|
686
|
+
summary.entitiesTotal = total;
|
|
687
|
+
summary.entitiesClaimed = claimedCount;
|
|
602
688
|
}
|
|
603
689
|
|
|
604
690
|
return summary;
|