@smallpen/core 0.1.0-alpha.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.
@@ -0,0 +1,324 @@
1
+ import { findComponentVariant } from "./components-domain.mjs";
2
+ import { fail } from "./errors.mjs";
3
+
4
+ function screenIndex(manifest, entries) {
5
+ return new Map(
6
+ manifest.entries.screens.map((entry) => {
7
+ const screen = entries[entry];
8
+ return [screen.id, screen];
9
+ }),
10
+ );
11
+ }
12
+
13
+ function interactionIndex(screens) {
14
+ const interactions = new Map();
15
+ for (const screen of screens.values()) {
16
+ for (const presentation of screen.presentations) {
17
+ for (const interaction of presentation.interactions) {
18
+ if (interactions.has(interaction.id)) {
19
+ fail(
20
+ "duplicate_interaction_id",
21
+ `Duplicate Interaction id: ${interaction.id}`,
22
+ );
23
+ }
24
+ interactions.set(interaction.id, {
25
+ interaction,
26
+ presentation,
27
+ screen,
28
+ });
29
+ }
30
+ }
31
+ }
32
+ return interactions;
33
+ }
34
+
35
+ function localScreenTarget(target, manifest, screens, path) {
36
+ if (target.screen.packageId !== manifest.packageId) return undefined;
37
+ const screen = screens.get(target.screen.assetId);
38
+ if (!screen) {
39
+ fail("missing_screen", `Screen not found: ${target.screen.assetId}`, {
40
+ path,
41
+ reference: target.screen,
42
+ });
43
+ }
44
+ return screen;
45
+ }
46
+
47
+ function localComponentTarget(target, manifest, componentSets, path) {
48
+ if (target.component.packageId !== manifest.packageId) return undefined;
49
+ const componentSet = componentSets.get(target.component.assetId);
50
+ if (!componentSet) {
51
+ fail(
52
+ "missing_component",
53
+ `Component not found: ${target.component.assetId}`,
54
+ { path, reference: target.component },
55
+ );
56
+ }
57
+ const match = findComponentVariant(componentSet, target.variant);
58
+ if (!match.variant) {
59
+ fail(
60
+ "missing_variant",
61
+ `No exact variant exists for ${target.component.assetId}`,
62
+ { path, selection: target.variant },
63
+ );
64
+ }
65
+ return match.variant;
66
+ }
67
+
68
+ function validateScenario(
69
+ scenario,
70
+ manifest,
71
+ screens,
72
+ componentSets,
73
+ contextAxes,
74
+ ) {
75
+ const path = `scenarios.${scenario.id}`;
76
+ let nodes;
77
+ if (scenario.target.kind === "screen") {
78
+ const screen = localScreenTarget(
79
+ scenario.target,
80
+ manifest,
81
+ screens,
82
+ `${path}.target.screen`,
83
+ );
84
+ if (screen) {
85
+ const presentation = screen.presentations.find(
86
+ ({ id }) => id === scenario.target.presentationId,
87
+ );
88
+ if (!presentation) {
89
+ fail(
90
+ "missing_presentation",
91
+ `Presentation not found: ${scenario.target.presentationId}`,
92
+ { path: `${path}.target.presentationId` },
93
+ );
94
+ }
95
+ nodes = presentation.nodes;
96
+ }
97
+ } else {
98
+ const variant = localComponentTarget(
99
+ scenario.target,
100
+ manifest,
101
+ componentSets,
102
+ `${path}.target.component`,
103
+ );
104
+ nodes = variant?.nodes;
105
+ }
106
+ for (const [axisId, valueId] of Object.entries(scenario.context)) {
107
+ const axis = contextAxes.get(axisId);
108
+ if (!axis) {
109
+ if (manifest.role === "foundation") {
110
+ fail(
111
+ "invalid_scenario_context",
112
+ `Scenario references unknown Context Axis ${axisId}`,
113
+ { axisId, path: `${path}.context.${axisId}`, valueId },
114
+ );
115
+ }
116
+ continue;
117
+ }
118
+ if (!axis.values.some(({ id }) => id === valueId)) {
119
+ fail(
120
+ "invalid_scenario_context",
121
+ `Scenario references unknown Context value ${valueId}`,
122
+ { axisId, path: `${path}.context.${axisId}`, valueId },
123
+ );
124
+ }
125
+ }
126
+ if (!nodes) return;
127
+ for (const nodeId of scenario.expectedVisibleNodeIds) {
128
+ if (!nodes[nodeId]) {
129
+ fail(
130
+ "missing_scenario_node",
131
+ `Scenario visibility expectation references missing Node ${nodeId}`,
132
+ { nodeId, path: `${path}.expectedVisibleNodeIds` },
133
+ );
134
+ }
135
+ }
136
+ for (const [index, action] of scenario.actions.entries()) {
137
+ if (
138
+ (action.type === "set-text" || action.type === "set-visibility") &&
139
+ !nodes[action.nodeId]
140
+ ) {
141
+ fail(
142
+ "missing_scenario_node",
143
+ `Scenario action references missing Node ${action.nodeId}`,
144
+ { nodeId: action.nodeId, path: `${path}.actions[${index}].nodeId` },
145
+ );
146
+ }
147
+ if (action.type === "set-text" && nodes[action.nodeId].type !== "TEXT") {
148
+ fail(
149
+ "scenario_node_type_mismatch",
150
+ `set-text target is not a TEXT Node: ${action.nodeId}`,
151
+ { nodeId: action.nodeId, path: `${path}.actions[${index}].nodeId` },
152
+ );
153
+ }
154
+ }
155
+ }
156
+
157
+ function validateDesignTarget(
158
+ target,
159
+ manifest,
160
+ screens,
161
+ interactions,
162
+ flows,
163
+ scenarios,
164
+ path,
165
+ ) {
166
+ if (target.kind === "interaction") {
167
+ if (!interactions.has(target.interactionId)) {
168
+ fail(
169
+ "missing_interaction",
170
+ `Interaction not found: ${target.interactionId}`,
171
+ { path },
172
+ );
173
+ }
174
+ return;
175
+ }
176
+ if (target.kind === "flow") {
177
+ if (!flows.has(target.flowId)) {
178
+ fail("missing_flow", `Flow not found: ${target.flowId}`, { path });
179
+ }
180
+ return;
181
+ }
182
+ if (target.kind === "scenario") {
183
+ if (!scenarios.has(target.scenarioId)) {
184
+ fail("missing_scenario", `Scenario not found: ${target.scenarioId}`, {
185
+ path,
186
+ });
187
+ }
188
+ return;
189
+ }
190
+ const screen = localScreenTarget(target, manifest, screens, path);
191
+ if (!screen || target.kind === "screen") return;
192
+ const presentation = screen.presentations.find(
193
+ ({ id }) => id === target.presentationId,
194
+ );
195
+ if (!presentation) {
196
+ fail(
197
+ "missing_presentation",
198
+ `Presentation not found: ${target.presentationId}`,
199
+ { path },
200
+ );
201
+ }
202
+ if (!presentation.nodes[target.nodeId]) {
203
+ fail("missing_node", `Node not found: ${target.nodeId}`, { path });
204
+ }
205
+ }
206
+
207
+ function validateLocalAssetReferences(manifest, entries, domain) {
208
+ const ids = new Set([
209
+ ...domain.tokens.keys(),
210
+ ...domain.componentSets.keys(),
211
+ ...domain.locatedComponents.keys(),
212
+ ...manifest.entries.screens.map((entry) => entries[entry].id),
213
+ ]);
214
+ const visit = (value, path) => {
215
+ if (Array.isArray(value)) {
216
+ value.forEach((child, index) => visit(child, `${path}[${index}]`));
217
+ return;
218
+ }
219
+ if (!value || typeof value !== "object") return;
220
+ if (
221
+ typeof value.packageId === "string" &&
222
+ typeof value.assetId === "string"
223
+ ) {
224
+ if (value.packageId === manifest.packageId && !ids.has(value.assetId)) {
225
+ fail("missing_local_asset", `Local asset is missing: ${value.assetId}`, {
226
+ path,
227
+ reference: structuredClone(value),
228
+ });
229
+ }
230
+ return;
231
+ }
232
+ for (const [field, child] of Object.entries(value)) {
233
+ visit(child, `${path}.${field}`);
234
+ }
235
+ };
236
+ for (const entry of Object.keys(entries).sort()) visit(entries[entry], entry);
237
+ }
238
+
239
+ export function validateDesignReferences(manifest, entries, domain) {
240
+ const screens = screenIndex(manifest, entries);
241
+ const interactions = interactionIndex(screens);
242
+ for (const screen of screens.values()) {
243
+ for (const presentation of screen.presentations) {
244
+ for (const interaction of presentation.interactions) {
245
+ const action = interaction.action;
246
+ if (action.type !== "navigate") continue;
247
+ const target = localScreenTarget(
248
+ { screen: action.screen },
249
+ manifest,
250
+ screens,
251
+ `interactions.${interaction.id}.action.screen`,
252
+ );
253
+ if (
254
+ target &&
255
+ action.presentationId !== undefined &&
256
+ !target.presentations.some(({ id }) => id === action.presentationId)
257
+ ) {
258
+ fail(
259
+ "missing_presentation",
260
+ `Presentation not found: ${action.presentationId}`,
261
+ { path: `interactions.${interaction.id}.action.presentationId` },
262
+ );
263
+ }
264
+ }
265
+ for (const node of Object.values(presentation.nodes)) {
266
+ if (!node.instance) continue;
267
+ localComponentTarget(
268
+ {
269
+ component: node.instance.component,
270
+ variant: node.instance.variant,
271
+ },
272
+ manifest,
273
+ domain.componentSets,
274
+ `nodes.${node.id}.instance.component`,
275
+ );
276
+ }
277
+ }
278
+ }
279
+ for (const scenario of domain.scenarios.values()) {
280
+ validateScenario(
281
+ scenario,
282
+ manifest,
283
+ screens,
284
+ domain.componentSets,
285
+ domain.contextAxes,
286
+ );
287
+ }
288
+ for (const flow of domain.flows.values()) {
289
+ for (const interactionId of flow.interactionIds) {
290
+ if (!interactions.has(interactionId)) {
291
+ fail(
292
+ "missing_interaction",
293
+ `Flow ${flow.id} references missing Interaction ${interactionId}`,
294
+ { flowId: flow.id, interactionId },
295
+ );
296
+ }
297
+ }
298
+ }
299
+ for (const requirement of domain.requirements.values()) {
300
+ requirement.links.forEach((target, index) =>
301
+ validateDesignTarget(
302
+ target,
303
+ manifest,
304
+ screens,
305
+ interactions,
306
+ domain.flows,
307
+ domain.scenarios,
308
+ `requirements.${requirement.id}.links[${index}]`,
309
+ ),
310
+ );
311
+ }
312
+ for (const annotation of domain.annotations.values()) {
313
+ validateDesignTarget(
314
+ annotation.target,
315
+ manifest,
316
+ screens,
317
+ interactions,
318
+ domain.flows,
319
+ domain.scenarios,
320
+ `annotations.${annotation.id}.target`,
321
+ );
322
+ }
323
+ validateLocalAssetReferences(manifest, entries, domain);
324
+ }