@metaobjectsdev/metadata 0.24.0 → 0.24.2
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-contradictions.d.ts +52 -0
- package/dist/attr-contradictions.d.ts.map +1 -0
- package/dist/attr-contradictions.js +100 -0
- package/dist/attr-contradictions.js.map +1 -0
- package/dist/core/identity/identity-definition.embedded.js +2 -2
- package/dist/core/identity/identity-definition.embedded.js.map +1 -1
- package/dist/core/index/index-definition.embedded.js +2 -2
- package/dist/core/index/index-definition.embedded.js.map +1 -1
- package/dist/core/requirement/meta-requirement.d.ts +9 -1
- package/dist/core/requirement/meta-requirement.d.ts.map +1 -1
- package/dist/core/requirement/meta-requirement.js +15 -2
- package/dist/core/requirement/meta-requirement.js.map +1 -1
- package/dist/core/requirement/requirement-constants.d.ts +30 -2
- package/dist/core/requirement/requirement-constants.d.ts.map +1 -1
- package/dist/core/requirement/requirement-constants.js +32 -1
- 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 +22 -4
- package/dist/core/requirement/requirement-definition.embedded.js.map +1 -1
- package/dist/core/vocabulary-rewrite-yaml.d.ts +21 -0
- package/dist/core/vocabulary-rewrite-yaml.d.ts.map +1 -0
- package/dist/core/vocabulary-rewrite-yaml.js +348 -0
- package/dist/core/vocabulary-rewrite-yaml.js.map +1 -0
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +31 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +8 -1
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/validation-passes.d.ts +2 -0
- package/dist/loader/validation-passes.d.ts.map +1 -1
- package/dist/loader/validation-passes.js +328 -43
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.js +5 -5
- package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
- package/dist/registry-manifest.d.ts +1 -1
- package/dist/registry-manifest.js +1 -1
- package/dist/retired-vocabulary.d.ts.map +1 -1
- package/dist/retired-vocabulary.js +54 -11
- package/dist/retired-vocabulary.js.map +1 -1
- package/dist/vocabulary-rewrite.d.ts.map +1 -1
- package/dist/vocabulary-rewrite.js +127 -15
- package/dist/vocabulary-rewrite.js.map +1 -1
- package/package.json +6 -1
- package/src/attr-contradictions.ts +141 -0
- package/src/core/identity/identity-definition.embedded.ts +2 -2
- package/src/core/index/index-definition.embedded.ts +2 -2
- package/src/core/requirement/meta-requirement.ts +18 -1
- package/src/core/requirement/requirement-constants.ts +34 -1
- package/src/core/requirement/requirement-definition.embedded.ts +22 -4
- package/src/core/vocabulary-rewrite-yaml.ts +375 -0
- package/src/errors.ts +31 -2
- package/src/index.ts +9 -0
- package/src/loader/meta-data-loader.ts +10 -1
- package/src/loader/validation-passes.ts +410 -47
- package/src/persistence/origin/origin-definition.embedded.ts +5 -5
- package/src/registry-manifest.ts +1 -1
- package/src/retired-vocabulary.ts +54 -11
- package/src/vocabulary-rewrite.ts +137 -15
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// server/typescript/packages/metadata/src/core/vocabulary-rewrite-yaml.ts
|
|
2
|
+
//
|
|
3
|
+
// The YAML arm of the raw-document rewriter behind `meta upgrade`.
|
|
4
|
+
//
|
|
5
|
+
// WHY IT IS A SEPARATE MODULE. `vocabulary-rewrite.ts` is reachable from `src/index.ts`, so
|
|
6
|
+
// it may not import `yaml` — that package is Node-only and would land in the browser bundle.
|
|
7
|
+
// This file carries the `yaml` dependency and is reachable ONLY through its own package
|
|
8
|
+
// subpath, which `meta upgrade` dynamic-imports. Same split, and same reason, as
|
|
9
|
+
// `yaml-positions.ts` / `yaml-positions-walker.ts` (see that file's header).
|
|
10
|
+
//
|
|
11
|
+
// WHY IT IS PARSER-DRIVEN WHERE THE JSON ARM IS REGEX-DRIVEN. A hand-rolled YAML mode was
|
|
12
|
+
// tried once and shipped a file-corrupting bug: a multi-item block sequence lost every item
|
|
13
|
+
// but the first, because a hand-written value scanner stops at a newline, and the dominant
|
|
14
|
+
// in-repo flow style (`{ name: x, readOnly: true }`) was not matched at all — so the rename
|
|
15
|
+
// silently did nothing. Both failures are the same failure: YAML's value extent is not
|
|
16
|
+
// derivable by scanning. Here the PARSER reports it. `pair.value.range` covers a four-line
|
|
17
|
+
// block sequence and a one-line flow mapping alike, so neither case is a special case.
|
|
18
|
+
//
|
|
19
|
+
// STILL SURGICAL, NOT PARSE-AND-REPRINT. `doc.toString()` would reflow an adopter's file —
|
|
20
|
+
// line width, quote style, indentation of flow collections — and hand them a diff whose real
|
|
21
|
+
// changes are invisible inside it. So the parse is used only to LOCATE spans; every edit is a
|
|
22
|
+
// span replacement on the original text, and any region not deliberately changed comes back
|
|
23
|
+
// byte-identical. That is the same guarantee the JSON arm makes, by the same means.
|
|
24
|
+
//
|
|
25
|
+
// IT ALSO RESOLVES ATTRIBUTE CONTRADICTIONS (`../attr-contradictions.ts`), matched per NODE
|
|
26
|
+
// rather than per pair — the illegal thing is the PAIR of keys, so the unit is the mapping
|
|
27
|
+
// that holds one node's own keys. `eachNodeBody` below is that walk. Doing it by proximity
|
|
28
|
+
// instead was tried in the JSON arm and took a `fields` belonging to a sibling node.
|
|
29
|
+
//
|
|
30
|
+
// SIGIL-FREE, PER ADR-0006. YAML authoring writes bare attribute keys (`violation:`) and the
|
|
31
|
+
// desugar re-adds the `@` when lowering to canonical JSON. So a rename emits a BARE key here
|
|
32
|
+
// where the JSON arm emits `"@name"`. A leading `@` is still matched on input — an author who
|
|
33
|
+
// wrote one gets it fixed rather than skipped — but is never introduced.
|
|
34
|
+
|
|
35
|
+
import { LineCounter, isMap, isSeq, parseDocument, type Node, type Pair } from "yaml";
|
|
36
|
+
import { ATTR_CONTRADICTIONS, contradictionScopeMatches } from "../attr-contradictions.js";
|
|
37
|
+
import type { AttrContradiction } from "../attr-contradictions.js";
|
|
38
|
+
import {
|
|
39
|
+
RETIRED_VOCABULARY,
|
|
40
|
+
note,
|
|
41
|
+
scopeMatches,
|
|
42
|
+
type RetiredEntry,
|
|
43
|
+
} from "../retired-vocabulary.js";
|
|
44
|
+
import type { RewriteChange, RewriteRefusal, RewriteOpts, RewriteResult } from "../vocabulary-rewrite.js";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A rewrite result that can also report "I could not read this file".
|
|
48
|
+
*
|
|
49
|
+
* A YAML document that does not parse yields no changes and no refusals, which is
|
|
50
|
+
* indistinguishable from a clean one — and a fixer that reports a file it could not open as
|
|
51
|
+
* clean is the exact defect this arm was written to remove (#339). The flag makes the caller
|
|
52
|
+
* say so out loud.
|
|
53
|
+
*/
|
|
54
|
+
export interface YamlRewriteResult extends RewriteResult {
|
|
55
|
+
readonly unparseable: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A canonical node key: `<type>.<subType>`. Identical to the JSON arm's scope shape. */
|
|
59
|
+
const TYPE_KEY = /^[a-z][A-Za-z0-9]*\.[A-Za-z0-9_*]+$/;
|
|
60
|
+
|
|
61
|
+
/** `0.24.0` → `[0,24,0]`, for an ordered comparison rather than a string one. */
|
|
62
|
+
function parts(v: string): number[] {
|
|
63
|
+
return v.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function atOrBefore(a: string, b: string): boolean {
|
|
67
|
+
const [x, y] = [parts(a), parts(b)];
|
|
68
|
+
for (let i = 0; i < Math.max(x.length, y.length); i++) {
|
|
69
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
70
|
+
if (d !== 0) return d < 0;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The plain string a mapping key carries, or undefined when it is not a plain scalar. */
|
|
76
|
+
function keyText(key: unknown): string | undefined {
|
|
77
|
+
const v = (key as { value?: unknown } | null)?.value;
|
|
78
|
+
return typeof v === "string" ? v : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Source offsets of a pair's key and of the end of its value. */
|
|
82
|
+
function pairSpan(pair: Pair): { keyStart: number; keyEnd: number; valueEnd: number } | undefined {
|
|
83
|
+
const k = pair.key as { range?: [number, number, number] } | null;
|
|
84
|
+
const v = pair.value as { range?: [number, number, number] } | null;
|
|
85
|
+
if (k?.range === undefined) return undefined;
|
|
86
|
+
return {
|
|
87
|
+
keyStart: k.range[0],
|
|
88
|
+
keyEnd: k.range[1],
|
|
89
|
+
// A valueless key (`verifiedBy:` with nothing after it) still has to be removable.
|
|
90
|
+
valueEnd: v?.range?.[1] ?? k.range[1],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The span to delete so that removing a pair leaves loadable YAML.
|
|
96
|
+
*
|
|
97
|
+
* Two shapes, and they need opposite treatment — which is precisely what the previous
|
|
98
|
+
* hand-rolled attempt got wrong by handling only one.
|
|
99
|
+
*/
|
|
100
|
+
function dropSpan(
|
|
101
|
+
source: string,
|
|
102
|
+
span: { keyStart: number; valueEnd: number },
|
|
103
|
+
flow: boolean,
|
|
104
|
+
): { start: number; end: number } | undefined {
|
|
105
|
+
let { keyStart: start } = span;
|
|
106
|
+
let end = span.valueEnd;
|
|
107
|
+
|
|
108
|
+
if (flow) {
|
|
109
|
+
// `{ a: 1, readOnly: true }` — take a trailing comma if there is one, else a preceding
|
|
110
|
+
// one, so the mapping ends up with neither a dangling nor a doubled separator.
|
|
111
|
+
//
|
|
112
|
+
// The probe must not commit: scanning forward over the spaces and THEN finding `}`
|
|
113
|
+
// rather than `,` would leave `end` past the space that separates the survivor from the
|
|
114
|
+
// brace, silently reformatting `{ name: x }` into `{ name: x}`.
|
|
115
|
+
let probe = end;
|
|
116
|
+
while (probe < source.length && /[ \t]/.test(source[probe] ?? "")) probe++;
|
|
117
|
+
if (source[probe] === ",") {
|
|
118
|
+
end = probe + 1;
|
|
119
|
+
while (end < source.length && /[ \t]/.test(source[end] ?? "")) end++;
|
|
120
|
+
} else {
|
|
121
|
+
let back = start;
|
|
122
|
+
while (back > 0 && /\s/.test(source[back - 1] ?? "")) back--;
|
|
123
|
+
if (source[back - 1] === ",") start = back - 1;
|
|
124
|
+
}
|
|
125
|
+
return { start, end };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Block mapping — the pair owns whole lines. Absorb its indentation and its line
|
|
129
|
+
// terminator, so removal leaves neither a ragged line nor a blank one.
|
|
130
|
+
while (start > 0 && /[ \t]/.test(source[start - 1] ?? "")) start--;
|
|
131
|
+
|
|
132
|
+
// A pair that is the first key of a block SEQUENCE item (`- verifiedBy: x`) shares its
|
|
133
|
+
// line with the `-`. Deleting it would strand the dash and silently change the sequence's
|
|
134
|
+
// shape, so this refuses rather than guesses — the caller reports it as needing a hand.
|
|
135
|
+
if (source[start - 1] === "-") return undefined;
|
|
136
|
+
|
|
137
|
+
// A multi-line value (a block sequence) already ends ON the newline that closes its last
|
|
138
|
+
// item, so the terminator is spent. Consuming another one here would delete the FOLLOWING
|
|
139
|
+
// key — which is the multi-item-sequence corruption this arm exists to avoid, arriving by
|
|
140
|
+
// a different route.
|
|
141
|
+
if (end === 0 || source[end - 1] !== "\n") {
|
|
142
|
+
while (end < source.length && /[ \t]/.test(source[end] ?? "")) end++;
|
|
143
|
+
// A trailing comment on the key's own line goes with the key it annotates.
|
|
144
|
+
if (source[end] === "#") while (end < source.length && source[end] !== "\n") end++;
|
|
145
|
+
if (source[end] === "\n") end++;
|
|
146
|
+
}
|
|
147
|
+
return { start, end };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Visit every mapping pair with the `<type>.<subType>` scope governing it. */
|
|
151
|
+
function eachPair(
|
|
152
|
+
node: unknown,
|
|
153
|
+
scope: string | undefined,
|
|
154
|
+
visit: (pair: Pair, scope: string | undefined, flow: boolean) => void,
|
|
155
|
+
): void {
|
|
156
|
+
if (isMap(node)) {
|
|
157
|
+
const flow = node.flow === true;
|
|
158
|
+
for (const pair of node.items) {
|
|
159
|
+
const k = keyText(pair.key);
|
|
160
|
+
visit(pair, scope, flow);
|
|
161
|
+
// A type key scopes its own BODY, not itself — so the pair above is reported under the
|
|
162
|
+
// enclosing scope while its value descends under this one.
|
|
163
|
+
const inner = k !== undefined && TYPE_KEY.test(k) ? k : scope;
|
|
164
|
+
if (pair.value != null) eachPair(pair.value, inner, visit);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (isSeq(node)) {
|
|
169
|
+
for (const item of node.items) eachPair(item as Node, scope, visit);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** A node's own key set: the mapping that a `<type>.<subType>:` key introduces. */
|
|
174
|
+
interface NodeBody {
|
|
175
|
+
readonly items: readonly Pair[];
|
|
176
|
+
readonly flow: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Visit every node BODY in the document.
|
|
181
|
+
*
|
|
182
|
+
* The unit is the body rather than the pair because a contradiction is a property of a
|
|
183
|
+
* SIBLING SET. `body.items` is exactly this node's own keys — a child node lives inside the
|
|
184
|
+
* value of a `children:` pair, so it is reached by recursion and never mistaken for a
|
|
185
|
+
* sibling.
|
|
186
|
+
*/
|
|
187
|
+
function eachNodeBody(node: unknown, visit: (typeKey: string, body: NodeBody) => void): void {
|
|
188
|
+
if (isMap(node)) {
|
|
189
|
+
for (const pair of node.items) {
|
|
190
|
+
const k = keyText(pair.key);
|
|
191
|
+
if (k !== undefined && TYPE_KEY.test(k) && isMap(pair.value)) {
|
|
192
|
+
visit(k, { items: pair.value.items as Pair[], flow: pair.value.flow === true });
|
|
193
|
+
}
|
|
194
|
+
if (pair.value != null) eachNodeBody(pair.value, visit);
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (isSeq(node)) {
|
|
199
|
+
for (const item of node.items) eachNodeBody(item as Node, visit);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** An authored `@` must not hide a key from either table; the sigil is never introduced. */
|
|
204
|
+
function bareKey(pair: Pair): string | undefined {
|
|
205
|
+
const k = keyText(pair.key);
|
|
206
|
+
if (k === undefined) return undefined;
|
|
207
|
+
return k.startsWith("@") ? k.slice(1) : k;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** True when `keep` holds one of the entry's `keepValues` (or the entry names none,
|
|
211
|
+
* in which case mere presence is the contradiction). Mirrors the JSON rewriter's
|
|
212
|
+
* `keepValueMatches` — one rule, two doors, and a divergence here is a file format
|
|
213
|
+
* silently migrating differently from the other. */
|
|
214
|
+
function keepValueMatches(pair: Pair, c: AttrContradiction): boolean {
|
|
215
|
+
if (c.keepValues === undefined) return true;
|
|
216
|
+
const v = (pair.value as { value?: unknown } | null)?.value;
|
|
217
|
+
return typeof v === "string" && c.keepValues.includes(v);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Does this pair carry a string that actually says something? */
|
|
221
|
+
function suppliesText(pair: Pair): boolean {
|
|
222
|
+
const v = (pair.value as { value?: unknown } | null)?.value;
|
|
223
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Rewrite retired vocabulary in one raw YAML metadata document.
|
|
228
|
+
*
|
|
229
|
+
* Pure: no filesystem, no loader, no registry. Mirrors `rewriteDocument`'s contract exactly —
|
|
230
|
+
* same result shape, same scoping rule, same refusal policy — so `meta upgrade` reports a
|
|
231
|
+
* YAML estate and a JSON estate identically.
|
|
232
|
+
*/
|
|
233
|
+
export function rewriteYamlDocument(source: string, opts: RewriteOpts = {}): YamlRewriteResult {
|
|
234
|
+
const changes: RewriteChange[] = [];
|
|
235
|
+
const refusals: RewriteRefusal[] = [];
|
|
236
|
+
const edits: { start: number; end: number; text: string }[] = [];
|
|
237
|
+
|
|
238
|
+
const lineCounter = new LineCounter();
|
|
239
|
+
const doc = parseDocument(source, { lineCounter, keepSourceTokens: true });
|
|
240
|
+
// A document we cannot parse is a document we must not edit. Reporting nothing here is
|
|
241
|
+
// correct: `meta verify` owns malformed YAML, and guessing at spans in a broken file is
|
|
242
|
+
// how a fixer corrupts one.
|
|
243
|
+
if (doc.errors.length > 0 || doc.contents == null) {
|
|
244
|
+
return { text: source, changes, refusals, unparseable: true };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const lineOf = (offset: number): number => lineCounter.linePos(offset).line;
|
|
248
|
+
const inWindow = (e: RetiredEntry): boolean =>
|
|
249
|
+
opts.maxVersion === undefined || atOrBefore(e.since, opts.maxVersion);
|
|
250
|
+
|
|
251
|
+
// ── Attribute contradictions: two LIVE attrs that may not sit on one node ──
|
|
252
|
+
//
|
|
253
|
+
// THE TWO SIDES ARE ASKED DIFFERENT QUESTIONS, mirroring the loader's Rule 1a exactly
|
|
254
|
+
// (`validation-passes.ts`, `hasFieldsAttr` vs `hasExpr`) and the JSON arm's copy of it.
|
|
255
|
+
// The DROP side counts on PRESENCE — an empty `fields: []` beside `expr` is still a
|
|
256
|
+
// declaration of both, and is the case where the discard is total. The KEEP side counts
|
|
257
|
+
// only when it supplies a key, so a blank `expr: ""` beside `fields` is a plain column
|
|
258
|
+
// index the loader accepts and this must leave alone.
|
|
259
|
+
//
|
|
260
|
+
// IT SEES ONLY THIS NODE'S OWN KEYS. A node declaring `expr` while INHERITING `fields`
|
|
261
|
+
// through `extends` contradicts itself in the loaded model and not on the page; no
|
|
262
|
+
// raw-document rewriter can resolve a super-reference, so that stays the loader's refusal.
|
|
263
|
+
eachNodeBody(doc.contents, (typeKey, body) => {
|
|
264
|
+
for (const c of ATTR_CONTRADICTIONS) {
|
|
265
|
+
if (opts.maxVersion !== undefined && !atOrBefore(c.since, opts.maxVersion)) continue;
|
|
266
|
+
if (!contradictionScopeMatches(c, typeKey)) continue;
|
|
267
|
+
// `keep` must be present AND, when the entry names values, hold one of them —
|
|
268
|
+
// otherwise status and implementedBy would contradict on every status.
|
|
269
|
+
if (!body.items.some(
|
|
270
|
+
(p) => bareKey(p) === c.keep && suppliesText(p) && keepValueMatches(p, c),
|
|
271
|
+
)) continue;
|
|
272
|
+
|
|
273
|
+
for (const pair of body.items) {
|
|
274
|
+
if (bareKey(pair) !== c.drop) continue;
|
|
275
|
+
const span = pairSpan(pair);
|
|
276
|
+
if (span === undefined) continue;
|
|
277
|
+
const d = dropSpan(source, span, body.flow);
|
|
278
|
+
// Undeletable in place (a sequence item's leading key) — leave it, and let the
|
|
279
|
+
// loader keep refusing rather than reshape the author's sequence.
|
|
280
|
+
if (d === undefined) continue;
|
|
281
|
+
edits.push({ ...d, text: "" });
|
|
282
|
+
changes.push({
|
|
283
|
+
attr: c.drop,
|
|
284
|
+
from: c.drop,
|
|
285
|
+
to: `(removed — ${c.keep} keys this node)`,
|
|
286
|
+
line: lineOf(span.keyStart),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
eachPair(doc.contents, undefined, (pair, scope, flow) => {
|
|
293
|
+
const key = keyText(pair.key);
|
|
294
|
+
if (key === undefined) return;
|
|
295
|
+
const span = pairSpan(pair);
|
|
296
|
+
if (span === undefined) return;
|
|
297
|
+
|
|
298
|
+
// A retired SUBTYPE has no attribute to rewrite — the node itself has to be re-modelled,
|
|
299
|
+
// which is the adopter's judgment. Reporting it is what keeps `meta upgrade` from exiting
|
|
300
|
+
// 0 on a document that still will not load.
|
|
301
|
+
if (TYPE_KEY.test(key)) {
|
|
302
|
+
for (const entry of RETIRED_VOCABULARY) {
|
|
303
|
+
if (entry.isSubTypeRetirement !== true || !inWindow(entry)) continue;
|
|
304
|
+
if (`${entry.type}.${entry.subType}` !== key) continue;
|
|
305
|
+
refusals.push({ ...note(entry), subject: key, line: lineOf(span.keyStart) });
|
|
306
|
+
}
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Sigil-free authoring is the norm, but an authored `@` must not make a retirement
|
|
311
|
+
// invisible.
|
|
312
|
+
const bare = key.startsWith("@") ? key.slice(1) : key;
|
|
313
|
+
const raw = source.slice(span.keyEnd, span.valueEnd).replace(/^\s*:\s*/, "").trim();
|
|
314
|
+
const line = lineOf(span.keyStart);
|
|
315
|
+
|
|
316
|
+
for (const entry of RETIRED_VOCABULARY) {
|
|
317
|
+
if (entry.attr !== bare || !inWindow(entry)) continue;
|
|
318
|
+
if (scope === undefined || !scopeMatches(entry, scope)) continue;
|
|
319
|
+
|
|
320
|
+
// A VALUE-scoped retirement only fires on the retired values — the same attribute with
|
|
321
|
+
// a live value must come through untouched.
|
|
322
|
+
if (entry.attrValues !== undefined && !entry.attrValues.some((v) => raw === v || raw === `"${v}"` || raw === `'${v}'`)) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const refuse = (): void => {
|
|
327
|
+
refusals.push({ ...note(entry), subject: `@${bare}`, ...(raw !== "" ? { value: raw } : {}), line });
|
|
328
|
+
};
|
|
329
|
+
const drop = (): void => {
|
|
330
|
+
const d = dropSpan(source, span, flow);
|
|
331
|
+
// Undeletable in place (a sequence item's leading key) — report it instead of
|
|
332
|
+
// producing YAML that parses as something else.
|
|
333
|
+
if (d === undefined) {
|
|
334
|
+
refuse();
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
edits.push({ ...d, text: "" });
|
|
338
|
+
changes.push({ attr: bare, from: bare, to: "(removed)", line });
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const rw = entry.rewrite;
|
|
342
|
+
if (rw === undefined) refuse();
|
|
343
|
+
else if (rw.kind === "renameAttr") {
|
|
344
|
+
// NOTE — the JSON rewriter refuses a rename onto a key the node already declares,
|
|
345
|
+
// because two `"@counterexample"` members in one object parse silently with the
|
|
346
|
+
// last one winning. YAML needs no such guard: a duplicate key is a hard PARSE
|
|
347
|
+
// ERROR, so the same document fails loudly on the next load rather than quietly
|
|
348
|
+
// losing the author's surviving sentence. Same rule, different blast radius —
|
|
349
|
+
// if `eachPair` ever gains sibling access, mirror the JSON guard here anyway.
|
|
350
|
+
//
|
|
351
|
+
// Preserve the author's quoting style; YAML keys are usually bare, but a quoted key
|
|
352
|
+
// must stay quoted or the surrounding style stops being self-consistent.
|
|
353
|
+
const rawKey = source.slice(span.keyStart, span.keyEnd);
|
|
354
|
+
const q = rawKey[0] === '"' || rawKey[0] === "'" ? rawKey[0] : "";
|
|
355
|
+
edits.push({ start: span.keyStart, end: span.keyEnd, text: `${q}${rw.to}${q}` });
|
|
356
|
+
changes.push({ attr: bare, from: bare, to: rw.to, line });
|
|
357
|
+
} else if (rw.kind === "dropAttr") drop();
|
|
358
|
+
else if (raw === String(rw.fromValue) || raw === `"${rw.fromValue}"` || raw === `'${rw.fromValue}'`) {
|
|
359
|
+
const valText = typeof rw.toValue === "string" ? String(rw.toValue) : JSON.stringify(rw.toValue);
|
|
360
|
+
edits.push({ start: span.keyStart, end: span.keyEnd, text: rw.toAttr });
|
|
361
|
+
edits.push({ start: span.keyEnd, end: span.valueEnd, text: `: ${valText}` });
|
|
362
|
+
changes.push({ attr: bare, from: `${bare}: ${raw}`, to: `${rw.toAttr}: ${valText}`, line });
|
|
363
|
+
} else if (rw.otherwise === "drop") drop();
|
|
364
|
+
else refuse();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Applied right-to-left against the ORIGINAL text: rewriting incrementally would invalidate
|
|
369
|
+
// every later offset.
|
|
370
|
+
edits.sort((a, b) => b.start - a.start);
|
|
371
|
+
let text = source;
|
|
372
|
+
for (const e of edits) text = text.slice(0, e.start) + e.text + text.slice(e.end);
|
|
373
|
+
|
|
374
|
+
return { text, changes, refusals, unparseable: false };
|
|
375
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -159,6 +159,23 @@ export const ERROR_CODES = [
|
|
|
159
159
|
// SP-H Unit9 — @filterable: true on a field subtype with no filter-operator
|
|
160
160
|
// band (e.g. field.object). Would silently generate an empty-ops filter.
|
|
161
161
|
"ERR_FILTERABLE_UNSUPPORTED_SUBTYPE",
|
|
162
|
+
// #335 Half B — @sortable: true on an array field or a subtype with no
|
|
163
|
+
// filter-operator band (e.g. field.object). Would silently emit a sort
|
|
164
|
+
// entry over a column no dialect can ORDER BY.
|
|
165
|
+
"ERR_SORTABLE_UNSUPPORTED_SUBTYPE",
|
|
166
|
+
// #335 Half A — a whole-object @agg:collect (no @of; the carrying field.object
|
|
167
|
+
// rolls related rows up as its declared @objectRef value object) is malformed:
|
|
168
|
+
// carrier is not a field.object with @objectRef, @via absent, @distinct declared
|
|
169
|
+
// (refused — a no-op whenever the value object carries the primary key), an
|
|
170
|
+
// @orderBy key not on the @via TERMINAL entity, or a member's declared type
|
|
171
|
+
// disagreeing with the matched terminal field's. Distinct from ERR_INVALID_ORIGIN
|
|
172
|
+
// so a fixture can tell this arm from a loader that still requires @of.
|
|
173
|
+
"ERR_COLLECT_WHOLE_OBJECT",
|
|
174
|
+
// #335 Half A — a whole-object @agg:collect's value-object member has no
|
|
175
|
+
// matching field (by name) on the @via terminal entity. The lowering
|
|
176
|
+
// projects exactly the declared members; failing open here is how #270
|
|
177
|
+
// turned a curated value object into the full entity.
|
|
178
|
+
"ERR_COLLECT_MEMBER_UNRESOLVED",
|
|
162
179
|
// ADR-0023 — a registration was attempted against a registry sealed after its
|
|
163
180
|
// agreed metamodel-provider bootstrap. Codegen cannot invent metamodel attrs.
|
|
164
181
|
"ERR_REGISTRY_SEALED",
|
|
@@ -174,9 +191,21 @@ export const ERROR_CODES = [
|
|
|
174
191
|
// clash / required-child cycle / conflicting attr redefinition. The detail names
|
|
175
192
|
// which of the six checks fired and the offending type(s).
|
|
176
193
|
"ERR_INVALID_METAMODEL_CONSTRAINT",
|
|
177
|
-
//
|
|
178
|
-
//
|
|
194
|
+
// Index-key resolution for index.lookup AND identity.secondary (#342) — the key is
|
|
195
|
+
// @fields XOR @expr: neither declared, BOTH declared (@expr is used INSTEAD of
|
|
196
|
+
// @fields), whichever is declared supplies no key, or a named field does not exist
|
|
197
|
+
// on the owning entity's effective (resolved via extends) field set.
|
|
179
198
|
"ERR_INVALID_INDEX",
|
|
199
|
+
// FR-039 — a requirement.* with @status: retired declares @implementedBy. Refused
|
|
200
|
+
// rather than exempted: a retired capability has no implementation BY DEFINITION,
|
|
201
|
+
// so forbidding the attribute makes the dangling-reference class unreachable
|
|
202
|
+
// instead of silently tolerated (which is what 0.24.0 removed the old vocabulary
|
|
203
|
+
// over — 29 unresolvable refs across 14 entries reported as zero).
|
|
204
|
+
"ERR_REQUIREMENT_RETIRED_HAS_IMPLEMENTORS",
|
|
205
|
+
// FR-039 — @supersededBy on a requirement whose @status is not `retired`. The
|
|
206
|
+
// attribute names what REPLACED a withdrawn capability; on a live one there is
|
|
207
|
+
// nothing to have replaced it.
|
|
208
|
+
"ERR_REQUIREMENT_SUPERSEDED_BY_NOT_RETIRED",
|
|
180
209
|
// #195 — origin.computed @expr: the expression tree's inferred root type does
|
|
181
210
|
// not equal the carrying field's declared field.<subType>. A computed column's
|
|
182
211
|
// type is DERIVED from its expression, never asserted (no @convert escape),
|
package/src/index.ts
CHANGED
|
@@ -299,5 +299,14 @@ export {
|
|
|
299
299
|
retirementHint,
|
|
300
300
|
} from "./retired-vocabulary.js";
|
|
301
301
|
export type { RetirementNote, RetiredEntry, VocabularyRewrite } from "./retired-vocabulary.js";
|
|
302
|
+
// Its sibling: pairs of LIVE attributes that may not sit on one node. Same two consumers,
|
|
303
|
+
// same reason — a retirement removes a name, a contradiction refuses a combination.
|
|
304
|
+
export {
|
|
305
|
+
ATTR_CONTRADICTIONS,
|
|
306
|
+
contradictionsFor,
|
|
307
|
+
contradictionScopeMatches,
|
|
308
|
+
contradictionHint,
|
|
309
|
+
} from "./attr-contradictions.js";
|
|
310
|
+
export type { AttrContradiction } from "./attr-contradictions.js";
|
|
302
311
|
export { rewriteDocument } from "./vocabulary-rewrite.js";
|
|
303
312
|
export type { RewriteResult, RewriteChange, RewriteRefusal, RewriteOpts } from "./vocabulary-rewrite.js";
|
|
@@ -18,7 +18,7 @@ import { ParseError } from "../errors.js";
|
|
|
18
18
|
import type { LoaderWarning } from "../source.js";
|
|
19
19
|
import { codeSource, resolvedSource } from "../source.js";
|
|
20
20
|
import { parseJson } from "../parser-json.js";
|
|
21
|
-
import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateIndexLookupFields, validateProjectionFilter } from "./validation-passes.js";
|
|
21
|
+
import { validateDataGridSortFields, validateFilterableHasIndex, validateFilterableHasSupportedOps, validateSortableHasSupportedSubtype, validateOriginPaths, validateDerivedFieldProvidability, validateDataGridFilterValues, validateFieldObjectStorage, validateFieldMap, validateTemplatePayloadRefs, validateFieldDefaults, validateRelationships, validateIndexLookupFields, validateProjectionFilter, validateRetiredRequirementLinks } from "./validation-passes.js";
|
|
22
22
|
import { runRegisteredValidation } from "./validation-registry.js";
|
|
23
23
|
import { validateSourceRoles } from "../persistence/source/validate-source-roles.js";
|
|
24
24
|
import { validateSourceEscapes } from "../persistence/source/validate-source-escapes.js";
|
|
@@ -578,6 +578,10 @@ export class MetaDataLoader {
|
|
|
578
578
|
// (would silently generate a filter that rejects every request).
|
|
579
579
|
errors.push(...validateFilterableHasSupportedOps(root));
|
|
580
580
|
|
|
581
|
+
// #335 Half B — @sortable on an array field or a subtype with no operator
|
|
582
|
+
// band → error (would silently emit a sort entry no dialect can execute).
|
|
583
|
+
errors.push(...validateSortableHasSupportedSubtype(root));
|
|
584
|
+
|
|
581
585
|
// Sixth pass: origin path validation — validates passthrough.@from,
|
|
582
586
|
// aggregate.@of, and .@via relationship chains.
|
|
583
587
|
errors.push(...validateOriginPaths(root));
|
|
@@ -607,6 +611,11 @@ export class MetaDataLoader {
|
|
|
607
611
|
// (ADR-0039: resolving accessor, so inherited fields via extends are visible).
|
|
608
612
|
errors.push(...validateIndexLookupFields(root));
|
|
609
613
|
|
|
614
|
+
// FR-039 — a retired requirement carries no @implementedBy (refused, not
|
|
615
|
+
// exempted, so the dangling-ref class is unreachable) and @supersededBy is
|
|
616
|
+
// legal only on `retired`.
|
|
617
|
+
errors.push(...validateRetiredRequirementLinks(root));
|
|
618
|
+
|
|
610
619
|
// Phase 2 — validation DERIVED FROM THE TYPE REGISTRY: each node's TypeDefinition
|
|
611
620
|
// carries its reference descriptors + imperative validator, run as one recursive walk
|
|
612
621
|
// over a built-once symbol table. A downstream provider's custom type validates itself
|