@metaobjectsdev/cli 1.0.13 → 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.
- package/README.md +32 -22
- package/dist/bin/meta.js +14 -2
- package/dist/bin/meta.js.map +1 -1
- package/dist/src/commands/docs.d.ts +1 -14
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +11 -835
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/eject.d.ts +24 -1
- package/dist/src/commands/eject.d.ts.map +1 -1
- package/dist/src/commands/eject.js +240 -69
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/gen.d.ts +7 -0
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +91 -21
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/generator.js +2 -2
- package/dist/src/commands/generator.js.map +1 -1
- package/dist/src/commands/init.d.ts +7 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +71 -36
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts +1 -1
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +224 -125
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/types.d.ts.map +1 -1
- package/dist/src/commands/types.js +16 -2
- package/dist/src/commands/types.js.map +1 -1
- package/dist/src/commands/upgrade.d.ts.map +1 -1
- package/dist/src/commands/upgrade.js +4 -1
- package/dist/src/commands/upgrade.js.map +1 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +189 -125
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +59 -72
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/agent-context-staleness.d.ts +8 -1
- package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
- package/dist/src/lib/agent-context-staleness.js +20 -5
- package/dist/src/lib/agent-context-staleness.js.map +1 -1
- package/dist/src/lib/agent-schema.d.ts +9 -0
- package/dist/src/lib/agent-schema.d.ts.map +1 -0
- package/dist/src/lib/agent-schema.js +61 -0
- package/dist/src/lib/agent-schema.js.map +1 -0
- package/dist/src/lib/allow.d.ts +6 -0
- package/dist/src/lib/allow.d.ts.map +1 -1
- package/dist/src/lib/allow.js +49 -5
- package/dist/src/lib/allow.js.map +1 -1
- package/dist/src/lib/anti-patterns.d.ts.map +1 -1
- package/dist/src/lib/anti-patterns.js +4 -2
- package/dist/src/lib/anti-patterns.js.map +1 -1
- package/dist/src/lib/args.d.ts +51 -10
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +44 -18
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/catalog-listing.d.ts +21 -1
- package/dist/src/lib/catalog-listing.d.ts.map +1 -1
- package/dist/src/lib/catalog-listing.js +85 -16
- package/dist/src/lib/catalog-listing.js.map +1 -1
- package/dist/src/lib/catalog.d.ts +30 -0
- package/dist/src/lib/catalog.d.ts.map +1 -1
- package/dist/src/lib/catalog.js +40 -5
- package/dist/src/lib/catalog.js.map +1 -1
- package/dist/src/lib/codegen-drift.d.ts +31 -2
- package/dist/src/lib/codegen-drift.d.ts.map +1 -1
- package/dist/src/lib/codegen-drift.js +109 -4
- package/dist/src/lib/codegen-drift.js.map +1 -1
- package/dist/src/lib/collection-load-options.d.ts +1 -1
- package/dist/src/lib/collection-load-options.d.ts.map +1 -1
- package/dist/src/lib/collection-load-options.js +1 -0
- package/dist/src/lib/collection-load-options.js.map +1 -1
- package/dist/src/lib/config.d.ts +4 -2
- package/dist/src/lib/config.d.ts.map +1 -1
- package/dist/src/lib/config.js +4 -2
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/decimal-precision-advisory.d.ts +10 -0
- package/dist/src/lib/decimal-precision-advisory.d.ts.map +1 -0
- package/dist/src/lib/decimal-precision-advisory.js +34 -0
- package/dist/src/lib/decimal-precision-advisory.js.map +1 -0
- package/dist/src/lib/deprecation-lint.d.ts.map +1 -1
- package/dist/src/lib/deprecation-lint.js +17 -26
- package/dist/src/lib/deprecation-lint.js.map +1 -1
- package/dist/src/lib/docs-removed.d.ts +13 -0
- package/dist/src/lib/docs-removed.d.ts.map +1 -0
- package/dist/src/lib/docs-removed.js +31 -0
- package/dist/src/lib/docs-removed.js.map +1 -0
- package/dist/src/lib/docs-site-eject.d.ts +13 -0
- package/dist/src/lib/docs-site-eject.d.ts.map +1 -0
- package/dist/src/lib/docs-site-eject.js +40 -0
- package/dist/src/lib/docs-site-eject.js.map +1 -0
- package/dist/src/lib/field-lint.d.ts +25 -0
- package/dist/src/lib/field-lint.d.ts.map +1 -0
- package/dist/src/lib/field-lint.js +102 -0
- package/dist/src/lib/field-lint.js.map +1 -0
- package/dist/src/lib/fk-index-advisory.d.ts +1 -2
- package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
- package/dist/src/lib/fk-index-advisory.js +4 -3
- package/dist/src/lib/fk-index-advisory.js.map +1 -1
- package/dist/src/lib/format.d.ts +7 -0
- package/dist/src/lib/format.d.ts.map +1 -1
- package/dist/src/lib/format.js +13 -0
- package/dist/src/lib/format.js.map +1 -1
- package/dist/src/lib/gen-migrate-advice.d.ts +3 -1
- package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
- package/dist/src/lib/gen-migrate-advice.js +3 -4
- package/dist/src/lib/gen-migrate-advice.js.map +1 -1
- package/dist/src/lib/install-set.d.ts +10 -1
- package/dist/src/lib/install-set.d.ts.map +1 -1
- package/dist/src/lib/install-set.js +20 -2
- package/dist/src/lib/install-set.js.map +1 -1
- package/dist/src/lib/load-metaobjects-config.d.ts +13 -11
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +53 -75
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/manifest-ignored-check.d.ts +4 -1
- package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
- package/dist/src/lib/manifest-ignored-check.js +9 -4
- package/dist/src/lib/manifest-ignored-check.js.map +1 -1
- package/dist/src/lib/output.d.ts +15 -0
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +9 -1
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/payload-field-tree.d.ts +4 -5
- package/dist/src/lib/payload-field-tree.d.ts.map +1 -1
- package/dist/src/lib/payload-field-tree.js +16 -31
- package/dist/src/lib/payload-field-tree.js.map +1 -1
- package/dist/src/lib/requirement-check.d.ts +7 -0
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +14 -7
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/dist/src/lib/strict-args.d.ts +28 -0
- package/dist/src/lib/strict-args.d.ts.map +1 -0
- package/dist/src/lib/strict-args.js +49 -0
- package/dist/src/lib/strict-args.js.map +1 -0
- package/dist/src/lib/template-eject.d.ts +108 -0
- package/dist/src/lib/template-eject.d.ts.map +1 -0
- package/dist/src/lib/template-eject.js +438 -0
- package/dist/src/lib/template-eject.js.map +1 -0
- package/dist/src/lib/version.d.ts +9 -0
- package/dist/src/lib/version.d.ts.map +1 -1
- package/dist/src/lib/version.js +15 -0
- package/dist/src/lib/version.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/docs.ts +11 -952
- package/src/commands/eject.ts +318 -73
- package/src/commands/gen.ts +94 -22
- package/src/commands/generator.ts +2 -2
- package/src/commands/init.ts +80 -38
- package/src/commands/migrate.ts +248 -117
- package/src/commands/types.ts +18 -2
- package/src/commands/upgrade.ts +5 -1
- package/src/commands/verify.ts +229 -136
- package/src/index.ts +59 -72
- package/src/lib/agent-context-staleness.ts +20 -5
- package/src/lib/agent-schema.ts +74 -0
- package/src/lib/allow.ts +57 -6
- package/src/lib/anti-patterns.ts +4 -2
- package/src/lib/args.ts +83 -28
- package/src/lib/catalog-listing.ts +105 -25
- package/src/lib/catalog.ts +51 -6
- package/src/lib/codegen-drift.ts +138 -3
- package/src/lib/collection-load-options.ts +2 -1
- package/src/lib/config.ts +5 -3
- package/src/lib/decimal-precision-advisory.ts +50 -0
- package/src/lib/deprecation-lint.ts +16 -24
- package/src/lib/docs-removed.ts +41 -0
- package/src/lib/docs-site-eject.ts +48 -0
- package/src/lib/field-lint.ts +122 -0
- package/src/lib/fk-index-advisory.ts +4 -4
- package/src/lib/format.ts +12 -0
- package/src/lib/gen-migrate-advice.ts +6 -4
- package/src/lib/install-set.ts +20 -1
- package/src/lib/load-metaobjects-config.ts +57 -78
- package/src/lib/manifest-ignored-check.ts +14 -10
- package/src/lib/output.ts +18 -1
- package/src/lib/payload-field-tree.ts +18 -35
- package/src/lib/requirement-check.ts +21 -6
- package/src/lib/strict-args.ts +52 -0
- package/src/lib/template-eject.ts +526 -0
- package/src/lib/version.ts +18 -0
- package/dist/src/lib/docs-drift.d.ts +0 -42
- package/dist/src/lib/docs-drift.d.ts.map +0 -1
- package/dist/src/lib/docs-drift.js +0 -264
- package/dist/src/lib/docs-drift.js.map +0 -1
- package/src/lib/docs-drift.ts +0 -312
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
isMetaObject,
|
|
23
23
|
resolveColumnName,
|
|
24
|
-
|
|
24
|
+
physicalNamingOf,
|
|
25
25
|
type MetaData,
|
|
26
26
|
} from "@metaobjectsdev/metadata";
|
|
27
27
|
import {
|
|
@@ -41,7 +41,6 @@ export interface UnindexedFkFinding {
|
|
|
41
41
|
|
|
42
42
|
export interface UnindexedFkOptions {
|
|
43
43
|
dialect: Dialect;
|
|
44
|
-
columnNamingStrategy: ColumnNamingStrategy;
|
|
45
44
|
/** Skip an entity by FQN — imported (dependency) metadata is not the adopter's to index. */
|
|
46
45
|
skip?: (fqn: string) => boolean;
|
|
47
46
|
}
|
|
@@ -65,8 +64,9 @@ function pascal(s: string): string {
|
|
|
65
64
|
export function scanForUnindexedForeignKeys(root: MetaData, opts: UnindexedFkOptions): UnindexedFkFinding[] {
|
|
66
65
|
const { snapshot, provenance } = buildExpectedSchemaWithProvenance(root, {
|
|
67
66
|
dialect: opts.dialect,
|
|
68
|
-
columnNamingStrategy: opts.columnNamingStrategy,
|
|
69
67
|
});
|
|
68
|
+
// ADR-0058: the same column rule the expected schema just used, from the loaded root.
|
|
69
|
+
const columnNamingStrategy = physicalNamingOf(root).columns;
|
|
70
70
|
// ADR-0039: effective children — resolve rather than rely on root being unextended.
|
|
71
71
|
const objectsByFqn = new Map(
|
|
72
72
|
root.children().filter(isMetaObject).map((o) => [o.resolutionKey(), o] as const),
|
|
@@ -90,7 +90,7 @@ export function scanForUnindexedForeignKeys(root: MetaData, opts: UnindexedFkOpt
|
|
|
90
90
|
const ref = entity.referenceIdentities().find((r) => {
|
|
91
91
|
const cols = r.fields.map((f) => {
|
|
92
92
|
const field = entity.fields().find((x) => x.name === f);
|
|
93
|
-
return field !== undefined ? resolveColumnName(field,
|
|
93
|
+
return field !== undefined ? resolveColumnName(field, columnNamingStrategy) : f;
|
|
94
94
|
});
|
|
95
95
|
return cols.length === fk.columns.length && cols.every((c, i) => c === fk.columns[i]);
|
|
96
96
|
});
|
package/src/lib/format.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { encode } from "@toon-format/toon";
|
|
2
|
+
import { log } from "./log.js";
|
|
2
3
|
|
|
3
4
|
export type OutputFormat = "toon" | "json" | "text";
|
|
4
5
|
|
|
@@ -35,3 +36,14 @@ export function emitStructured(payload: unknown, fmt: OutputFormat): void {
|
|
|
35
36
|
if (fmt === "json") console.log(JSON.stringify(payload, null, 2));
|
|
36
37
|
else if (fmt === "toon") console.log(toonEncode(payload));
|
|
37
38
|
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* One narration line, on the other side of the split {@link emitStructured} states:
|
|
42
|
+
* stdout in text format, stderr in a structured run, so a document never has a
|
|
43
|
+
* sentence in front of it. Every command that prints prose alongside a document
|
|
44
|
+
* routes it through here.
|
|
45
|
+
*/
|
|
46
|
+
export function narrate(fmt: OutputFormat, line: string): void {
|
|
47
|
+
if (fmt === "text") log.info(line);
|
|
48
|
+
else log.warn(line);
|
|
49
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
parseWranglerConfig,
|
|
27
27
|
readSnapshot,
|
|
28
28
|
snapshotPath,
|
|
29
|
+
withoutFlywayHistory,
|
|
29
30
|
type Change,
|
|
30
31
|
type D1Binding,
|
|
31
32
|
} from "@metaobjectsdev/migrate-ts";
|
|
@@ -43,10 +44,12 @@ export interface GenMigrateAdviceInput {
|
|
|
43
44
|
dialect: "sqlite" | "postgres" | "mysql" | undefined;
|
|
44
45
|
/** `.metaobjects/config.json` `migrate.dialect`, when set (may say `d1`). */
|
|
45
46
|
migrateDialect: "sqlite" | "postgres" | "d1" | undefined;
|
|
46
|
-
columnNamingStrategy: "snake_case" | "literal" | "kebab-case" | undefined;
|
|
47
47
|
projectRoot: string;
|
|
48
48
|
/** The migrations directory `meta migrate` uses (holds the committed snapshot). */
|
|
49
49
|
migrateOutDir: string;
|
|
50
|
+
/** `.metaobjects/config.json` `migrate.format`. Under `flyway` the snapshot is read without
|
|
51
|
+
* Flyway's history table, as `meta migrate` reads it. */
|
|
52
|
+
migrateFormat: "default" | "flyway";
|
|
50
53
|
/** Paths (any form) of the files this run wrote, merged or removed. */
|
|
51
54
|
changedFiles: readonly string[];
|
|
52
55
|
}
|
|
@@ -63,7 +66,6 @@ export async function genMigrateAdvice(input: GenMigrateAdviceInput): Promise<st
|
|
|
63
66
|
try {
|
|
64
67
|
built = buildExpectedSchemaWithProvenance(input.metadata, {
|
|
65
68
|
dialect: schemaDialect,
|
|
66
|
-
...(input.columnNamingStrategy !== undefined ? { columnNamingStrategy: input.columnNamingStrategy } : {}),
|
|
67
69
|
});
|
|
68
70
|
} catch {
|
|
69
71
|
// A model `meta migrate` would refuse (it says why); gen is not the place to report it.
|
|
@@ -75,7 +77,8 @@ export async function genMigrateAdvice(input: GenMigrateAdviceInput): Promise<st
|
|
|
75
77
|
const snapPath = snapshotPath(resolvePath(input.projectRoot, input.migrateOutDir), schemaDialect);
|
|
76
78
|
const snapshot = existsSync(snapPath) ? await readSnapshot(snapPath) : null;
|
|
77
79
|
if (snapshot !== null) {
|
|
78
|
-
const
|
|
80
|
+
const committed = input.migrateFormat === "flyway" ? withoutFlywayHistory(snapshot) : snapshot;
|
|
81
|
+
const actual = { ...committed, views: [] };
|
|
79
82
|
try {
|
|
80
83
|
const d = await diff({ expected, actual, dialect: schemaDialect });
|
|
81
84
|
if (d.changes.every((c) => VIEW_CHANGE_KINDS.has(c.kind))) return undefined;
|
|
@@ -107,7 +110,6 @@ function mysqlAdvice(input: GenMigrateAdviceInput): string | undefined {
|
|
|
107
110
|
try {
|
|
108
111
|
provenance = buildExpectedSchemaWithProvenance(input.metadata, {
|
|
109
112
|
dialect: "postgres",
|
|
110
|
-
...(input.columnNamingStrategy !== undefined ? { columnNamingStrategy: input.columnNamingStrategy } : {}),
|
|
111
113
|
}).provenance;
|
|
112
114
|
} catch {
|
|
113
115
|
return undefined;
|
package/src/lib/install-set.ts
CHANGED
|
@@ -38,6 +38,19 @@ import type { PackageManifest } from "./package-manifest.js";
|
|
|
38
38
|
* never gated — `.` is the one subpath every package exports.
|
|
39
39
|
*/
|
|
40
40
|
export function peerRangesOf(packageName: string): Record<string, string> {
|
|
41
|
+
return manifestRangesOf(packageName, "peerDependencies");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The ranges a package declares for its own dependencies, or `{}` if they cannot be read.
|
|
46
|
+
* What a copied template module imports from outside the engine is read from the package it
|
|
47
|
+
* was copied from — never restated here, for the reason `peerRangesOf` gives.
|
|
48
|
+
*/
|
|
49
|
+
export function dependencyRangesOf(packageName: string): Record<string, string> {
|
|
50
|
+
return manifestRangesOf(packageName, "dependencies");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function manifestRangesOf(packageName: string, field: "peerDependencies" | "dependencies"): Record<string, string> {
|
|
41
54
|
try {
|
|
42
55
|
const req = createRequire(import.meta.url);
|
|
43
56
|
let dir = dirname(req.resolve(packageName));
|
|
@@ -50,7 +63,7 @@ export function peerRangesOf(packageName: string): Record<string, string> {
|
|
|
50
63
|
};
|
|
51
64
|
// Guard against stopping at a nested manifest that is not the package itself.
|
|
52
65
|
if (manifest.name === packageName) {
|
|
53
|
-
return (manifest
|
|
66
|
+
return (manifest[field] ?? {}) as Record<string, string>;
|
|
54
67
|
}
|
|
55
68
|
}
|
|
56
69
|
const up = dirname(dir);
|
|
@@ -125,6 +138,9 @@ export function installSetFor(
|
|
|
125
138
|
* project, and what that copy imports. Their output no longer imports the runtime package,
|
|
126
139
|
* so it is not installed on their account; the copy's own imports are, instead. */
|
|
127
140
|
ownedRuntime?: OwnedRuntimeInstall,
|
|
141
|
+
/** Third-party packages the copied template modules import, with the range the codegen
|
|
142
|
+
* package that ships them declares. Build-time, so dev. */
|
|
143
|
+
templateThirdParty: ReadonlyMap<string, string | undefined> = new Map(),
|
|
128
144
|
): InstallSet {
|
|
129
145
|
const version = cliVersion();
|
|
130
146
|
const dev = new Map<string, string | undefined>();
|
|
@@ -157,6 +173,9 @@ export function installSetFor(
|
|
|
157
173
|
for (const p of templatePackages) {
|
|
158
174
|
if (!runtime.has(p)) addPackage(dev, p, `^${version}`);
|
|
159
175
|
}
|
|
176
|
+
for (const [name, range] of templateThirdParty) {
|
|
177
|
+
if (!runtime.has(name)) addPackage(dev, name, range);
|
|
178
|
+
}
|
|
160
179
|
|
|
161
180
|
const devList = specs(dev);
|
|
162
181
|
const runtimeList = specs(runtime);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { physicalNamingGuideUrl } from "@metaobjectsdev/metadata/release-docs";
|
|
2
3
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -7,6 +8,7 @@ import { randomBytes } from "node:crypto";
|
|
|
7
8
|
import { createJiti } from "jiti";
|
|
8
9
|
import type { MetaDataTypeProvider, MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
|
|
9
10
|
import { resolveCollection, type Collection } from "@metaobjectsdev/sdk";
|
|
11
|
+
import { physicalNamingRemovedMessage } from "@metaobjectsdev/metadata";
|
|
10
12
|
|
|
11
13
|
const CONFIG_FILE = "metaobjects.config.ts";
|
|
12
14
|
|
|
@@ -132,88 +134,53 @@ function resolveCliPkg(specifier: string): string {
|
|
|
132
134
|
* jiti's alias map on non-Bun runtimes).
|
|
133
135
|
*/
|
|
134
136
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* this check the removal surfaces as a bare runtime TypeError from the transpiled config —
|
|
138
|
-
* `(0, _index2.entityFile) is not a function` — which names neither MetaObjects, nor the
|
|
139
|
-
* removal, nor the one-command remedy. A breaking change an adopter WILL hit deserves a
|
|
140
|
-
* diagnostic, and this is the only place that can give one: the subpath still exists (it is
|
|
141
|
-
* the supported home of the generators with no ownable copy), so nothing upstream of here
|
|
142
|
-
* can tell a correct import from a removed one.
|
|
137
|
+
* Generator factories `@metaobjectsdev/codegen-ts/generators` used to export and no longer
|
|
138
|
+
* does, each with what an adopter does instead.
|
|
143
139
|
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
140
|
+
* Without this, a removal an adopter WILL hit surfaces as whatever the runtime says about a
|
|
141
|
+
* missing export: `(0 , _generators.mermaidErDiagram) is not a function` from a transpiled
|
|
142
|
+
* config, or `does not provide an export named 'mermaidErDiagram'` from native ESM. Neither
|
|
143
|
+
* names MetaObjects, the removal or the remedy. This is the only place that can: the
|
|
144
|
+
* subpath still exists and serves every other factory, so nothing upstream of here can tell
|
|
145
|
+
* a correct import from a removed one.
|
|
146
|
+
*
|
|
147
|
+
* The diagnostic is recovered from the THROW, in `removedGeneratorRuntimeError`, so it
|
|
148
|
+
* answers for a live import only, wherever that import is: the config itself or a module
|
|
149
|
+
* the config imports (an owned generator under `codegen/generators/`, typically).
|
|
150
|
+
*
|
|
151
|
+
* `mermaidErDiagram` (removed in 1.1) wrote the ER diagram to a file of its own. It was the
|
|
152
|
+
* older home of a diagram the `docs` generator (`docsFile()`) draws.
|
|
152
153
|
*/
|
|
153
|
-
const REMOVED_GENERATOR_EXPORTS
|
|
154
|
+
const REMOVED_GENERATOR_EXPORTS: Readonly<Record<string, string>> = {
|
|
155
|
+
mermaidErDiagram:
|
|
156
|
+
"The `docs` generator (`docsFile()`) draws the ER diagram, for the whole model on its " +
|
|
157
|
+
"overview page (README.md) and around each entity on that entity's page.\n" +
|
|
158
|
+
" Fix: delete the import and the `mermaidErDiagram()` entry; list `docsFile()` instead, " +
|
|
159
|
+
"with a `docs` target, and run `meta gen`. " +
|
|
160
|
+
"The file it wrote (`docs/model.md` under your output directory, unless you moved it " +
|
|
161
|
+
"with `outFile`) is no longer generated; delete that too.",
|
|
162
|
+
};
|
|
154
163
|
const GENERATORS_SUBPATH = "@metaobjectsdev/codegen-ts/generators";
|
|
155
164
|
|
|
156
|
-
export function removedGeneratorImportError(source: string): string | undefined {
|
|
157
|
-
const re = new RegExp(
|
|
158
|
-
`import\\s*(?:type\\s+)?\\{([^}]*)\\}\\s*from\\s*['"]${GENERATORS_SUBPATH.replace(/[/\\]/g, "\\$&")}['"]`,
|
|
159
|
-
"g",
|
|
160
|
-
);
|
|
161
|
-
const hits = new Set<string>();
|
|
162
|
-
for (const m of source.matchAll(re)) {
|
|
163
|
-
for (const raw of (m[1] ?? "").split(",")) {
|
|
164
|
-
const name = raw.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim();
|
|
165
|
-
if (name && (REMOVED_GENERATOR_EXPORTS as readonly string[]).includes(name)) hits.add(name);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (hits.size === 0) return undefined;
|
|
169
|
-
const names = [...hits].sort();
|
|
170
|
-
const templateOf = (n: string) => n.replace(/File$/, "");
|
|
171
|
-
// One command per name: `meta eject` takes at most one generator name.
|
|
172
|
-
const commands = names.map((n) => ` meta eject ${templateOf(n)}`).join("\n");
|
|
173
|
-
const example = names[0] ?? "";
|
|
174
|
-
return (
|
|
175
|
-
`metaobjects.config.ts imports ${names.join(", ")} from "${GENERATORS_SUBPATH}". ` +
|
|
176
|
-
`1.0 REMOVED ${names.length === 1 ? "that export" : "those exports"} (ADR-0034 scaffold-and-own): ` +
|
|
177
|
-
`these generators are yours to own, so the package no longer ships them as an import.\n` +
|
|
178
|
-
` Fix — copy ${names.length === 1 ? "it" : "each of them"} into your repo:\n${commands}\n` +
|
|
179
|
-
` then import the copies, e.g. ` +
|
|
180
|
-
`import { ${example} } from "./codegen/generators/${templateOf(example)}";\n` +
|
|
181
|
-
` The subpath itself is fine — promptRender, outputParser, routesFileHono and the rest ` +
|
|
182
|
-
`still live there. Only ${names.length === 1 ? "this one" : `these ${names.length}`} moved.\n` +
|
|
183
|
-
` Guide: docs/features/migrations/0.x-to-1.0.md §11`
|
|
184
|
-
);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
165
|
/**
|
|
188
|
-
* The
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* produced it — that limitation is real, it is just not a reason to say nothing), so it
|
|
194
|
-
* names what it does know: the symbol, the removal, and the one-command remedy. Matching is
|
|
195
|
-
* on the SYMBOL, not the message shape, because the shape is the transpiler's: Bun says
|
|
196
|
-
* `(0 , _generators.entityFile) is not a function` and Node ESM says
|
|
197
|
-
* `entityFile is not a function`.
|
|
166
|
+
* The diagnostic for a throw that names a removed factory; undefined for any other throw.
|
|
167
|
+
* It cannot name the file (the error carries no reference back to the import that produced
|
|
168
|
+
* it), so it says where to look instead. Matching is on the SYMBOL and on the fact of a
|
|
169
|
+
* missing export, not on one message shape: the shape is the runtime's, and an adopter's
|
|
170
|
+
* runtime is not ours to choose.
|
|
198
171
|
*/
|
|
199
172
|
export function removedGeneratorRuntimeError(err: unknown): string | undefined {
|
|
200
173
|
const message = err instanceof Error ? err.message : String(err ?? "");
|
|
201
|
-
if (!/is not a function/.test(message))
|
|
202
|
-
|
|
174
|
+
if (!/is not a function|does not provide an export named|[Ee]xport named .* not found/.test(message)) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const hit = Object.keys(REMOVED_GENERATOR_EXPORTS).find((n) => new RegExp(`\\b${n}\\b`).test(message));
|
|
203
178
|
if (hit === undefined) return undefined;
|
|
204
|
-
const template = hit.replace(/File$/, "");
|
|
205
179
|
return (
|
|
206
|
-
`${hit}
|
|
207
|
-
`
|
|
208
|
-
`
|
|
209
|
-
|
|
210
|
-
`codegen/generators/, which is exactly what 'meta eject' writes.\n` +
|
|
211
|
-
` Fix — copy it into your repo:\n meta eject ${template}\n` +
|
|
212
|
-
` then change that module's import to: ` +
|
|
213
|
-
`import { ${hit} } from "./codegen/generators/${template}";\n` +
|
|
214
|
-
` The subpath itself is fine — promptRender, outputParser, routesFileHono and the ` +
|
|
215
|
-
`rest still live there.\n` +
|
|
216
|
-
` Guide: docs/features/migrations/0.x-to-1.0.md §11`
|
|
180
|
+
`${hit} is no longer exported from "${GENERATORS_SUBPATH}": 1.1 removed that generator. ` +
|
|
181
|
+
`The import is in your metaobjects.config.ts or in a module the config imports, most ` +
|
|
182
|
+
`often one of your own under codegen/generators/. ` +
|
|
183
|
+
`${REMOVED_GENERATOR_EXPORTS[hit] ?? ""}`
|
|
217
184
|
);
|
|
218
185
|
}
|
|
219
186
|
|
|
@@ -342,6 +309,14 @@ export async function resolveGenCollection(
|
|
|
342
309
|
}
|
|
343
310
|
}
|
|
344
311
|
|
|
312
|
+
/**
|
|
313
|
+
* A `metaobjects.config.ts` that still sets column naming (ADR-0058). Its own class because
|
|
314
|
+
* the commands that tolerate a missing or broken config (`meta migrate` falls back to
|
|
315
|
+
* defaults) must NOT tolerate this one: falling back to the default rule would propose
|
|
316
|
+
* renaming every column of a database built with the old value.
|
|
317
|
+
*/
|
|
318
|
+
export class RemovedNamingConfigError extends Error {}
|
|
319
|
+
|
|
345
320
|
export async function loadMetaobjectsConfig(projectRoot: string): Promise<MetaobjectsGenConfig> {
|
|
346
321
|
const fullPath = resolve(projectRoot, CONFIG_FILE);
|
|
347
322
|
if (!existsSync(fullPath)) {
|
|
@@ -450,8 +425,6 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
|
|
|
450
425
|
// a belt-and-suspenders fallback for runtimes where jiti's transformer IS
|
|
451
426
|
// active, but under Bun the pre-processed source is the operative fix.
|
|
452
427
|
const original = await readFile(fullPath, "utf8");
|
|
453
|
-
const removed = removedGeneratorImportError(original);
|
|
454
|
-
if (removed !== undefined) throw new Error(removed);
|
|
455
428
|
const processed = rewriteImportSpecifiers(original, aliasMap);
|
|
456
429
|
|
|
457
430
|
// When any specifiers were rewritten, write the modified source to a temp
|
|
@@ -501,10 +474,6 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
|
|
|
501
474
|
try {
|
|
502
475
|
raw = (await jiti.import(loadPath)) as MetaobjectsGenConfig | { default: MetaobjectsGenConfig };
|
|
503
476
|
} catch (err) {
|
|
504
|
-
// The second arm of the ADR-0034 diagnostic: a removed export imported by a module the
|
|
505
|
-
// config imports rather than by the config itself. Rethrown as the migration message,
|
|
506
|
-
// because what reaches the adopter otherwise is `(0 , _generators.entityFile) is not a
|
|
507
|
-
// function` — the exact unactionable failure the source scan above exists to prevent.
|
|
508
477
|
const runtime = removedGeneratorRuntimeError(err);
|
|
509
478
|
if (runtime !== undefined) throw new Error(runtime);
|
|
510
479
|
throw err;
|
|
@@ -531,6 +500,16 @@ export async function loadMetaobjectsConfig(projectRoot: string): Promise<Metaob
|
|
|
531
500
|
`verbatim — the tokens are unchanged.`,
|
|
532
501
|
);
|
|
533
502
|
}
|
|
503
|
+
// ADR-0058 — column naming moved to `naming` in .metaobjects/config.json, the one
|
|
504
|
+
// setting every port reads. Refused, never dropped: the old value may be the one that
|
|
505
|
+
// matches this project's database, and dropping it would rename every column.
|
|
506
|
+
const removedNaming = (cfg as unknown as Record<string, unknown>).columnNamingStrategy;
|
|
507
|
+
if (removedNaming !== undefined) {
|
|
508
|
+
throw new RemovedNamingConfigError(
|
|
509
|
+
`metaobjects.config.ts at ${fullPath}: ` +
|
|
510
|
+
physicalNamingRemovedMessage("columnNamingStrategy in metaobjects.config.ts", String(removedNaming), physicalNamingGuideUrl()),
|
|
511
|
+
);
|
|
512
|
+
}
|
|
534
513
|
return cfg;
|
|
535
514
|
} finally {
|
|
536
515
|
// Restoring is safe: Babel's installer self-neuters after the first call, so
|
|
@@ -30,15 +30,18 @@ export const HASH_MANIFEST_REL = join(".metaobjects", ".gen-state", ".hashes.jso
|
|
|
30
30
|
* Silent when: it is tracked correctly, the project is not a git repository, git is
|
|
31
31
|
* unavailable, or no generated output has ever been produced (nothing to protect yet).
|
|
32
32
|
* Never throws, never blocks.
|
|
33
|
+
*
|
|
34
|
+
* Returns the line it printed, so a command with a structured result can carry it
|
|
35
|
+
* there too; undefined when it said nothing.
|
|
33
36
|
*/
|
|
34
|
-
export function warnIfManifestIgnored(cwd: string):
|
|
37
|
+
export function warnIfManifestIgnored(cwd: string): string | undefined {
|
|
35
38
|
// Nothing generated yet ⇒ nothing at risk ⇒ nothing worth saying.
|
|
36
|
-
if (!existsSync(join(cwd, HASH_MANIFEST_REL))) return;
|
|
39
|
+
if (!existsSync(join(cwd, HASH_MANIFEST_REL))) return undefined;
|
|
37
40
|
|
|
38
41
|
// honourGlobalExcludes: TRUE here, and that is the opposite call to the docs gate.
|
|
39
42
|
// The question this warning asks is "will this file reach another machine?", and a
|
|
40
43
|
// per-user ignore file answers it just as truly as a committed .gitignore does.
|
|
41
|
-
if (isGitIgnored(cwd, HASH_MANIFEST_REL, { honourGlobalExcludes: true }) !== true) return;
|
|
44
|
+
if (isGitIgnored(cwd, HASH_MANIFEST_REL, { honourGlobalExcludes: true }) !== true) return undefined;
|
|
42
45
|
|
|
43
46
|
// WHERE the rule actually lives, asked of git rather than assumed. This used to name
|
|
44
47
|
// `.metaobjects/.gitignore` and the literal pattern `.gen-state/`, which is only one of
|
|
@@ -56,12 +59,13 @@ export function warnIfManifestIgnored(cwd: string): void {
|
|
|
56
59
|
: `Find the rule with 'git check-ignore -v ${HASH_MANIFEST_REL}' and replace it with ` +
|
|
57
60
|
`a '/*' form plus a '!' negation for the manifest`;
|
|
58
61
|
|
|
59
|
-
|
|
62
|
+
const msg =
|
|
60
63
|
`${HASH_MANIFEST_REL} is git-ignored, so it never reaches another machine — ` +
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
);
|
|
64
|
+
`on a fresh clone or CI runner 'meta gen' cannot tell your hand edits from its ` +
|
|
65
|
+
`own stale output, and will refuse to overwrite rather than guess. ${where} ` +
|
|
66
|
+
`(the glob matters — git will not descend into an excluded directory, so a negation ` +
|
|
67
|
+
`INSIDE an excluded directory can never apply), then commit the manifest. ` +
|
|
68
|
+
`If you deliberately do not commit generated output, ignore this.`;
|
|
69
|
+
log.warn(msg);
|
|
70
|
+
return msg;
|
|
67
71
|
}
|
package/src/lib/output.ts
CHANGED
|
@@ -203,6 +203,13 @@ export interface MigrateResultShape {
|
|
|
203
203
|
* caller sees the risk before applying.
|
|
204
204
|
*/
|
|
205
205
|
warnings?: string[];
|
|
206
|
+
/**
|
|
207
|
+
* Explanations of planned changes that are not what they look like — today, views the
|
|
208
|
+
* migration drops and recreates only because it alters a table they read (D3).
|
|
209
|
+
*/
|
|
210
|
+
notes?: string[];
|
|
211
|
+
/** A dry run's SQL. Text format prints it as a preview; JSON/toon carry it here. */
|
|
212
|
+
sql?: { up: string; down: string };
|
|
206
213
|
}
|
|
207
214
|
|
|
208
215
|
export function formatMigrateResult(result: MigrateResultShape, _opts: FormatOptions): string {
|
|
@@ -219,6 +226,12 @@ export function formatMigrateResult(result: MigrateResultShape, _opts: FormatOpt
|
|
|
219
226
|
lines.push(` Changes: ${summary}`, "");
|
|
220
227
|
}
|
|
221
228
|
|
|
229
|
+
const notes = result.notes ?? [];
|
|
230
|
+
if (notes.length > 0) {
|
|
231
|
+
for (const n of notes) lines.push(` Note: ${n}`);
|
|
232
|
+
lines.push("");
|
|
233
|
+
}
|
|
234
|
+
|
|
222
235
|
if (result.blocked.length > 0) {
|
|
223
236
|
const anyRename = result.blocked.some((b) => b.renameFlag !== undefined);
|
|
224
237
|
lines.push(anyRename ? " Blocked (re-run with the flag shown):" : " Blocked (re-run with --allow):");
|
|
@@ -292,7 +305,7 @@ export function genResultToData(result: GenResultShape): {
|
|
|
292
305
|
const help = result.files.length !== 0
|
|
293
306
|
? ["typecheck the generated code with `npx tsc`", ...(result.migrateAdvice !== undefined ? [result.migrateAdvice] : [])]
|
|
294
307
|
: noGenerators
|
|
295
|
-
? ["add generators to the `generators: []` array in metaobjects.config.ts — `meta
|
|
308
|
+
? ["add generators to the `generators: []` array in metaobjects.config.ts — `meta gen --list` names each one with the import that runs it as shipped, and `meta eject <name>` copies one into your repo to change what it emits"]
|
|
296
309
|
: ["author entities in this project's metadata sources then re-run `meta gen`"];
|
|
297
310
|
// An absent section is STATED, never omitted: a reader must be able to tell
|
|
298
311
|
// "the scan found nothing" from "the scan never ran".
|
|
@@ -329,6 +342,8 @@ export function migrateResultToData(result: MigrateResultShape): {
|
|
|
329
342
|
summary: string;
|
|
330
343
|
help: string[];
|
|
331
344
|
warnings?: string[];
|
|
345
|
+
notes?: string[];
|
|
346
|
+
sql?: { up: string; down: string };
|
|
332
347
|
} {
|
|
333
348
|
const changeEntries = Object.entries(result.changeCounts).filter(([, v]) => v > 0);
|
|
334
349
|
const changes = changeEntries.map(([kind, count]) => ({ kind, count }));
|
|
@@ -395,6 +410,8 @@ export function migrateResultToData(result: MigrateResultShape): {
|
|
|
395
410
|
changes, written: result.writtenPaths, summary, help,
|
|
396
411
|
// Only when present, so a run with no hazard keeps its existing shape.
|
|
397
412
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
413
|
+
...((result.notes ?? []).length > 0 ? { notes: result.notes } : {}),
|
|
414
|
+
...(result.sql !== undefined ? { sql: result.sql } : {}),
|
|
398
415
|
};
|
|
399
416
|
}
|
|
400
417
|
|
|
@@ -7,54 +7,37 @@
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
type MetaData,
|
|
10
|
-
TYPE_FIELD,
|
|
11
10
|
FIELD_SUBTYPE_OBJECT,
|
|
12
11
|
FIELD_ATTR_OBJECT_REF,
|
|
13
|
-
|
|
12
|
+
isMetaField,
|
|
13
|
+
refTarget,
|
|
14
14
|
} from "@metaobjectsdev/metadata";
|
|
15
15
|
import type { PayloadField } from "@metaobjectsdev/render";
|
|
16
16
|
|
|
17
|
-
function findObject(root: MetaData, name: string, referrerPkg: string): MetaData | undefined {
|
|
18
|
-
// ADR-0039: effective children — resolve rather than rely on root being unextended.
|
|
19
|
-
// ADR-0042: package-local — an FQN @objectRef binds the exact package; a bare ref
|
|
20
|
-
// binds the referrer's package FIRST (else root-level), never a same-named object.value
|
|
21
|
-
// in another package. The SAME resolver render-helper.ts's findObject uses (must not drift).
|
|
22
|
-
return resolveObjectRef(root, name, referrerPkg).node;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
17
|
/**
|
|
26
18
|
* Walk an `object.value` view-object into a render `PayloadField[]`. Object-ref
|
|
27
|
-
* fields recurse into
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* for a nested recursion), so a bare ref resolves package-locally.
|
|
19
|
+
* fields recurse into the view-object the loader bound for their `@objectRef`
|
|
20
|
+
* (ADR-0059 R7); a `seen` set guards against a (pathological) reference cycle.
|
|
21
|
+
* Returns `[]` when `vo` is undefined (the ref did not resolve).
|
|
31
22
|
*/
|
|
32
23
|
export function derivePayloadFieldTree(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
referrerPkg = "",
|
|
36
|
-
seen: ReadonlySet<string> = new Set(),
|
|
24
|
+
vo: MetaData | undefined,
|
|
25
|
+
seen: ReadonlySet<MetaData> = new Set(),
|
|
37
26
|
): PayloadField[] {
|
|
38
|
-
if (seen.has(
|
|
39
|
-
const
|
|
40
|
-
if (!vo) return [];
|
|
41
|
-
// A nested @objectRef resolves in the FIELD's own declaring package (fallback
|
|
42
|
-
// below), which differs from this VO's when the field is inherited via extends
|
|
43
|
-
// from an abstract VO in another package (the bare ref was authored there).
|
|
44
|
-
const voPkg = vo.package ?? vo.fileDefaultPackage ?? "";
|
|
45
|
-
const nextSeen = new Set(seen).add(voName);
|
|
27
|
+
if (vo === undefined || seen.has(vo)) return [];
|
|
28
|
+
const nextSeen = new Set(seen).add(vo);
|
|
46
29
|
const fields: PayloadField[] = [];
|
|
47
|
-
for (const f of vo.children().filter(
|
|
48
|
-
|
|
30
|
+
for (const f of vo.children().filter(isMetaField)) {
|
|
31
|
+
// ADR-0039: effective array-ness — `isArray` may be inherited via extends. Carried only
|
|
32
|
+
// when true, so verify can tell a list of objects from a single object and a list of
|
|
33
|
+
// scalars from a scalar.
|
|
34
|
+
const arrayness = f.resolvedIsArray() ? { isArray: true } : {};
|
|
35
|
+
if (f.subType === FIELD_SUBTYPE_OBJECT && typeof f.attr(FIELD_ATTR_OBJECT_REF) === "string") {
|
|
49
36
|
// ADR-0039: effective attr — @objectRef may be inherited via extends.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const fieldPkg = f.parent?.package ?? f.parent?.fileDefaultPackage ?? voPkg;
|
|
53
|
-
fields.push({ name: f.name, fields: derivePayloadFieldTree(root, ref, fieldPkg, nextSeen) });
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
37
|
+
fields.push({ name: f.name, fields: derivePayloadFieldTree(refTarget(f, FIELD_ATTR_OBJECT_REF), nextSeen), ...arrayness });
|
|
38
|
+
continue;
|
|
56
39
|
}
|
|
57
|
-
fields.push({ name: f.name });
|
|
40
|
+
fields.push({ name: f.name, ...arrayness });
|
|
58
41
|
}
|
|
59
42
|
return fields;
|
|
60
43
|
}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
// One shared resolver (FR-038): codegen's requirement-test fan-out needs the same
|
|
37
37
|
// ADR-0042 package-local binding, so these moved to @metaobjectsdev/metadata rather
|
|
38
38
|
// than being reimplemented there.
|
|
39
|
+
effectiveRequirementPackage,
|
|
39
40
|
resolveClaimTarget,
|
|
40
41
|
resolveMember,
|
|
41
42
|
didYouMeanHint,
|
|
@@ -233,7 +234,7 @@ export function resolveRequirementRef(
|
|
|
233
234
|
): MetaRequirement | undefined {
|
|
234
235
|
const keyed = new Map<string, MetaRequirement>();
|
|
235
236
|
for (const { node, path } of addressed) {
|
|
236
|
-
const pkg = node
|
|
237
|
+
const pkg = effectiveRequirementPackage(node);
|
|
237
238
|
if (pkg !== "") keyed.set(`${pkg}::${path}`, node);
|
|
238
239
|
// Bare path is registered too, so a single-package ledger can reference
|
|
239
240
|
// without repeating its own package on every line.
|
|
@@ -279,6 +280,11 @@ export interface RequirementScan {
|
|
|
279
280
|
* printed a ratio the gate had not enforced would be a measurement nobody could
|
|
280
281
|
* reconcile with the diagnostics beneath it. */
|
|
281
282
|
readonly measureCoverage: boolean;
|
|
283
|
+
/** ADR-0057 — the strict switch. When true, the functional EXISTENCE finding
|
|
284
|
+
* (`WARN_REQUIREMENT_NOTHING_IMPLEMENTS`) is reported at severity `error`; the code,
|
|
285
|
+
* path and message do not change, and no other diagnostic is touched. Carried on
|
|
286
|
+
* the scan, like `measureCoverage`, so a caller decides it once per run. */
|
|
287
|
+
readonly requireImplementers: boolean;
|
|
282
288
|
}
|
|
283
289
|
|
|
284
290
|
/**
|
|
@@ -306,7 +312,7 @@ export interface RequirementScan {
|
|
|
306
312
|
function projectAuthoredRequirements(addressed: readonly AddressedRequirement[]): boolean {
|
|
307
313
|
const libPkgs = libraryPackages();
|
|
308
314
|
return addressed.some((r) => {
|
|
309
|
-
const pkg = r.node
|
|
315
|
+
const pkg = effectiveRequirementPackage(r.node);
|
|
310
316
|
return !libPkgs.has(pkg);
|
|
311
317
|
});
|
|
312
318
|
}
|
|
@@ -321,6 +327,8 @@ export function scanRequirements(
|
|
|
321
327
|
* by its own ledger" is exactly what is being asserted — so the derivation would
|
|
322
328
|
* switch the check off precisely where it is the point. */
|
|
323
329
|
measureCoverage?: boolean;
|
|
330
|
+
/** Raise WARN_REQUIREMENT_NOTHING_IMPLEMENTS to severity "error". Default false. */
|
|
331
|
+
requireImplementers?: boolean;
|
|
324
332
|
},
|
|
325
333
|
): RequirementScan {
|
|
326
334
|
const addressed = collectAddressedRequirements(root);
|
|
@@ -328,6 +336,7 @@ export function scanRequirements(
|
|
|
328
336
|
addressed,
|
|
329
337
|
claimedObjects: claimedObjectKeys(root, addressed.map((r) => r.node)),
|
|
330
338
|
measureCoverage: opts?.measureCoverage ?? projectAuthoredRequirements(addressed),
|
|
339
|
+
requireImplementers: opts?.requireImplementers ?? false,
|
|
331
340
|
// `exactOptionalPropertyTypes` — an omitted key, never an explicit `undefined`.
|
|
332
341
|
...(opts?.coverable !== undefined ? { coverable: opts.coverable } : {}),
|
|
333
342
|
};
|
|
@@ -353,7 +362,7 @@ function claimedObjectKeys(root: MetaData, reqs: MetaRequirement[]): Set<string>
|
|
|
353
362
|
// referrerPkg is the requirement's own effective package, so a bare ref
|
|
354
363
|
// binds package-locally under the ADR-0042 contract — the loader's own
|
|
355
364
|
// resolver, never a parallel name scan (#228).
|
|
356
|
-
const referrerPkg = req
|
|
365
|
+
const referrerPkg = effectiveRequirementPackage(req);
|
|
357
366
|
for (const ref of req.implementedBy()) {
|
|
358
367
|
const { owner, path } = splitMemberRef(ref);
|
|
359
368
|
const node = resolveClaimTarget(root, owner, referrerPkg);
|
|
@@ -471,7 +480,7 @@ export function checkRequirements(root: MetaData, scan: RequirementScan = scanRe
|
|
|
471
480
|
// referrerPkg is the requirement's own effective package, so a bare ref
|
|
472
481
|
// binds package-locally under the ADR-0042 contract — the loader's own
|
|
473
482
|
// resolver, never a parallel name scan (#228).
|
|
474
|
-
const referrerPkg = req
|
|
483
|
+
const referrerPkg = effectiveRequirementPackage(req);
|
|
475
484
|
const node = resolveClaimTarget(root, owner, referrerPkg);
|
|
476
485
|
const isObjectRef = path.length === 0;
|
|
477
486
|
|
|
@@ -536,7 +545,7 @@ export function checkRequirements(root: MetaData, scan: RequirementScan = scanRe
|
|
|
536
545
|
// requirement's own effective package, exactly as @implementedBy does.
|
|
537
546
|
const superseded = req.supersededBy();
|
|
538
547
|
if (superseded !== undefined) {
|
|
539
|
-
const referrerPkg = req
|
|
548
|
+
const referrerPkg = effectiveRequirementPackage(req);
|
|
540
549
|
if (resolveRequirementRef(addressed, superseded, referrerPkg) === undefined) {
|
|
541
550
|
out.push({
|
|
542
551
|
severity: "error", code: ERR_REQUIREMENT_DANGLING_REF, path: reqPath,
|
|
@@ -593,9 +602,15 @@ export function checkRequirements(root: MetaData, scan: RequirementScan = scanRe
|
|
|
593
602
|
// tree. So the question is not "does this node claim anything" but "does
|
|
594
603
|
// anything in this subtree claim anything". A live L1 whose entire subtree
|
|
595
604
|
// is empty is a capability declared and built by nobody.
|
|
605
|
+
//
|
|
606
|
+
// The strict switch (ADR-0057, `meta verify --require-implementers`) raises the
|
|
607
|
+
// SEVERITY for a project whose ledger has caught up with its links. The code
|
|
608
|
+
// keeps its `WARN_` name on purpose: it identifies the finding, and one finding
|
|
609
|
+
// under two codes would split every suppression and corpus case that keys on it.
|
|
596
610
|
if (!architectural && live && !subtreeClaimsAnything(req)) {
|
|
597
611
|
out.push({
|
|
598
|
-
severity: "
|
|
612
|
+
severity: scan.requireImplementers ? "error" : "warn",
|
|
613
|
+
code: WARN_REQUIREMENT_NOTHING_IMPLEMENTS, path: reqPath,
|
|
599
614
|
message: `is '${String(status)}' but neither it nor anything nested under it names an ` +
|
|
600
615
|
`implementing node. A functional requirement's check is existence — a subtree that claims ` +
|
|
601
616
|
`nothing is a capability nobody built.`,
|