@prisma-next/framework-components 0.12.0-dev.9 → 0.13.0-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/authoring.d.mts +2 -2
  2. package/dist/authoring.mjs +2 -2
  3. package/dist/{codec-BFOsuHKK.d.mts → codec-DCQAerzB.d.mts} +1 -1
  4. package/dist/{codec-BFOsuHKK.d.mts.map → codec-DCQAerzB.d.mts.map} +1 -1
  5. package/dist/codec.d.mts +1 -1
  6. package/dist/codec.d.mts.map +1 -1
  7. package/dist/components.d.mts +1 -1
  8. package/dist/components.mjs +1 -1
  9. package/dist/components.mjs.map +1 -1
  10. package/dist/control.d.mts +85 -13
  11. package/dist/control.d.mts.map +1 -1
  12. package/dist/control.mjs +89 -7
  13. package/dist/control.mjs.map +1 -1
  14. package/dist/{emission-types-CMv_053d.d.mts → emission-types-vfpSTe63.d.mts} +2 -2
  15. package/dist/{emission-types-CMv_053d.d.mts.map → emission-types-vfpSTe63.d.mts.map} +1 -1
  16. package/dist/emission.d.mts +3 -3
  17. package/dist/execution.d.mts +1 -1
  18. package/dist/execution.d.mts.map +1 -1
  19. package/dist/execution.mjs +1 -1
  20. package/dist/{framework-authoring-DcEZ5Lin.mjs → framework-authoring-CnwPJCO4.mjs} +76 -5
  21. package/dist/framework-authoring-CnwPJCO4.mjs.map +1 -0
  22. package/dist/framework-authoring-R0TYCkvG.d.mts +380 -0
  23. package/dist/framework-authoring-R0TYCkvG.d.mts.map +1 -0
  24. package/dist/{framework-components-CuoUhyB5.d.mts → framework-components-DDQXmW0b.d.mts} +6 -5
  25. package/dist/{framework-components-CuoUhyB5.d.mts.map → framework-components-DDQXmW0b.d.mts.map} +1 -1
  26. package/dist/{framework-components-FdqmlGUj.mjs → framework-components-DbCS57go.mjs} +1 -1
  27. package/dist/{framework-components-FdqmlGUj.mjs.map → framework-components-DbCS57go.mjs.map} +1 -1
  28. package/dist/ir.d.mts +20 -18
  29. package/dist/ir.d.mts.map +1 -1
  30. package/dist/ir.mjs +17 -14
  31. package/dist/ir.mjs.map +1 -1
  32. package/dist/{psl-ast-BDXL7iCg.d.mts → psl-ast-Cn50B-UG.d.mts} +90 -18
  33. package/dist/psl-ast-Cn50B-UG.d.mts.map +1 -0
  34. package/dist/psl-ast.d.mts +37 -2
  35. package/dist/psl-ast.d.mts.map +1 -0
  36. package/dist/psl-ast.mjs +222 -4
  37. package/dist/psl-ast.mjs.map +1 -1
  38. package/dist/runtime.d.mts +1 -1
  39. package/dist/runtime.d.mts.map +1 -1
  40. package/dist/runtime.mjs.map +1 -1
  41. package/dist/{types-import-spec-BxI5cSQy.d.mts → types-import-spec-DRKzrJ20.d.mts} +1 -1
  42. package/dist/{types-import-spec-BxI5cSQy.d.mts.map → types-import-spec-DRKzrJ20.d.mts.map} +1 -1
  43. package/dist/utils.mjs.map +1 -1
  44. package/package.json +9 -9
  45. package/src/control/control-instances.ts +15 -5
  46. package/src/control/control-migration-types.ts +20 -2
  47. package/src/control/control-result-types.ts +4 -1
  48. package/src/control/control-stack.ts +123 -4
  49. package/src/control/psl-ast.ts +234 -34
  50. package/src/control/psl-extension-block-validator.ts +324 -0
  51. package/src/control/verifier-disposition.ts +62 -0
  52. package/src/exports/authoring.ts +16 -0
  53. package/src/exports/control.ts +7 -0
  54. package/src/exports/psl-ast.ts +2 -0
  55. package/src/ir/namespace.ts +15 -13
  56. package/src/ir/storage.ts +8 -7
  57. package/src/shared/framework-authoring.ts +215 -2
  58. package/src/shared/framework-components.ts +2 -0
  59. package/src/shared/psl-extension-block.ts +184 -0
  60. package/dist/framework-authoring-BPPe9C9D.d.mts +0 -183
  61. package/dist/framework-authoring-BPPe9C9D.d.mts.map +0 -1
  62. package/dist/framework-authoring-DcEZ5Lin.mjs.map +0 -1
  63. 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
+ }
@@ -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';
@@ -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,
@@ -1 +1,3 @@
1
1
  export * from '../control/psl-ast';
2
+ export type { ExtensionBlockRefResolutionContext } from '../control/psl-extension-block-validator';
3
+ export { validateExtensionBlock } from '../control/psl-extension-block-validator';
@@ -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
- * `tables`, Mongo contributes `collections`, future families pick their
45
- * own native idiom). Generic consumers walking "all named entries" go
46
- * through a family-typed namespace, not the framework `Namespace`.
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), `kind`
51
- * (non-enumerable string discriminator set via `Object.defineProperty`),
52
- * and one or more entity-kind slot maps each an own-enumerable property
53
- * whose key is the entity kind (`tables`, `types`, `collections`,
54
- * target-pack-contributed slot names) and whose value is a
55
- * `Record<entityName, EntityIRClass>`. No other own-enumerable data lives
56
- * on a namespace; non-entity computed data lives on the surrounding storage
57
- * or contract IR. The framework's `elementCoordinates(storage)` walk relies
58
- * on this invariant to enumerate entities structurally without
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 own-enumerable properties structurally.
34
- * Skips `id` (known scalar); `kind` is non-enumerable on namespace
35
- * concretions and does not appear in `Object.entries`. For every other
36
- * property whose value is a non-null object, yields one coordinate per
37
- * entry key in that map. No family-specific slot vocabulary is required.
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
- for (const [entityKind, slot] of Object.entries(ns)) {
44
- if (entityKind === 'id') continue;
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 };