@metaobjectsdev/metadata 0.24.0-rc.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attr-schema-validate.d.ts.map +1 -1
- package/dist/attr-schema-validate.js +17 -2
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/core/field/field-constants.d.ts +18 -5
- package/dist/core/field/field-constants.d.ts.map +1 -1
- package/dist/core/field/field-constants.js +21 -5
- package/dist/core/field/field-constants.js.map +1 -1
- package/dist/core/field/field-definition.embedded.d.ts.map +1 -1
- package/dist/core/field/field-definition.embedded.js +8 -3
- package/dist/core/field/field-definition.embedded.js.map +1 -1
- package/dist/core/field/validate-field-mutability.d.ts +22 -0
- package/dist/core/field/validate-field-mutability.d.ts.map +1 -0
- package/dist/core/field/validate-field-mutability.js +208 -0
- package/dist/core/field/validate-field-mutability.js.map +1 -0
- package/dist/core/identity/identity-constants.d.ts +12 -0
- package/dist/core/identity/identity-constants.d.ts.map +1 -1
- package/dist/core/identity/identity-constants.js +15 -0
- package/dist/core/identity/identity-constants.js.map +1 -1
- package/dist/core/object/object-definition.embedded.js +2 -2
- package/dist/core/object/object-definition.embedded.js.map +1 -1
- package/dist/core/requirement/meta-requirement.d.ts +1 -2
- package/dist/core/requirement/meta-requirement.d.ts.map +1 -1
- package/dist/core/requirement/meta-requirement.js +2 -6
- package/dist/core/requirement/meta-requirement.js.map +1 -1
- package/dist/core/requirement/requirement-constants.d.ts +3 -8
- package/dist/core/requirement/requirement-constants.d.ts.map +1 -1
- package/dist/core/requirement/requirement-constants.js +2 -9
- package/dist/core/requirement/requirement-constants.js.map +1 -1
- package/dist/core/requirement/requirement-definition.embedded.d.ts.map +1 -1
- package/dist/core/requirement/requirement-definition.embedded.js +8 -46
- package/dist/core/requirement/requirement-definition.embedded.js.map +1 -1
- package/dist/core/requirement/resolve-claim.d.ts +37 -0
- package/dist/core/requirement/resolve-claim.d.ts.map +1 -0
- package/dist/core/requirement/resolve-claim.js +83 -0
- package/dist/core/requirement/resolve-claim.js.map +1 -0
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +10 -10
- package/dist/core-types.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +16 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/library/library-sources.d.ts +11 -0
- package/dist/library/library-sources.d.ts.map +1 -1
- package/dist/library/library-sources.js +13 -0
- package/dist/library/library-sources.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +10 -9
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/validation-passes.js +2 -2
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/naming-refs.d.ts.map +1 -1
- package/dist/naming-refs.js +2 -3
- package/dist/naming-refs.js.map +1 -1
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +22 -5
- package/dist/parser-core.js.map +1 -1
- package/dist/persistence/origin/meta-origin.d.ts +0 -10
- package/dist/persistence/origin/meta-origin.d.ts.map +1 -1
- package/dist/persistence/origin/meta-origin.js +1 -15
- package/dist/persistence/origin/meta-origin.js.map +1 -1
- package/dist/persistence/origin/origin-constants.d.ts +2 -4
- package/dist/persistence/origin/origin-constants.d.ts.map +1 -1
- package/dist/persistence/origin/origin-constants.js +9 -7
- package/dist/persistence/origin/origin-constants.js.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.d.ts.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.js +0 -16
- package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
- package/dist/registry-manifest.d.ts +9 -4
- package/dist/registry-manifest.d.ts.map +1 -1
- package/dist/registry-manifest.js +9 -4
- package/dist/registry-manifest.js.map +1 -1
- package/dist/retired-vocabulary.d.ts +92 -0
- package/dist/retired-vocabulary.d.ts.map +1 -0
- package/dist/retired-vocabulary.js +204 -0
- package/dist/retired-vocabulary.js.map +1 -0
- package/dist/super-resolve.d.ts +33 -0
- package/dist/super-resolve.d.ts.map +1 -1
- package/dist/super-resolve.js +56 -1
- package/dist/super-resolve.js.map +1 -1
- package/dist/template/prompt-definition.embedded.js +1 -1
- package/dist/template/prompt-definition.embedded.js.map +1 -1
- package/dist/template/template-constants.d.ts +6 -0
- package/dist/template/template-constants.d.ts.map +1 -1
- package/dist/template/template-constants.js +24 -1
- package/dist/template/template-constants.js.map +1 -1
- package/dist/template/template-definition.embedded.d.ts.map +1 -1
- package/dist/template/template-definition.embedded.js +34 -21
- package/dist/template/template-definition.embedded.js.map +1 -1
- package/dist/vocabulary-rewrite.d.ts +33 -0
- package/dist/vocabulary-rewrite.d.ts.map +1 -0
- package/dist/vocabulary-rewrite.js +310 -0
- package/dist/vocabulary-rewrite.js.map +1 -0
- package/package.json +6 -1
- package/src/attr-schema-validate.ts +17 -2
- package/src/core/field/field-constants.ts +24 -5
- package/src/core/field/field-definition.embedded.ts +8 -3
- package/src/core/field/validate-field-mutability.ts +259 -0
- package/src/core/identity/identity-constants.ts +16 -0
- package/src/core/object/object-definition.embedded.ts +2 -2
- package/src/core/requirement/meta-requirement.ts +1 -7
- package/src/core/requirement/requirement-constants.ts +2 -9
- package/src/core/requirement/requirement-definition.embedded.ts +8 -46
- package/src/core/requirement/resolve-claim.ts +98 -0
- package/src/core-types.ts +9 -10
- package/src/errors.ts +16 -2
- package/src/index.ts +29 -1
- package/src/library/library-sources.ts +14 -0
- package/src/loader/meta-data-loader.ts +10 -9
- package/src/loader/validation-passes.ts +3 -3
- package/src/naming-refs.ts +1 -3
- package/src/parser-core.ts +27 -5
- package/src/persistence/origin/meta-origin.ts +0 -16
- package/src/persistence/origin/origin-constants.ts +9 -8
- package/src/persistence/origin/origin-definition.embedded.ts +0 -16
- package/src/registry-manifest.ts +9 -4
- package/src/retired-vocabulary.ts +280 -0
- package/src/super-resolve.ts +59 -1
- package/src/template/prompt-definition.embedded.ts +1 -1
- package/src/template/template-constants.ts +27 -1
- package/src/template/template-definition.embedded.ts +34 -21
- package/src/vocabulary-rewrite.ts +346 -0
- package/dist/core/field/validate-field-readonly.d.ts +0 -9
- package/dist/core/field/validate-field-readonly.d.ts.map +0 -1
- package/dist/core/field/validate-field-readonly.js +0 -128
- package/dist/core/field/validate-field-readonly.js.map +0 -1
- package/src/core/field/validate-field-readonly.ts +0 -154
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
// server/typescript/packages/metadata/src/vocabulary-rewrite.ts
|
|
2
|
+
//
|
|
3
|
+
// The raw-document rewriter behind `meta upgrade`.
|
|
4
|
+
//
|
|
5
|
+
// WHY IT CANNOT USE THE LOADER — the constraint that shapes everything here. Once an
|
|
6
|
+
// attribute is deregistered, metadata carrying it FAILS THE LOAD; that is the point of a
|
|
7
|
+
// retirement. So load → transform → canonical-serialize is impossible: the input does not
|
|
8
|
+
// load, and the canonical serializer needs a loaded model. This operates on RAW TEXT.
|
|
9
|
+
//
|
|
10
|
+
// That is also what makes the upgrade path exist at all. An adopter installs the new CLI and
|
|
11
|
+
// runs this against metadata the new CLI REFUSES. A fixer that needed a successful load
|
|
12
|
+
// would be a chicken-and-egg with no exit.
|
|
13
|
+
//
|
|
14
|
+
// SURGICAL, NOT PARSE-AND-REPRINT. Adopters author JSONC with comments and meaningful key
|
|
15
|
+
// order — `meta gen`'s output order, review conventions, "do not reorder" notes. A
|
|
16
|
+
// round-trip through JSON.parse/stringify destroys all of it while reporting success. So
|
|
17
|
+
// every edit here is a span replacement on the original text, and any region we did not
|
|
18
|
+
// deliberately change comes back byte-identical.
|
|
19
|
+
//
|
|
20
|
+
// IT SCOPES EVERY OCCURRENCE, NOT EVERY FILE. Retirements are type-scoped — `@unique` is
|
|
21
|
+
// retired on `identity.secondary` and perfectly live on a field — so the governing type is
|
|
22
|
+
// a property of WHERE the attribute sits, not of the document it sits in. A file-level
|
|
23
|
+
// scope was the original shape and it was wrong in both directions: a caller that ran one
|
|
24
|
+
// pass per type key present in the file let the `identity.secondary` scope reach a
|
|
25
|
+
// `field.string`'s `@unique` (branding live vocabulary retired, and with a `dropAttr` entry
|
|
26
|
+
// DELETING it), and reported every wildcard-scoped refusal once per subtype key in the file.
|
|
27
|
+
// `scopeRanges` below recovers the enclosing `"<type>.<subType>"` for each occurrence, so
|
|
28
|
+
// one pass over the document answers both correctly.
|
|
29
|
+
//
|
|
30
|
+
// CANONICAL JSON ONLY. YAML authoring is real (ADR-0006) but is not rewritable here: a
|
|
31
|
+
// correct YAML editor needs the `yaml` package's CST, and this module is reachable from
|
|
32
|
+
// `src/index.ts`, which the browser-safety test forbids from importing it. A hand-rolled
|
|
33
|
+
// YAML mode was tried and shipped a file-corrupting bug — a multi-item block sequence lost
|
|
34
|
+
// every item but the first, because the value scanner stops at a newline — while the
|
|
35
|
+
// dominant in-repo authoring style (flow mappings, `{ name: x, readOnly: true }`) was not
|
|
36
|
+
// matched at all, so the rename silently did nothing. `meta upgrade` refuses YAML by name
|
|
37
|
+
// instead; a refusal an adopter can act on beats a success they cannot trust.
|
|
38
|
+
//
|
|
39
|
+
// IT REFUSES WHAT IT CANNOT KNOW. A retirement with no `rewrite` (`@status: abandoned`) is
|
|
40
|
+
// reported, never guessed at. Deleting the node, retyping it, and fixing the residue it
|
|
41
|
+
// describes are all defensible, and a wrong guess emits metadata that LOADS and means
|
|
42
|
+
// something different — strictly worse than leaving it alone, because the adopter would
|
|
43
|
+
// believe the migration finished.
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
RETIRED_VOCABULARY,
|
|
47
|
+
note,
|
|
48
|
+
scopeMatches,
|
|
49
|
+
type RetiredEntry,
|
|
50
|
+
type RetirementNote,
|
|
51
|
+
} from "./retired-vocabulary.js";
|
|
52
|
+
|
|
53
|
+
export interface RewriteChange {
|
|
54
|
+
/** Attribute name, without the sigil. */
|
|
55
|
+
readonly attr: string;
|
|
56
|
+
readonly from: string;
|
|
57
|
+
readonly to: string;
|
|
58
|
+
/** 1-indexed line in the ORIGINAL document. */
|
|
59
|
+
readonly line: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface RewriteRefusal extends RetirementNote {
|
|
63
|
+
/** What was refused, ready to print: `@status` for an attribute, `origin.collection` for
|
|
64
|
+
* a retired subtype. Carries its own sigil so a caller never has to guess which. */
|
|
65
|
+
readonly subject: string;
|
|
66
|
+
readonly value?: string;
|
|
67
|
+
readonly line: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RewriteResult {
|
|
71
|
+
readonly text: string;
|
|
72
|
+
readonly changes: readonly RewriteChange[];
|
|
73
|
+
readonly refusals: readonly RewriteRefusal[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface RewriteOpts {
|
|
77
|
+
/** Only apply retirements at or before this version. */
|
|
78
|
+
readonly maxVersion?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `0.24.0` → `[0,24,0]`, for an ordered comparison rather than a string one. */
|
|
82
|
+
function parts(v: string): number[] {
|
|
83
|
+
return v.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function atOrBefore(a: string, b: string): boolean {
|
|
87
|
+
const [x, y] = [parts(a), parts(b)];
|
|
88
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
89
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
90
|
+
if (d !== 0) return d < 0;
|
|
91
|
+
}
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 1-indexed line of `index` in `text`. */
|
|
96
|
+
function lineAt(text: string, index: number): number {
|
|
97
|
+
let line = 1;
|
|
98
|
+
for (let i = 0; i < index && i < text.length; i++) if (text[i] === "\n") line++;
|
|
99
|
+
return line;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Offset just past the bracket closing the one at `open`, or undefined if unbalanced.
|
|
104
|
+
* String-aware, so a brace inside a JSON string literal cannot unbalance the scan.
|
|
105
|
+
*/
|
|
106
|
+
function closingBracket(text: string, open: number): number | undefined {
|
|
107
|
+
const close = text[open] === "[" ? "]" : "}";
|
|
108
|
+
const openCh = text[open];
|
|
109
|
+
let depth = 0;
|
|
110
|
+
let inStr = false;
|
|
111
|
+
for (let j = open; j < text.length; j++) {
|
|
112
|
+
const c = text[j];
|
|
113
|
+
if (inStr) {
|
|
114
|
+
if (c === "\\") j++;
|
|
115
|
+
else if (c === '"') inStr = false;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (c === '"') inStr = true;
|
|
119
|
+
else if (c === openCh) depth++;
|
|
120
|
+
else if (c === close) {
|
|
121
|
+
depth--;
|
|
122
|
+
if (depth === 0) return j + 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** One `{ "<type>.<subType>": { … } }` node body, and where it sits in the text. */
|
|
129
|
+
interface ScopeRange {
|
|
130
|
+
readonly typeKey: string;
|
|
131
|
+
/** Offset of the opening quote of the type key — where a reader would point. */
|
|
132
|
+
readonly keyIndex: number;
|
|
133
|
+
readonly bodyStart: number;
|
|
134
|
+
readonly bodyEnd: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Every node body in the document, in source order.
|
|
139
|
+
*
|
|
140
|
+
* A canonical-JSON node is `{ "<type>.<subType>": { …body… } }`, so an attribute belongs to
|
|
141
|
+
* the INNERMOST body containing it — which is what makes per-occurrence scoping possible
|
|
142
|
+
* without a parser we deliberately do not have.
|
|
143
|
+
*/
|
|
144
|
+
function scopeRanges(source: string): ScopeRange[] {
|
|
145
|
+
const ranges: ScopeRange[] = [];
|
|
146
|
+
const re = /"([a-z][A-Za-z0-9]*)\.([A-Za-z0-9_*]+)"\s*:\s*\{/g;
|
|
147
|
+
let m: RegExpExecArray | null;
|
|
148
|
+
while ((m = re.exec(source)) !== null) {
|
|
149
|
+
const bodyStart = m.index + m[0].length - 1;
|
|
150
|
+
const bodyEnd = closingBracket(source, bodyStart);
|
|
151
|
+
if (bodyEnd === undefined) continue;
|
|
152
|
+
ranges.push({ typeKey: `${m[1]}.${m[2]}`, keyIndex: m.index, bodyStart, bodyEnd });
|
|
153
|
+
}
|
|
154
|
+
return ranges;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The type key governing `offset` — the innermost body containing it. */
|
|
158
|
+
function scopeAt(ranges: readonly ScopeRange[], offset: number): string | undefined {
|
|
159
|
+
let best: ScopeRange | undefined;
|
|
160
|
+
for (const r of ranges) {
|
|
161
|
+
if (offset <= r.bodyStart || offset >= r.bodyEnd) continue;
|
|
162
|
+
// Properly nested ranges: the innermost containing one starts last.
|
|
163
|
+
if (best === undefined || r.bodyStart > best.bodyStart) best = r;
|
|
164
|
+
}
|
|
165
|
+
return best?.typeKey;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Matches `"@name"` at a key position, plus the separator that follows, capturing the key
|
|
170
|
+
* separately so a replacement can keep the original spacing.
|
|
171
|
+
*
|
|
172
|
+
* Deliberately NOT a full parser. A parser would have to reproduce the document to emit it,
|
|
173
|
+
* which is the thing this exists to avoid; matching a key occurrence lets every untouched
|
|
174
|
+
* byte survive by construction.
|
|
175
|
+
*/
|
|
176
|
+
function keyPattern(attr: string): RegExp {
|
|
177
|
+
return new RegExp(`("@?${attr}")(\\s*:\\s*)`, "g");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The JSON value token starting at `from` — a scalar, or a bracketed array/object. */
|
|
181
|
+
function valueSpan(text: string, from: number): { start: number; end: number; raw: string } | undefined {
|
|
182
|
+
let i = from;
|
|
183
|
+
while (i < text.length && /\s/.test(text[i] ?? "")) i++;
|
|
184
|
+
const open = text[i];
|
|
185
|
+
if (open === "[" || open === "{") {
|
|
186
|
+
const end = closingBracket(text, i);
|
|
187
|
+
return end === undefined ? undefined : { start: i, end, raw: text.slice(i, end) };
|
|
188
|
+
}
|
|
189
|
+
// Scalar: to the next comma, closing brace, or newline, whichever comes first.
|
|
190
|
+
let j = i;
|
|
191
|
+
let inStr = false;
|
|
192
|
+
for (; j < text.length; j++) {
|
|
193
|
+
const c = text[j];
|
|
194
|
+
if (inStr) {
|
|
195
|
+
if (c === "\\") j++;
|
|
196
|
+
else if (c === '"') inStr = false;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (c === '"') inStr = true;
|
|
200
|
+
else if (c === "," || c === "}" || c === "\n") break;
|
|
201
|
+
}
|
|
202
|
+
return { start: i, end: j, raw: text.slice(i, j).trim() };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Does the raw token equal this literal? Compares JSON-decoded where possible so
|
|
206
|
+
* `"readOnly"` and `readOnly` both match a string literal. */
|
|
207
|
+
function rawEquals(raw: string, want: unknown): boolean {
|
|
208
|
+
try {
|
|
209
|
+
return JSON.parse(raw) === want;
|
|
210
|
+
} catch {
|
|
211
|
+
return raw === String(want);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The span to delete so that removing a key leaves loadable JSON.
|
|
217
|
+
*
|
|
218
|
+
* The arithmetic is fiddly and every branch is load-bearing, so it lives here with its
|
|
219
|
+
* steps named rather than inline in the dispatch below.
|
|
220
|
+
*/
|
|
221
|
+
function dropSpan(source: string, keyStart: number, valueEnd: number): { start: number; end: number } {
|
|
222
|
+
// 1. Absorb the value's trailing spaces, then its trailing comma if it has one.
|
|
223
|
+
let end = valueEnd;
|
|
224
|
+
while (end < source.length && /[ \t]/.test(source[end] ?? "")) end++;
|
|
225
|
+
const hadTrailingComma = source[end] === ",";
|
|
226
|
+
if (hadTrailingComma) end++;
|
|
227
|
+
|
|
228
|
+
// 2. Absorb the key's own indentation, so the removal does not leave a ragged line.
|
|
229
|
+
let start = keyStart;
|
|
230
|
+
while (start > 0 && /[ \t]/.test(source[start - 1] ?? "")) start--;
|
|
231
|
+
|
|
232
|
+
// 3. THE LAST-KEY CASE, found by dogfooding rather than by a unit test: when the retired
|
|
233
|
+
// attr is last in its object there IS no trailing comma — the comma belongs to the
|
|
234
|
+
// PRECEDING key. Dropping without taking it leaves `"...",\n}`, which does not parse.
|
|
235
|
+
// A tool whose whole job is producing loadable metadata cannot emit invalid JSON.
|
|
236
|
+
if (!hadTrailingComma) {
|
|
237
|
+
let back = start;
|
|
238
|
+
while (back > 0 && /\s/.test(source[back - 1] ?? "")) back--;
|
|
239
|
+
if (source[back - 1] === ",") start = back - 1;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 4. If the key owned its whole line, take the line terminator too rather than leaving a
|
|
243
|
+
// blank line. Otherwise the key shares a line with other content, so undo step 2's
|
|
244
|
+
// indentation trim — it would eat the separating space after the preceding key's comma.
|
|
245
|
+
if (source[start - 1] === "\n" && source[end] === "\n") end++;
|
|
246
|
+
else if (start > 0 && source[start - 1] !== "\n" && hadTrailingComma) start = keyStart;
|
|
247
|
+
|
|
248
|
+
return { start, end };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Rewrite retired vocabulary in one raw canonical-JSON metadata document.
|
|
253
|
+
*
|
|
254
|
+
* Pure: no filesystem, no loader, no registry. Returns the new text plus every change and
|
|
255
|
+
* every refusal, so a caller can print a diff and exit non-zero when work remains.
|
|
256
|
+
*/
|
|
257
|
+
export function rewriteDocument(source: string, opts: RewriteOpts = {}): RewriteResult {
|
|
258
|
+
const changes: RewriteChange[] = [];
|
|
259
|
+
const refusals: RewriteRefusal[] = [];
|
|
260
|
+
|
|
261
|
+
// Edits are collected as spans against the ORIGINAL text and applied in one pass at the
|
|
262
|
+
// end, right-to-left. Rewriting incrementally would invalidate every later offset.
|
|
263
|
+
const edits: { start: number; end: number; text: string }[] = [];
|
|
264
|
+
|
|
265
|
+
const inWindow = (e: RetiredEntry): boolean =>
|
|
266
|
+
opts.maxVersion === undefined || atOrBefore(e.since, opts.maxVersion);
|
|
267
|
+
const ranges = scopeRanges(source);
|
|
268
|
+
|
|
269
|
+
// A retired SUBTYPE has no attribute to rewrite — the node itself has to be re-modelled
|
|
270
|
+
// (`origin.collection` becomes `origin.aggregate @agg: collect`), which is a judgment the
|
|
271
|
+
// adopter makes. Reporting it is what keeps `meta upgrade` from exiting 0 on a document
|
|
272
|
+
// that still will not load; it used to be filtered out entirely and was invisible.
|
|
273
|
+
for (const r of ranges) {
|
|
274
|
+
for (const entry of RETIRED_VOCABULARY) {
|
|
275
|
+
if (entry.isSubTypeRetirement !== true || !inWindow(entry)) continue;
|
|
276
|
+
if (`${entry.type}.${entry.subType}` !== r.typeKey) continue;
|
|
277
|
+
refusals.push({ ...note(entry), subject: r.typeKey, line: lineAt(source, r.keyIndex) });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
for (const entry of RETIRED_VOCABULARY.filter((e) => e.attr !== undefined && inWindow(e))) {
|
|
282
|
+
const attr = entry.attr as string;
|
|
283
|
+
const re = keyPattern(attr);
|
|
284
|
+
let m: RegExpExecArray | null;
|
|
285
|
+
|
|
286
|
+
while ((m = re.exec(source)) !== null) {
|
|
287
|
+
const keyStart = m.index;
|
|
288
|
+
const keyEnd = keyStart + (m[1]?.length ?? 0);
|
|
289
|
+
const afterKey = m.index + m[0].length;
|
|
290
|
+
|
|
291
|
+
// Scope is decided HERE, per occurrence, from the enclosing node.
|
|
292
|
+
const scope = scopeAt(ranges, keyStart);
|
|
293
|
+
if (scope === undefined || !scopeMatches(entry, scope)) continue;
|
|
294
|
+
|
|
295
|
+
const line = lineAt(source, keyStart);
|
|
296
|
+
const span = valueSpan(source, afterKey);
|
|
297
|
+
|
|
298
|
+
// A VALUE-scoped retirement only fires on the retired values. `@dbColumnType: jsonb`
|
|
299
|
+
// is live vocabulary on the same attribute; touching it would silently change the
|
|
300
|
+
// column type.
|
|
301
|
+
if (entry.attrValues !== undefined) {
|
|
302
|
+
if (!entry.attrValues.some((v) => rawEquals(span?.raw ?? "", v))) continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const refuse = (): void => {
|
|
306
|
+
refusals.push({
|
|
307
|
+
...note(entry),
|
|
308
|
+
subject: `@${attr}`,
|
|
309
|
+
...(span?.raw !== undefined ? { value: span.raw } : {}),
|
|
310
|
+
line,
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const drop = (): void => {
|
|
315
|
+
if (span === undefined) return;
|
|
316
|
+
const { start, end } = dropSpan(source, keyStart, span.end);
|
|
317
|
+
edits.push({ start, end, text: "" });
|
|
318
|
+
changes.push({ attr, from: attr, to: "(removed)", line });
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const rw = entry.rewrite;
|
|
322
|
+
if (rw === undefined) refuse();
|
|
323
|
+
else if (rw.kind === "renameAttr") {
|
|
324
|
+
edits.push({ start: keyStart, end: keyEnd, text: `"@${rw.to}"` });
|
|
325
|
+
changes.push({ attr, from: attr, to: rw.to, line });
|
|
326
|
+
} else if (rw.kind === "dropAttr") drop();
|
|
327
|
+
else if (span === undefined) continue;
|
|
328
|
+
else if (rawEquals(span.raw, rw.fromValue)) {
|
|
329
|
+
const valText = JSON.stringify(rw.toValue);
|
|
330
|
+
edits.push({ start: keyStart, end: keyEnd, text: `"@${rw.toAttr}"` });
|
|
331
|
+
edits.push({ start: span.start, end: span.end, text: valText });
|
|
332
|
+
changes.push({ attr, from: `${attr}: ${span.raw}`, to: `${rw.toAttr}: ${valText}`, line });
|
|
333
|
+
}
|
|
334
|
+
// Every other value of a retired attribute still has to go somewhere — the entry says
|
|
335
|
+
// which, and the type makes saying it mandatory.
|
|
336
|
+
else if (rw.otherwise === "drop") drop();
|
|
337
|
+
else refuse();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
edits.sort((a, b) => b.start - a.start);
|
|
342
|
+
let text = source;
|
|
343
|
+
for (const e of edits) text = text.slice(0, e.start) + e.text + text.slice(e.end);
|
|
344
|
+
|
|
345
|
+
return { text, changes, refusals };
|
|
346
|
+
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { MetaData } from "../../shared/meta-data.js";
|
|
2
|
-
import { ParseError } from "../../errors.js";
|
|
3
|
-
import type { LoaderWarning } from "../../source.js";
|
|
4
|
-
export interface FieldReadOnlyValidationResult {
|
|
5
|
-
errors: ParseError[];
|
|
6
|
-
warnings: LoaderWarning[];
|
|
7
|
-
}
|
|
8
|
-
export declare function validateFieldReadOnly(root: MetaData): FieldReadOnlyValidationResult;
|
|
9
|
-
//# sourceMappingURL=validate-field-readonly.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"validate-field-readonly.d.ts","sourceRoot":"","sources":["../../../src/core/field/validate-field-readonly.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAerD,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,QAAQ,GAAG,6BAA6B,CAsEnF"}
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
// Validation pass: field-level @readOnly cross-attribute rules (FR-013).
|
|
2
|
-
//
|
|
3
|
-
// Codes:
|
|
4
|
-
// ERR_READONLY_ASSIGNED_PRIMARY — @readOnly: true on a field that is the
|
|
5
|
-
// target of an identity.primary with @generation: "assigned". The application
|
|
6
|
-
// has no path to populate the identity value (no setter; not generated; not
|
|
7
|
-
// defaulted).
|
|
8
|
-
// ERR_READONLY_DOWNGRADE — a concrete subtype declares @readOnly: false on a
|
|
9
|
-
// field whose extends-chain parent declares @readOnly: true. Read-only-ness
|
|
10
|
-
// can only be upgraded, never downgraded.
|
|
11
|
-
// WARN_READONLY_VALUE_OBJECT — @readOnly: true on a field child of object.value.
|
|
12
|
-
// The persistence implication does not apply; the attr is retained for
|
|
13
|
-
// language-specific record/struct treatment.
|
|
14
|
-
import { ParseError } from "../../errors.js";
|
|
15
|
-
import { TYPE_OBJECT, TYPE_FIELD, TYPE_IDENTITY, } from "../../shared/base-types.js";
|
|
16
|
-
import { OBJECT_SUBTYPE_VALUE } from "../object/object-constants.js";
|
|
17
|
-
import { IDENTITY_SUBTYPE_PRIMARY, IDENTITY_ATTR_GENERATION, IDENTITY_ATTR_FIELDS, GENERATION_ASSIGNED, } from "../identity/identity-constants.js";
|
|
18
|
-
import { FIELD_ATTR_READ_ONLY } from "./field-constants.js";
|
|
19
|
-
export function validateFieldReadOnly(root) {
|
|
20
|
-
const errors = [];
|
|
21
|
-
const warnings = [];
|
|
22
|
-
// ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
|
|
23
|
-
for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
|
|
24
|
-
const isValueObject = obj.subType === OBJECT_SUBTYPE_VALUE;
|
|
25
|
-
// 1) WARN_READONLY_VALUE_OBJECT — any @readOnly field child of an object.value.
|
|
26
|
-
if (isValueObject) {
|
|
27
|
-
// ADR-0039: own — warns on @readOnly DECLARED on this value's own fields
|
|
28
|
-
// (readOnlyFlag reads the explicit own flag; the FR-013 read-only checks
|
|
29
|
-
// are own-vs-super comparisons by design).
|
|
30
|
-
for (const child of obj.ownChildren()) {
|
|
31
|
-
if (child.type === TYPE_FIELD && readOnlyFlag(child) === true) {
|
|
32
|
-
warnings.push({
|
|
33
|
-
code: "WARN_READONLY_VALUE_OBJECT",
|
|
34
|
-
message: `field "${child.name}" on object.value "${obj.name}" declares ` +
|
|
35
|
-
`@readOnly: true; value-objects have no persistence semantics so ` +
|
|
36
|
-
`the read-only contract is advisory (codegen may use it for record/` +
|
|
37
|
-
`struct treatment).`,
|
|
38
|
-
source: child.source,
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
// 2) ERR_READONLY_DOWNGRADE — read-only-ness can only be upgraded across
|
|
44
|
-
// extends. Compare own field vs. inherited field's effective @readOnly.
|
|
45
|
-
// ADR-0039: own — the downgrade check compares this object's OWN explicit
|
|
46
|
-
// @readOnly:false against the inherited effective value (own-vs-super by design).
|
|
47
|
-
for (const ownField of obj.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
|
|
48
|
-
const ownVal = readOnlyFlag(ownField);
|
|
49
|
-
if (ownVal !== false)
|
|
50
|
-
continue; // only the explicit downgrade case matters
|
|
51
|
-
const inherited = inheritedField(obj, ownField.name);
|
|
52
|
-
if (inherited !== undefined && readOnlyFlag(inherited) === true) {
|
|
53
|
-
errors.push(new ParseError(`field "${ownField.name}" on "${obj.name}" sets @readOnly: false, but the ` +
|
|
54
|
-
`extends-chain parent declares @readOnly: true. Read-only-ness can only be ` +
|
|
55
|
-
`upgraded, not downgraded (FR-013).`, { code: "ERR_READONLY_DOWNGRADE", source: ownField.source }));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
// 3) ERR_READONLY_ASSIGNED_PRIMARY — @readOnly: true on a field used in an
|
|
59
|
-
// identity.primary whose @generation is "assigned" (effective tree).
|
|
60
|
-
if (!isValueObject) {
|
|
61
|
-
const primaryAssignedFields = primaryAssignedFieldNames(obj);
|
|
62
|
-
if (primaryAssignedFields.size > 0) {
|
|
63
|
-
for (const field of obj.children().filter((c) => c.type === TYPE_FIELD)) {
|
|
64
|
-
if (!primaryAssignedFields.has(field.name))
|
|
65
|
-
continue;
|
|
66
|
-
if (readOnlyFlag(field) !== true)
|
|
67
|
-
continue;
|
|
68
|
-
errors.push(new ParseError(`field "${field.name}" on "${obj.name}" is @readOnly: true AND the target ` +
|
|
69
|
-
`of identity.primary with @generation: "assigned"; the application has no ` +
|
|
70
|
-
`path to populate the identity value (FR-013).`, { code: "ERR_READONLY_ASSIGNED_PRIMARY", source: field.source }));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return { errors, warnings };
|
|
76
|
-
}
|
|
77
|
-
/** Read the explicit @readOnly value from a field's own attrs. Returns
|
|
78
|
-
* true / false when explicitly set, undefined when absent. */
|
|
79
|
-
function readOnlyFlag(field) {
|
|
80
|
-
// ADR-0039: own — the FR-013 downgrade rule needs the EXPLICIT own @readOnly on
|
|
81
|
-
// THIS node (detecting an own :false against an inherited :true); resolving would
|
|
82
|
-
// mask the downgrade. Deliberate own-vs-super comparison.
|
|
83
|
-
const v = field.ownAttr(FIELD_ATTR_READ_ONLY);
|
|
84
|
-
if (typeof v === "boolean")
|
|
85
|
-
return v;
|
|
86
|
-
return undefined;
|
|
87
|
-
}
|
|
88
|
-
/** Walk the extends chain looking for a field with the same name; return its
|
|
89
|
-
* declaring node (own attrs preserved) if found. */
|
|
90
|
-
function inheritedField(obj, name) {
|
|
91
|
-
let cursor = obj.superResolved;
|
|
92
|
-
while (cursor !== undefined) {
|
|
93
|
-
// ADR-0039: own — super-chain walk reading each level's OWN fields to find the
|
|
94
|
-
// declaring node (the FR-013 comparison needs the declaring node's own flag).
|
|
95
|
-
const f = cursor.ownChildren().find((c) => c.type === TYPE_FIELD && c.name === name);
|
|
96
|
-
if (f !== undefined)
|
|
97
|
-
return f;
|
|
98
|
-
cursor = cursor.superResolved;
|
|
99
|
-
}
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
/** Names of fields participating in any identity.primary with @generation:
|
|
103
|
-
* "assigned" on `obj` or its extends chain. */
|
|
104
|
-
function primaryAssignedFieldNames(obj) {
|
|
105
|
-
const out = new Set();
|
|
106
|
-
for (const id of obj.children()) {
|
|
107
|
-
if (id.type !== TYPE_IDENTITY)
|
|
108
|
-
continue;
|
|
109
|
-
if (id.subType !== IDENTITY_SUBTYPE_PRIMARY)
|
|
110
|
-
continue;
|
|
111
|
-
// ADR-0039: resolving — an identity may inherit @generation / @fields via extends.
|
|
112
|
-
const gen = id.attr(IDENTITY_ATTR_GENERATION);
|
|
113
|
-
if (gen !== GENERATION_ASSIGNED)
|
|
114
|
-
continue;
|
|
115
|
-
const fields = id.attr(IDENTITY_ATTR_FIELDS);
|
|
116
|
-
if (Array.isArray(fields)) {
|
|
117
|
-
for (const fName of fields) {
|
|
118
|
-
if (typeof fName === "string")
|
|
119
|
-
out.add(fName);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
else if (typeof fields === "string") {
|
|
123
|
-
out.add(fields);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return out;
|
|
127
|
-
}
|
|
128
|
-
//# sourceMappingURL=validate-field-readonly.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"validate-field-readonly.js","sourceRoot":"","sources":["../../../src/core/field/validate-field-readonly.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
// Validation pass: field-level @readOnly cross-attribute rules (FR-013).
|
|
2
|
-
//
|
|
3
|
-
// Codes:
|
|
4
|
-
// ERR_READONLY_ASSIGNED_PRIMARY — @readOnly: true on a field that is the
|
|
5
|
-
// target of an identity.primary with @generation: "assigned". The application
|
|
6
|
-
// has no path to populate the identity value (no setter; not generated; not
|
|
7
|
-
// defaulted).
|
|
8
|
-
// ERR_READONLY_DOWNGRADE — a concrete subtype declares @readOnly: false on a
|
|
9
|
-
// field whose extends-chain parent declares @readOnly: true. Read-only-ness
|
|
10
|
-
// can only be upgraded, never downgraded.
|
|
11
|
-
// WARN_READONLY_VALUE_OBJECT — @readOnly: true on a field child of object.value.
|
|
12
|
-
// The persistence implication does not apply; the attr is retained for
|
|
13
|
-
// language-specific record/struct treatment.
|
|
14
|
-
|
|
15
|
-
import type { MetaData } from "../../shared/meta-data.js";
|
|
16
|
-
import { ParseError } from "../../errors.js";
|
|
17
|
-
import type { LoaderWarning } from "../../source.js";
|
|
18
|
-
import {
|
|
19
|
-
TYPE_OBJECT,
|
|
20
|
-
TYPE_FIELD,
|
|
21
|
-
TYPE_IDENTITY,
|
|
22
|
-
} from "../../shared/base-types.js";
|
|
23
|
-
import { OBJECT_SUBTYPE_VALUE } from "../object/object-constants.js";
|
|
24
|
-
import {
|
|
25
|
-
IDENTITY_SUBTYPE_PRIMARY,
|
|
26
|
-
IDENTITY_ATTR_GENERATION,
|
|
27
|
-
IDENTITY_ATTR_FIELDS,
|
|
28
|
-
GENERATION_ASSIGNED,
|
|
29
|
-
} from "../identity/identity-constants.js";
|
|
30
|
-
import { FIELD_ATTR_READ_ONLY } from "./field-constants.js";
|
|
31
|
-
|
|
32
|
-
export interface FieldReadOnlyValidationResult {
|
|
33
|
-
errors: ParseError[];
|
|
34
|
-
warnings: LoaderWarning[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function validateFieldReadOnly(root: MetaData): FieldReadOnlyValidationResult {
|
|
38
|
-
const errors: ParseError[] = [];
|
|
39
|
-
const warnings: LoaderWarning[] = [];
|
|
40
|
-
|
|
41
|
-
// ADR-0039: root has no super; children()==ownChildren() but resolving is the default.
|
|
42
|
-
for (const obj of root.children().filter((c) => c.type === TYPE_OBJECT)) {
|
|
43
|
-
const isValueObject = obj.subType === OBJECT_SUBTYPE_VALUE;
|
|
44
|
-
|
|
45
|
-
// 1) WARN_READONLY_VALUE_OBJECT — any @readOnly field child of an object.value.
|
|
46
|
-
if (isValueObject) {
|
|
47
|
-
// ADR-0039: own — warns on @readOnly DECLARED on this value's own fields
|
|
48
|
-
// (readOnlyFlag reads the explicit own flag; the FR-013 read-only checks
|
|
49
|
-
// are own-vs-super comparisons by design).
|
|
50
|
-
for (const child of obj.ownChildren()) {
|
|
51
|
-
if (child.type === TYPE_FIELD && readOnlyFlag(child) === true) {
|
|
52
|
-
warnings.push({
|
|
53
|
-
code: "WARN_READONLY_VALUE_OBJECT",
|
|
54
|
-
message:
|
|
55
|
-
`field "${child.name}" on object.value "${obj.name}" declares ` +
|
|
56
|
-
`@readOnly: true; value-objects have no persistence semantics so ` +
|
|
57
|
-
`the read-only contract is advisory (codegen may use it for record/` +
|
|
58
|
-
`struct treatment).`,
|
|
59
|
-
source: child.source,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// 2) ERR_READONLY_DOWNGRADE — read-only-ness can only be upgraded across
|
|
66
|
-
// extends. Compare own field vs. inherited field's effective @readOnly.
|
|
67
|
-
// ADR-0039: own — the downgrade check compares this object's OWN explicit
|
|
68
|
-
// @readOnly:false against the inherited effective value (own-vs-super by design).
|
|
69
|
-
for (const ownField of obj.ownChildren().filter((c) => c.type === TYPE_FIELD)) {
|
|
70
|
-
const ownVal = readOnlyFlag(ownField);
|
|
71
|
-
if (ownVal !== false) continue; // only the explicit downgrade case matters
|
|
72
|
-
const inherited = inheritedField(obj, ownField.name);
|
|
73
|
-
if (inherited !== undefined && readOnlyFlag(inherited) === true) {
|
|
74
|
-
errors.push(
|
|
75
|
-
new ParseError(
|
|
76
|
-
`field "${ownField.name}" on "${obj.name}" sets @readOnly: false, but the ` +
|
|
77
|
-
`extends-chain parent declares @readOnly: true. Read-only-ness can only be ` +
|
|
78
|
-
`upgraded, not downgraded (FR-013).`,
|
|
79
|
-
{ code: "ERR_READONLY_DOWNGRADE", source: ownField.source },
|
|
80
|
-
),
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 3) ERR_READONLY_ASSIGNED_PRIMARY — @readOnly: true on a field used in an
|
|
86
|
-
// identity.primary whose @generation is "assigned" (effective tree).
|
|
87
|
-
if (!isValueObject) {
|
|
88
|
-
const primaryAssignedFields = primaryAssignedFieldNames(obj);
|
|
89
|
-
if (primaryAssignedFields.size > 0) {
|
|
90
|
-
for (const field of obj.children().filter((c) => c.type === TYPE_FIELD)) {
|
|
91
|
-
if (!primaryAssignedFields.has(field.name)) continue;
|
|
92
|
-
if (readOnlyFlag(field) !== true) continue;
|
|
93
|
-
errors.push(
|
|
94
|
-
new ParseError(
|
|
95
|
-
`field "${field.name}" on "${obj.name}" is @readOnly: true AND the target ` +
|
|
96
|
-
`of identity.primary with @generation: "assigned"; the application has no ` +
|
|
97
|
-
`path to populate the identity value (FR-013).`,
|
|
98
|
-
{ code: "ERR_READONLY_ASSIGNED_PRIMARY", source: field.source },
|
|
99
|
-
),
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return { errors, warnings };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/** Read the explicit @readOnly value from a field's own attrs. Returns
|
|
110
|
-
* true / false when explicitly set, undefined when absent. */
|
|
111
|
-
function readOnlyFlag(field: MetaData): boolean | undefined {
|
|
112
|
-
// ADR-0039: own — the FR-013 downgrade rule needs the EXPLICIT own @readOnly on
|
|
113
|
-
// THIS node (detecting an own :false against an inherited :true); resolving would
|
|
114
|
-
// mask the downgrade. Deliberate own-vs-super comparison.
|
|
115
|
-
const v = field.ownAttr(FIELD_ATTR_READ_ONLY);
|
|
116
|
-
if (typeof v === "boolean") return v;
|
|
117
|
-
return undefined;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Walk the extends chain looking for a field with the same name; return its
|
|
121
|
-
* declaring node (own attrs preserved) if found. */
|
|
122
|
-
function inheritedField(obj: MetaData, name: string): MetaData | undefined {
|
|
123
|
-
let cursor = obj.superResolved;
|
|
124
|
-
while (cursor !== undefined) {
|
|
125
|
-
// ADR-0039: own — super-chain walk reading each level's OWN fields to find the
|
|
126
|
-
// declaring node (the FR-013 comparison needs the declaring node's own flag).
|
|
127
|
-
const f = cursor.ownChildren().find((c) => c.type === TYPE_FIELD && c.name === name);
|
|
128
|
-
if (f !== undefined) return f;
|
|
129
|
-
cursor = cursor.superResolved;
|
|
130
|
-
}
|
|
131
|
-
return undefined;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** Names of fields participating in any identity.primary with @generation:
|
|
135
|
-
* "assigned" on `obj` or its extends chain. */
|
|
136
|
-
function primaryAssignedFieldNames(obj: MetaData): Set<string> {
|
|
137
|
-
const out = new Set<string>();
|
|
138
|
-
for (const id of obj.children()) {
|
|
139
|
-
if (id.type !== TYPE_IDENTITY) continue;
|
|
140
|
-
if (id.subType !== IDENTITY_SUBTYPE_PRIMARY) continue;
|
|
141
|
-
// ADR-0039: resolving — an identity may inherit @generation / @fields via extends.
|
|
142
|
-
const gen = id.attr(IDENTITY_ATTR_GENERATION);
|
|
143
|
-
if (gen !== GENERATION_ASSIGNED) continue;
|
|
144
|
-
const fields = id.attr(IDENTITY_ATTR_FIELDS);
|
|
145
|
-
if (Array.isArray(fields)) {
|
|
146
|
-
for (const fName of fields) {
|
|
147
|
-
if (typeof fName === "string") out.add(fName);
|
|
148
|
-
}
|
|
149
|
-
} else if (typeof fields === "string") {
|
|
150
|
-
out.add(fields);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return out;
|
|
154
|
-
}
|