@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.
Files changed (131) hide show
  1. package/dist/attr-schema-validate.d.ts.map +1 -1
  2. package/dist/attr-schema-validate.js +17 -2
  3. package/dist/attr-schema-validate.js.map +1 -1
  4. package/dist/core/field/field-constants.d.ts +18 -5
  5. package/dist/core/field/field-constants.d.ts.map +1 -1
  6. package/dist/core/field/field-constants.js +21 -5
  7. package/dist/core/field/field-constants.js.map +1 -1
  8. package/dist/core/field/field-definition.embedded.d.ts.map +1 -1
  9. package/dist/core/field/field-definition.embedded.js +8 -3
  10. package/dist/core/field/field-definition.embedded.js.map +1 -1
  11. package/dist/core/field/validate-field-mutability.d.ts +22 -0
  12. package/dist/core/field/validate-field-mutability.d.ts.map +1 -0
  13. package/dist/core/field/validate-field-mutability.js +208 -0
  14. package/dist/core/field/validate-field-mutability.js.map +1 -0
  15. package/dist/core/identity/identity-constants.d.ts +12 -0
  16. package/dist/core/identity/identity-constants.d.ts.map +1 -1
  17. package/dist/core/identity/identity-constants.js +15 -0
  18. package/dist/core/identity/identity-constants.js.map +1 -1
  19. package/dist/core/object/object-definition.embedded.js +2 -2
  20. package/dist/core/object/object-definition.embedded.js.map +1 -1
  21. package/dist/core/requirement/meta-requirement.d.ts +1 -2
  22. package/dist/core/requirement/meta-requirement.d.ts.map +1 -1
  23. package/dist/core/requirement/meta-requirement.js +2 -6
  24. package/dist/core/requirement/meta-requirement.js.map +1 -1
  25. package/dist/core/requirement/requirement-constants.d.ts +3 -8
  26. package/dist/core/requirement/requirement-constants.d.ts.map +1 -1
  27. package/dist/core/requirement/requirement-constants.js +2 -9
  28. package/dist/core/requirement/requirement-constants.js.map +1 -1
  29. package/dist/core/requirement/requirement-definition.embedded.d.ts.map +1 -1
  30. package/dist/core/requirement/requirement-definition.embedded.js +8 -46
  31. package/dist/core/requirement/requirement-definition.embedded.js.map +1 -1
  32. package/dist/core/requirement/resolve-claim.d.ts +37 -0
  33. package/dist/core/requirement/resolve-claim.d.ts.map +1 -0
  34. package/dist/core/requirement/resolve-claim.js +83 -0
  35. package/dist/core/requirement/resolve-claim.js.map +1 -0
  36. package/dist/core-types.d.ts.map +1 -1
  37. package/dist/core-types.js +10 -10
  38. package/dist/core-types.js.map +1 -1
  39. package/dist/errors.d.ts +2 -2
  40. package/dist/errors.d.ts.map +1 -1
  41. package/dist/errors.js +16 -2
  42. package/dist/errors.js.map +1 -1
  43. package/dist/index.d.ts +7 -1
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +13 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/library/library-sources.d.ts +11 -0
  48. package/dist/library/library-sources.d.ts.map +1 -1
  49. package/dist/library/library-sources.js +13 -0
  50. package/dist/library/library-sources.js.map +1 -1
  51. package/dist/loader/meta-data-loader.d.ts.map +1 -1
  52. package/dist/loader/meta-data-loader.js +10 -9
  53. package/dist/loader/meta-data-loader.js.map +1 -1
  54. package/dist/loader/validation-passes.js +2 -2
  55. package/dist/loader/validation-passes.js.map +1 -1
  56. package/dist/naming-refs.d.ts.map +1 -1
  57. package/dist/naming-refs.js +2 -3
  58. package/dist/naming-refs.js.map +1 -1
  59. package/dist/parser-core.d.ts.map +1 -1
  60. package/dist/parser-core.js +22 -5
  61. package/dist/parser-core.js.map +1 -1
  62. package/dist/persistence/origin/meta-origin.d.ts +0 -10
  63. package/dist/persistence/origin/meta-origin.d.ts.map +1 -1
  64. package/dist/persistence/origin/meta-origin.js +1 -15
  65. package/dist/persistence/origin/meta-origin.js.map +1 -1
  66. package/dist/persistence/origin/origin-constants.d.ts +2 -4
  67. package/dist/persistence/origin/origin-constants.d.ts.map +1 -1
  68. package/dist/persistence/origin/origin-constants.js +9 -7
  69. package/dist/persistence/origin/origin-constants.js.map +1 -1
  70. package/dist/persistence/origin/origin-definition.embedded.d.ts.map +1 -1
  71. package/dist/persistence/origin/origin-definition.embedded.js +0 -16
  72. package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
  73. package/dist/registry-manifest.d.ts +9 -4
  74. package/dist/registry-manifest.d.ts.map +1 -1
  75. package/dist/registry-manifest.js +9 -4
  76. package/dist/registry-manifest.js.map +1 -1
  77. package/dist/retired-vocabulary.d.ts +92 -0
  78. package/dist/retired-vocabulary.d.ts.map +1 -0
  79. package/dist/retired-vocabulary.js +204 -0
  80. package/dist/retired-vocabulary.js.map +1 -0
  81. package/dist/super-resolve.d.ts +33 -0
  82. package/dist/super-resolve.d.ts.map +1 -1
  83. package/dist/super-resolve.js +56 -1
  84. package/dist/super-resolve.js.map +1 -1
  85. package/dist/template/prompt-definition.embedded.js +1 -1
  86. package/dist/template/prompt-definition.embedded.js.map +1 -1
  87. package/dist/template/template-constants.d.ts +6 -0
  88. package/dist/template/template-constants.d.ts.map +1 -1
  89. package/dist/template/template-constants.js +24 -1
  90. package/dist/template/template-constants.js.map +1 -1
  91. package/dist/template/template-definition.embedded.d.ts.map +1 -1
  92. package/dist/template/template-definition.embedded.js +34 -21
  93. package/dist/template/template-definition.embedded.js.map +1 -1
  94. package/dist/vocabulary-rewrite.d.ts +33 -0
  95. package/dist/vocabulary-rewrite.d.ts.map +1 -0
  96. package/dist/vocabulary-rewrite.js +310 -0
  97. package/dist/vocabulary-rewrite.js.map +1 -0
  98. package/package.json +6 -1
  99. package/src/attr-schema-validate.ts +17 -2
  100. package/src/core/field/field-constants.ts +24 -5
  101. package/src/core/field/field-definition.embedded.ts +8 -3
  102. package/src/core/field/validate-field-mutability.ts +259 -0
  103. package/src/core/identity/identity-constants.ts +16 -0
  104. package/src/core/object/object-definition.embedded.ts +2 -2
  105. package/src/core/requirement/meta-requirement.ts +1 -7
  106. package/src/core/requirement/requirement-constants.ts +2 -9
  107. package/src/core/requirement/requirement-definition.embedded.ts +8 -46
  108. package/src/core/requirement/resolve-claim.ts +98 -0
  109. package/src/core-types.ts +9 -10
  110. package/src/errors.ts +16 -2
  111. package/src/index.ts +29 -1
  112. package/src/library/library-sources.ts +14 -0
  113. package/src/loader/meta-data-loader.ts +10 -9
  114. package/src/loader/validation-passes.ts +3 -3
  115. package/src/naming-refs.ts +1 -3
  116. package/src/parser-core.ts +27 -5
  117. package/src/persistence/origin/meta-origin.ts +0 -16
  118. package/src/persistence/origin/origin-constants.ts +9 -8
  119. package/src/persistence/origin/origin-definition.embedded.ts +0 -16
  120. package/src/registry-manifest.ts +9 -4
  121. package/src/retired-vocabulary.ts +280 -0
  122. package/src/super-resolve.ts +59 -1
  123. package/src/template/prompt-definition.embedded.ts +1 -1
  124. package/src/template/template-constants.ts +27 -1
  125. package/src/template/template-definition.embedded.ts +34 -21
  126. package/src/vocabulary-rewrite.ts +346 -0
  127. package/dist/core/field/validate-field-readonly.d.ts +0 -9
  128. package/dist/core/field/validate-field-readonly.d.ts.map +0 -1
  129. package/dist/core/field/validate-field-readonly.js +0 -128
  130. package/dist/core/field/validate-field-readonly.js.map +0 -1
  131. 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
- }