@prisma-next/framework-components 0.12.0 → 0.13.0-dev.10
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/authoring.d.mts +2 -2
- package/dist/authoring.mjs +2 -2
- package/dist/{codec-BFOsuHKK.d.mts → codec-DCQAerzB.d.mts} +1 -1
- package/dist/{codec-BFOsuHKK.d.mts.map → codec-DCQAerzB.d.mts.map} +1 -1
- package/dist/codec.d.mts +1 -1
- package/dist/codec.d.mts.map +1 -1
- package/dist/components.d.mts +1 -1
- package/dist/components.mjs +1 -1
- package/dist/components.mjs.map +1 -1
- package/dist/control.d.mts +85 -13
- package/dist/control.d.mts.map +1 -1
- package/dist/control.mjs +89 -7
- package/dist/control.mjs.map +1 -1
- package/dist/{emission-types-CMv_053d.d.mts → emission-types-vfpSTe63.d.mts} +2 -2
- package/dist/{emission-types-CMv_053d.d.mts.map → emission-types-vfpSTe63.d.mts.map} +1 -1
- package/dist/emission.d.mts +3 -3
- package/dist/execution.d.mts +1 -1
- package/dist/execution.d.mts.map +1 -1
- package/dist/execution.mjs +1 -1
- package/dist/{framework-authoring-DcEZ5Lin.mjs → framework-authoring-CnwPJCO4.mjs} +76 -5
- package/dist/framework-authoring-CnwPJCO4.mjs.map +1 -0
- package/dist/framework-authoring-R0TYCkvG.d.mts +380 -0
- package/dist/framework-authoring-R0TYCkvG.d.mts.map +1 -0
- package/dist/{framework-components-CuoUhyB5.d.mts → framework-components-DDQXmW0b.d.mts} +6 -5
- package/dist/{framework-components-CuoUhyB5.d.mts.map → framework-components-DDQXmW0b.d.mts.map} +1 -1
- package/dist/{framework-components-FdqmlGUj.mjs → framework-components-DbCS57go.mjs} +1 -1
- package/dist/{framework-components-FdqmlGUj.mjs.map → framework-components-DbCS57go.mjs.map} +1 -1
- package/dist/ir.d.mts +20 -18
- package/dist/ir.d.mts.map +1 -1
- package/dist/ir.mjs +17 -14
- package/dist/ir.mjs.map +1 -1
- package/dist/{psl-ast-BDXL7iCg.d.mts → psl-ast-Cn50B-UG.d.mts} +90 -18
- package/dist/psl-ast-Cn50B-UG.d.mts.map +1 -0
- package/dist/psl-ast.d.mts +37 -2
- package/dist/psl-ast.d.mts.map +1 -0
- package/dist/psl-ast.mjs +222 -4
- package/dist/psl-ast.mjs.map +1 -1
- package/dist/runtime.d.mts +1 -1
- package/dist/runtime.d.mts.map +1 -1
- package/dist/runtime.mjs.map +1 -1
- package/dist/{types-import-spec-BxI5cSQy.d.mts → types-import-spec-DRKzrJ20.d.mts} +1 -1
- package/dist/{types-import-spec-BxI5cSQy.d.mts.map → types-import-spec-DRKzrJ20.d.mts.map} +1 -1
- package/dist/utils.mjs.map +1 -1
- package/package.json +9 -9
- package/src/control/control-instances.ts +15 -5
- package/src/control/control-migration-types.ts +20 -2
- package/src/control/control-result-types.ts +4 -1
- package/src/control/control-stack.ts +123 -4
- package/src/control/psl-ast.ts +234 -34
- package/src/control/psl-extension-block-validator.ts +324 -0
- package/src/control/verifier-disposition.ts +62 -0
- package/src/exports/authoring.ts +16 -0
- package/src/exports/control.ts +7 -0
- package/src/exports/psl-ast.ts +2 -0
- package/src/ir/namespace.ts +15 -13
- package/src/ir/storage.ts +8 -7
- package/src/shared/framework-authoring.ts +215 -2
- package/src/shared/framework-components.ts +2 -0
- package/src/shared/psl-extension-block.ts +184 -0
- package/dist/framework-authoring-BPPe9C9D.d.mts +0 -183
- package/dist/framework-authoring-BPPe9C9D.d.mts.map +0 -1
- package/dist/framework-authoring-DcEZ5Lin.mjs.map +0 -1
- package/dist/psl-ast-BDXL7iCg.d.mts.map +0 -1
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic validator for extension-contributed top-level PSL blocks.
|
|
3
|
+
*
|
|
4
|
+
* One function — {@link validateExtensionBlock} — takes a parsed
|
|
5
|
+
* {@link PslExtensionBlock}, its {@link AuthoringPslBlockDescriptor}, a
|
|
6
|
+
* {@link CodecLookup} (for `value` parameters), and the set of
|
|
7
|
+
* {@link PslNamespace} objects from the document (for `ref` resolution), and
|
|
8
|
+
* returns the full list of {@link PslDiagnostic} objects for the block.
|
|
9
|
+
*
|
|
10
|
+
* Detection logic per failure mode:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Unknown parameter** — keys present in `node.parameters` that are absent
|
|
13
|
+
* from `descriptor.parameters` (key-set difference). The parser stores
|
|
14
|
+
* unknown parameters as `kind:'value'` stubs; the validator discovers them
|
|
15
|
+
* by comparing the key sets, not by inspecting the captured kind.
|
|
16
|
+
*
|
|
17
|
+
* 2. **Missing required parameter** — `descriptor.parameters` entries with
|
|
18
|
+
* `required: true` whose key is absent from `node.parameters`.
|
|
19
|
+
*
|
|
20
|
+
* 3. **`option` value outside its set** — the captured `token` is not in
|
|
21
|
+
* `descriptor.values`.
|
|
22
|
+
*
|
|
23
|
+
* 4. **`value` rejected by its codec** — the raw string is first parsed as
|
|
24
|
+
* JSON (`JSON.parse(raw)`). If `JSON.parse` throws, the literal is not valid
|
|
25
|
+
* JSON and a `PSL_EXTENSION_INVALID_VALUE` diagnostic is emitted. If parsing
|
|
26
|
+
* succeeds but `codec.decodeJson(jsonValue)` throws, the JSON value is not
|
|
27
|
+
* acceptable to the codec and a `PSL_EXTENSION_INVALID_VALUE` diagnostic is
|
|
28
|
+
* emitted. If `codecLookup.get(codecId)` returns `undefined` (unknown codec
|
|
29
|
+
* id), a `PSL_EXTENSION_INVALID_VALUE` diagnostic is also emitted.
|
|
30
|
+
*
|
|
31
|
+
* 5. **`ref` that does not resolve within its scope** — the captured
|
|
32
|
+
* `identifier` is looked up in the PSL document's `PslNamespace` objects
|
|
33
|
+
* according to `param.scope`:
|
|
34
|
+
* - `same-namespace`: the referent must be in the same namespace as the
|
|
35
|
+
* block (the namespace containing the block).
|
|
36
|
+
* - `same-space`: the referent may be in any namespace in the document.
|
|
37
|
+
* - `cross-space`: pass-through — enforcement is scoped to first-consumer
|
|
38
|
+
* need (RLS roles). This case is documented and clearly flagged; the
|
|
39
|
+
* caller is responsible for wiring cross-space resolution when needed.
|
|
40
|
+
*
|
|
41
|
+
* 6. **`list`** — each element is validated against `param.of` recursively.
|
|
42
|
+
*
|
|
43
|
+
* ### `char`/`varchar` length
|
|
44
|
+
* Not enforced. RLS `using`/`check` strings are unbounded text and the codec
|
|
45
|
+
* already rejects structurally invalid literals; length constraints are a
|
|
46
|
+
* database-side concern, not a PSL authoring constraint.
|
|
47
|
+
*
|
|
48
|
+
* ### `cross-space` scope
|
|
49
|
+
* Implemented as a documented pass-through. The spec permits scoping
|
|
50
|
+
* cross-space enforcement to first-consumer need (RLS roles). When RLS roles
|
|
51
|
+
* arrive, wire `cross-space` resolution through the cross-contract-space
|
|
52
|
+
* coordinate model `(spaceId, namespaceId, entityKind, entityName)`.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
import type { JsonValue } from '@prisma-next/contract/types';
|
|
56
|
+
import { blindCast } from '@prisma-next/utils/casts';
|
|
57
|
+
import type { CodecLookup } from '../shared/codec-types';
|
|
58
|
+
import type { AuthoringPslBlockDescriptor } from '../shared/framework-authoring';
|
|
59
|
+
import type {
|
|
60
|
+
PslBlockParam,
|
|
61
|
+
PslBlockParamRef,
|
|
62
|
+
PslExtensionBlock,
|
|
63
|
+
PslExtensionBlockParamValue,
|
|
64
|
+
PslSpan,
|
|
65
|
+
} from '../shared/psl-extension-block';
|
|
66
|
+
import type { PslDiagnostic, PslNamespace } from './psl-ast';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Context for ref resolution during extension-block validation.
|
|
70
|
+
*
|
|
71
|
+
* - `ownerNamespace` is the `PslNamespace` that contains the block being
|
|
72
|
+
* validated. Used for `same-namespace` scope checks.
|
|
73
|
+
* - `allNamespaces` is every namespace in the document. Used for `same-space`
|
|
74
|
+
* scope checks.
|
|
75
|
+
*/
|
|
76
|
+
export interface ExtensionBlockRefResolutionContext {
|
|
77
|
+
readonly ownerNamespace: PslNamespace;
|
|
78
|
+
readonly allNamespaces: readonly PslNamespace[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate a single parsed extension block against its descriptor.
|
|
83
|
+
*
|
|
84
|
+
* Returns an array of {@link PslDiagnostic} objects (possibly empty). The
|
|
85
|
+
* caller is responsible for threading `sourceId` into each returned diagnostic
|
|
86
|
+
* — the returned objects already have `sourceId` set from the `sourceId`
|
|
87
|
+
* parameter.
|
|
88
|
+
*
|
|
89
|
+
* @param node - The parsed block node produced by the generic framework parser.
|
|
90
|
+
* @param descriptor - The descriptor that claims this block's keyword.
|
|
91
|
+
* @param sourceId - The PSL source file identifier (threaded into diagnostics).
|
|
92
|
+
* @param codecLookup - Used to validate `value`-kind parameter literals via
|
|
93
|
+
* `codecLookup.get(codecId)?.decodeJson(JSON.parse(raw))`.
|
|
94
|
+
* @param refCtx - Namespace context for `ref`-kind scope resolution. Required
|
|
95
|
+
* when any descriptor parameter is `kind: 'ref'`; may be omitted if none are.
|
|
96
|
+
*/
|
|
97
|
+
export function validateExtensionBlock(
|
|
98
|
+
node: PslExtensionBlock,
|
|
99
|
+
descriptor: AuthoringPslBlockDescriptor,
|
|
100
|
+
sourceId: string,
|
|
101
|
+
codecLookup: CodecLookup,
|
|
102
|
+
refCtx?: ExtensionBlockRefResolutionContext,
|
|
103
|
+
): readonly PslDiagnostic[] {
|
|
104
|
+
const diagnostics: PslDiagnostic[] = [];
|
|
105
|
+
|
|
106
|
+
const descriptorKeys = new Set(Object.keys(descriptor.parameters));
|
|
107
|
+
const nodeKeys = new Set(Object.keys(node.parameters));
|
|
108
|
+
|
|
109
|
+
// 1. Unknown parameters — keys in the node not in the descriptor.
|
|
110
|
+
for (const key of nodeKeys) {
|
|
111
|
+
if (!descriptorKeys.has(key)) {
|
|
112
|
+
const captured = node.parameters[key];
|
|
113
|
+
diagnostics.push({
|
|
114
|
+
code: 'PSL_EXTENSION_UNKNOWN_PARAMETER',
|
|
115
|
+
message: `Unknown parameter "${key}" in "${descriptor.keyword}" block "${node.name}". The descriptor does not declare this parameter.`,
|
|
116
|
+
sourceId,
|
|
117
|
+
span: captured?.span ?? node.span,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 2. Missing required parameters — required descriptor keys absent from the node.
|
|
123
|
+
for (const [key, param] of Object.entries(descriptor.parameters)) {
|
|
124
|
+
if (param.required === true && !nodeKeys.has(key)) {
|
|
125
|
+
diagnostics.push({
|
|
126
|
+
code: 'PSL_EXTENSION_MISSING_REQUIRED_PARAMETER',
|
|
127
|
+
message: `Required parameter "${key}" is missing from "${descriptor.keyword}" block "${node.name}".`,
|
|
128
|
+
sourceId,
|
|
129
|
+
span: node.span,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 3–5. Per-parameter validation for parameters that are present.
|
|
135
|
+
for (const [key, param] of Object.entries(descriptor.parameters)) {
|
|
136
|
+
const captured = node.parameters[key];
|
|
137
|
+
if (captured === undefined) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
validateParam(
|
|
141
|
+
node,
|
|
142
|
+
descriptor,
|
|
143
|
+
key,
|
|
144
|
+
param,
|
|
145
|
+
captured,
|
|
146
|
+
sourceId,
|
|
147
|
+
codecLookup,
|
|
148
|
+
refCtx,
|
|
149
|
+
diagnostics,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return diagnostics;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function validateParam(
|
|
157
|
+
node: PslExtensionBlock,
|
|
158
|
+
descriptor: AuthoringPslBlockDescriptor,
|
|
159
|
+
key: string,
|
|
160
|
+
param: PslBlockParam,
|
|
161
|
+
captured: PslExtensionBlockParamValue,
|
|
162
|
+
sourceId: string,
|
|
163
|
+
codecLookup: CodecLookup,
|
|
164
|
+
refCtx: ExtensionBlockRefResolutionContext | undefined,
|
|
165
|
+
diagnostics: PslDiagnostic[],
|
|
166
|
+
): void {
|
|
167
|
+
switch (param.kind) {
|
|
168
|
+
case 'option': {
|
|
169
|
+
if (captured.kind !== 'option') {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (!param.values.includes(captured.token)) {
|
|
173
|
+
diagnostics.push({
|
|
174
|
+
code: 'PSL_EXTENSION_OPTION_OUT_OF_SET',
|
|
175
|
+
message: `Parameter "${key}" in "${descriptor.keyword}" block "${node.name}" has value "${captured.token}" which is not one of the allowed values: ${param.values.map((v) => `"${v}"`).join(', ')}.`,
|
|
176
|
+
sourceId,
|
|
177
|
+
span: captured.span,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
case 'value': {
|
|
184
|
+
if (captured.kind !== 'value') {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const codec = codecLookup.get(param.codecId);
|
|
188
|
+
if (codec === undefined) {
|
|
189
|
+
diagnostics.push({
|
|
190
|
+
code: 'PSL_EXTENSION_INVALID_VALUE',
|
|
191
|
+
message: `Parameter "${key}" in "${descriptor.keyword}" block "${node.name}" references unknown codec "${param.codecId}".`,
|
|
192
|
+
sourceId,
|
|
193
|
+
span: captured.span,
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
let jsonValue: unknown;
|
|
198
|
+
try {
|
|
199
|
+
jsonValue = JSON.parse(captured.raw);
|
|
200
|
+
} catch {
|
|
201
|
+
diagnostics.push({
|
|
202
|
+
code: 'PSL_EXTENSION_INVALID_VALUE',
|
|
203
|
+
message: `Parameter "${key}" in "${descriptor.keyword}" block "${node.name}" is not a valid JSON literal (expected a JSON string, number, boolean, or null): ${captured.raw}`,
|
|
204
|
+
sourceId,
|
|
205
|
+
span: captured.span,
|
|
206
|
+
});
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
codec.decodeJson(
|
|
211
|
+
blindCast<JsonValue, 'JSON.parse returns a JsonValue-compatible value'>(jsonValue),
|
|
212
|
+
);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
215
|
+
diagnostics.push({
|
|
216
|
+
code: 'PSL_EXTENSION_INVALID_VALUE',
|
|
217
|
+
message: `Parameter "${key}" in "${descriptor.keyword}" block "${node.name}" was rejected by codec "${param.codecId}": ${reason}`,
|
|
218
|
+
sourceId,
|
|
219
|
+
span: captured.span,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case 'ref': {
|
|
226
|
+
if (captured.kind !== 'ref') {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
validateRef(
|
|
230
|
+
node,
|
|
231
|
+
descriptor,
|
|
232
|
+
key,
|
|
233
|
+
param,
|
|
234
|
+
captured.identifier,
|
|
235
|
+
captured.span,
|
|
236
|
+
sourceId,
|
|
237
|
+
refCtx,
|
|
238
|
+
diagnostics,
|
|
239
|
+
);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
case 'list': {
|
|
244
|
+
if (captured.kind !== 'list') {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
for (const item of captured.items) {
|
|
248
|
+
validateParam(
|
|
249
|
+
node,
|
|
250
|
+
descriptor,
|
|
251
|
+
key,
|
|
252
|
+
param.of,
|
|
253
|
+
item,
|
|
254
|
+
sourceId,
|
|
255
|
+
codecLookup,
|
|
256
|
+
refCtx,
|
|
257
|
+
diagnostics,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function validateRef(
|
|
266
|
+
node: PslExtensionBlock,
|
|
267
|
+
descriptor: AuthoringPslBlockDescriptor,
|
|
268
|
+
key: string,
|
|
269
|
+
param: PslBlockParamRef,
|
|
270
|
+
identifier: string,
|
|
271
|
+
span: PslSpan,
|
|
272
|
+
sourceId: string,
|
|
273
|
+
refCtx: ExtensionBlockRefResolutionContext | undefined,
|
|
274
|
+
diagnostics: PslDiagnostic[],
|
|
275
|
+
): void {
|
|
276
|
+
if (param.scope === 'cross-space') {
|
|
277
|
+
// cross-space enforcement is a documented pass-through. The spec permits
|
|
278
|
+
// scoping cross-space resolution to first-consumer need (RLS roles). When
|
|
279
|
+
// that consumer arrives, wire resolution here through the
|
|
280
|
+
// cross-contract-space coordinate model
|
|
281
|
+
// (spaceId, namespaceId, entityKind, entityName).
|
|
282
|
+
// For now, cross-space refs pass validation unconditionally.
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (refCtx === undefined) {
|
|
287
|
+
// If no resolution context was provided, skip ref resolution. This matches
|
|
288
|
+
// the closed-grammar invariant: callers that register ref parameters must
|
|
289
|
+
// provide resolution context; callers without namespaces (e.g. unit tests
|
|
290
|
+
// that only exercise other validation modes) can omit it.
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const namespacesToSearch: readonly PslNamespace[] =
|
|
295
|
+
param.scope === 'same-namespace' ? [refCtx.ownerNamespace] : refCtx.allNamespaces;
|
|
296
|
+
|
|
297
|
+
if (!resolveEntityInNamespaces(identifier, param.refKind, namespacesToSearch)) {
|
|
298
|
+
const scopeLabel =
|
|
299
|
+
param.scope === 'same-namespace' ? 'the same namespace' : 'any namespace in the schema';
|
|
300
|
+
diagnostics.push({
|
|
301
|
+
code: 'PSL_EXTENSION_UNRESOLVED_REF',
|
|
302
|
+
message: `Parameter "${key}" in "${descriptor.keyword}" block "${node.name}" refers to "${identifier}" (expected ${param.refKind}), but no entity with that name and kind was found in ${scopeLabel}.`,
|
|
303
|
+
sourceId,
|
|
304
|
+
span,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* True if an entity named `name` of kind `refKind` exists in any of the given
|
|
311
|
+
* namespaces. Built-in and extension kinds resolve the same way, through
|
|
312
|
+
* `entries[refKind]`.
|
|
313
|
+
*/
|
|
314
|
+
function resolveEntityInNamespaces(
|
|
315
|
+
name: string,
|
|
316
|
+
refKind: string,
|
|
317
|
+
namespaces: readonly PslNamespace[],
|
|
318
|
+
): boolean {
|
|
319
|
+
for (const ns of namespaces) {
|
|
320
|
+
const kindMap = ns.entries[refKind];
|
|
321
|
+
if (kindMap !== undefined && Object.hasOwn(kindMap, name)) return true;
|
|
322
|
+
}
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ControlPolicy } from '@prisma-next/contract/types';
|
|
2
|
+
import type { SchemaVerificationNode } from './control-result-types';
|
|
3
|
+
|
|
4
|
+
export type VerificationStatus = SchemaVerificationNode['status'];
|
|
5
|
+
|
|
6
|
+
export type VerifierOutcome = VerificationStatus | 'suppress';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Target-neutral classification of a verifier finding, abstracted away from any
|
|
10
|
+
* one storage model's vocabulary. Each family classifies its own concrete issue
|
|
11
|
+
* kinds into these categories; the framework only grades the category against a
|
|
12
|
+
* control policy.
|
|
13
|
+
*
|
|
14
|
+
* - `declaredMissing` — a declared object/element is absent from the database.
|
|
15
|
+
* - `declaredIncompatible` — a declared object/element exists but its shape diverges.
|
|
16
|
+
* - `valueDrift` — the value set of an existing type drifted (e.g. enum values).
|
|
17
|
+
* - `extraNestedElement` — an undeclared element nested inside a declared object
|
|
18
|
+
* (a SQL column, a document field).
|
|
19
|
+
* - `extraAuxiliary` — an undeclared auxiliary attached to a declared object
|
|
20
|
+
* (a SQL constraint/index, a Mongo index/validator).
|
|
21
|
+
* - `extraTopLevelObject` — an undeclared top-level object (a SQL table, a
|
|
22
|
+
* Mongo collection).
|
|
23
|
+
*/
|
|
24
|
+
export type VerifierIssueCategory =
|
|
25
|
+
| 'declaredMissing'
|
|
26
|
+
| 'declaredIncompatible'
|
|
27
|
+
| 'valueDrift'
|
|
28
|
+
| 'extraNestedElement'
|
|
29
|
+
| 'extraAuxiliary'
|
|
30
|
+
| 'extraTopLevelObject';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Grades a target-neutral issue category against a control policy.
|
|
34
|
+
*
|
|
35
|
+
* - `observed` warns on everything.
|
|
36
|
+
* - `tolerated` suppresses only an extra nested element (everything else fails).
|
|
37
|
+
* - `external` suppresses every extra category and value drift (existence and
|
|
38
|
+
* declared-shape divergences still fail).
|
|
39
|
+
* - `managed` (and any other) fails.
|
|
40
|
+
*/
|
|
41
|
+
export function dispositionForCategory(
|
|
42
|
+
controlPolicy: ControlPolicy,
|
|
43
|
+
category: VerifierIssueCategory,
|
|
44
|
+
): VerifierOutcome {
|
|
45
|
+
if (controlPolicy === 'observed') {
|
|
46
|
+
return 'warn';
|
|
47
|
+
}
|
|
48
|
+
if (controlPolicy === 'tolerated' && category === 'extraNestedElement') {
|
|
49
|
+
return 'suppress';
|
|
50
|
+
}
|
|
51
|
+
if (controlPolicy === 'external') {
|
|
52
|
+
if (
|
|
53
|
+
category === 'extraNestedElement' ||
|
|
54
|
+
category === 'extraAuxiliary' ||
|
|
55
|
+
category === 'extraTopLevelObject' ||
|
|
56
|
+
category === 'valueDrift'
|
|
57
|
+
) {
|
|
58
|
+
return 'suppress';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return 'fail';
|
|
62
|
+
}
|
package/src/exports/authoring.ts
CHANGED
|
@@ -11,6 +11,8 @@ export type {
|
|
|
11
11
|
AuthoringFieldNamespace,
|
|
12
12
|
AuthoringFieldPresetDescriptor,
|
|
13
13
|
AuthoringFieldPresetOutput,
|
|
14
|
+
AuthoringPslBlockDescriptor,
|
|
15
|
+
AuthoringPslBlockDescriptorNamespace,
|
|
14
16
|
AuthoringStorageTypeTemplate,
|
|
15
17
|
AuthoringTemplateValue,
|
|
16
18
|
AuthoringTypeConstructorDescriptor,
|
|
@@ -25,8 +27,22 @@ export {
|
|
|
25
27
|
isAuthoringArgRef,
|
|
26
28
|
isAuthoringEntityTypeDescriptor,
|
|
27
29
|
isAuthoringFieldPresetDescriptor,
|
|
30
|
+
isAuthoringPslBlockDescriptor,
|
|
28
31
|
isAuthoringTypeConstructorDescriptor,
|
|
29
32
|
mergeAuthoringNamespaces,
|
|
30
33
|
resolveAuthoringTemplateValue,
|
|
31
34
|
validateAuthoringHelperArguments,
|
|
32
35
|
} from '../shared/framework-authoring';
|
|
36
|
+
export type {
|
|
37
|
+
PslBlockParam,
|
|
38
|
+
PslBlockParamList,
|
|
39
|
+
PslBlockParamOption,
|
|
40
|
+
PslBlockParamRef,
|
|
41
|
+
PslBlockParamValue,
|
|
42
|
+
PslExtensionBlock,
|
|
43
|
+
PslExtensionBlockParamList,
|
|
44
|
+
PslExtensionBlockParamOption,
|
|
45
|
+
PslExtensionBlockParamRef,
|
|
46
|
+
PslExtensionBlockParamScalarValue,
|
|
47
|
+
PslExtensionBlockParamValue,
|
|
48
|
+
} from '../shared/psl-extension-block';
|
package/src/exports/control.ts
CHANGED
|
@@ -95,6 +95,7 @@ export {
|
|
|
95
95
|
assembleControlMutationDefaults,
|
|
96
96
|
assembleScalarTypeDescriptors,
|
|
97
97
|
assertUniqueCodecOwner,
|
|
98
|
+
buildExtensionLoadOrder,
|
|
98
99
|
createControlStack,
|
|
99
100
|
extractCodecLookup,
|
|
100
101
|
extractCodecTypeImports,
|
|
@@ -106,6 +107,12 @@ export type {
|
|
|
106
107
|
SchemaVerifyOptions,
|
|
107
108
|
SchemaVerifyResult,
|
|
108
109
|
} from '../control/schema-verifier';
|
|
110
|
+
export type {
|
|
111
|
+
VerificationStatus,
|
|
112
|
+
VerifierIssueCategory,
|
|
113
|
+
VerifierOutcome,
|
|
114
|
+
} from '../control/verifier-disposition';
|
|
115
|
+
export { dispositionForCategory } from '../control/verifier-disposition';
|
|
109
116
|
export type {
|
|
110
117
|
ControlMutationDefaultEntry,
|
|
111
118
|
ControlMutationDefaultRegistry,
|
package/src/exports/psl-ast.ts
CHANGED
package/src/ir/namespace.ts
CHANGED
|
@@ -41,28 +41,30 @@ export const UNBOUND_NAMESPACE_ID = '__unbound__' as const;
|
|
|
41
41
|
*
|
|
42
42
|
* The framework promises only the coordinate (`id`) — the named storage
|
|
43
43
|
* entities a namespace contains are family-typed (SQL contributes
|
|
44
|
-
* `
|
|
45
|
-
* own native idiom). Generic consumers walking "all
|
|
46
|
-
* through a family-typed namespace, not the framework
|
|
44
|
+
* `table` / `type`, Mongo contributes `collection`, future families pick
|
|
45
|
+
* their own native idiom under `entries`). Generic consumers walking "all
|
|
46
|
+
* named entries" go through a family-typed namespace, not the framework
|
|
47
|
+
* `Namespace`.
|
|
47
48
|
*
|
|
48
49
|
* Every namespace concretion (e.g. family-built SQL namespaces,
|
|
49
50
|
* `MongoUnboundNamespace`, target-promoted namespaces like
|
|
50
|
-
* `PostgresSchema`) carries exactly: `id` (enumerable string),
|
|
51
|
-
* (
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* on
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* family-specific knowledge.
|
|
51
|
+
* `PostgresSchema`) carries exactly: `id` (enumerable string),
|
|
52
|
+
* `entries` (frozen object holding entity-kind slot maps), and `kind`
|
|
53
|
+
* (non-enumerable string discriminator set via `Object.defineProperty`).
|
|
54
|
+
* Each slot map under `entries` uses a singular essence key (`table`,
|
|
55
|
+
* `type`, `collection`, …) mapping entity names to IR classes. No other
|
|
56
|
+
* own-enumerable data lives on a namespace; non-entity computed data lives
|
|
57
|
+
* on the surrounding storage or contract IR. The framework's
|
|
58
|
+
* `elementCoordinates(storage)` walk relies on this invariant to enumerate
|
|
59
|
+
* entities structurally without family-specific knowledge.
|
|
60
60
|
*/
|
|
61
61
|
export interface Namespace extends IRNode, StorageNamespace {
|
|
62
62
|
readonly kind: string;
|
|
63
|
+
readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export abstract class NamespaceBase extends IRNodeBase implements Namespace {
|
|
66
67
|
abstract readonly id: string;
|
|
68
|
+
abstract readonly entries: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
67
69
|
abstract override readonly kind: string;
|
|
68
70
|
}
|
package/src/ir/storage.ts
CHANGED
|
@@ -30,18 +30,19 @@ export interface EntityCoordinate {
|
|
|
30
30
|
* value, yielded as {@link EntityCoordinate} tuples with
|
|
31
31
|
* `plane: 'storage'` (the parameter type binds the plane).
|
|
32
32
|
*
|
|
33
|
-
* Iterates each namespace's
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* Iterates each namespace's `entries` slot maps structurally. Skips
|
|
34
|
+
* non-object `entries`; `id` and `kind` are not walked (`kind` is
|
|
35
|
+
* non-enumerable on concretions). For every entity-kind key under
|
|
36
|
+
* `entries` whose value is a non-null object, yields one coordinate per
|
|
37
|
+
* entity name in that map. No family-specific slot vocabulary is required.
|
|
38
38
|
*/
|
|
39
39
|
export function* elementCoordinates(
|
|
40
40
|
storage: Pick<StorageBase, 'namespaces'>,
|
|
41
41
|
): Generator<EntityCoordinate> {
|
|
42
42
|
for (const [namespaceId, ns] of Object.entries(storage.namespaces)) {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
const entries = ns.entries;
|
|
44
|
+
if (entries === null || typeof entries !== 'object') continue;
|
|
45
|
+
for (const [entityKind, slot] of Object.entries(entries)) {
|
|
45
46
|
if (slot === null || typeof slot !== 'object') continue;
|
|
46
47
|
for (const entityName of Object.keys(slot)) {
|
|
47
48
|
yield { plane: 'storage', namespaceId, entityKind, entityName };
|