@metaobjectsdev/metadata 0.24.0-rc.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attr-schema-validate.d.ts.map +1 -1
- package/dist/attr-schema-validate.js +17 -2
- package/dist/attr-schema-validate.js.map +1 -1
- package/dist/core/field/field-constants.d.ts +18 -5
- package/dist/core/field/field-constants.d.ts.map +1 -1
- package/dist/core/field/field-constants.js +21 -5
- package/dist/core/field/field-constants.js.map +1 -1
- package/dist/core/field/field-definition.embedded.d.ts.map +1 -1
- package/dist/core/field/field-definition.embedded.js +8 -3
- package/dist/core/field/field-definition.embedded.js.map +1 -1
- package/dist/core/field/validate-field-mutability.d.ts +22 -0
- package/dist/core/field/validate-field-mutability.d.ts.map +1 -0
- package/dist/core/field/validate-field-mutability.js +208 -0
- package/dist/core/field/validate-field-mutability.js.map +1 -0
- package/dist/core/identity/identity-constants.d.ts +12 -0
- package/dist/core/identity/identity-constants.d.ts.map +1 -1
- package/dist/core/identity/identity-constants.js +15 -0
- package/dist/core/identity/identity-constants.js.map +1 -1
- package/dist/core/object/object-definition.embedded.js +2 -2
- package/dist/core/object/object-definition.embedded.js.map +1 -1
- package/dist/core/requirement/meta-requirement.d.ts +1 -2
- package/dist/core/requirement/meta-requirement.d.ts.map +1 -1
- package/dist/core/requirement/meta-requirement.js +2 -6
- package/dist/core/requirement/meta-requirement.js.map +1 -1
- package/dist/core/requirement/requirement-constants.d.ts +3 -8
- package/dist/core/requirement/requirement-constants.d.ts.map +1 -1
- package/dist/core/requirement/requirement-constants.js +2 -9
- package/dist/core/requirement/requirement-constants.js.map +1 -1
- package/dist/core/requirement/requirement-definition.embedded.d.ts.map +1 -1
- package/dist/core/requirement/requirement-definition.embedded.js +8 -46
- package/dist/core/requirement/requirement-definition.embedded.js.map +1 -1
- package/dist/core/requirement/resolve-claim.d.ts +37 -0
- package/dist/core/requirement/resolve-claim.d.ts.map +1 -0
- package/dist/core/requirement/resolve-claim.js +83 -0
- package/dist/core/requirement/resolve-claim.js.map +1 -0
- package/dist/core-types.d.ts.map +1 -1
- package/dist/core-types.js +10 -10
- package/dist/core-types.js.map +1 -1
- package/dist/errors.d.ts +2 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +16 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/library/library-sources.d.ts +11 -0
- package/dist/library/library-sources.d.ts.map +1 -1
- package/dist/library/library-sources.js +13 -0
- package/dist/library/library-sources.js.map +1 -1
- package/dist/loader/meta-data-loader.d.ts.map +1 -1
- package/dist/loader/meta-data-loader.js +10 -9
- package/dist/loader/meta-data-loader.js.map +1 -1
- package/dist/loader/validation-passes.js +2 -2
- package/dist/loader/validation-passes.js.map +1 -1
- package/dist/naming-refs.d.ts.map +1 -1
- package/dist/naming-refs.js +2 -3
- package/dist/naming-refs.js.map +1 -1
- package/dist/parser-core.d.ts.map +1 -1
- package/dist/parser-core.js +22 -5
- package/dist/parser-core.js.map +1 -1
- package/dist/persistence/origin/meta-origin.d.ts +0 -10
- package/dist/persistence/origin/meta-origin.d.ts.map +1 -1
- package/dist/persistence/origin/meta-origin.js +1 -15
- package/dist/persistence/origin/meta-origin.js.map +1 -1
- package/dist/persistence/origin/origin-constants.d.ts +2 -4
- package/dist/persistence/origin/origin-constants.d.ts.map +1 -1
- package/dist/persistence/origin/origin-constants.js +9 -7
- package/dist/persistence/origin/origin-constants.js.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.d.ts.map +1 -1
- package/dist/persistence/origin/origin-definition.embedded.js +0 -16
- package/dist/persistence/origin/origin-definition.embedded.js.map +1 -1
- package/dist/registry-manifest.d.ts +9 -4
- package/dist/registry-manifest.d.ts.map +1 -1
- package/dist/registry-manifest.js +9 -4
- package/dist/registry-manifest.js.map +1 -1
- package/dist/retired-vocabulary.d.ts +92 -0
- package/dist/retired-vocabulary.d.ts.map +1 -0
- package/dist/retired-vocabulary.js +204 -0
- package/dist/retired-vocabulary.js.map +1 -0
- package/dist/super-resolve.d.ts +33 -0
- package/dist/super-resolve.d.ts.map +1 -1
- package/dist/super-resolve.js +56 -1
- package/dist/super-resolve.js.map +1 -1
- package/dist/template/prompt-definition.embedded.js +1 -1
- package/dist/template/prompt-definition.embedded.js.map +1 -1
- package/dist/template/template-constants.d.ts +6 -0
- package/dist/template/template-constants.d.ts.map +1 -1
- package/dist/template/template-constants.js +24 -1
- package/dist/template/template-constants.js.map +1 -1
- package/dist/template/template-definition.embedded.d.ts.map +1 -1
- package/dist/template/template-definition.embedded.js +34 -21
- package/dist/template/template-definition.embedded.js.map +1 -1
- package/dist/vocabulary-rewrite.d.ts +33 -0
- package/dist/vocabulary-rewrite.d.ts.map +1 -0
- package/dist/vocabulary-rewrite.js +310 -0
- package/dist/vocabulary-rewrite.js.map +1 -0
- package/package.json +6 -1
- package/src/attr-schema-validate.ts +17 -2
- package/src/core/field/field-constants.ts +24 -5
- package/src/core/field/field-definition.embedded.ts +8 -3
- package/src/core/field/validate-field-mutability.ts +259 -0
- package/src/core/identity/identity-constants.ts +16 -0
- package/src/core/object/object-definition.embedded.ts +2 -2
- package/src/core/requirement/meta-requirement.ts +1 -7
- package/src/core/requirement/requirement-constants.ts +2 -9
- package/src/core/requirement/requirement-definition.embedded.ts +8 -46
- package/src/core/requirement/resolve-claim.ts +98 -0
- package/src/core-types.ts +9 -10
- package/src/errors.ts +16 -2
- package/src/index.ts +29 -1
- package/src/library/library-sources.ts +14 -0
- package/src/loader/meta-data-loader.ts +10 -9
- package/src/loader/validation-passes.ts +3 -3
- package/src/naming-refs.ts +1 -3
- package/src/parser-core.ts +27 -5
- package/src/persistence/origin/meta-origin.ts +0 -16
- package/src/persistence/origin/origin-constants.ts +9 -8
- package/src/persistence/origin/origin-definition.embedded.ts +0 -16
- package/src/registry-manifest.ts +9 -4
- package/src/retired-vocabulary.ts +280 -0
- package/src/super-resolve.ts +59 -1
- package/src/template/prompt-definition.embedded.ts +1 -1
- package/src/template/template-constants.ts +27 -1
- package/src/template/template-definition.embedded.ts +34 -21
- package/src/vocabulary-rewrite.ts +346 -0
- package/dist/core/field/validate-field-readonly.d.ts +0 -9
- package/dist/core/field/validate-field-readonly.d.ts.map +0 -1
- package/dist/core/field/validate-field-readonly.js +0 -128
- package/dist/core/field/validate-field-readonly.js.map +0 -1
- package/src/core/field/validate-field-readonly.ts +0 -154
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// server/typescript/packages/metadata/src/retired-vocabulary.ts
|
|
2
|
+
//
|
|
3
|
+
// Vocabulary this project USED to register, and what to do instead.
|
|
4
|
+
//
|
|
5
|
+
// WHY THIS EXISTS (#337). Under ADR-0023 the registry is strict and sealed, so retired
|
|
6
|
+
// vocabulary fails the load — correctly. But the message it failed with said the attribute
|
|
7
|
+
// was "not declared by any registered provider", which tells an author their metadata is
|
|
8
|
+
// malformed when the truth is that the vocabulary was retired ON PURPOSE, in a named
|
|
9
|
+
// release, with a written migration. An adopter read that error, concluded the tool had a
|
|
10
|
+
// registration bug, filed it as one, and argued against a decision whose reasoning they had
|
|
11
|
+
// never seen — because nothing in the error pointed at it. The migration guide answered
|
|
12
|
+
// their objection precisely. They never found it.
|
|
13
|
+
//
|
|
14
|
+
// THIS CHANGES NO LOAD OUTCOME. Retired vocabulary still fails, with the same error code,
|
|
15
|
+
// at the same site. Only the message gains a sentence. That is deliberate: making any of
|
|
16
|
+
// these load again would undo an adjudicated breaking change, and a "helpful" shim is how a
|
|
17
|
+
// retirement quietly stops being one.
|
|
18
|
+
//
|
|
19
|
+
// SCOPE IS BY TYPE, NEVER BY BARE NAME. `@unique` is retired on `identity.secondary` and
|
|
20
|
+
// perfectly live on a field; a name-only match would brand a valid declaration as retired,
|
|
21
|
+
// which is worse than the generic message it replaces. Every entry names the type it
|
|
22
|
+
// applies to, and `*` means "every subtype of this type" — never "every type".
|
|
23
|
+
//
|
|
24
|
+
// A GENUINE TYPO MUST STAY GENERIC. This map is consulted only after the normal check has
|
|
25
|
+
// already decided to fail, and it returns undefined for anything it does not recognise, so
|
|
26
|
+
// `@maxLenght` still reports as an unknown attribute. The map speaks only where it KNOWS.
|
|
27
|
+
//
|
|
28
|
+
// CROSS-PORT: this is a DIAGNOSTIC, not registered vocabulary — it affects no registry
|
|
29
|
+
// manifest and no load outcome, so it carries no registry-conformance obligation. The other
|
|
30
|
+
// four ports fail identically today, just with the generic message; mirroring this map is a
|
|
31
|
+
// per-port ergonomics follow-up, not a conformance gap.
|
|
32
|
+
|
|
33
|
+
/** What a reader needs at the moment their load fails. */
|
|
34
|
+
export interface RetirementNote {
|
|
35
|
+
/** The release that retired it. */
|
|
36
|
+
readonly since: string;
|
|
37
|
+
/** One line: what it was, and why it went. */
|
|
38
|
+
readonly why: string;
|
|
39
|
+
/** The vocabulary that replaced it, when something did. */
|
|
40
|
+
readonly replacedBy?: string;
|
|
41
|
+
/** Repo-relative migration guide. */
|
|
42
|
+
readonly migration?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The mechanical fix, when one exists.
|
|
47
|
+
*
|
|
48
|
+
* ABSENT MEANS JUDGMENT — `@status: abandoned` can be resolved by deleting the node, by
|
|
49
|
+
* retyping it, or by fixing the residue it describes, and only a human knows which. A tool
|
|
50
|
+
* that guessed would emit metadata that LOADS and means something different, which is worse
|
|
51
|
+
* than refusing: the adopter would believe the migration finished.
|
|
52
|
+
*/
|
|
53
|
+
export type VocabularyRewrite =
|
|
54
|
+
/** The attribute name changed; the value is untouched. */
|
|
55
|
+
| { readonly kind: "renameAttr"; readonly to: string }
|
|
56
|
+
/** The attribute went away with no replacement — drop it. */
|
|
57
|
+
| { readonly kind: "dropAttr" }
|
|
58
|
+
/**
|
|
59
|
+
* Both the name and the value changed (`@readOnly: true` → `@mutability: "readOnly"`).
|
|
60
|
+
*
|
|
61
|
+
* `otherwise` is REQUIRED, and that is the point. The attribute is retired for EVERY
|
|
62
|
+
* value it could hold, so an entry that names only the value it can rewrite has said
|
|
63
|
+
* nothing about the rest — and the rewriter's only honest options are to drop them or to
|
|
64
|
+
* refuse them. Leaving it implicit is how `@readOnly: false` came to be silently skipped:
|
|
65
|
+
* the entry's prose said "treated as a drop", the code fell through to `continue`, and
|
|
66
|
+
* `meta upgrade` exited 0 on a file that still would not load.
|
|
67
|
+
*/
|
|
68
|
+
| {
|
|
69
|
+
readonly kind: "renameAttrValue";
|
|
70
|
+
readonly toAttr: string;
|
|
71
|
+
readonly fromValue: unknown;
|
|
72
|
+
readonly toValue: unknown;
|
|
73
|
+
/** What happens to every value other than `fromValue`. */
|
|
74
|
+
readonly otherwise: "drop" | "refuse";
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** One retirement. `subType: "*"` means every subtype of `type`. */
|
|
78
|
+
export interface RetiredEntry extends RetirementNote {
|
|
79
|
+
readonly type: string;
|
|
80
|
+
/** `*` for every subtype of `type`, else the exact subtype. */
|
|
81
|
+
readonly subType: string;
|
|
82
|
+
/** Retired ATTRIBUTE name (no `@`), when the attribute itself went. */
|
|
83
|
+
readonly attr?: string;
|
|
84
|
+
/** Retired VALUES of a surviving attribute, when only some members went. */
|
|
85
|
+
readonly attrValues?: readonly string[];
|
|
86
|
+
/** Set when the SUBTYPE itself was retired (`attr`/`attrValues` absent). */
|
|
87
|
+
readonly isSubTypeRetirement?: boolean;
|
|
88
|
+
/** How `meta upgrade` fixes it. Absent ⇒ the human decides; the tool refuses and
|
|
89
|
+
* prints `migration`. */
|
|
90
|
+
readonly rewrite?: VocabularyRewrite;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const REQUIREMENT_MIGRATION = "docs/features/migrations/verified-by-retirement.md";
|
|
94
|
+
|
|
95
|
+
export const RETIRED_VOCABULARY: readonly RetiredEntry[] = [
|
|
96
|
+
// ── 0.24.0: `@violation` is renamed `@counterexample` ──
|
|
97
|
+
//
|
|
98
|
+
// Pure rename — no semantics change. The field always held a static falsifiability test
|
|
99
|
+
// ("what would contradict this"), authored once, never a state. `@violation` READ as a
|
|
100
|
+
// status, and did so to the person who approved the vocabulary, who asked whether it
|
|
101
|
+
// meant "we know this requirement is currently violated". A name that misleads its own
|
|
102
|
+
// owner has earned replacing.
|
|
103
|
+
//
|
|
104
|
+
// Fully mechanical, which is the point: adopters migrate with `meta upgrade --apply`
|
|
105
|
+
// rather than a hand sweep. This entry is the first real user of that tool.
|
|
106
|
+
{
|
|
107
|
+
type: "requirement", subType: "*", attr: "violation",
|
|
108
|
+
since: "0.24.0",
|
|
109
|
+
why: "it named a static falsifiability test but read as a status — 'this requirement " +
|
|
110
|
+
"is in violation' — which is not what the field has ever held",
|
|
111
|
+
replacedBy: "@counterexample",
|
|
112
|
+
migration: "docs/features/migrations/violation-to-counterexample.md",
|
|
113
|
+
rewrite: { kind: "renameAttr", to: "counterexample" },
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// ── FR-038: the requirement vocabulary becomes prescriptive-only (0.24.0) ──
|
|
117
|
+
{
|
|
118
|
+
type: "requirement", subType: "*", attr: "verifiedBy",
|
|
119
|
+
since: "0.24.0",
|
|
120
|
+
why: "it asked you to name a test, and verify could only check that the NAME occurred " +
|
|
121
|
+
"somewhere in your test sources — never that the named test verified the claim",
|
|
122
|
+
migration: REQUIREMENT_MIGRATION,
|
|
123
|
+
// Nothing replaced it, so the fix is removal. Safe to automate: the attribute drove no
|
|
124
|
+
// behaviour anyone else can observe.
|
|
125
|
+
rewrite: { kind: "dropAttr" },
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
type: "requirement", subType: "*", attr: "supersededBy",
|
|
129
|
+
since: "0.24.0",
|
|
130
|
+
why: "a requirement is prescriptive — it states what should be true and is never a " +
|
|
131
|
+
"journal of what happened",
|
|
132
|
+
migration: REQUIREMENT_MIGRATION,
|
|
133
|
+
rewrite: { kind: "dropAttr" },
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
type: "requirement", subType: "*", attr: "status",
|
|
137
|
+
attrValues: ["abandoned", "superseded"],
|
|
138
|
+
since: "0.24.0",
|
|
139
|
+
why: "retiring a capability is DELETING its requirement; version control holds that it " +
|
|
140
|
+
"existed, and `notes` on a surviving entry holds what a reader still needs",
|
|
141
|
+
migration: REQUIREMENT_MIGRATION,
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// ── FR-037 R1: @readOnly becomes the @mutability enum (0.24.0) ──
|
|
145
|
+
{
|
|
146
|
+
type: "field", subType: "*", attr: "readOnly",
|
|
147
|
+
since: "0.24.0",
|
|
148
|
+
why: "a boolean could not express write-once, so the axis became an enum",
|
|
149
|
+
replacedBy: "@mutability",
|
|
150
|
+
migration: "docs/features/migrations/readonly-to-mutability.md",
|
|
151
|
+
// Key AND value: `@readOnly: true` becomes `@mutability: "readOnly"`. `@readOnly: false`
|
|
152
|
+
// was the default and simply goes away — hence `otherwise: "drop"` rather than inventing
|
|
153
|
+
// a mutability the author never stated. Both arms must be stated: the attribute is
|
|
154
|
+
// deregistered for every value, so an unhandled arm is a file that still fails to load
|
|
155
|
+
// after a run that reported success.
|
|
156
|
+
rewrite: {
|
|
157
|
+
kind: "renameAttrValue",
|
|
158
|
+
toAttr: "mutability",
|
|
159
|
+
fromValue: true,
|
|
160
|
+
toValue: "readOnly",
|
|
161
|
+
otherwise: "drop",
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
// ── FR-037 R2: origin.collection retires to reserved-not-registered (0.24.0) ──
|
|
166
|
+
{
|
|
167
|
+
type: "origin", subType: "collection", isSubTypeRetirement: true,
|
|
168
|
+
since: "0.24.0",
|
|
169
|
+
why: "it duplicated `origin.aggregate @agg: collect` on a strictly smaller attribute " +
|
|
170
|
+
"set, and nothing dispatched on it",
|
|
171
|
+
replacedBy: "origin.aggregate @agg: collect",
|
|
172
|
+
migration: "docs/features/migrations/origin-collection-retirement.md",
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// ── ADR-0040: uniqueness lives in the TYPE, not an attribute (0.15.1) ──
|
|
176
|
+
{
|
|
177
|
+
type: "identity", subType: "secondary", attr: "unique",
|
|
178
|
+
since: "0.15.1",
|
|
179
|
+
why: "`identity.secondary` is now always a unique alternate key — uniqueness is encoded " +
|
|
180
|
+
"in the type, so the attribute had nothing left to say",
|
|
181
|
+
replacedBy: "index.lookup (for a NON-unique retrieval index)",
|
|
182
|
+
migration: "docs/features/migrations/identity-secondary-to-index-lookup.md",
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
// ── Metamodel 1.0: @dbColumnType slim-and-derive (0.15.0) ──
|
|
186
|
+
{
|
|
187
|
+
type: "field", subType: "*", attr: "dbColumnType",
|
|
188
|
+
attrValues: ["uuid_array", "text_array"],
|
|
189
|
+
since: "0.15.0",
|
|
190
|
+
why: "array-ness is DERIVED from `isArray`, so an array-flavoured physical type restated " +
|
|
191
|
+
"something the model already knew",
|
|
192
|
+
replacedBy: "isArray: true on the field",
|
|
193
|
+
// On a VALUE-scoped entry, `dropAttr` means "drop it when the value is one of the
|
|
194
|
+
// retired ones" — never unconditionally. `@dbColumnType: jsonb` is live vocabulary on
|
|
195
|
+
// the same attribute, and removing it would silently change the column type.
|
|
196
|
+
//
|
|
197
|
+
// Safe because the attribute said nothing the model did not already know: the field
|
|
198
|
+
// carrying `uuid_array` necessarily has `isArray: true`, which is where array-ness now
|
|
199
|
+
// comes from. There is no guide for this one, so without the rewrite an adopter would
|
|
200
|
+
// be told it is retired and given nowhere to go — which is what the dead-end test that
|
|
201
|
+
// caught this exists to prevent.
|
|
202
|
+
rewrite: { kind: "dropAttr" },
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* True when `entry` governs `typeKey` (`"<type>.<subType>"`).
|
|
208
|
+
*
|
|
209
|
+
* Exported because the rewriter scopes every occurrence with the SAME rule. It used to
|
|
210
|
+
* carry its own copy, and a scoping rule that lives in two files is one that will be fixed
|
|
211
|
+
* in one of them.
|
|
212
|
+
*/
|
|
213
|
+
export function scopeMatches(entry: RetiredEntry, typeKey: string): boolean {
|
|
214
|
+
const dot = typeKey.indexOf(".");
|
|
215
|
+
if (dot < 0) return false;
|
|
216
|
+
if (entry.type !== typeKey.slice(0, dot)) return false;
|
|
217
|
+
return entry.subType === "*" || entry.subType === typeKey.slice(dot + 1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** The reader-facing half of an entry, without the matching machinery. */
|
|
221
|
+
export function note(entry: RetiredEntry): RetirementNote {
|
|
222
|
+
return {
|
|
223
|
+
since: entry.since,
|
|
224
|
+
why: entry.why,
|
|
225
|
+
...(entry.replacedBy !== undefined ? { replacedBy: entry.replacedBy } : {}),
|
|
226
|
+
...(entry.migration !== undefined ? { migration: entry.migration } : {}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** First entry satisfying `match`, as a note. The three lookups below differ only in it. */
|
|
231
|
+
function noted(match: (e: RetiredEntry) => boolean): RetirementNote | undefined {
|
|
232
|
+
const hit = RETIRED_VOCABULARY.find(match);
|
|
233
|
+
return hit === undefined ? undefined : note(hit);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The attribute NAME itself was retired for this type. Returns undefined for an attr whose
|
|
238
|
+
* name is unknown here — a typo must keep reporting as a typo.
|
|
239
|
+
*
|
|
240
|
+
* An entry carrying `attrValues` is a VALUE retirement on a surviving attribute, so it is
|
|
241
|
+
* deliberately not matched here: `@status` is still perfectly good vocabulary.
|
|
242
|
+
*/
|
|
243
|
+
export function retiredAttr(typeKey: string, attrName: string): RetirementNote | undefined {
|
|
244
|
+
return noted(
|
|
245
|
+
(e) => e.attr === attrName && e.attrValues === undefined && scopeMatches(e, typeKey),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** A specific VALUE of a surviving attribute was retired. */
|
|
250
|
+
export function retiredAttrValue(
|
|
251
|
+
typeKey: string,
|
|
252
|
+
attrName: string,
|
|
253
|
+
value: unknown,
|
|
254
|
+
): RetirementNote | undefined {
|
|
255
|
+
if (typeof value !== "string") return undefined;
|
|
256
|
+
return noted(
|
|
257
|
+
(e) => e.attr === attrName && e.attrValues?.includes(value) === true && scopeMatches(e, typeKey),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** The SUBTYPE itself was retired (`origin.collection`). */
|
|
262
|
+
export function retiredSubType(type: string, subType: string): RetirementNote | undefined {
|
|
263
|
+
return noted((e) => e.isSubTypeRetirement === true && e.type === type && e.subType === subType);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The sentence appended to a failing diagnostic. Kept here rather than at each call site so
|
|
268
|
+
* all three failure sites word a retirement identically — an adopter who has seen one
|
|
269
|
+
* recognises the next.
|
|
270
|
+
*/
|
|
271
|
+
export function retirementHint(n: RetirementNote): string {
|
|
272
|
+
// Sentences are built WITHOUT their terminator and punctuated once at the end. The
|
|
273
|
+
// previous form appended a period to each fragment and then repaired the doubling with a
|
|
274
|
+
// global `".." → "."` — which would silently rewrite an ellipsis, or a `../` in a
|
|
275
|
+
// migration path, inside the user-facing load error.
|
|
276
|
+
const parts = [`retired in ${n.since} — ${n.why}`];
|
|
277
|
+
if (n.replacedBy !== undefined) parts.push(`Use ${n.replacedBy} instead`);
|
|
278
|
+
if (n.migration !== undefined) parts.push(`Migration: ${n.migration}`);
|
|
279
|
+
return `${parts.join(". ")}.`;
|
|
280
|
+
}
|
package/src/super-resolve.ts
CHANGED
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
import type { MetaData } from "./shared/meta-data.js";
|
|
23
23
|
import { PACKAGE_SEPARATOR, CHILD_REF_SEPARATOR } from "./shared/structural.js";
|
|
24
|
+
import { TYPE_IDENTITY } from "./shared/base-types.js";
|
|
25
|
+
import { IDENTITY_UNIQUE_KEY_SUBTYPES } from "./core/identity/identity-constants.js";
|
|
24
26
|
|
|
25
27
|
// ---------------------------------------------------------------------------
|
|
26
28
|
// Tree search helper
|
|
@@ -87,6 +89,51 @@ export function isChildTargetingRef(ref: string): boolean {
|
|
|
87
89
|
return lastSegment.includes(CHILD_REF_SEPARATOR);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Whether a dotted `extends` target is acceptable for the extending node.
|
|
94
|
+
*
|
|
95
|
+
* Type must match exactly. Subtype must match too — with ONE exception, on identities
|
|
96
|
+
* (#310). For an identity, subtype equality was answering the wrong question: it read as
|
|
97
|
+
* "the same kind of thing", but ADR-0040 encodes uniqueness in the TYPE, so `primary` and
|
|
98
|
+
* `secondary` are both UNIQUE KEYS and differ only in which one the entity nominated as
|
|
99
|
+
* its main handle. Borrowing a key borrows uniqueness, not that nomination — so a read
|
|
100
|
+
* model may key off a business key the entity models as `identity.secondary` while never
|
|
101
|
+
* surfacing its surrogate `identity.primary` at all.
|
|
102
|
+
*
|
|
103
|
+
* The gate was never written for identities in the first place: its only conformance
|
|
104
|
+
* fixture is `error-extends-entity-field-type-mismatch`, a `field.uuid` extending a
|
|
105
|
+
* `field.string`. For a FIELD, subtype IS the datatype and inheriting across it is
|
|
106
|
+
* incoherent; for an identity, subtype is a ROLE. A field-shape rule was generalized onto
|
|
107
|
+
* a role axis without a fixture ever exercising it.
|
|
108
|
+
*
|
|
109
|
+
* `identity.reference` is excluded on both sides: a foreign key is not unique, so it can
|
|
110
|
+
* never back a borrowed key — and a set-membership test says that once, rather than
|
|
111
|
+
* enumerating the legal pairs.
|
|
112
|
+
*
|
|
113
|
+
* EXPORTED because there are two doors: this module's deferred resolution and
|
|
114
|
+
* `parser-core`'s eager check. They had independent copies of the boolean, so a change to
|
|
115
|
+
* one silently left the other enforcing the old rule on whichever path a given loader
|
|
116
|
+
* configuration takes.
|
|
117
|
+
*/
|
|
118
|
+
export function extendsTargetCompatible(node: MetaData, target: MetaData): boolean {
|
|
119
|
+
if (target.type !== node.type) return false;
|
|
120
|
+
if (target.subType === node.subType) return true;
|
|
121
|
+
return (
|
|
122
|
+
node.type === TYPE_IDENTITY &&
|
|
123
|
+
IDENTITY_UNIQUE_KEY_SUBTYPES.includes(node.subType) &&
|
|
124
|
+
IDENTITY_UNIQUE_KEY_SUBTYPES.includes(target.subType)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The one wording for `ERR_EXTENDS_TARGET_MISMATCH`, shared by both doors and mirrored in
|
|
130
|
+
* the other three ports. States the exception, so an author who hits it on a `field` is
|
|
131
|
+
* not left wondering why their identity case was allowed.
|
|
132
|
+
*/
|
|
133
|
+
export const EXTENDS_TARGET_MISMATCH_RULE =
|
|
134
|
+
"a dotted extends must target a node of the same type and subtype — the one exception " +
|
|
135
|
+
"is an identity, which may extend any UNIQUE key (identity.primary or identity.secondary)";
|
|
136
|
+
|
|
90
137
|
/**
|
|
91
138
|
* FR-024: split a child-targeting ref into the owner-object ref and the child
|
|
92
139
|
* traversal path. A multi-dot ref (`X.y.z`) traverses child names to any depth
|
|
@@ -276,9 +323,20 @@ export function resolveDeferredSupers(root: MetaData): DeferredSuperFailure[] {
|
|
|
276
323
|
if (target !== undefined) {
|
|
277
324
|
// FR-024: a dotted ref must target a node of the SAME type and subtype
|
|
278
325
|
// as the extending node. Dotted-only — top-level extends is unchanged.
|
|
326
|
+
//
|
|
327
|
+
// #310 — with ONE exception, on identities. Subtype equality here was standing in
|
|
328
|
+
// for "the target is the same kind of thing", and for identities that is the wrong
|
|
329
|
+
// question: ADR-0040 put uniqueness in the TYPE, so `identity.primary` and
|
|
330
|
+
// `identity.secondary` are both UNIQUE KEYS and differ only in which one the entity
|
|
331
|
+
// nominated as its main handle. A projection borrowing a key is borrowing
|
|
332
|
+
// uniqueness, not that nomination — so a read model may key off a business key the
|
|
333
|
+
// entity models as `identity.secondary` while never surfacing its surrogate
|
|
334
|
+
// `identity.primary` at all, which is the shape that was foreclosed.
|
|
335
|
+
//
|
|
336
|
+
// `identity.reference` stays out: an FK is not unique, so it can never back a key.
|
|
279
337
|
if (
|
|
280
338
|
isChildTargetingRef(node.superRef) &&
|
|
281
|
-
(
|
|
339
|
+
!extendsTargetCompatible(node, target)
|
|
282
340
|
) {
|
|
283
341
|
failures.push({
|
|
284
342
|
nodeFqn: node.fqn(),
|
|
@@ -19,7 +19,7 @@ export const PROMPT_DEFINITION: ProviderDefinition = {
|
|
|
19
19
|
"name": "xmlText",
|
|
20
20
|
"min": 0,
|
|
21
21
|
"max": 1,
|
|
22
|
-
"description": "When true, this field receives its element's XML TEXT CONTENT during tolerant extract (JAXB @XmlValue / Jackson @JacksonXmlText / .NET [XmlText]) instead of a same-named child. No effect
|
|
22
|
+
"description": "When true, this field receives its element's XML TEXT CONTENT during tolerant extract (JAXB @XmlValue / Jackson @JacksonXmlText / .NET [XmlText]) instead of a same-named child. No effect when the eliciting prompt's @responseFormat is json."
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"type": "attr",
|
|
@@ -66,6 +66,10 @@ export const TEMPLATE_ATTR_HTML_BODY_REF = "htmlBodyRef";
|
|
|
66
66
|
export const TEMPLATE_ATTR_TEXT_BODY_REF = "textBodyRef";
|
|
67
67
|
|
|
68
68
|
// Prompt-overlay attrs (template.prompt only).
|
|
69
|
+
//
|
|
70
|
+
// ADR-0052: @responseRef is also the INBOUND gate. Its presence — never a format
|
|
71
|
+
// value — is what makes a prompt emit a parser-on-receipt, a tolerant extract, and
|
|
72
|
+
// the FR-010 response-format fragment. A template.output emits none of those.
|
|
69
73
|
export const TEMPLATE_ATTR_RESPONSE_REF = "responseRef";
|
|
70
74
|
export const TEMPLATE_ATTR_MAX_TOKENS = "maxTokens";
|
|
71
75
|
export const TEMPLATE_ATTR_REQUIRED_SLOTS = "requiredSlots";
|
|
@@ -94,7 +98,8 @@ export const TEMPLATE_FORMATS = [
|
|
|
94
98
|
] as const;
|
|
95
99
|
export type TemplateFormat = (typeof TEMPLATE_FORMATS)[number];
|
|
96
100
|
|
|
97
|
-
// FR-010
|
|
101
|
+
// FR-010 response-format fragment presentation (template.prompt only — ADR-0052 moved
|
|
102
|
+
// it off template.output, where its own name contradicted the subtype). Closed enum;
|
|
98
103
|
// guidance is NEVER carried in comments. Default "guide". Set project-wide via an
|
|
99
104
|
// abstract template base + extends, with a render-time override on top.
|
|
100
105
|
export const TEMPLATE_ATTR_PROMPT_STYLE = "promptStyle";
|
|
@@ -110,6 +115,27 @@ export const PROMPT_STYLES = [
|
|
|
110
115
|
] as const;
|
|
111
116
|
export type PromptStyle = (typeof PROMPT_STYLES)[number];
|
|
112
117
|
|
|
118
|
+
// ADR-0053 — the syntax of the model's REPLY (template.prompt only). Distinct from
|
|
119
|
+
// @format, which is the syntax of the rendered PROMPT body: the two genuinely differ
|
|
120
|
+
// (a plain-text prompt may elicit an XML reply), which is why one attribute cannot
|
|
121
|
+
// serve both directions.
|
|
122
|
+
//
|
|
123
|
+
// Two members, not TEMPLATE_FORMATS' seven, because two is what every shipping
|
|
124
|
+
// consumer dispatches on (Format.JSON / Format.XML) in all five ports. The rest are
|
|
125
|
+
// reserved-not-registered under ADR-0007 Amendment 2's re-entry bar: a member enters
|
|
126
|
+
// the registry only when a shipping consumer dispatches on it.
|
|
127
|
+
//
|
|
128
|
+
// Default "json" reproduces the trace helper's pre-ADR-0053 fallback exactly (anything
|
|
129
|
+
// not "xml" was treated as JSON), so the default is behaviour-preserving rather than a
|
|
130
|
+
// new policy.
|
|
131
|
+
export const TEMPLATE_ATTR_RESPONSE_FORMAT = "responseFormat";
|
|
132
|
+
export const RESPONSE_FORMAT_JSON = "json";
|
|
133
|
+
export const RESPONSE_FORMAT_XML = "xml";
|
|
134
|
+
|
|
135
|
+
export const TEMPLATE_RESPONSE_FORMATS = [RESPONSE_FORMAT_JSON, RESPONSE_FORMAT_XML] as const;
|
|
136
|
+
export type ResponseFormat = (typeof TEMPLATE_RESPONSE_FORMATS)[number];
|
|
137
|
+
export const RESPONSE_FORMAT_DEFAULT: ResponseFormat = RESPONSE_FORMAT_JSON;
|
|
138
|
+
|
|
113
139
|
// @xmlText — a FIELD-level marker (boolean) for the tolerant extract engine: this field
|
|
114
140
|
// receives its element's TEXT CONTENT when a template.output response is parsed from XML
|
|
115
141
|
// (JAXB @XmlValue / Jackson @JacksonXmlText / .NET [XmlText]). Registered on field.* by
|
|
@@ -18,9 +18,9 @@ export const TEMPLATE_DEFINITION: ProviderDefinition = {
|
|
|
18
18
|
{
|
|
19
19
|
"type": "template",
|
|
20
20
|
"subType": "prompt",
|
|
21
|
-
"description": "An LLM-targeted renderable prompt template (FR-004). Carries the generic reference + governance attrs plus the LLM overlay (@maxTokens / @requiredSlots / @model / @responseRef). Its renderable body is required via @textRef.",
|
|
22
|
-
"whenToUse": "You are sending text to an LLM. Declare a prompt template with a typed payload so the prompt is versioned, drift-checked against its fields, and cache-stable — instead of string-building it in code.",
|
|
23
|
-
"rules": "prompt requires @payloadRef (the typed payload it renders against) AND @textRef (the body text, provider-resolved at render time — enforced in the loader's validateTemplatePayloadRefs pass, not at the attr layer where @textRef is relaxed to optional so template.output email can omit it). @format is a closed enum keyed by the render engine's escaper. @responseRef (optional) names the response value-object the prompt expects
|
|
21
|
+
"description": "An LLM-targeted renderable prompt template (FR-004). Carries the generic reference + governance attrs plus the LLM overlay (@maxTokens / @requiredSlots / @model / @responseRef). Its renderable body is required via @textRef. A prompt declaring @responseRef also owns the INBOUND half (ADR-0052): the parser-on-receipt, the FR-010 response-format fragment (@promptStyle), and the reply syntax (@responseFormat).",
|
|
22
|
+
"whenToUse": "You are sending text to an LLM. Declare a prompt template with a typed payload so the prompt is versioned, drift-checked against its fields, and cache-stable — instead of string-building it in code. Add @responseRef when you also want the reply parsed into a typed shape.",
|
|
23
|
+
"rules": "prompt requires @payloadRef (the typed payload it renders against) AND @textRef (the body text, provider-resolved at render time — enforced in the loader's validateTemplatePayloadRefs pass, not at the attr layer where @textRef is relaxed to optional so template.output email can omit it). @format is the syntax of the rendered PROMPT body, a closed enum keyed by the render engine's escaper; @responseFormat (ADR-0053) is the syntax of the model's REPLY. @responseRef (optional) names the response value-object the prompt expects, drives typed LLM-call trace derivation, and is the sole gate on the inbound codegen tier — which keys on its presence, never on a format value.",
|
|
24
24
|
"children": [
|
|
25
25
|
{
|
|
26
26
|
"type": "attr",
|
|
@@ -120,16 +120,43 @@ export const TEMPLATE_DEFINITION: ProviderDefinition = {
|
|
|
120
120
|
"name": "responseRef",
|
|
121
121
|
"min": 0,
|
|
122
122
|
"max": 1,
|
|
123
|
-
"description": "Optional ref to the response value-object this prompt expects (peer of @payloadRef; drives typed LLM-call trace derivation)."
|
|
123
|
+
"description": "Optional ref to the response value-object this prompt expects (peer of @payloadRef; drives typed LLM-call trace derivation). Its presence is the sole gate on the inbound codegen tier (ADR-0052): parser-on-receipt, tolerant extract, and the FR-010 response-format fragment."
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"type": "attr",
|
|
127
|
+
"subType": "string",
|
|
128
|
+
"name": "promptStyle",
|
|
129
|
+
"min": 0,
|
|
130
|
+
"max": 1,
|
|
131
|
+
"default": "guide",
|
|
132
|
+
"allowedValues": [
|
|
133
|
+
"guide",
|
|
134
|
+
"inline",
|
|
135
|
+
"exampleOnly"
|
|
136
|
+
],
|
|
137
|
+
"description": "FR-010 response-format fragment presentation: 'guide' (prose list + example), 'inline' (inline placeholders / enum choices), or 'exampleOnly' (filled skeleton). Guidance is never emitted as comments."
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"type": "attr",
|
|
141
|
+
"subType": "string",
|
|
142
|
+
"name": "responseFormat",
|
|
143
|
+
"min": 0,
|
|
144
|
+
"max": 1,
|
|
145
|
+
"default": "json",
|
|
146
|
+
"allowedValues": [
|
|
147
|
+
"json",
|
|
148
|
+
"xml"
|
|
149
|
+
],
|
|
150
|
+
"description": "ADR-0053: the syntax of the model's REPLY, read by the parser-on-receipt and the FR-010 response-format fragment. Distinct from @format, which is the syntax of the rendered PROMPT body. Two members because two is what every shipping consumer dispatches on; the other @format members are reserved-not-registered."
|
|
124
151
|
}
|
|
125
152
|
]
|
|
126
153
|
},
|
|
127
154
|
{
|
|
128
155
|
"type": "template",
|
|
129
156
|
"subType": "output",
|
|
130
|
-
"description": "An output / serialization template (FR-004): every rendered artifact other than an LLM prompt — a document (email, export, docs, config) or an email. Carries the generic reference + governance attrs
|
|
131
|
-
"whenToUse": "You render a document/email/serialized output from typed data. Declare an output template so the {{fields}} are drift-checked against the payload VO at build time.",
|
|
132
|
-
"rules": "output is either a document (@kind=\"document\" or absent → renders @textRef in @format to one string) or an email (@kind=\"email\" → renders subject + html + optional text to a structured EmailDocument). The cross-field presence rule is enforced in the loader's validateTemplatePayloadRefs pass: document requires @textRef; email requires @subjectRef AND @htmlBodyRef (with @textBodyRef optional) and carries NO @textRef. @format is a closed enum keyed by the render engine's escaper
|
|
157
|
+
"description": "An output / serialization template (FR-004): every rendered artifact other than an LLM prompt — a document (email, export, docs, config) or an email. Carries the generic reference + governance attrs and the @kind + email part-refs. OUTBOUND ONLY (ADR-0052) — it renders, and generates no parser.",
|
|
158
|
+
"whenToUse": "You render a document/email/serialized output from typed data. Declare an output template so the {{fields}} are drift-checked against the payload VO at build time. To parse a model's reply instead, declare @responseRef on the template.prompt that elicits it — never a template.output.",
|
|
159
|
+
"rules": "output is either a document (@kind=\"document\" or absent → renders @textRef in @format to one string) or an email (@kind=\"email\" → renders subject + html + optional text to a structured EmailDocument). The cross-field presence rule is enforced in the loader's validateTemplatePayloadRefs pass: document requires @textRef; email requires @subjectRef AND @htmlBodyRef (with @textBodyRef optional) and carries NO @textRef. @format is a closed enum keyed by the render engine's escaper. template.output is OUTBOUND ONLY (ADR-0052): it emits a render helper and nothing that reads a model's reply — the parser-on-receipt, the tolerant extract and the FR-010 response-format fragment all belong to template.prompt @responseRef.",
|
|
133
160
|
"children": [
|
|
134
161
|
{
|
|
135
162
|
"type": "attr",
|
|
@@ -198,20 +225,6 @@ export const TEMPLATE_DEFINITION: ProviderDefinition = {
|
|
|
198
225
|
"max": 1,
|
|
199
226
|
"description": "Output tags the rendered text must contain (drives the verify output-tag check)."
|
|
200
227
|
},
|
|
201
|
-
{
|
|
202
|
-
"type": "attr",
|
|
203
|
-
"subType": "string",
|
|
204
|
-
"name": "promptStyle",
|
|
205
|
-
"min": 0,
|
|
206
|
-
"max": 1,
|
|
207
|
-
"default": "guide",
|
|
208
|
-
"allowedValues": [
|
|
209
|
-
"guide",
|
|
210
|
-
"inline",
|
|
211
|
-
"exampleOnly"
|
|
212
|
-
],
|
|
213
|
-
"description": "FR-010 output-format prompt presentation: 'guide' (prose list + example), 'inline' (inline placeholders / enum choices), or 'exampleOnly' (filled skeleton). Guidance is never emitted as comments."
|
|
214
|
-
},
|
|
215
228
|
{
|
|
216
229
|
"type": "attr",
|
|
217
230
|
"subType": "string",
|