@ankhorage/contracts 22.0.4 → 22.1.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.
Files changed (84) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/auth.d.ts +2 -1
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js.map +1 -1
  5. package/dist/bindings.d.ts +6 -5
  6. package/dist/bindings.d.ts.map +1 -1
  7. package/dist/bindings.js.map +1 -1
  8. package/dist/collections.d.ts +4 -0
  9. package/dist/collections.d.ts.map +1 -1
  10. package/dist/collections.js.map +1 -1
  11. package/dist/data/apis.d.ts +2 -1
  12. package/dist/data/apis.d.ts.map +1 -1
  13. package/dist/data/apis.js.map +1 -1
  14. package/dist/data/endpoints.d.ts +5 -4
  15. package/dist/data/endpoints.d.ts.map +1 -1
  16. package/dist/data/endpoints.js.map +1 -1
  17. package/dist/data/operations.d.ts +2 -1
  18. package/dist/data/operations.d.ts.map +1 -1
  19. package/dist/data/operations.js.map +1 -1
  20. package/dist/data/schemas.d.ts +3 -2
  21. package/dist/data/schemas.d.ts.map +1 -1
  22. package/dist/data/schemas.js.map +1 -1
  23. package/dist/data/sources.d.ts +2 -1
  24. package/dist/data/sources.d.ts.map +1 -1
  25. package/dist/data/sources.js.map +1 -1
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/media.d.ts +2 -1
  31. package/dist/media.d.ts.map +1 -1
  32. package/dist/media.js.map +1 -1
  33. package/dist/navigator.d.ts +2 -1
  34. package/dist/navigator.d.ts.map +1 -1
  35. package/dist/navigator.js.map +1 -1
  36. package/dist/structure/index.d.ts +4 -0
  37. package/dist/structure/index.d.ts.map +1 -0
  38. package/dist/structure/index.js +3 -0
  39. package/dist/structure/index.js.map +1 -0
  40. package/dist/structure/isStructureDescriptor.d.ts +4 -0
  41. package/dist/structure/isStructureDescriptor.d.ts.map +1 -0
  42. package/dist/structure/isStructureDescriptor.js +132 -0
  43. package/dist/structure/isStructureDescriptor.js.map +1 -0
  44. package/dist/structure/isStructureDescriptorDocument.d.ts +4 -0
  45. package/dist/structure/isStructureDescriptorDocument.d.ts.map +1 -0
  46. package/dist/structure/isStructureDescriptorDocument.js +114 -0
  47. package/dist/structure/isStructureDescriptorDocument.js.map +1 -0
  48. package/dist/structure/types.d.ts +63 -0
  49. package/dist/structure/types.d.ts.map +1 -0
  50. package/dist/structure/types.js +2 -0
  51. package/dist/structure/types.js.map +1 -0
  52. package/dist/theme.d.ts +7 -6
  53. package/dist/theme.d.ts.map +1 -1
  54. package/dist/theme.js.map +1 -1
  55. package/dist/types/infraManifest.d.ts +3 -3
  56. package/dist/types/infraManifest.d.ts.map +1 -1
  57. package/dist/types/infraManifest.js.map +1 -1
  58. package/dist/types/infraWorkload.d.ts +6 -6
  59. package/dist/types/infraWorkload.d.ts.map +1 -1
  60. package/dist/types/infraWorkload.js.map +1 -1
  61. package/dist/types.d.ts +3 -2
  62. package/dist/types.d.ts.map +1 -1
  63. package/dist/types.js.map +1 -1
  64. package/package.json +5 -1
  65. package/src/auth.ts +2 -1
  66. package/src/bindings.ts +9 -6
  67. package/src/collections.ts +10 -0
  68. package/src/data/apis.ts +2 -1
  69. package/src/data/endpoints.ts +5 -4
  70. package/src/data/operations.ts +2 -1
  71. package/src/data/schemas.ts +3 -2
  72. package/src/data/sources.ts +2 -1
  73. package/src/index.ts +1 -0
  74. package/src/media.ts +3 -1
  75. package/src/navigator.ts +2 -1
  76. package/src/structure/index.ts +21 -0
  77. package/src/structure/isStructureDescriptor.ts +163 -0
  78. package/src/structure/isStructureDescriptorDocument.ts +164 -0
  79. package/src/structure/types.ts +92 -0
  80. package/src/structure.test.ts +255 -0
  81. package/src/theme.ts +8 -6
  82. package/src/types/infraManifest.ts +3 -3
  83. package/src/types/infraWorkload.ts +6 -6
  84. package/src/types.ts +3 -2
@@ -0,0 +1,164 @@
1
+ import { hasOnlyKeys, isRecord } from '@ankhorage/utility/object';
2
+ import { isNonEmptyString } from '@ankhorage/utility/string';
3
+
4
+ import { isStructureDescriptor } from './isStructureDescriptor';
5
+ import type {
6
+ StructureDescriptor,
7
+ StructureDescriptorDefinition,
8
+ StructureDescriptorDocument,
9
+ StructureDescriptorId,
10
+ StructureDescriptorRegistry,
11
+ } from './types';
12
+
13
+ /*** Validate a complete package structural-descriptor document and all resolvable references. */
14
+ export function isStructureDescriptorDocument(
15
+ value: unknown,
16
+ ): value is StructureDescriptorDocument {
17
+ if (
18
+ !isRecord(value) ||
19
+ !hasOnlyKeys(value, [
20
+ 'protocolVersion',
21
+ 'packageName',
22
+ 'packageVersion',
23
+ 'roots',
24
+ 'descriptors',
25
+ ]) ||
26
+ value.protocolVersion !== 1 ||
27
+ !isNonEmptyString(value.packageName) ||
28
+ !isNonEmptyString(value.packageVersion) ||
29
+ !isRootMap(value.roots) ||
30
+ !isDescriptorRegistry(value.descriptors)
31
+ ) {
32
+ return false;
33
+ }
34
+
35
+ const document: StructureDescriptorDocument = {
36
+ protocolVersion: 1,
37
+ packageName: value.packageName,
38
+ packageVersion: value.packageVersion,
39
+ roots: value.roots,
40
+ descriptors: value.descriptors,
41
+ };
42
+ return hasResolvableRoots(document) && isDescriptorGraphValid(document);
43
+ }
44
+
45
+ /*** Validate the root-name to local-descriptor-ID map. */
46
+ function isRootMap(value: unknown): value is Readonly<Record<string, StructureDescriptorId>> {
47
+ return (
48
+ isRecord(value) &&
49
+ Object.keys(value).length > 0 &&
50
+ Object.entries(value).every(
51
+ ([name, descriptorId]) => isNonEmptyString(name) && isNonEmptyString(descriptorId),
52
+ )
53
+ );
54
+ }
55
+
56
+ /*** Validate descriptor definitions and enforce record-key to definition-ID identity. */
57
+ function isDescriptorRegistry(value: unknown): value is StructureDescriptorRegistry {
58
+ return (
59
+ isRecord(value) &&
60
+ Object.keys(value).length > 0 &&
61
+ Object.entries(value).every(([descriptorId, definition]) =>
62
+ isDefinition(definition, descriptorId),
63
+ )
64
+ );
65
+ }
66
+
67
+ /*** Validate one descriptor definition against its stable registry key. */
68
+ function isDefinition(
69
+ value: unknown,
70
+ descriptorId: string,
71
+ ): value is StructureDescriptorDefinition {
72
+ return (
73
+ isRecord(value) &&
74
+ hasOnlyKeys(value, ['id', 'descriptor']) &&
75
+ value.id === descriptorId &&
76
+ isNonEmptyString(value.id) &&
77
+ isStructureDescriptor(value.descriptor)
78
+ );
79
+ }
80
+
81
+ /*** Require every document root to resolve to one local descriptor definition. */
82
+ function hasResolvableRoots(document: StructureDescriptorDocument): boolean {
83
+ const descriptors = new Map(Object.entries(document.descriptors));
84
+ return Object.values(document.roots).every((descriptorId) => descriptors.has(descriptorId));
85
+ }
86
+
87
+ /*** Validate local references, recursive graphs, and string-only collection key/member descriptors. */
88
+ function isDescriptorGraphValid(document: StructureDescriptorDocument): boolean {
89
+ const definitions = new Map(Object.entries(document.descriptors));
90
+ return Array.from(definitions.values()).every((definition) =>
91
+ validateGraphNode(definition.descriptor, document, new Set()),
92
+ );
93
+ }
94
+
95
+ /*** Validate one graph node while treating revisited local refs as a valid recursive cycle. */
96
+ function validateGraphNode(
97
+ descriptor: StructureDescriptor,
98
+ document: StructureDescriptorDocument,
99
+ visitedRefs: Set<string>,
100
+ ): boolean {
101
+ switch (descriptor.kind) {
102
+ case 'object':
103
+ return Object.values(descriptor.fields).every((field) =>
104
+ validateGraphNode(field.value, document, new Set(visitedRefs)),
105
+ );
106
+ case 'entity-registry':
107
+ case 'value-map':
108
+ return (
109
+ isStringCompatible(descriptor.key, document, new Set()) &&
110
+ validateGraphNode(descriptor.value, document, new Set(visitedRefs))
111
+ );
112
+ case 'set':
113
+ return isStringCompatible(descriptor.member, document, new Set());
114
+ case 'ordered-list':
115
+ return validateGraphNode(descriptor.item, document, new Set(visitedRefs));
116
+ case 'union':
117
+ return descriptor.variants.every((variant) =>
118
+ validateGraphNode(variant, document, new Set(visitedRefs)),
119
+ );
120
+ case 'ref':
121
+ return validateReference(descriptor, document, visitedRefs);
122
+ default:
123
+ return true;
124
+ }
125
+ }
126
+
127
+ /*** Resolve and validate one local reference while deferring package-qualified refs to composition. */
128
+ function validateReference(
129
+ descriptor: Extract<StructureDescriptor, { readonly kind: 'ref' }>,
130
+ document: StructureDescriptorDocument,
131
+ visitedRefs: Set<string>,
132
+ ): boolean {
133
+ if (descriptor.packageName && descriptor.packageName !== document.packageName) return true;
134
+ if (visitedRefs.has(descriptor.id)) return true;
135
+
136
+ const definition = new Map(Object.entries(document.descriptors)).get(descriptor.id);
137
+ if (!definition) return false;
138
+
139
+ const nextRefs = new Set(visitedRefs);
140
+ nextRefs.add(descriptor.id);
141
+ return validateGraphNode(definition.descriptor, document, nextRefs);
142
+ }
143
+
144
+ /*** Prove that a collection key/member descriptor is string-valued when locally resolvable. */
145
+ function isStringCompatible(
146
+ descriptor: StructureDescriptor,
147
+ document: StructureDescriptorDocument,
148
+ visitedRefs: Set<string>,
149
+ ): boolean {
150
+ if (descriptor.kind === 'scalar') return descriptor.type === 'string';
151
+ if (descriptor.kind === 'enum') {
152
+ return descriptor.values.every((value) => typeof value === 'string');
153
+ }
154
+ if (descriptor.kind !== 'ref') return false;
155
+ if (descriptor.packageName && descriptor.packageName !== document.packageName) return true;
156
+ if (visitedRefs.has(descriptor.id)) return false;
157
+
158
+ const definition = new Map(Object.entries(document.descriptors)).get(descriptor.id);
159
+ if (!definition) return false;
160
+
161
+ const nextRefs = new Set(visitedRefs);
162
+ nextRefs.add(descriptor.id);
163
+ return isStringCompatible(definition.descriptor, document, nextRefs);
164
+ }
@@ -0,0 +1,92 @@
1
+ import type { EntityRegistry, ValueMap } from '../collections';
2
+
3
+ export type StructureDescriptorId = string;
4
+
5
+ export type StructureScalarType = 'boolean' | 'integer' | 'null' | 'number' | 'string';
6
+
7
+ export type StructureLiteralValue = boolean | number | string | null;
8
+
9
+ export interface StructureScalarDescriptor {
10
+ readonly kind: 'scalar';
11
+ readonly type: StructureScalarType;
12
+ }
13
+
14
+ export interface StructureEnumDescriptor {
15
+ readonly kind: 'enum';
16
+ readonly values: readonly StructureLiteralValue[];
17
+ }
18
+
19
+ export interface StructureObjectField {
20
+ readonly value: StructureDescriptor;
21
+ readonly optional?: boolean;
22
+ }
23
+
24
+ export interface StructureObjectDescriptor {
25
+ readonly kind: 'object';
26
+ readonly fields: ValueMap<string, StructureObjectField>;
27
+ }
28
+
29
+ export interface StructureEntityRegistryDescriptor {
30
+ readonly kind: 'entity-registry';
31
+ readonly key: StructureDescriptor;
32
+ readonly value: StructureDescriptor;
33
+ readonly identityField?: string;
34
+ }
35
+
36
+ export interface StructureValueMapDescriptor {
37
+ readonly kind: 'value-map';
38
+ readonly key: StructureDescriptor;
39
+ readonly value: StructureDescriptor;
40
+ }
41
+
42
+ export interface StructureSetDescriptor {
43
+ readonly kind: 'set';
44
+ readonly member: StructureDescriptor;
45
+ }
46
+
47
+ export interface StructureOrderedListDescriptor {
48
+ readonly kind: 'ordered-list';
49
+ readonly item: StructureDescriptor;
50
+ }
51
+
52
+ export interface StructureUnionDescriptor {
53
+ readonly kind: 'union';
54
+ readonly variants: readonly StructureDescriptor[];
55
+ readonly discriminator?: string;
56
+ }
57
+
58
+ export interface StructureReferenceDescriptor {
59
+ readonly kind: 'ref';
60
+ readonly id: StructureDescriptorId;
61
+ readonly packageName?: string;
62
+ }
63
+
64
+ export type StructureDescriptor =
65
+ | StructureEntityRegistryDescriptor
66
+ | StructureEnumDescriptor
67
+ | StructureObjectDescriptor
68
+ | StructureOrderedListDescriptor
69
+ | StructureReferenceDescriptor
70
+ | StructureScalarDescriptor
71
+ | StructureSetDescriptor
72
+ | StructureUnionDescriptor
73
+ | StructureValueMapDescriptor;
74
+
75
+ export interface StructureDescriptorDefinition {
76
+ readonly id: StructureDescriptorId;
77
+ readonly descriptor: StructureDescriptor;
78
+ }
79
+
80
+ export type StructureDescriptorRegistry = EntityRegistry<
81
+ StructureDescriptorId,
82
+ StructureDescriptorDefinition,
83
+ 'id'
84
+ >;
85
+
86
+ export interface StructureDescriptorDocument {
87
+ readonly protocolVersion: 1;
88
+ readonly packageName: string;
89
+ readonly packageVersion: string;
90
+ readonly roots: ValueMap<string, StructureDescriptorId>;
91
+ readonly descriptors: StructureDescriptorRegistry;
92
+ }
@@ -0,0 +1,255 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
4
+ import { expect, test } from 'bun:test';
5
+
6
+ import {
7
+ isStructureDescriptor,
8
+ isStructureDescriptorDocument,
9
+ type StructureDescriptorDocument,
10
+ } from './structure';
11
+
12
+ test('accepts scalar, enum and object descriptors', () => {
13
+ expect(isStructureDescriptor({ kind: 'scalar', type: 'string' })).toBe(true);
14
+ expect(isStructureDescriptor({ kind: 'enum', values: ['light', 'dark'] })).toBe(true);
15
+ expect(
16
+ isStructureDescriptor({
17
+ kind: 'object',
18
+ fields: {
19
+ name: { value: { kind: 'scalar', type: 'string' } },
20
+ enabled: { value: { kind: 'scalar', type: 'boolean' }, optional: true },
21
+ },
22
+ }),
23
+ ).toBe(true);
24
+ });
25
+
26
+ test('accepts registry, map, set and ordered-list descriptors', () => {
27
+ expect(
28
+ isStructureDescriptor({
29
+ kind: 'entity-registry',
30
+ key: { kind: 'scalar', type: 'string' },
31
+ value: { kind: 'ref', id: 'theme' },
32
+ identityField: 'id',
33
+ }),
34
+ ).toBe(true);
35
+ expect(
36
+ isStructureDescriptor({
37
+ kind: 'value-map',
38
+ key: { kind: 'scalar', type: 'string' },
39
+ value: { kind: 'scalar', type: 'number' },
40
+ }),
41
+ ).toBe(true);
42
+ expect(
43
+ isStructureDescriptor({
44
+ kind: 'set',
45
+ member: { kind: 'enum', values: ['camera', 'microphone'] },
46
+ }),
47
+ ).toBe(true);
48
+ expect(
49
+ isStructureDescriptor({
50
+ kind: 'ordered-list',
51
+ item: { kind: 'scalar', type: 'string' },
52
+ }),
53
+ ).toBe(true);
54
+ });
55
+
56
+ test('accepts unions and references', () => {
57
+ expect(
58
+ isStructureDescriptor({
59
+ kind: 'union',
60
+ discriminator: 'kind',
61
+ variants: [
62
+ { kind: 'ref', id: 'literal-source' },
63
+ { kind: 'ref', id: 'state-source' },
64
+ ],
65
+ }),
66
+ ).toBe(true);
67
+ expect(
68
+ isStructureDescriptor({
69
+ kind: 'ref',
70
+ packageName: '@ankhorage/example-owner',
71
+ id: 'external-config',
72
+ }),
73
+ ).toBe(true);
74
+ });
75
+
76
+ test('rejects malformed scalar and union declarations', () => {
77
+ expect(isStructureDescriptor({ kind: 'unknown' })).toBe(false);
78
+ expect(isStructureDescriptor({ kind: 'enum', values: [] })).toBe(false);
79
+ expect(isStructureDescriptor({ kind: 'enum', values: ['x', 'x'] })).toBe(false);
80
+ expect(isStructureDescriptor({ kind: 'enum', values: [Number.NaN] })).toBe(false);
81
+ expect(
82
+ isStructureDescriptor({
83
+ kind: 'union',
84
+ variants: [{ kind: 'scalar', type: 'string' }],
85
+ }),
86
+ ).toBe(false);
87
+ });
88
+
89
+ test('rejects non-string map keys and set members', () => {
90
+ expect(
91
+ isStructureDescriptor({
92
+ kind: 'value-map',
93
+ key: { kind: 'scalar', type: 'number' },
94
+ value: { kind: 'scalar', type: 'string' },
95
+ }),
96
+ ).toBe(false);
97
+ expect(
98
+ isStructureDescriptor({
99
+ kind: 'set',
100
+ member: { kind: 'scalar', type: 'boolean' },
101
+ }),
102
+ ).toBe(false);
103
+ });
104
+
105
+ test('rejects cyclic inline object graphs without overflowing', () => {
106
+ const cyclic: Record<string, unknown> = {
107
+ kind: 'object',
108
+ fields: {},
109
+ };
110
+ cyclic.fields = {
111
+ self: { value: cyclic },
112
+ };
113
+
114
+ expect(isStructureDescriptor(cyclic)).toBe(false);
115
+ });
116
+
117
+ test('accepts recursive local and package-qualified references', () => {
118
+ expect(isStructureDescriptorDocument(STRUCTURE_DOCUMENT_FIXTURE)).toBe(true);
119
+ expect(JSON.parse(JSON.stringify(STRUCTURE_DOCUMENT_FIXTURE))).toEqual(
120
+ STRUCTURE_DOCUMENT_FIXTURE,
121
+ );
122
+ });
123
+
124
+ test('rejects unresolved local roots and references', () => {
125
+ expect(
126
+ isStructureDescriptorDocument({
127
+ ...STRUCTURE_DOCUMENT_FIXTURE,
128
+ roots: { app: 'missing' },
129
+ }),
130
+ ).toBe(false);
131
+ expect(
132
+ isStructureDescriptorDocument({
133
+ ...STRUCTURE_DOCUMENT_FIXTURE,
134
+ descriptors: {
135
+ ...STRUCTURE_DOCUMENT_FIXTURE.descriptors,
136
+ app: {
137
+ id: 'app',
138
+ descriptor: { kind: 'ref', id: 'missing' },
139
+ },
140
+ },
141
+ }),
142
+ ).toBe(false);
143
+ });
144
+
145
+ test('rejects registry key and definition identity mismatches', () => {
146
+ expect(
147
+ isStructureDescriptorDocument({
148
+ ...STRUCTURE_DOCUMENT_FIXTURE,
149
+ descriptors: {
150
+ ...STRUCTURE_DOCUMENT_FIXTURE.descriptors,
151
+ app: {
152
+ id: 'different-id',
153
+ descriptor: { kind: 'scalar', type: 'string' },
154
+ },
155
+ },
156
+ }),
157
+ ).toBe(false);
158
+ });
159
+
160
+ test('proves referenced set members are strings', () => {
161
+ expect(
162
+ isStructureDescriptorDocument({
163
+ ...STRUCTURE_DOCUMENT_FIXTURE,
164
+ descriptors: {
165
+ ...STRUCTURE_DOCUMENT_FIXTURE.descriptors,
166
+ numeric: {
167
+ id: 'numeric',
168
+ descriptor: { kind: 'scalar', type: 'number' },
169
+ },
170
+ app: {
171
+ id: 'app',
172
+ descriptor: {
173
+ kind: 'set',
174
+ member: { kind: 'ref', id: 'numeric' },
175
+ },
176
+ },
177
+ },
178
+ }),
179
+ ).toBe(false);
180
+ });
181
+
182
+ test('publishes the dedicated structure subpath', async () => {
183
+ const packageJson = JSON.parse(await readFile(join(process.cwd(), 'package.json'), 'utf8')) as {
184
+ exports?: Readonly<Record<string, { default?: string; types?: string }>>;
185
+ };
186
+ const exports = new Map(Object.entries(packageJson.exports ?? {}));
187
+
188
+ expect(exports.get('./structure')).toEqual({
189
+ types: './dist/structure/index.d.ts',
190
+ default: './dist/structure/index.js',
191
+ });
192
+ });
193
+
194
+ const STRUCTURE_DOCUMENT_FIXTURE = {
195
+ protocolVersion: 1,
196
+ packageName: '@ankhorage/contracts',
197
+ packageVersion: '0.0.0-test',
198
+ roots: { app: 'app' },
199
+ descriptors: {
200
+ app: {
201
+ id: 'app',
202
+ descriptor: {
203
+ kind: 'object',
204
+ fields: {
205
+ themes: {
206
+ value: {
207
+ kind: 'entity-registry',
208
+ key: { kind: 'scalar', type: 'string' },
209
+ value: { kind: 'ref', id: 'theme' },
210
+ identityField: 'id',
211
+ },
212
+ },
213
+ permissions: {
214
+ optional: true,
215
+ value: {
216
+ kind: 'set',
217
+ member: { kind: 'ref', id: 'permission-name' },
218
+ },
219
+ },
220
+ external: {
221
+ optional: true,
222
+ value: {
223
+ kind: 'ref',
224
+ packageName: '@ankhorage/example-owner',
225
+ id: 'external-config',
226
+ },
227
+ },
228
+ },
229
+ },
230
+ },
231
+ theme: {
232
+ id: 'theme',
233
+ descriptor: {
234
+ kind: 'object',
235
+ fields: {
236
+ id: { value: { kind: 'scalar', type: 'string' } },
237
+ children: {
238
+ optional: true,
239
+ value: {
240
+ kind: 'ordered-list',
241
+ item: { kind: 'ref', id: 'theme' },
242
+ },
243
+ },
244
+ },
245
+ },
246
+ },
247
+ 'permission-name': {
248
+ id: 'permission-name',
249
+ descriptor: {
250
+ kind: 'enum',
251
+ values: ['camera', 'microphone'],
252
+ },
253
+ },
254
+ },
255
+ } as const satisfies StructureDescriptorDocument;
package/src/theme.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import type { ValueMap } from './collections';
2
+
1
3
  /** Numeric token values authored at theme level. */
2
- export type ThemeNumericTokenOverrides = Readonly<Record<string, number>>;
4
+ export type ThemeNumericTokenOverrides = ValueMap<string, number>;
3
5
 
4
6
  /** String token values authored at theme level. */
5
- export type ThemeStringTokenOverrides = Readonly<Record<string, string>>;
7
+ export type ThemeStringTokenOverrides = ValueMap<string, string>;
6
8
 
7
9
  /** Partial authored override for one semantic heading recipe. */
8
10
  export interface ThemeTypographyHeadingOverrides {
@@ -13,7 +15,7 @@ export interface ThemeTypographyHeadingOverrides {
13
15
 
14
16
  /** Theme-global typography source overrides. Font installation remains module-owned. */
15
17
  export interface ThemeTypographyTokenOverrides {
16
- readonly headings?: Readonly<Record<string, ThemeTypographyHeadingOverrides>>;
18
+ readonly headings?: ValueMap<string, ThemeTypographyHeadingOverrides>;
17
19
  readonly sizes?: ThemeNumericTokenOverrides;
18
20
  readonly weights?: ThemeStringTokenOverrides;
19
21
  }
@@ -36,7 +38,7 @@ export interface ThemeGlobalTokenOverrides {
36
38
  export type ThemeRecipeOverrideValue = boolean | string;
37
39
 
38
40
  /** Persisted values for one component or pattern recipe. */
39
- export type ThemeRecipeFieldOverrides = Readonly<Record<string, ThemeRecipeOverrideValue>>;
41
+ export type ThemeRecipeFieldOverrides = ValueMap<string, ThemeRecipeOverrideValue>;
40
42
 
41
43
  /**
42
44
  * Generic persisted recipe values.
@@ -45,6 +47,6 @@ export type ThemeRecipeFieldOverrides = Readonly<Record<string, ThemeRecipeOverr
45
47
  * relationships. Contracts stores selected values only and does not duplicate that metadata.
46
48
  */
47
49
  export interface ThemeRecipeOverrides {
48
- readonly components?: Readonly<Record<string, ThemeRecipeFieldOverrides>>;
49
- readonly patterns?: Readonly<Record<string, ThemeRecipeFieldOverrides>>;
50
+ readonly components?: ValueMap<string, ThemeRecipeFieldOverrides>;
51
+ readonly patterns?: ValueMap<string, ThemeRecipeFieldOverrides>;
50
52
  }
@@ -1,5 +1,5 @@
1
1
  import type { AuthFlowConfig, AuthOAuthConfig } from '../auth';
2
- import type { SerializableSet } from '../collections';
2
+ import type { EntityRegistry, SerializableSet, ValueMap } from '../collections';
3
3
  import type { ApiDefinitionRegistry, DataContractValue } from '../data';
4
4
  import type { AppEnvironmentId } from '../environments';
5
5
  import type { INFRA_ADAPTER_CATALOG, INFRA_RUNTIME_COMPATIBILITY } from '../infra/constants';
@@ -109,7 +109,7 @@ export interface InfraAuthConfigMap {
109
109
  export interface InfraAuthzConfigMap {
110
110
  readonly cerbos: {
111
111
  readonly kind: 'RBAC' | 'ABAC';
112
- readonly policies?: Readonly<Record<string, string>>;
112
+ readonly policies?: ValueMap<string, string>;
113
113
  };
114
114
  }
115
115
  export interface InfraSecretStoreConfigMap {
@@ -167,7 +167,7 @@ export interface InfraModuleSpec {
167
167
  readonly config?: DataContractValue;
168
168
  }
169
169
 
170
- export type InfraModuleRegistry = Readonly<Record<InfraModuleId, InfraModuleSpec>>;
170
+ export type InfraModuleRegistry = EntityRegistry<InfraModuleId, InfraModuleSpec>;
171
171
 
172
172
  export interface InfraManifest {
173
173
  readonly environments: Readonly<
@@ -1,4 +1,4 @@
1
- import type { SerializableSet } from '../collections';
1
+ import type { EntityRegistry, SerializableSet, ValueMap } from '../collections';
2
2
  import type { InfraControlPlaneCredentialRef, InfraSecretReference } from './infraSecrets';
3
3
 
4
4
  export interface InfraWorkloadArtifact {
@@ -58,9 +58,9 @@ export interface InfraWorkloadVolumeSpec {
58
58
  }
59
59
 
60
60
  export type InfraWorkloadId = string;
61
- export type InfraWorkloadPortRegistry = Readonly<Record<string, InfraWorkloadPort>>;
62
- export type InfraWorkloadFileMap = Readonly<Record<string, InfraWorkloadValue>>;
63
- export type InfraWorkloadVolumeRegistry = Readonly<Record<string, InfraWorkloadVolumeSpec>>;
61
+ export type InfraWorkloadPortRegistry = EntityRegistry<string, InfraWorkloadPort>;
62
+ export type InfraWorkloadFileMap = ValueMap<string, InfraWorkloadValue>;
63
+ export type InfraWorkloadVolumeRegistry = EntityRegistry<string, InfraWorkloadVolumeSpec, 'id'>;
64
64
 
65
65
  export interface InfraWorkloadSpec {
66
66
  readonly id: InfraWorkloadId;
@@ -68,7 +68,7 @@ export interface InfraWorkloadSpec {
68
68
  readonly command?: readonly string[];
69
69
  readonly args?: readonly string[];
70
70
  readonly ports?: InfraWorkloadPortRegistry;
71
- readonly environment?: Readonly<Record<string, InfraWorkloadValue>>;
71
+ readonly environment?: ValueMap<string, InfraWorkloadValue>;
72
72
  readonly files?: InfraWorkloadFileMap;
73
73
  readonly health?: InfraWorkloadHealthSpec;
74
74
  readonly resources?: InfraWorkloadResourceSpec;
@@ -79,4 +79,4 @@ export interface InfraWorkloadSpec {
79
79
  readonly dependsOn?: SerializableSet;
80
80
  }
81
81
 
82
- export type InfraWorkloadRegistry = Readonly<Record<InfraWorkloadId, InfraWorkloadSpec>>;
82
+ export type InfraWorkloadRegistry = EntityRegistry<InfraWorkloadId, InfraWorkloadSpec, 'id'>;
package/src/types.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  ComponentDataBindingRegistry,
8
8
  ScreenDataLoaderDefinition,
9
9
  } from './bindings';
10
+ import type { EntityRegistry } from './collections';
10
11
  import type { DataSourceRegistry } from './data';
11
12
  import type { AppDeployManifest } from './deploy';
12
13
  import type { InfraManifest } from './infra';
@@ -36,7 +37,7 @@ export interface ThemeConfig {
36
37
  recipes?: ThemeRecipeOverrides;
37
38
  }
38
39
 
39
- export type ThemeRegistry = Readonly<Record<ThemeId, ThemeConfig>>;
40
+ export type ThemeRegistry = EntityRegistry<ThemeId, ThemeConfig, 'id'>;
40
41
 
41
42
  export type ActionType =
42
43
  'navigate' | 'alert' | 'console' | 'toggleDarkMode' | 'setLanguage' | 'search' | 'filter';
@@ -286,7 +287,7 @@ export interface AppManifest {
286
287
  state?: AppStateSpec;
287
288
  infra: InfraManifest;
288
289
  navigator: AppNavigatorManifest;
289
- screens: Readonly<Record<string, ScreenSpec>>;
290
+ screens: EntityRegistry<string, ScreenSpec, 'id'>;
290
291
  dataSources?: DataSourceRegistry;
291
292
  dataBindings?: ComponentDataBindingRegistry;
292
293
  repository?: RepositoryManifest;