@metaobjectsdev/codegen-ts 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/dist/catalog-gates.d.ts +52 -0
- package/dist/catalog-gates.d.ts.map +1 -0
- package/dist/catalog-gates.js +183 -0
- package/dist/catalog-gates.js.map +1 -0
- package/dist/constants.d.ts +24 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +26 -0
- package/dist/constants.js.map +1 -1
- package/dist/enum-shared.d.ts +9 -2
- package/dist/enum-shared.d.ts.map +1 -1
- package/dist/enum-shared.js +12 -3
- package/dist/enum-shared.js.map +1 -1
- package/dist/generator-registry.d.ts +73 -0
- package/dist/generator-registry.d.ts.map +1 -1
- package/dist/generator-registry.js +182 -38
- package/dist/generator-registry.js.map +1 -1
- package/dist/generator.d.ts +48 -1
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/entity-file.d.ts.map +1 -1
- package/dist/generators/entity-file.js +4 -1
- package/dist/generators/entity-file.js.map +1 -1
- package/dist/generators/index.d.ts +1 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +5 -0
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/shared-model-file.d.ts +23 -0
- package/dist/generators/shared-model-file.d.ts.map +1 -0
- package/dist/generators/shared-model-file.js +235 -0
- package/dist/generators/shared-model-file.js.map +1 -0
- package/dist/generators/trace-helper-file.d.ts.map +1 -1
- package/dist/generators/trace-helper-file.js +54 -11
- package/dist/generators/trace-helper-file.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +0 -15
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +111 -4
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/relation-resolver.d.ts.map +1 -1
- package/dist/relation-resolver.js +11 -9
- package/dist/relation-resolver.js.map +1 -1
- package/dist/routes-expose.d.ts +30 -0
- package/dist/routes-expose.d.ts.map +1 -1
- package/dist/routes-expose.js +46 -0
- package/dist/routes-expose.js.map +1 -1
- package/dist/runner.d.ts +29 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +38 -1
- package/dist/runner.js.map +1 -1
- package/dist/templates/entity-file.js +2 -2
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/enums-file.d.ts +11 -1
- package/dist/templates/enums-file.d.ts.map +1 -1
- package/dist/templates/enums-file.js +10 -2
- package/dist/templates/enums-file.js.map +1 -1
- package/dist/templates/queries-file.js +5 -5
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/routes-file-hono.d.ts.map +1 -1
- package/dist/templates/routes-file-hono.js +10 -3
- package/dist/templates/routes-file-hono.js.map +1 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +21 -4
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/value-object-file.js +2 -2
- package/dist/templates/value-object-file.js.map +1 -1
- package/package.json +6 -6
- package/src/catalog-gates.ts +222 -0
- package/src/constants.ts +27 -0
- package/src/enum-shared.ts +17 -3
- package/src/generator-registry.ts +253 -41
- package/src/generator.ts +48 -1
- package/src/generators/entity-file.ts +7 -1
- package/src/generators/index.ts +11 -0
- package/src/generators/shared-model-file.ts +292 -0
- package/src/generators/trace-helper-file.ts +55 -12
- package/src/index.ts +17 -3
- package/src/metaobjects-config.ts +0 -15
- package/src/projection/extract-view-spec.ts +126 -5
- package/src/reference/entity.ts +12 -3
- package/src/reference/queries.ts +2 -2
- package/src/relation-resolver.ts +14 -8
- package/src/routes-expose.ts +57 -0
- package/src/runner.ts +77 -1
- package/src/templates/entity-file.ts +2 -2
- package/src/templates/enums-file.ts +13 -2
- package/src/templates/queries-file.ts +5 -5
- package/src/templates/routes-file-hono.ts +10 -3
- package/src/templates/routes-file.ts +21 -4
- package/src/templates/value-object-file.ts +2 -2
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// FR-023 §4.3 — sharedModelFile(): the publisher's flattened shared-model artifact.
|
|
2
|
+
//
|
|
3
|
+
// A publisher project wires this generator to select a subset of its own metadata
|
|
4
|
+
// (by the same include/exclude scope-pattern grammar `scope` already uses) and emit
|
|
5
|
+
// it as ONE canonical-JSON `metadata.root` document plus a manifest — the artifact a
|
|
6
|
+
// consumer's `meta deps sync` copies into its own repo (design §2.2, §3.2, §3.5).
|
|
7
|
+
//
|
|
8
|
+
// The algorithm (design §4.3), in order:
|
|
9
|
+
// 1. Standalone load of `files` (default: the run's own source files, `ctx.sourceFiles`)
|
|
10
|
+
// with `ctx.registry` (the run's composed registry — the SAME vocabulary the
|
|
11
|
+
// project's own metadata loaded with), strict.
|
|
12
|
+
// 2. Select top-level nodes: never `requirement.*` / `template.*` (a publisher's
|
|
13
|
+
// ledger and prompts are not a consumer's shapes), and matching the compiled
|
|
14
|
+
// include/exclude scope.
|
|
15
|
+
// 3. Closure check: every `extends` target and every REF_BEARING_ATTR_NAMES value
|
|
16
|
+
// of a selected node (and of its own descendants) must resolve to a SELECTED
|
|
17
|
+
// node, else the selection is not self-contained and the build fails naming the
|
|
18
|
+
// (referrer, target) pairs.
|
|
19
|
+
// 4. Serialize the selection with `serializeSharedDocument` — raw own-layer form,
|
|
20
|
+
// `extends` preserved, sorted by resolution key.
|
|
21
|
+
// 5. Re-load the emitted document standalone with CORE providers only, strict — a
|
|
22
|
+
// consumer loads it with ITS providers, and a Phase 1 export must not need the
|
|
23
|
+
// publisher's non-core vocabulary.
|
|
24
|
+
// 6. Emit `<name>.metaobjects.json` + `metaobjects.pkg.json` (design §3.2).
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
27
|
+
import { dirname, join } from "node:path";
|
|
28
|
+
import {
|
|
29
|
+
type MetaData,
|
|
30
|
+
type MetaRoot,
|
|
31
|
+
MetaDataLoader,
|
|
32
|
+
InMemoryStringSource,
|
|
33
|
+
compileScope,
|
|
34
|
+
matchesScope,
|
|
35
|
+
REF_BEARING_ATTR_NAMES,
|
|
36
|
+
resolveObjectRef,
|
|
37
|
+
serializeSharedDocument,
|
|
38
|
+
packageOfResolutionKey,
|
|
39
|
+
PACKAGE_SEPARATOR,
|
|
40
|
+
CHILD_REF_SEPARATOR,
|
|
41
|
+
TYPE_REQUIREMENT,
|
|
42
|
+
TYPE_TEMPLATE,
|
|
43
|
+
METAMODEL_VERSION,
|
|
44
|
+
} from "@metaobjectsdev/metadata";
|
|
45
|
+
import { FileSource } from "@metaobjectsdev/metadata/core";
|
|
46
|
+
import { oncePerRun, type Generator, type GenContext, type EmittedFile } from "../generator.js";
|
|
47
|
+
import { effectivePackage } from "../docs-paths.js";
|
|
48
|
+
|
|
49
|
+
// The FR-023 manifest/artifact tool-file conventions (design §3.2, §4.3, "Hash
|
|
50
|
+
// format"). Their single canonical source is `sdk/src/dependencies.ts`
|
|
51
|
+
// (`MANIFEST_FILE`, `ARTIFACT_SUFFIX`, `INTEGRITY_PREFIX`) — codegen-ts cannot
|
|
52
|
+
// import sdk (the dependency runs the other way: `cli` depends on both, design
|
|
53
|
+
// §4.3's closing note), so these mirror those values locally. Keep in sync —
|
|
54
|
+
// exported (not merely local) so `cli`, which depends on both packages, can
|
|
55
|
+
// assert equality against `sdk`'s exports in a test; see
|
|
56
|
+
// `cli/test/shared-model-constants-parity.test.ts`.
|
|
57
|
+
export const MANIFEST_FILE = "metaobjects.pkg.json";
|
|
58
|
+
export const ARTIFACT_SUFFIX = ".metaobjects.json";
|
|
59
|
+
export const INTEGRITY_PREFIX = "sha256-";
|
|
60
|
+
/** design §3.2 — the manifest schema's own version, distinct from `metamodelVersion`. */
|
|
61
|
+
const MANIFEST_SCHEMA_VERSION = 1;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Separator for `checkClosure`'s dedupe key. A resolution key never contains a NUL,
|
|
65
|
+
* so this cannot collide with either half it joins — unlike a printable separator
|
|
66
|
+
* (`->`, `→`), which would need a separate argument for why no legal FQN could ever
|
|
67
|
+
* contain it. Written as the `\0` ESCAPE, never a raw control byte: a raw 0x00 in
|
|
68
|
+
* this source file previously made `git` treat it as binary, which (a) makes
|
|
69
|
+
* `git diff`/GitHub show no content for the file at all, and (b) more seriously,
|
|
70
|
+
* makes `.githooks/pre-commit`'s public-repo-hygiene grep skip its added lines
|
|
71
|
+
* entirely (a binary file's diff is never text-enumerated) — silently defeating that
|
|
72
|
+
* guard for any future edit to this file. The escape produces the byte-identical
|
|
73
|
+
* runtime string; only the SOURCE FILE's byte content changes (readable text instead
|
|
74
|
+
* of an embedded control byte).
|
|
75
|
+
*/
|
|
76
|
+
const DEDUPE_KEY_SEPARATOR = "\0";
|
|
77
|
+
|
|
78
|
+
export interface SharedModelFileOpts {
|
|
79
|
+
/** The dependency key a consumer will `meta deps sync` under. Also the artifact's
|
|
80
|
+
* basename: emits `<name>.metaobjects.json`. */
|
|
81
|
+
name: string;
|
|
82
|
+
/** Scope-pattern include list (the same `acme::platform::**` grammar as `scope`). */
|
|
83
|
+
include: readonly string[];
|
|
84
|
+
/** Applied after `include`. */
|
|
85
|
+
exclude?: readonly string[];
|
|
86
|
+
/** Which of the publisher's own source files may contribute. Default: the run's
|
|
87
|
+
* own source files (`ctx.sourceFiles` — everything `files?` narrows). */
|
|
88
|
+
files?: readonly string[];
|
|
89
|
+
/** The published version. Default: the nearest `package.json` `version` walking
|
|
90
|
+
* up from `ctx.projectRoot`. */
|
|
91
|
+
version?: string;
|
|
92
|
+
/** Named output target, as on every other generator. */
|
|
93
|
+
target?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Everything before the first `CHILD_REF_SEPARATOR` that follows the LAST
|
|
97
|
+
* `PACKAGE_SEPARATOR` — the object part of a possibly dotted ref (ADR-0029's
|
|
98
|
+
* addressing model: package segments never fold onto a non-root child, so a
|
|
99
|
+
* `.` before the final `::`-segment can't occur). Mirrors the private
|
|
100
|
+
* `splitChildTail` in `naming-refs.ts` (not exported); `extends` and the
|
|
101
|
+
* origin `@from`/`@of`/`@via` attrs are the only REF_BEARING_ATTR_NAMES that
|
|
102
|
+
* can carry a dotted child tail — every other ref-bearing attr is already a
|
|
103
|
+
* bare object ref, and stripping a tail that isn't there is a no-op. */
|
|
104
|
+
function refOwner(ref: string): string {
|
|
105
|
+
const lastSep = ref.lastIndexOf(PACKAGE_SEPARATOR);
|
|
106
|
+
const segStart = lastSep === -1 ? 0 : lastSep + PACKAGE_SEPARATOR.length;
|
|
107
|
+
const dotInSeg = ref.indexOf(CHILD_REF_SEPARATOR, segStart);
|
|
108
|
+
return dotInSeg === -1 ? ref : ref.slice(0, dotInSeg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Depth-first walk of a node and its OWN descendants, node itself first. ADR-0039
|
|
112
|
+
* sanctioned own-only case: `serializeSharedDocument` emits each selected node's
|
|
113
|
+
* OWN layer only (`extends` preserved, not flattened — design §3.5), so the
|
|
114
|
+
* closure check must walk exactly what will be emitted, never what `extends`
|
|
115
|
+
* would additionally contribute (that is the base's own layer, checked
|
|
116
|
+
* separately if the base is itself selected). */
|
|
117
|
+
function walkOwn(node: MetaData): MetaData[] {
|
|
118
|
+
const out: MetaData[] = [node];
|
|
119
|
+
for (const child of node.ownChildren()) out.push(...walkOwn(child));
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
interface ClosurePair {
|
|
124
|
+
readonly referrer: string;
|
|
125
|
+
readonly target: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** design §4.3 step 3. `standaloneRoot` already loaded successfully (step 1), so
|
|
129
|
+
* every ref found here is guaranteed to RESOLVE to some node in it — the loader's
|
|
130
|
+
* own validation passes reject a dangling `extends`/object-ref before this ever
|
|
131
|
+
* runs. What is NOT guaranteed is that the resolved node is in `selectedKeys`. */
|
|
132
|
+
function checkClosure(
|
|
133
|
+
standaloneRoot: MetaRoot,
|
|
134
|
+
selected: readonly MetaData[],
|
|
135
|
+
selectedKeys: ReadonlySet<string>,
|
|
136
|
+
): ClosurePair[] {
|
|
137
|
+
const pairs: ClosurePair[] = [];
|
|
138
|
+
const seen = new Set<string>();
|
|
139
|
+
for (const top of selected) {
|
|
140
|
+
const referrerKey = top.resolutionKey();
|
|
141
|
+
for (const node of walkOwn(top)) {
|
|
142
|
+
const referrerPkg = effectivePackage(node) ?? "";
|
|
143
|
+
const candidates: string[] = [];
|
|
144
|
+
if (node.superRef !== undefined) candidates.push(node.superRef);
|
|
145
|
+
// ADR-0039 sanctioned own-only case (same rationale as walkOwn() above): an
|
|
146
|
+
// INHERITED ref-bearing attr belongs to the base node's own layer, not this
|
|
147
|
+
// one's, and is checked when the base itself is walked — reading it via the
|
|
148
|
+
// resolving `attr()` here would attribute the base's reference to this node.
|
|
149
|
+
for (const attrName of REF_BEARING_ATTR_NAMES) {
|
|
150
|
+
const value = node.ownAttr(attrName);
|
|
151
|
+
if (typeof value === "string") candidates.push(value);
|
|
152
|
+
}
|
|
153
|
+
for (const raw of candidates) {
|
|
154
|
+
const resolved = resolveObjectRef(standaloneRoot, refOwner(raw), referrerPkg).node;
|
|
155
|
+
if (resolved === undefined) continue; // already validated at load — defensive only
|
|
156
|
+
const targetKey = resolved.resolutionKey();
|
|
157
|
+
if (selectedKeys.has(targetKey)) continue;
|
|
158
|
+
const dedupeKey = `${referrerKey}${DEDUPE_KEY_SEPARATOR}${targetKey}`;
|
|
159
|
+
if (seen.has(dedupeKey)) continue;
|
|
160
|
+
seen.add(dedupeKey);
|
|
161
|
+
pairs.push({ referrer: referrerKey, target: targetKey });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return pairs.sort((a, b) =>
|
|
166
|
+
a.referrer === b.referrer ? a.target.localeCompare(b.target) : a.referrer.localeCompare(b.referrer),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** design §2.2 — "version defaults to the host package's version". Walks up from
|
|
171
|
+
* `startDir` for the nearest `package.json` carrying a string `version`. */
|
|
172
|
+
function nearestPackageVersion(startDir: string): string | undefined {
|
|
173
|
+
let dir = startDir;
|
|
174
|
+
for (let i = 0; i < 64; i++) {
|
|
175
|
+
const pkgPath = join(dir, "package.json");
|
|
176
|
+
if (existsSync(pkgPath)) {
|
|
177
|
+
try {
|
|
178
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8")) as { version?: unknown };
|
|
179
|
+
if (typeof pkg.version === "string") return pkg.version;
|
|
180
|
+
} catch {
|
|
181
|
+
// An unparsable package.json is not a version source — keep walking up.
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const parent = dirname(dir);
|
|
185
|
+
if (parent === dir) return undefined;
|
|
186
|
+
dir = parent;
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function sharedModelFile(opts: SharedModelFileOpts): Generator {
|
|
192
|
+
const generator: Generator = {
|
|
193
|
+
name: "shared-model",
|
|
194
|
+
generate: oncePerRun(async (_entities, ctx: GenContext): Promise<EmittedFile[]> => {
|
|
195
|
+
const files = opts.files ?? ctx.sourceFiles ?? [];
|
|
196
|
+
if (files.length === 0) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`sharedModelFile("${opts.name}"): no source files to load — pass \`files\`, or run ` +
|
|
199
|
+
`through \`meta gen\`, which supplies the project's own files as \`ctx.sourceFiles\`.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Step 1 — standalone load, the run's own composed registry (so the publisher's
|
|
204
|
+
// own consumer-supplied providers are honoured here exactly as they were for
|
|
205
|
+
// the project's main load), strict.
|
|
206
|
+
const loader = new MetaDataLoader({
|
|
207
|
+
...(ctx.registry !== undefined ? { registry: ctx.registry } : {}),
|
|
208
|
+
strict: true,
|
|
209
|
+
});
|
|
210
|
+
const loaded = await loader.load(files.map((p) => new FileSource(p)));
|
|
211
|
+
if (loaded.errors.length > 0) {
|
|
212
|
+
throw loaded.errors[0];
|
|
213
|
+
}
|
|
214
|
+
const standaloneRoot = loaded.root;
|
|
215
|
+
|
|
216
|
+
// Step 2 — select top-level nodes. ADR-0039 sanctioned own-only case: `MetaRoot`
|
|
217
|
+
// has no super chain (it never `extends`), so its own children ARE its effective
|
|
218
|
+
// children — the same case `MetaRoot.objects()`/`findObject()` document.
|
|
219
|
+
const compiled = compileScope({ include: opts.include, exclude: opts.exclude ?? [] });
|
|
220
|
+
const selected = standaloneRoot
|
|
221
|
+
.ownChildren()
|
|
222
|
+
.filter((node) => node.type !== TYPE_REQUIREMENT && node.type !== TYPE_TEMPLATE)
|
|
223
|
+
.filter((node) => matchesScope(node.resolutionKey(), compiled));
|
|
224
|
+
if (selected.length === 0) {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`sharedModelFile("${opts.name}"): selected no nodes (include/exclude, always ` +
|
|
227
|
+
`excluding requirement.* and template.*) — nothing to publish. Check \`include\`.`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Step 3 — closure.
|
|
232
|
+
const selectedKeys = new Set(selected.map((n) => n.resolutionKey()));
|
|
233
|
+
const closureFailures = checkClosure(standaloneRoot, selected, selectedKeys);
|
|
234
|
+
if (closureFailures.length > 0) {
|
|
235
|
+
const lines = closureFailures.map((p) => ` ${p.referrer} → ${p.target}`).join("\n");
|
|
236
|
+
throw new Error(
|
|
237
|
+
`sharedModelFile("${opts.name}"): the selection is not closed — these references ` +
|
|
238
|
+
`point outside it; include it or exclude the referrer:\n${lines}`,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Step 4 — serialize (already sorted by resolution key; the callee also
|
|
243
|
+
// throws if a selected node has no package, per its own contract).
|
|
244
|
+
const artifactContent = serializeSharedDocument(selected);
|
|
245
|
+
|
|
246
|
+
// Step 5 — re-load standalone with CORE providers only (registry omitted ⇒
|
|
247
|
+
// MetaDataLoader's own default registry), strict. A consumer loads this
|
|
248
|
+
// artifact with ITS OWN providers, never the publisher's — so an export
|
|
249
|
+
// needing publisher-only vocabulary must fail HERE, not at the consumer.
|
|
250
|
+
const artifactPath = `${opts.name}${ARTIFACT_SUFFIX}`;
|
|
251
|
+
const coreLoader = new MetaDataLoader({ strict: true });
|
|
252
|
+
const coreLoaded = await coreLoader.load([
|
|
253
|
+
new InMemoryStringSource(artifactContent, { id: artifactPath }),
|
|
254
|
+
]);
|
|
255
|
+
if (coreLoaded.errors.length > 0) {
|
|
256
|
+
const first = coreLoaded.errors[0]!;
|
|
257
|
+
throw new Error(
|
|
258
|
+
`sharedModelFile("${opts.name}"): the export needs a provider this toolchain does ` +
|
|
259
|
+
`not ship; Phase 1 exports must load with core vocabulary: ${first.message}`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Step 6 — emit the artifact + manifest.
|
|
264
|
+
const integrity = `${INTEGRITY_PREFIX}${createHash("sha256").update(artifactContent, "utf-8").digest("hex")}`;
|
|
265
|
+
const packages = [...new Set(selected.map((n) => packageOfResolutionKey(n.resolutionKey())))].sort();
|
|
266
|
+
const nodes = selected.map((n) => n.resolutionKey()).sort();
|
|
267
|
+
const version = opts.version ?? nearestPackageVersion(ctx.projectRoot ?? process.cwd());
|
|
268
|
+
if (version === undefined) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`sharedModelFile("${opts.name}"): no version available — pass \`version\` explicitly, ` +
|
|
271
|
+
`or run inside a project with a package.json version at or above the project root.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
const manifest = {
|
|
275
|
+
schema_version: MANIFEST_SCHEMA_VERSION,
|
|
276
|
+
name: opts.name,
|
|
277
|
+
version,
|
|
278
|
+
metamodelVersion: METAMODEL_VERSION,
|
|
279
|
+
artifact: artifactPath,
|
|
280
|
+
integrity,
|
|
281
|
+
packages,
|
|
282
|
+
nodes,
|
|
283
|
+
};
|
|
284
|
+
return [
|
|
285
|
+
{ path: artifactPath, content: artifactContent },
|
|
286
|
+
{ path: MANIFEST_FILE, content: JSON.stringify(manifest, null, 2) + "\n" },
|
|
287
|
+
];
|
|
288
|
+
}),
|
|
289
|
+
};
|
|
290
|
+
if (opts.target !== undefined) generator.target = opts.target;
|
|
291
|
+
return generator;
|
|
292
|
+
}
|
|
@@ -28,9 +28,11 @@ import {
|
|
|
28
28
|
TEMPLATE_ATTR_TEXT_REF,
|
|
29
29
|
} from "@metaobjectsdev/metadata";
|
|
30
30
|
import { responseFormatOf } from "../templates/find-inbound.js";
|
|
31
|
-
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
31
|
+
import type { MetaData, MetaObject } from "@metaobjectsdev/metadata";
|
|
32
|
+
import { libraryManifests } from "@metaobjectsdev/metadata/library";
|
|
32
33
|
import {
|
|
33
34
|
type EmittedFile,
|
|
35
|
+
type GenContext,
|
|
34
36
|
type Generator,
|
|
35
37
|
type GeneratorFactory,
|
|
36
38
|
perEntity,
|
|
@@ -39,8 +41,40 @@ import { generatePayloadInterfacesBatch } from "../payload-codegen.js";
|
|
|
39
41
|
import { GENERATED_HEADER } from "../constants.js";
|
|
40
42
|
import { tphDiscriminatorPin } from "../templates/zod-validators.js";
|
|
41
43
|
|
|
42
|
-
/**
|
|
43
|
-
const
|
|
44
|
+
/** This generator's stable name — the key a library manifest declares its anchor under. */
|
|
45
|
+
const STABLE_NAME = "trace-helper";
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The FQNs this generator keys on, from the LIBRARY MANIFESTS rather than a constant
|
|
49
|
+
* here (FR-043 §6).
|
|
50
|
+
*
|
|
51
|
+
* What it replaces: `const LLM_CALL_BASE = "LlmCallBase"`, compared against `.name`
|
|
52
|
+
* anywhere in the super chain — so any adopter entity called `LlmCallBase`, in any
|
|
53
|
+
* package, triggered the generator. An anchor is a fully-qualified node the library
|
|
54
|
+
* declares, and it is resolved to a node and compared by identity below.
|
|
55
|
+
*
|
|
56
|
+
* `ctx.libraries` undefined means the caller never said which libraries are selected
|
|
57
|
+
* (a programmatic `runGen()`); every shipped manifest's anchor is then a candidate,
|
|
58
|
+
* which is still FQN-anchored. An EMPTY array is the opposite and is honoured: the
|
|
59
|
+
* caller looked, this project opted into none, and the generator matches nothing.
|
|
60
|
+
*/
|
|
61
|
+
function anchorFqns(ctx: GenContext): string[] {
|
|
62
|
+
const manifests = ctx.libraries ?? Object.values(libraryManifests());
|
|
63
|
+
const out: string[] = [];
|
|
64
|
+
for (const manifest of manifests) {
|
|
65
|
+
for (const g of manifest.generators ?? []) {
|
|
66
|
+
if (g.name === STABLE_NAME && g.anchor !== undefined) out.push(g.anchor);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolve each anchor FQN to the node it names, skipping any the model does not hold. */
|
|
73
|
+
function anchorNodes(ctx: GenContext): MetaData[] {
|
|
74
|
+
const wanted = new Set(anchorFqns(ctx));
|
|
75
|
+
if (wanted.size === 0) return [];
|
|
76
|
+
return ctx.loadedRoot.children().filter((n) => wanted.has(n.resolutionKey()));
|
|
77
|
+
}
|
|
44
78
|
|
|
45
79
|
export interface TraceHelperOpts {
|
|
46
80
|
/** Output directory prefix relative to the target's outDir. Default: "" (root). */
|
|
@@ -49,12 +83,17 @@ export interface TraceHelperOpts {
|
|
|
49
83
|
target?: string;
|
|
50
84
|
}
|
|
51
85
|
|
|
52
|
-
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Walk the super chain looking for one of the anchor NODES.
|
|
88
|
+
*
|
|
89
|
+
* Node identity, not `.name` and not even the FQN string: the anchors were resolved
|
|
90
|
+
* against this run's own loaded root, so an entity whose chain reaches one reaches
|
|
91
|
+
* exactly the node the library declared.
|
|
92
|
+
*/
|
|
93
|
+
function extendsAnchor(obj: MetaObject, anchors: readonly MetaData[]): boolean {
|
|
94
|
+
if (anchors.length === 0) return false;
|
|
95
|
+
for (let cur = obj.superResolved; cur !== undefined; cur = cur.superResolved) {
|
|
96
|
+
if (anchors.includes(cur)) return true;
|
|
58
97
|
}
|
|
59
98
|
return false;
|
|
60
99
|
}
|
|
@@ -66,12 +105,16 @@ function pascal(s: string): string {
|
|
|
66
105
|
|
|
67
106
|
export const traceHelperFile = function traceHelperFile(opts?: TraceHelperOpts): Generator {
|
|
68
107
|
const dirPrefix = opts?.outDir ? `${opts.outDir.replace(/\/$/, "")}/` : "";
|
|
108
|
+
// Resolved once per run, on first use: `perEntity` calls back per entity and the
|
|
109
|
+
// anchor set is a property of the run, not of the entity.
|
|
110
|
+
let anchors: MetaData[] | undefined;
|
|
69
111
|
const generator: Generator = {
|
|
70
|
-
name:
|
|
112
|
+
name: STABLE_NAME,
|
|
71
113
|
generate: perEntity((entity, ctx) => {
|
|
72
|
-
|
|
114
|
+
anchors ??= anchorNodes(ctx);
|
|
115
|
+
// Only concrete entities derived from a library's declared anchor.
|
|
73
116
|
if (entity.isAbstract) return [];
|
|
74
|
-
if (!
|
|
117
|
+
if (!extendsAnchor(entity, anchors)) return [];
|
|
75
118
|
|
|
76
119
|
// Find the nested template.prompt.
|
|
77
120
|
// ADR-0039: resolving — a concrete trace entity may inherit its
|
package/src/index.ts
CHANGED
|
@@ -38,8 +38,22 @@ export {
|
|
|
38
38
|
generatorRegistry,
|
|
39
39
|
listGenerators,
|
|
40
40
|
getGenerator,
|
|
41
|
+
GENERATOR_LAYERS,
|
|
41
42
|
} from "./generator-registry.js";
|
|
42
|
-
export type {
|
|
43
|
+
export type {
|
|
44
|
+
GeneratorRegistryEntry,
|
|
45
|
+
GeneratorTier,
|
|
46
|
+
GeneratorFramework,
|
|
47
|
+
Layer,
|
|
48
|
+
} from "./generator-registry.js";
|
|
49
|
+
|
|
50
|
+
// The post-selection audits `meta gen` runs over a wired suite, and the impl-name →
|
|
51
|
+
// stable-name resolver both of them go through. Public so the CLI can gate it.
|
|
52
|
+
export {
|
|
53
|
+
warnUnsatisfiedRequires,
|
|
54
|
+
warnMixedApiFrameworks,
|
|
55
|
+
stableNameIndex,
|
|
56
|
+
} from "./catalog-gates.js";
|
|
43
57
|
|
|
44
58
|
export type { MetaobjectsGenConfig, NormalizedMetaobjectsGenConfig, ResolvedGenConfig, Dialect, ExtStyle, ColumnNamingStrategy, MetaDataTypeProvider, GeneratorSpec, DocsConfig, ResolvedDocsConfig, DocsSurface, ApiSurface, VerifyConfig } from "./metaobjects-config.js";
|
|
45
59
|
export { defineConfig, normalizeConfig, resolveGenerators, resolveDocsConfig } from "./metaobjects-config.js";
|
|
@@ -82,7 +96,7 @@ export { decideAndWrite, GitMissingError, WRITE_STATUSES } from "./overwrite-pol
|
|
|
82
96
|
export { contentHash, readGeneratedHash, listGeneratedPaths } from "./overwrite-policy.js";
|
|
83
97
|
|
|
84
98
|
export { CodegenError } from "./errors.js";
|
|
85
|
-
export { GENERATED_HEADER, NAMES_FILE_SUFFIX, DEFAULT_OUT_DIR, RETIRED_CODEGEN_ATTRS, type RetiredCodegenAttr } from "./constants.js";
|
|
99
|
+
export { GENERATED_HEADER, NAMES_FILE_SUFFIX, DEFAULT_OUT_DIR, RETIRED_CODEGEN_ATTRS, sidecarLine, type RetiredCodegenAttr } from "./constants.js";
|
|
86
100
|
export { warnRetiredCodegenAttrs } from "./retired-codegen-attrs.js";
|
|
87
101
|
|
|
88
102
|
export { formatTs } from "./format.js";
|
|
@@ -210,7 +224,7 @@ export { renderEntityConstants, resourcePath } from "./templates/entity-constant
|
|
|
210
224
|
export { renderQueriesFile } from "./templates/queries-file.js";
|
|
211
225
|
// #348 — which CRUD verbs a generated routes file mounts. Public because an OWNED
|
|
212
226
|
// routes generator (ADR-0034) composes the same render call and needs the same option.
|
|
213
|
-
export { CRUD_VERBS, TPH_POLYMORPHIC_VERBS, resolveExpose, intersectExpose, exposeLine } from "./routes-expose.js";
|
|
227
|
+
export { CRUD_VERBS, TPH_POLYMORPHIC_VERBS, resolveExpose, intersectExpose, exposeLine, authSeamJsDoc } from "./routes-expose.js";
|
|
214
228
|
export type { CrudVerb, ExposeOption } from "./routes-expose.js";
|
|
215
229
|
export { renderRoutesFile } from "./templates/routes-file.js";
|
|
216
230
|
export { renderRoutesFileHono } from "./templates/routes-file-hono.js";
|
|
@@ -198,21 +198,6 @@ export interface MetaobjectsGenConfig extends Omit<ResolvedGenConfig, "dbImport"
|
|
|
198
198
|
* loader. Composed AFTER the default core+forge bundle.
|
|
199
199
|
*/
|
|
200
200
|
providers?: readonly MetaDataTypeProvider[];
|
|
201
|
-
/**
|
|
202
|
-
* MetaObjects-shipped library packages this project loads alongside its own metadata —
|
|
203
|
-
* `["ai"]` makes `extends: "metaobjects::ai::LlmCallBase"` resolve.
|
|
204
|
-
*
|
|
205
|
-
* Sits beside `providers` because it answers the same shape of question: what does this
|
|
206
|
-
* project's model need in scope beyond the files it declares. Opt-in, because a library
|
|
207
|
-
* registers real top-level nodes and a project that never references one should not find
|
|
208
|
-
* them in its model, its generated output or its docs.
|
|
209
|
-
*
|
|
210
|
-
* Threaded to `loadMemory` by every CLI command that loads metadata. Before it existed,
|
|
211
|
-
* `librarySources` was reachable only from `MetaDataLoader.fromDirectory` — which the
|
|
212
|
-
* CLI does not use — so a generator that consumes a library was registered FOR the CLI
|
|
213
|
-
* while its input was unreachable THROUGH it (#333).
|
|
214
|
-
*/
|
|
215
|
-
libraries?: readonly string[];
|
|
216
201
|
}
|
|
217
202
|
|
|
218
203
|
/** MetaobjectsGenConfig after applying defaults. All fields required.
|
|
@@ -42,16 +42,19 @@ import {
|
|
|
42
42
|
SORT_ORDER_DESC,
|
|
43
43
|
RELATIONSHIP_ATTR_OBJECT_REF,
|
|
44
44
|
RELATIONSHIP_ATTR_CARDINALITY,
|
|
45
|
+
RELATIONSHIP_ATTR_SOURCE_REF_FIELD,
|
|
45
46
|
CARDINALITY_ONE,
|
|
46
47
|
IDENTITY_SUBTYPE_REFERENCE,
|
|
47
48
|
IDENTITY_REFERENCE_ATTR_REFERENCES,
|
|
48
49
|
FIELD_ATTR_COLUMN,
|
|
49
50
|
OBJECT_PROJECTION_ATTR_FILTER,
|
|
50
|
-
|
|
51
|
+
findReferencesBetween,
|
|
52
|
+
resolveRelationshipReference,
|
|
51
53
|
resolveObjectRef,
|
|
52
54
|
type AggregateFunction,
|
|
55
|
+
type ReferenceLookup,
|
|
53
56
|
} from "@metaobjectsdev/metadata";
|
|
54
|
-
import { type MetaData, type MetaField, type MetaRoot, type MetaSource, MetaObject } from "@metaobjectsdev/metadata";
|
|
57
|
+
import { type MetaData, type MetaField, type MetaReferenceIdentity, type MetaRoot, type MetaSource, MetaObject } from "@metaobjectsdev/metadata";
|
|
55
58
|
import { intValueMapOf } from "../enum-meta.js";
|
|
56
59
|
import {
|
|
57
60
|
columnNameFromField,
|
|
@@ -330,6 +333,43 @@ function resolveHop(
|
|
|
330
333
|
return undefined;
|
|
331
334
|
}
|
|
332
335
|
|
|
336
|
+
/**
|
|
337
|
+
* #368: which identity.reference does a resolved `@via` hop actually mean?
|
|
338
|
+
*
|
|
339
|
+
* `resolveHop` already found the EXACT node the hop segment named — a
|
|
340
|
+
* reference hop names the reference itself (nothing to disambiguate: two
|
|
341
|
+
* references onto the same target can coexist, but the hop picked one of them
|
|
342
|
+
* by name), and a relationship hop names a relationship whose backing
|
|
343
|
+
* reference `resolveRelationshipReference` resolves via the same SSOT ladder
|
|
344
|
+
* (unique candidate -> @sourceRefField -> unique name-pairing) that
|
|
345
|
+
* relation-resolver.ts already uses for the identical question elsewhere.
|
|
346
|
+
* Discarding `hop` and re-deriving purely from `holder`/`target` — as this
|
|
347
|
+
* file did before — throws away that specificity and reintroduces the exact
|
|
348
|
+
* ambiguity a named hop exists to resolve.
|
|
349
|
+
*
|
|
350
|
+
* Returns a single `ReferenceLookup` once resolved unambiguously, or the full
|
|
351
|
+
* candidate list when even the ladder cannot choose (a relationship hop whose
|
|
352
|
+
* name pairs with none of its candidates and no `@sourceRefField`) — the
|
|
353
|
+
* caller reports that list as a genuine ambiguity.
|
|
354
|
+
*/
|
|
355
|
+
function resolveHopReference(
|
|
356
|
+
holder: MetaObject,
|
|
357
|
+
hop: MetaData,
|
|
358
|
+
hopName: string,
|
|
359
|
+
target: MetaObject,
|
|
360
|
+
): ReferenceLookup | ReferenceLookup[] {
|
|
361
|
+
if (hop.type === TYPE_IDENTITY && hop.subType === IDENTITY_SUBTYPE_REFERENCE) {
|
|
362
|
+
return { holder, other: target, referenceIdentity: hop as unknown as MetaReferenceIdentity };
|
|
363
|
+
}
|
|
364
|
+
// ADR-0039: resolving — @sourceRefField may be inherited via extends.
|
|
365
|
+
const sourceRefField = hop.attr(RELATIONSHIP_ATTR_SOURCE_REF_FIELD) as string | undefined;
|
|
366
|
+
const matching = resolveRelationshipReference(holder, hopName, target.name, sourceRefField);
|
|
367
|
+
if (matching) {
|
|
368
|
+
return { holder, other: target, referenceIdentity: matching };
|
|
369
|
+
}
|
|
370
|
+
return findReferencesBetween(holder, target);
|
|
371
|
+
}
|
|
372
|
+
|
|
333
373
|
function viewName(projection: MetaObject, ctx: ExtractContext): string {
|
|
334
374
|
// The read-only source carries the physical view name. FR-016: physicalName
|
|
335
375
|
// implements the four-step rule (kind-matching alias → legacy @table →
|
|
@@ -750,14 +790,62 @@ function buildJoinTree(
|
|
|
750
790
|
// a traversed relationship/reference may inherit its target via extends.
|
|
751
791
|
const resolved = resolveHop(currentObj, relName);
|
|
752
792
|
if (!resolved) break;
|
|
753
|
-
const { targetName, cardinality } = resolved;
|
|
793
|
+
const { hop, targetName, cardinality } = resolved;
|
|
754
794
|
// @objectRef/@references may be package-qualified ("pkg::Entity"); resolve it
|
|
755
795
|
// package-aware relative to the hop's source entity (the loader qualifies a
|
|
756
796
|
// same-package ref even when authored bare), so the join binds the exact target.
|
|
757
797
|
const target = resolveEntityRef(root, targetName, packageOf(currentObj));
|
|
758
798
|
if (!target) break;
|
|
759
799
|
|
|
760
|
-
|
|
800
|
+
// #368: two identity.reference declarations onto the same target are legal
|
|
801
|
+
// (e.g. Match.homeTeamRef/awayTeamRef -> Team) — resolveHopReference prefers
|
|
802
|
+
// the SPECIFIC reference/relationship the hop already named over re-deriving
|
|
803
|
+
// one from the target alone, so an explicit `@via: "Match.homeTeamRef"` (or a
|
|
804
|
+
// relationship disambiguated by @sourceRefField/name-pairing) resolves cleanly.
|
|
805
|
+
// Only a relationship hop that even the ladder cannot choose reaches the throw.
|
|
806
|
+
const resolvedRef = resolveHopReference(currentObj as MetaObject, hop, relName, target);
|
|
807
|
+
let ref: ReferenceLookup | undefined;
|
|
808
|
+
if (Array.isArray(resolvedRef)) {
|
|
809
|
+
if (resolvedRef.length > 1) {
|
|
810
|
+
// #368 round 2: @sourceRefField cannot fix this, but WHY differs by shape, and
|
|
811
|
+
// asserting the wrong reason for a given shape is itself a bug (fix round 1 of
|
|
812
|
+
// this cleanup caught exactly that). resolveRelationshipReference's ladder reads
|
|
813
|
+
// ONLY the hop's own entity's candidates (referenceCandidatesFor(currentObj, ...));
|
|
814
|
+
// it never even looks at `target`'s references. So:
|
|
815
|
+
// - If `currentObj` itself holds one of the ambiguous candidates, resolution
|
|
816
|
+
// already tried @sourceRefField/name-pairing against it and failed — and that
|
|
817
|
+
// is only reachable at all when @cardinality isn't "one": a @cardinality "one"
|
|
818
|
+
// relationship with 2+ own-side candidates is rejected at LOAD by rule (e)
|
|
819
|
+
// (validateOneSideReferenceResolution) using this exact same ladder, so if we
|
|
820
|
+
// got this far with an own-side candidate, @cardinality is provably not "one",
|
|
821
|
+
// and @sourceRefField is provably illegal here (rule (d)).
|
|
822
|
+
// - If NONE of the candidates are `currentObj`'s own, @sourceRefField could not
|
|
823
|
+
// have mattered regardless of @cardinality — it only ever consults the hop's
|
|
824
|
+
// OWN identity.reference children, and it has none targeting `target`. This is
|
|
825
|
+
// rule (e)'s zero-candidate gap (validation-passes.ts:2226, `<= 1` skips 0 too):
|
|
826
|
+
// a @cardinality "one" relationship can reach here with the FK entirely on the
|
|
827
|
+
// far side, so @cardinality itself must NOT be asserted in this branch.
|
|
828
|
+
const holderName = (currentObj as MetaObject).name;
|
|
829
|
+
const holderOwnsACandidate = resolvedRef.some((r) => r.holder.name === holderName);
|
|
830
|
+
const whySourceRefFieldCannotHelp = holderOwnsACandidate
|
|
831
|
+
? `it only disambiguates a @cardinality "${CARDINALITY_ONE}" relationship, and this ` +
|
|
832
|
+
`relationship's @cardinality is not "${CARDINALITY_ONE}" (declaring @sourceRefField on it ` +
|
|
833
|
+
`is itself a load error)`
|
|
834
|
+
: `it only consults "${holderName}"'s own identity.reference children, and "${holderName}" ` +
|
|
835
|
+
`declares none targeting "${target.name}" -- every candidate above belongs to the other side ` +
|
|
836
|
+
`of this join`;
|
|
837
|
+
throw new Error(
|
|
838
|
+
`projection join hop "${relName}" from "${holderName}" to "${target.name}" is ambiguous: ` +
|
|
839
|
+
`${resolvedRef.map((r) => r.referenceIdentity.name).join(", ")}. ` +
|
|
840
|
+
`@sourceRefField cannot resolve this: ${whySourceRefFieldCannotHelp}. There is no attribute ` +
|
|
841
|
+
`that disambiguates a hop like this -- remove the extra identity.reference between these two ` +
|
|
842
|
+
`entities, or restructure the model so only one remains.`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
ref = resolvedRef[0];
|
|
846
|
+
} else {
|
|
847
|
+
ref = resolvedRef;
|
|
848
|
+
}
|
|
761
849
|
if (!ref) break;
|
|
762
850
|
|
|
763
851
|
const fkField = ref.referenceIdentity.fields[0];
|
|
@@ -1090,7 +1178,40 @@ function buildSelectSpec(
|
|
|
1090
1178
|
// The base↔child correlation FK — resolved exactly as buildJoinTree resolves a
|
|
1091
1179
|
// single hop (the identity.reference is the FK-direction SSOT). Single-hop @via;
|
|
1092
1180
|
// a multi-hop @via on origin.first is not lowered here (rare, and validated away).
|
|
1093
|
-
|
|
1181
|
+
// #368: two identity.reference declarations onto the same target are legal, so
|
|
1182
|
+
// this correlation cannot silently take the first — refuse rather than guess.
|
|
1183
|
+
// Unlike buildJoinTree's @via hop (a named relationship/reference this file can
|
|
1184
|
+
// resolve via resolveHopReference), origin.first's OWN @via (ORIGIN_FIRST_ATTR_VIA)
|
|
1185
|
+
// is never read anywhere in this file — buildJoinTree explicitly skips it
|
|
1186
|
+
// (ORIGIN_SUBTYPE_FIRST falls through to `continue` there) and this branch derives
|
|
1187
|
+
// childEntity from @of alone, so there is no hop name here to prefer. Fixing that is
|
|
1188
|
+
// a separate, larger change (wiring @via/single-hop-unique inference into this
|
|
1189
|
+
// branch to match _validateViaPath/_inferViaSingleHop) — out of scope for #368's
|
|
1190
|
+
// silent-first-match fix; the message below reflects the real, narrower remedy.
|
|
1191
|
+
const refs = findReferencesBetween(base, childEntity);
|
|
1192
|
+
// findReferencesBetween walks BOTH directions ([base,child] then [child,base]),
|
|
1193
|
+
// so ">1 entry" is not by itself #368 ambiguity: a mutual 1:1 —
|
|
1194
|
+
// Customer.primaryAddressRef -> Address PLUS Address.customerRef -> Customer —
|
|
1195
|
+
// returns two entries pointing in OPPOSITE directions. That shape is legal
|
|
1196
|
+
// (findReferenceBetween's own contract calls it "rare, but legal", and the
|
|
1197
|
+
// referenceHolder: "source" | "target" branch below exists to serve it), and
|
|
1198
|
+
// refusing it broke codegen for a model #368 says nothing about. The real
|
|
1199
|
+
// ambiguity is two references declared by the SAME holder onto the other side —
|
|
1200
|
+
// that is what the first-match below cannot choose between.
|
|
1201
|
+
const ambiguousHolder = [base, childEntity].find(
|
|
1202
|
+
(holder) => refs.filter((r) => r.holder === holder).length > 1,
|
|
1203
|
+
);
|
|
1204
|
+
if (ambiguousHolder !== undefined) {
|
|
1205
|
+
const ambiguous = refs.filter((r) => r.holder === ambiguousHolder);
|
|
1206
|
+
throw new Error(
|
|
1207
|
+
`origin.first correlation from "${base.name}" to "${childEntity.name}" is ambiguous: ` +
|
|
1208
|
+
`"${ambiguousHolder.name}" declares ${ambiguous.length} identity.reference nodes ` +
|
|
1209
|
+
`onto the other side (${ambiguous.map((r) => r.referenceIdentity.name).join(", ")}). ` +
|
|
1210
|
+
`origin.first's own @via is not consulted for this correlation — reduce to a ` +
|
|
1211
|
+
`single identity.reference between these two entities.`,
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1214
|
+
const ref = refs[0];
|
|
1094
1215
|
if (!ref) continue;
|
|
1095
1216
|
const fkField = ref.referenceIdentity.fields[0];
|
|
1096
1217
|
if (!fkField) continue;
|
package/src/reference/entity.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
namesRef,
|
|
71
71
|
namesConstArg,
|
|
72
72
|
GENERATED_HEADER,
|
|
73
|
+
sidecarLine,
|
|
73
74
|
} from "@metaobjectsdev/codegen-ts";
|
|
74
75
|
|
|
75
76
|
export interface RenderEntityOpts {
|
|
@@ -143,8 +144,7 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
|
|
|
143
144
|
const body = joinCode(sections, { on: "\n" }).toString();
|
|
144
145
|
const header =
|
|
145
146
|
`// ${GENERATED_HEADER} — DO NOT EDIT.\n` +
|
|
146
|
-
`// Source metadata: ${entity.name} (${entity.fqn()})\n`
|
|
147
|
-
`// Customize via ${entity.name}.extra.ts in this directory.\n`;
|
|
147
|
+
`// Source metadata: ${entity.name} (${entity.fqn()})\n${sidecarLine(`${entity.name}.extra.ts`)}`;
|
|
148
148
|
return header + body;
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -175,7 +175,16 @@ export const entityFile = function entityFile(opts?: EntityFileOpts): Generator
|
|
|
175
175
|
generate: async (ctx: GenContext): Promise<EmittedFile[]> => {
|
|
176
176
|
const files = await perEntityEmit(ctx);
|
|
177
177
|
// FR-019: emit the shared-enums module once per run (null → no file).
|
|
178
|
-
|
|
178
|
+
// FR-023 §11.1 item 2: this is a REFERENCE TEMPLATE `meta init` copies into
|
|
179
|
+
// every scaffolded project's codegen/generators/ (ADR-0034) — the DEFAULT
|
|
180
|
+
// path, not an opt-in one. `select` (never `ctx.matches`, which never runs
|
|
181
|
+
// for a whole-root render like this one) excludes an enum used only by an
|
|
182
|
+
// imported, out-of-scope entity; without it every `meta init` project would
|
|
183
|
+
// silently emit shared enums the library path correctly excludes.
|
|
184
|
+
const sharedEnums = renderSharedEnumsFile(
|
|
185
|
+
ctx.loadedRoot,
|
|
186
|
+
ctx.select !== undefined ? { select: ctx.select } : undefined,
|
|
187
|
+
);
|
|
179
188
|
if (sharedEnums !== null) {
|
|
180
189
|
files.push({ path: `${SHARED_ENUMS_BASENAME}.ts`, content: await formatTs(sharedEnums) });
|
|
181
190
|
}
|