@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
|
@@ -49,9 +49,9 @@ export const ORIGIN_DEFINITION: ProviderDefinition = {
|
|
|
49
49
|
{
|
|
50
50
|
"type": "origin",
|
|
51
51
|
"subType": "aggregate",
|
|
52
|
-
"description": "A value reduced from the related row-set reached along a relationship path (@via) from the base entity: count/sum/avg/min/max over a column (@of); any/all predicate quantifiers over a @filter; or collect (an array rollup of @of).",
|
|
52
|
+
"description": "A value reduced from the related row-set reached along a relationship path (@via) from the base entity: count/sum/avg/min/max over a column (@of); any/all predicate quantifiers over a @filter; or collect (an array rollup — of the @of column, or of the carrying field.object's declared @objectRef value object when @of is omitted).",
|
|
53
53
|
"whenToUse": "A projection needs a value derived by reducing related rows — a count/sum/avg/min/max, a 'did any/every related row match' flag, or an array of collected values. Declare it instead of hand-writing the aggregate query — it stays consistent and regenerates.",
|
|
54
|
-
"rules": "@via may be omitted only when exactly one single-hop relationship leads from the base entity to the @of entity (single-hop-unique inference; FR-024, ADR-0029). Multi-hop paths must always be stated explicitly. @of is required for count/sum/avg/min/max
|
|
54
|
+
"rules": "@via may be omitted only when exactly one single-hop relationship leads from the base entity to the @of entity (single-hop-unique inference; FR-024, ADR-0029). Multi-hop paths must always be stated explicitly. @of is required for count/sum/avg/min/max and forbidden for any/all (which quantify over rows via @filter, not a column). @filter is required for any/all. The field must be isArray:true for collect and isArray:false for every other @agg. @distinct and @orderBy are collect-only. On @agg:collect @of is OPTIONAL: omitting it declares a WHOLE-OBJECT rollup, which collects each related row as the carrying field's declared @objectRef value object instead of one scalar column. A whole-object rollup requires a field.object carrying @objectRef, requires that @objectRef to name an object.value, requires an explicit @via (there is no @of entity to infer the path from), and refuses @distinct. Its @orderBy keys resolve against the @via TERMINAL entity, not the head or a middle hop. Its value-object members bind to the terminal entity's fields BY NAME — member name == terminal field name, deliberately NOT extends, so one value object stays collectable from two different entities — and every member must match a terminal field agreeing on BOTH field.<subType> and array-ness.",
|
|
55
55
|
"children": [
|
|
56
56
|
{
|
|
57
57
|
"type": "attr",
|
|
@@ -69,7 +69,7 @@ export const ORIGIN_DEFINITION: ProviderDefinition = {
|
|
|
69
69
|
"all",
|
|
70
70
|
"collect"
|
|
71
71
|
],
|
|
72
|
-
"description": "The reducing function applied over the related row-set: count/sum/avg/min/max (numeric/ordinal reduces over @of); any/all (predicate quantifiers over @filter — @of forbidden; empty set → any=false, all=true); collect (array rollup of @of
|
|
72
|
+
"description": "The reducing function applied over the related row-set: count/sum/avg/min/max (numeric/ordinal reduces over @of); any/all (predicate quantifiers over @filter — @of forbidden; empty set → any=false, all=true); collect (array rollup — of the @of column, or of the carrying field.object's declared @objectRef value object when @of is omitted; the field must be isArray)."
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"type": "attr",
|
|
@@ -77,7 +77,7 @@ export const ORIGIN_DEFINITION: ProviderDefinition = {
|
|
|
77
77
|
"name": "of",
|
|
78
78
|
"min": 0,
|
|
79
79
|
"max": 1,
|
|
80
|
-
"description": "Dotted Entity.field reference identifying the column being aggregated (e.g. 'Week.durationMinutes'). Required for count/sum/avg/min/max
|
|
80
|
+
"description": "Dotted Entity.field reference identifying the column being aggregated (e.g. 'Week.durationMinutes'). Required for count/sum/avg/min/max; OPTIONAL for collect, where absent means a whole-object rollup of the field's declared @objectRef value object; forbidden for any/all (which quantify over rows via @filter, not a column)."
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"type": "attr",
|
|
@@ -101,7 +101,7 @@ export const ORIGIN_DEFINITION: ProviderDefinition = {
|
|
|
101
101
|
"name": "distinct",
|
|
102
102
|
"min": 0,
|
|
103
103
|
"max": 1,
|
|
104
|
-
"description": "Set (collect-only) to dedupe collected values (set semantics)."
|
|
104
|
+
"description": "Set (collect-only) to dedupe collected values (set semantics). Not supported on a whole-object collect (@of omitted): it is a guaranteed no-op whenever the value object carries the primary key, and a silent no-op is worse than a refusal."
|
|
105
105
|
},
|
|
106
106
|
{
|
|
107
107
|
"type": "attr",
|
package/src/registry-manifest.ts
CHANGED
|
@@ -112,7 +112,7 @@ interface ManifestType {
|
|
|
112
112
|
* constant read `"0.10"`). Bump with that script — never by hand — so the manifest and
|
|
113
113
|
* all four port constants move together.
|
|
114
114
|
*/
|
|
115
|
-
export const METAMODEL_VERSION = "0.
|
|
115
|
+
export const METAMODEL_VERSION = "0.13";
|
|
116
116
|
|
|
117
117
|
/** The full canonical manifest. All collections are sorted for byte-stability. */
|
|
118
118
|
interface RegistryManifest {
|
|
@@ -25,6 +25,11 @@
|
|
|
25
25
|
// already decided to fail, and it returns undefined for anything it does not recognise, so
|
|
26
26
|
// `@maxLenght` still reports as an unknown attribute. The map speaks only where it KNOWS.
|
|
27
27
|
//
|
|
28
|
+
// ITS SIBLING IS `attr-contradictions.ts` — pairs of LIVE attributes that may not sit on one
|
|
29
|
+
// node. Same two consumers (the loader's diagnostic and the `meta upgrade` rewriter), same
|
|
30
|
+
// reason for existing; kept apart because a retirement matches ONE name while a contradiction
|
|
31
|
+
// matches a PAIR, and merging them would give every entry here fields it can never use.
|
|
32
|
+
//
|
|
28
33
|
// CROSS-PORT: this is a DIAGNOSTIC, not registered vocabulary — it affects no registry
|
|
29
34
|
// manifest and no load outcome, so it carries no registry-conformance obligation. The other
|
|
30
35
|
// four ports fail identically today, just with the generic message; mirroring this map is a
|
|
@@ -91,6 +96,7 @@ export interface RetiredEntry extends RetirementNote {
|
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
const REQUIREMENT_MIGRATION = "docs/features/migrations/verified-by-retirement.md";
|
|
99
|
+
const RETIRED_STATUS_MIGRATION = "docs/features/migrations/retired-status-restore.md";
|
|
94
100
|
|
|
95
101
|
export const RETIRED_VOCABULARY: readonly RetiredEntry[] = [
|
|
96
102
|
// ── 0.24.0: `@violation` is renamed `@counterexample` ──
|
|
@@ -124,21 +130,58 @@ export const RETIRED_VOCABULARY: readonly RetiredEntry[] = [
|
|
|
124
130
|
// behaviour anyone else can observe.
|
|
125
131
|
rewrite: { kind: "dropAttr" },
|
|
126
132
|
},
|
|
133
|
+
// (@supersededBy is NOT retired vocabulary. 0.24.0 deregistered it; FR-039 registers
|
|
134
|
+
// it again on `retired` only, and this time it RESOLVES — which is what the
|
|
135
|
+
// 2026-08-10 ruling asked for at point 4 and never got. An entry here would make
|
|
136
|
+
// `meta upgrade` delete an attribute the loader now accepts.)
|
|
137
|
+
// ── FR-039 (0.24.2): `abandoned` / `superseded` become `retired` ──
|
|
138
|
+
//
|
|
139
|
+
// These were retired in 0.24.0 on the rule that a requirement never journals what
|
|
140
|
+
// happened, and `meta upgrade` REFUSED them because what becomes of a retired
|
|
141
|
+
// capability's record was judgement. FR-039 restores the capability under a name
|
|
142
|
+
// that states the standing rule rather than the history — `retired` means "this
|
|
143
|
+
// must not be rebuilt" — so the edit is no longer a judgement call and the tool
|
|
144
|
+
// can make it.
|
|
145
|
+
//
|
|
146
|
+
// TWO entries rather than one value-map, because the rewriter's `renameAttrValue`
|
|
147
|
+
// carries a single `fromValue` and `attrValues` already scopes each occurrence to
|
|
148
|
+
// the value it fires on. `otherwise` is therefore unreachable, and is `refuse` so
|
|
149
|
+
// that a future third member cannot be silently dropped by this entry.
|
|
150
|
+
//
|
|
151
|
+
// `@implementedBy` on one of these is handled by ATTR_CONTRADICTIONS, not here —
|
|
152
|
+
// it is a live attribute made illegal by a sibling's VALUE, which is a different
|
|
153
|
+
// match shape. Both passes run in one `meta upgrade`, so a legacy entry carrying
|
|
154
|
+
// both is fully repaired in a single run rather than rewritten into a file that
|
|
155
|
+
// still will not load.
|
|
127
156
|
{
|
|
128
|
-
type: "requirement", subType: "*", attr: "
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
157
|
+
type: "requirement", subType: "*", attr: "status",
|
|
158
|
+
attrValues: ["abandoned"],
|
|
159
|
+
since: "0.24.2",
|
|
160
|
+
why: "a retired capability is recorded as `retired`, which states the standing rule " +
|
|
161
|
+
"(do not rebuild this) rather than narrating what happened to it",
|
|
162
|
+
migration: RETIRED_STATUS_MIGRATION,
|
|
163
|
+
rewrite: {
|
|
164
|
+
kind: "renameAttrValue",
|
|
165
|
+
toAttr: "status",
|
|
166
|
+
fromValue: "abandoned",
|
|
167
|
+
toValue: "retired",
|
|
168
|
+
otherwise: "refuse",
|
|
169
|
+
},
|
|
134
170
|
},
|
|
135
171
|
{
|
|
136
172
|
type: "requirement", subType: "*", attr: "status",
|
|
137
|
-
attrValues: ["
|
|
138
|
-
since: "0.24.
|
|
139
|
-
why: "
|
|
140
|
-
"
|
|
141
|
-
migration:
|
|
173
|
+
attrValues: ["superseded"],
|
|
174
|
+
since: "0.24.2",
|
|
175
|
+
why: "`superseded` was `retired` plus a pointer, and the pointer is @supersededBy — " +
|
|
176
|
+
"which is registered again, and now RESOLVES",
|
|
177
|
+
migration: RETIRED_STATUS_MIGRATION,
|
|
178
|
+
rewrite: {
|
|
179
|
+
kind: "renameAttrValue",
|
|
180
|
+
toAttr: "status",
|
|
181
|
+
fromValue: "superseded",
|
|
182
|
+
toValue: "retired",
|
|
183
|
+
otherwise: "refuse",
|
|
184
|
+
},
|
|
142
185
|
},
|
|
143
186
|
|
|
144
187
|
// ── FR-037 R1: @readOnly becomes the @mutability enum (0.24.0) ──
|
|
@@ -27,14 +27,24 @@
|
|
|
27
27
|
// `scopeRanges` below recovers the enclosing `"<type>.<subType>"` for each occurrence, so
|
|
28
28
|
// one pass over the document answers both correctly.
|
|
29
29
|
//
|
|
30
|
-
// CANONICAL JSON ONLY
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
30
|
+
// CANONICAL JSON ONLY — YAML lives in `core/vocabulary-rewrite-yaml.ts`, not here. This
|
|
31
|
+
// module is reachable from `src/index.ts`, which may not import the Node-only `yaml`
|
|
32
|
+
// package, so the YAML arm sits behind its own package subpath and `meta upgrade`
|
|
33
|
+
// dynamic-imports it. The split is a bundling constraint, not a difference in contract: both
|
|
34
|
+
// arms return the same result shape, scope every occurrence the same way, and refuse the
|
|
35
|
+
// same retirements.
|
|
36
|
+
//
|
|
37
|
+
// The reason YAML gets a parser and this arm does not: a hand-rolled YAML mode was tried
|
|
38
|
+
// here first and shipped a file-corrupting bug — a multi-item block sequence lost every item
|
|
39
|
+
// but the first, because a scanner stops at a newline — while the dominant authoring style
|
|
40
|
+
// (flow mappings, `{ name: x, readOnly: true }`) was not matched at all. YAML's value extent
|
|
41
|
+
// is not derivable by scanning; JSON's is.
|
|
42
|
+
//
|
|
43
|
+
// IT ALSO RESOLVES ATTRIBUTE CONTRADICTIONS — two LIVE attributes that may not sit on one
|
|
44
|
+
// node (`attr-contradictions.ts`). Same machinery, different match: a retirement finds one
|
|
45
|
+
// key, a contradiction finds a PAIR inside one node body, which is exactly what `scopeRanges`
|
|
46
|
+
// already answers. Doing it by proximity instead ("a `fields` with an `expr` near it") was
|
|
47
|
+
// tried and took a `fields` whose neighbouring `expr` belonged to a SIBLING node.
|
|
38
48
|
//
|
|
39
49
|
// IT REFUSES WHAT IT CANNOT KNOW. A retirement with no `rewrite` (`@status: abandoned`) is
|
|
40
50
|
// reported, never guessed at. Deleting the node, retyping it, and fixing the residue it
|
|
@@ -42,6 +52,8 @@
|
|
|
42
52
|
// something different — strictly worse than leaving it alone, because the adopter would
|
|
43
53
|
// believe the migration finished.
|
|
44
54
|
|
|
55
|
+
import { ATTR_CONTRADICTIONS, contradictionScopeMatches } from "./attr-contradictions.js";
|
|
56
|
+
import type { AttrContradiction } from "./attr-contradictions.js";
|
|
45
57
|
import {
|
|
46
58
|
RETIRED_VOCABULARY,
|
|
47
59
|
note,
|
|
@@ -154,15 +166,71 @@ function scopeRanges(source: string): ScopeRange[] {
|
|
|
154
166
|
return ranges;
|
|
155
167
|
}
|
|
156
168
|
|
|
157
|
-
/** The
|
|
158
|
-
function
|
|
169
|
+
/** The innermost node body containing `offset`. */
|
|
170
|
+
function scopeRangeAt(ranges: readonly ScopeRange[], offset: number): ScopeRange | undefined {
|
|
159
171
|
let best: ScopeRange | undefined;
|
|
160
172
|
for (const r of ranges) {
|
|
161
173
|
if (offset <= r.bodyStart || offset >= r.bodyEnd) continue;
|
|
162
174
|
// Properly nested ranges: the innermost containing one starts last.
|
|
163
175
|
if (best === undefined || r.bodyStart > best.bodyStart) best = r;
|
|
164
176
|
}
|
|
165
|
-
return best
|
|
177
|
+
return best;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The type key governing `offset`. Derived from the range so the two cannot disagree —
|
|
181
|
+
* a retirement asks WHICH type, a contradiction asks WHICH NODE, and answering them from
|
|
182
|
+
* two separate walks is how the pair-matching would drift from the scoping. */
|
|
183
|
+
function scopeAt(ranges: readonly ScopeRange[], offset: number): string | undefined {
|
|
184
|
+
return scopeRangeAt(ranges, offset)?.typeKey;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** One key occurrence: where the key starts, and where its value begins. */
|
|
188
|
+
interface KeySite {
|
|
189
|
+
readonly keyStart: number;
|
|
190
|
+
readonly afterKey: number;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Every place `attr` appears as a key inside `range`'s OWN body — a nested node's key of
|
|
194
|
+
* the same name belongs to that node, not to this one. Containment alone is not enough:
|
|
195
|
+
* a node body contains its children's bodies, so the innermost enclosing range must BE
|
|
196
|
+
* this range. That identity test is what proximity matching cannot express. */
|
|
197
|
+
function ownKeys(
|
|
198
|
+
source: string,
|
|
199
|
+
ranges: readonly ScopeRange[],
|
|
200
|
+
range: ScopeRange,
|
|
201
|
+
attr: string,
|
|
202
|
+
): KeySite[] {
|
|
203
|
+
const out: KeySite[] = [];
|
|
204
|
+
const re = keyPattern(attr);
|
|
205
|
+
let m: RegExpExecArray | null;
|
|
206
|
+
while ((m = re.exec(source)) !== null) {
|
|
207
|
+
if (m.index <= range.bodyStart || m.index >= range.bodyEnd) continue;
|
|
208
|
+
if (scopeRangeAt(ranges, m.index)?.bodyStart !== range.bodyStart) continue;
|
|
209
|
+
out.push({ keyStart: m.index, afterKey: m.index + m[0].length });
|
|
210
|
+
}
|
|
211
|
+
return out;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
/** True when `keep` holds one of the entry's `keepValues` (or the entry names none,
|
|
216
|
+
* in which case mere presence is the contradiction). */
|
|
217
|
+
function keepValueMatches(source: string, site: KeySite, c: AttrContradiction): boolean {
|
|
218
|
+
if (c.keepValues === undefined) return true;
|
|
219
|
+
const raw = valueSpan(source, site.afterKey)?.raw;
|
|
220
|
+
if (raw === undefined) return false;
|
|
221
|
+
return c.keepValues.some((v) => rawEquals(raw, v));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Does this key carry a string that actually says something? */
|
|
225
|
+
function suppliesText(source: string, site: KeySite): boolean {
|
|
226
|
+
const raw = valueSpan(source, site.afterKey)?.raw;
|
|
227
|
+
if (raw === undefined) return false;
|
|
228
|
+
try {
|
|
229
|
+
const v: unknown = JSON.parse(raw);
|
|
230
|
+
return typeof v === "string" && v.trim().length > 0;
|
|
231
|
+
} catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
166
234
|
}
|
|
167
235
|
|
|
168
236
|
/**
|
|
@@ -278,6 +346,49 @@ export function rewriteDocument(source: string, opts: RewriteOpts = {}): Rewrite
|
|
|
278
346
|
}
|
|
279
347
|
}
|
|
280
348
|
|
|
349
|
+
// ── Attribute contradictions: two LIVE attrs that may not sit on one node ──
|
|
350
|
+
//
|
|
351
|
+
// Matched per NODE, not per occurrence, because the illegal thing is the pair. `ownKeys`
|
|
352
|
+
// supplies the node-identity test the proximity approach could not: a `@fields` and an
|
|
353
|
+
// `@expr` that merely appear near each other may belong to different siblings.
|
|
354
|
+
//
|
|
355
|
+
// THE TWO SIDES ARE ASKED DIFFERENT QUESTIONS, mirroring the loader's Rule 1a exactly
|
|
356
|
+
// (`validation-passes.ts`, `hasFieldsAttr` vs `hasExpr`). The DROP side counts on
|
|
357
|
+
// PRESENCE — `@fields: []` beside `@expr` is still a declaration of both, and is the case
|
|
358
|
+
// where the discard is total. The KEEP side counts only when it actually supplies a key,
|
|
359
|
+
// so `@expr: ""` beside `@fields` is a plain column index the loader accepts and this
|
|
360
|
+
// must not touch. If those two predicates ever diverge, this deletes an attribute from a
|
|
361
|
+
// document that was loading.
|
|
362
|
+
//
|
|
363
|
+
// IT SEES ONLY THIS NODE'S OWN TEXT. A node declaring `@expr` while INHERITING `@fields`
|
|
364
|
+
// through `extends` contradicts itself in the loaded model and not on the page, and no
|
|
365
|
+
// raw-text rewriter can resolve a super-reference. That case stays a refusal from the
|
|
366
|
+
// loader — correctly, since the fix is on the parent and is the adopter's call.
|
|
367
|
+
for (const range of ranges) {
|
|
368
|
+
for (const c of ATTR_CONTRADICTIONS) {
|
|
369
|
+
if (opts.maxVersion !== undefined && !atOrBefore(c.since, opts.maxVersion)) continue;
|
|
370
|
+
if (!contradictionScopeMatches(c, range.typeKey)) continue;
|
|
371
|
+
// `keep` must be present AND, when the entry names values, hold one of them —
|
|
372
|
+
// otherwise @status and @implementedBy would contradict on every status.
|
|
373
|
+
if (!ownKeys(source, ranges, range, c.keep).some(
|
|
374
|
+
(k) => suppliesText(source, k) && keepValueMatches(source, k, c),
|
|
375
|
+
)) continue;
|
|
376
|
+
|
|
377
|
+
for (const site of ownKeys(source, ranges, range, c.drop)) {
|
|
378
|
+
const span = valueSpan(source, site.afterKey);
|
|
379
|
+
if (span === undefined) continue;
|
|
380
|
+
const { start, end } = dropSpan(source, site.keyStart, span.end);
|
|
381
|
+
edits.push({ start, end, text: "" });
|
|
382
|
+
changes.push({
|
|
383
|
+
attr: c.drop,
|
|
384
|
+
from: c.drop,
|
|
385
|
+
to: `(removed — @${c.keep} keys this node)`,
|
|
386
|
+
line: lineAt(source, site.keyStart),
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
281
392
|
for (const entry of RETIRED_VOCABULARY.filter((e) => e.attr !== undefined && inWindow(e))) {
|
|
282
393
|
const attr = entry.attr as string;
|
|
283
394
|
const re = keyPattern(attr);
|
|
@@ -289,8 +400,9 @@ export function rewriteDocument(source: string, opts: RewriteOpts = {}): Rewrite
|
|
|
289
400
|
const afterKey = m.index + m[0].length;
|
|
290
401
|
|
|
291
402
|
// Scope is decided HERE, per occurrence, from the enclosing node.
|
|
292
|
-
const
|
|
293
|
-
|
|
403
|
+
const scopeRange = scopeRangeAt(ranges, keyStart);
|
|
404
|
+
const scope = scopeRange?.typeKey;
|
|
405
|
+
if (scope === undefined || scopeRange === undefined || !scopeMatches(entry, scope)) continue;
|
|
294
406
|
|
|
295
407
|
const line = lineAt(source, keyStart);
|
|
296
408
|
const span = valueSpan(source, afterKey);
|
|
@@ -321,8 +433,18 @@ export function rewriteDocument(source: string, opts: RewriteOpts = {}): Rewrite
|
|
|
321
433
|
const rw = entry.rewrite;
|
|
322
434
|
if (rw === undefined) refuse();
|
|
323
435
|
else if (rw.kind === "renameAttr") {
|
|
324
|
-
|
|
325
|
-
|
|
436
|
+
// A rename onto a key the node ALREADY declares would emit a duplicate — two
|
|
437
|
+
// `"@counterexample"` members in one object, where JSON parsers silently take
|
|
438
|
+
// the last and the author's surviving text is the one that loses. Refuse
|
|
439
|
+
// instead: which of the two sentences is the real one is exactly the judgement
|
|
440
|
+
// `meta upgrade` does not make.
|
|
441
|
+
const target = ownKeys(source, ranges, scopeRange, rw.to)
|
|
442
|
+
.filter((k) => k.keyStart !== keyStart);
|
|
443
|
+
if (target.length > 0) refuse();
|
|
444
|
+
else {
|
|
445
|
+
edits.push({ start: keyStart, end: keyEnd, text: `"@${rw.to}"` });
|
|
446
|
+
changes.push({ attr, from: attr, to: rw.to, line });
|
|
447
|
+
}
|
|
326
448
|
} else if (rw.kind === "dropAttr") drop();
|
|
327
449
|
else if (span === undefined) continue;
|
|
328
450
|
else if (rawEquals(span.raw, rw.fromValue)) {
|