@topogram/cli 0.3.51 → 0.3.52
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/ARCHITECTURE.md +4 -4
- package/CHANGELOG.md +11 -11
- package/package.json +1 -1
- package/src/adoption/plan.js +2 -2
- package/src/agent-ops/query-builders.js +42 -33
- package/src/cli.js +174 -129
- package/src/generator/adapters.d.ts +1 -0
- package/src/generator/adapters.js +64 -39
- package/src/generator/check.js +19 -12
- package/src/generator/context/diff.js +9 -9
- package/src/generator/context/domain-coverage.js +11 -10
- package/src/generator/context/domain-page.js +6 -6
- package/src/generator/context/shared.js +37 -21
- package/src/generator/context/slice.js +70 -65
- package/src/generator/index.js +12 -12
- package/src/generator/output.js +21 -20
- package/src/generator/registry.js +61 -49
- package/src/generator/runtime/app-bundle.js +15 -15
- package/src/generator/runtime/compile-check.js +7 -7
- package/src/generator/runtime/deployment.js +9 -9
- package/src/generator/runtime/environment.js +39 -39
- package/src/generator/runtime/runtime-check.js +5 -5
- package/src/generator/runtime/shared.js +40 -38
- package/src/generator/runtime/smoke.js +5 -5
- package/src/generator/surfaces/databases/contract.js +1 -1
- package/src/generator/surfaces/databases/lifecycle-shared.js +6 -5
- package/src/generator/surfaces/databases/postgres/drizzle.js +3 -2
- package/src/generator/surfaces/databases/postgres/prisma.js +3 -2
- package/src/generator/surfaces/databases/shared.js +3 -2
- package/src/generator/surfaces/databases/snapshot.js +1 -1
- package/src/generator/surfaces/databases/sqlite/prisma.js +3 -2
- package/src/generator/surfaces/native/swiftui-app.js +3 -3
- package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +1 -1
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +3 -3
- package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +3 -3
- package/src/generator/surfaces/services/persistence-wiring.js +3 -2
- package/src/generator/surfaces/services/server-contract.js +4 -4
- package/src/generator/surfaces/shared.js +2 -2
- package/src/generator/surfaces/web/design-intent.js +1 -1
- package/src/generator/surfaces/web/index.js +7 -7
- package/src/generator/surfaces/web/{react-components.js → react-widgets.js} +53 -53
- package/src/generator/surfaces/web/react.js +36 -36
- package/src/generator/surfaces/web/{sveltekit-components.js → sveltekit-widgets.js} +53 -53
- package/src/generator/surfaces/web/sveltekit.js +34 -34
- package/src/generator/surfaces/web/{ui-web-contract.js → ui-surface-contract.js} +8 -8
- package/src/generator/surfaces/web/vanilla.js +6 -6
- package/src/generator/{component-conformance.js → widget-conformance.js} +129 -128
- package/src/generator/widgets.js +40 -0
- package/src/generator-policy.js +10 -12
- package/src/import/core/runner.js +34 -34
- package/src/import/core/shared.js +1 -1
- package/src/import/extractors/ui/android-compose.js +1 -1
- package/src/import/extractors/ui/blazor.js +1 -1
- package/src/import/extractors/ui/razor-pages.js +1 -1
- package/src/import/extractors/ui/react-router.js +4 -4
- package/src/import/extractors/ui/sveltekit.js +4 -4
- package/src/import/extractors/ui/swiftui.js +1 -1
- package/src/import/extractors/ui/uikit.js +1 -1
- package/src/new-project.js +19 -18
- package/src/project-config.js +92 -42
- package/src/proofs/contract-audit.js +1 -1
- package/src/proofs/ios-parity.js +1 -1
- package/src/proofs/issues-parity.js +1 -1
- package/src/realization/backend/build-backend-runtime-realization.js +2 -2
- package/src/realization/ui/build-ui-shared-realization.js +33 -33
- package/src/realization/ui/build-web-realization.js +23 -20
- package/src/reconcile/journeys.js +1 -1
- package/src/resolver/index.js +148 -65
- package/src/validator/index.js +473 -423
- package/src/validator/kinds.js +36 -36
- package/src/validator/per-kind/{component.js → widget.js} +47 -47
- package/src/{component-behavior.js → widget-behavior.js} +3 -3
- package/src/workflows.js +39 -38
- package/template-helpers/react.js +4 -4
- package/template-helpers/sveltekit.js +4 -4
- package/src/generator/components.js +0 -39
- /package/src/resolver/enrich/{component.js → widget.js} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getProjection, sharedUiProjectionForWeb, uiProjectionCandidates } from "./surfaces/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { buildWidgetBehaviorRealizations } from "../widget-behavior.js";
|
|
3
3
|
|
|
4
4
|
function byId(entries = []) {
|
|
5
5
|
return new Map(entries.map((entry) => [entry.id, entry]));
|
|
@@ -13,8 +13,8 @@ function sourcePath(entry) {
|
|
|
13
13
|
return entry?.loc?.file || null;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
function
|
|
17
|
-
return
|
|
16
|
+
function widgetContract(widget) {
|
|
17
|
+
return widget?.widgetContract || null;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function summarizeProjection(projection) {
|
|
@@ -22,28 +22,28 @@ function summarizeProjection(projection) {
|
|
|
22
22
|
? {
|
|
23
23
|
id: projection.id,
|
|
24
24
|
name: projection.name || projection.id,
|
|
25
|
-
|
|
25
|
+
type: projection.type || projection.platform || null,
|
|
26
26
|
status: projection.status || null,
|
|
27
27
|
source_path: sourcePath(projection)
|
|
28
28
|
}
|
|
29
29
|
: null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function
|
|
33
|
-
return
|
|
32
|
+
function summarizeWidget(widget) {
|
|
33
|
+
return widget
|
|
34
34
|
? {
|
|
35
|
-
id:
|
|
36
|
-
name:
|
|
37
|
-
category:
|
|
38
|
-
version:
|
|
39
|
-
status:
|
|
40
|
-
source_path: sourcePath(
|
|
35
|
+
id: widget.id,
|
|
36
|
+
name: widget.name || widget.id,
|
|
37
|
+
category: widget.category || null,
|
|
38
|
+
version: widget.version || null,
|
|
39
|
+
status: widget.status || null,
|
|
40
|
+
source_path: sourcePath(widget)
|
|
41
41
|
}
|
|
42
42
|
: null;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
function
|
|
46
|
-
const contract =
|
|
45
|
+
function summarizeWidgetContract(widget) {
|
|
46
|
+
const contract = widgetContract(widget);
|
|
47
47
|
if (!contract) return null;
|
|
48
48
|
return {
|
|
49
49
|
id: contract.id,
|
|
@@ -57,7 +57,7 @@ function summarizeComponentContract(component) {
|
|
|
57
57
|
behavior: contract.behavior || [],
|
|
58
58
|
approvals: contract.approvals || [],
|
|
59
59
|
dependencies: contract.dependencies || [],
|
|
60
|
-
source_path: sourcePath(
|
|
60
|
+
source_path: sourcePath(widget)
|
|
61
61
|
};
|
|
62
62
|
}
|
|
63
63
|
|
|
@@ -136,7 +136,7 @@ function checkRecord({
|
|
|
136
136
|
message,
|
|
137
137
|
projection,
|
|
138
138
|
sourceProjection,
|
|
139
|
-
|
|
139
|
+
widget,
|
|
140
140
|
usage,
|
|
141
141
|
prop = null,
|
|
142
142
|
event = null,
|
|
@@ -149,7 +149,7 @@ function checkRecord({
|
|
|
149
149
|
message,
|
|
150
150
|
projection: projection?.id || null,
|
|
151
151
|
source_projection: sourceProjection?.id || null,
|
|
152
|
-
|
|
152
|
+
widget: widget?.id || usage.widget?.id || null,
|
|
153
153
|
screen: usage.screenId || null,
|
|
154
154
|
region: usage.region || null,
|
|
155
155
|
prop,
|
|
@@ -159,20 +159,20 @@ function checkRecord({
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
function
|
|
162
|
+
function widgetUsageKey(projection, sourceProjection, usage, index) {
|
|
163
163
|
return [
|
|
164
164
|
projection.id,
|
|
165
165
|
sourceProjection?.id || projection.id,
|
|
166
166
|
usage.screenId || "screen",
|
|
167
167
|
usage.region || "region",
|
|
168
|
-
usage.
|
|
168
|
+
usage.widget?.id || "widget",
|
|
169
169
|
String(index)
|
|
170
170
|
].join(":");
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
function collectUsageChecks({ graph, projection, sourceProjection, usage,
|
|
173
|
+
function collectUsageChecks({ graph, projection, sourceProjection, usage, widget }) {
|
|
174
174
|
const checks = [];
|
|
175
|
-
const contract =
|
|
175
|
+
const contract = widgetContract(widget);
|
|
176
176
|
const props = contract?.props || [];
|
|
177
177
|
const events = contract?.events || [];
|
|
178
178
|
const propNames = new Set(props.map((prop) => prop.name));
|
|
@@ -183,71 +183,71 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
183
183
|
const regionKeys = projectionRegionKeys(graph, sourceProjection);
|
|
184
184
|
const realizedIds = projectionContextRealizesIds(graph, sourceProjection);
|
|
185
185
|
|
|
186
|
-
if (!
|
|
186
|
+
if (!widget) {
|
|
187
187
|
checks.push(checkRecord({
|
|
188
|
-
code: "
|
|
188
|
+
code: "widget_missing",
|
|
189
189
|
severity: "error",
|
|
190
|
-
message: `
|
|
190
|
+
message: `Widget '${usage.widget?.id || "(missing)"}' could not be resolved.`,
|
|
191
191
|
projection,
|
|
192
192
|
sourceProjection,
|
|
193
|
-
|
|
193
|
+
widget,
|
|
194
194
|
usage,
|
|
195
|
-
suggestedFix: "Create the
|
|
195
|
+
suggestedFix: "Create the widget or update the projection widget_bindings binding."
|
|
196
196
|
}));
|
|
197
197
|
return checks;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
if (projection.status === "active" &&
|
|
200
|
+
if (projection.status === "active" && widget.status && widget.status !== "active") {
|
|
201
201
|
checks.push(checkRecord({
|
|
202
|
-
code: "
|
|
202
|
+
code: "widget_status_not_active",
|
|
203
203
|
severity: "warning",
|
|
204
|
-
message: `Active projection '${projection.id}' uses
|
|
204
|
+
message: `Active projection '${projection.id}' uses widget '${widget.id}' with status '${widget.status}'.`,
|
|
205
205
|
projection,
|
|
206
206
|
sourceProjection,
|
|
207
|
-
|
|
207
|
+
widget,
|
|
208
208
|
usage,
|
|
209
|
-
suggestedFix: "Promote the
|
|
209
|
+
suggestedFix: "Promote the widget to active or move the usage behind an explicit review boundary."
|
|
210
210
|
}));
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
if (!screens.has(usage.screenId)) {
|
|
214
214
|
checks.push(checkRecord({
|
|
215
|
-
code: "
|
|
215
|
+
code: "widget_usage_screen_missing",
|
|
216
216
|
severity: "error",
|
|
217
|
-
message: `
|
|
217
|
+
message: `Widget usage references missing screen '${usage.screenId}'.`,
|
|
218
218
|
projection,
|
|
219
219
|
sourceProjection,
|
|
220
|
-
|
|
220
|
+
widget,
|
|
221
221
|
usage,
|
|
222
|
-
suggestedFix: "Add the screen to
|
|
222
|
+
suggestedFix: "Add the screen to screens or update the widget usage screen id."
|
|
223
223
|
}));
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
if (!regionKeys.has(`${usage.screenId}:${usage.region}`)) {
|
|
227
227
|
checks.push(checkRecord({
|
|
228
|
-
code: "
|
|
228
|
+
code: "widget_usage_region_missing",
|
|
229
229
|
severity: "error",
|
|
230
|
-
message: `
|
|
230
|
+
message: `Widget usage references undeclared region '${usage.region}' on screen '${usage.screenId}'.`,
|
|
231
231
|
projection,
|
|
232
232
|
sourceProjection,
|
|
233
|
-
|
|
233
|
+
widget,
|
|
234
234
|
usage,
|
|
235
|
-
suggestedFix: "Add the region to
|
|
235
|
+
suggestedFix: "Add the region to screen_regions or update the widget usage region."
|
|
236
236
|
}));
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
for (const prop of props.filter((entry) => entry.requiredness === "required")) {
|
|
240
240
|
if (!boundProps.has(prop.name)) {
|
|
241
241
|
checks.push(checkRecord({
|
|
242
|
-
code: "
|
|
242
|
+
code: "widget_required_prop_missing",
|
|
243
243
|
severity: "error",
|
|
244
|
-
message: `Required prop '${prop.name}' is not bound for
|
|
244
|
+
message: `Required prop '${prop.name}' is not bound for widget '${widget.id}'.`,
|
|
245
245
|
projection,
|
|
246
246
|
sourceProjection,
|
|
247
|
-
|
|
247
|
+
widget,
|
|
248
248
|
usage,
|
|
249
249
|
prop: prop.name,
|
|
250
|
-
suggestedFix: `Add 'data ${prop.name} from <source>' to the projection
|
|
250
|
+
suggestedFix: `Add 'data ${prop.name} from <source>' to the projection widget_bindings entry.`
|
|
251
251
|
}));
|
|
252
252
|
}
|
|
253
253
|
}
|
|
@@ -255,25 +255,25 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
255
255
|
for (const binding of usage.dataBindings || []) {
|
|
256
256
|
if (!propNames.has(binding.prop)) {
|
|
257
257
|
checks.push(checkRecord({
|
|
258
|
-
code: "
|
|
258
|
+
code: "widget_prop_unknown",
|
|
259
259
|
severity: "error",
|
|
260
|
-
message: `Prop '${binding.prop}' is not declared by
|
|
260
|
+
message: `Prop '${binding.prop}' is not declared by widget '${widget.id}'.`,
|
|
261
261
|
projection,
|
|
262
262
|
sourceProjection,
|
|
263
|
-
|
|
263
|
+
widget,
|
|
264
264
|
usage,
|
|
265
265
|
prop: binding.prop || null,
|
|
266
|
-
suggestedFix: "Declare the prop on the
|
|
266
|
+
suggestedFix: "Declare the prop on the widget or update the projection binding."
|
|
267
267
|
}));
|
|
268
268
|
}
|
|
269
269
|
if (!binding.source?.id || !statements.has(binding.source.id)) {
|
|
270
270
|
checks.push(checkRecord({
|
|
271
|
-
code: "
|
|
271
|
+
code: "widget_data_source_missing",
|
|
272
272
|
severity: "error",
|
|
273
273
|
message: `Data binding for prop '${binding.prop}' references a missing source.`,
|
|
274
274
|
projection,
|
|
275
275
|
sourceProjection,
|
|
276
|
-
|
|
276
|
+
widget,
|
|
277
277
|
usage,
|
|
278
278
|
prop: binding.prop || null,
|
|
279
279
|
suggestedFix: "Bind the prop to an existing capability, projection, shape, or entity."
|
|
@@ -284,26 +284,26 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
284
284
|
for (const binding of usage.eventBindings || []) {
|
|
285
285
|
if (!eventNames.has(binding.event)) {
|
|
286
286
|
checks.push(checkRecord({
|
|
287
|
-
code: "
|
|
287
|
+
code: "widget_event_unknown",
|
|
288
288
|
severity: "error",
|
|
289
|
-
message: `Event '${binding.event}' is not declared by
|
|
289
|
+
message: `Event '${binding.event}' is not declared by widget '${widget.id}'.`,
|
|
290
290
|
projection,
|
|
291
291
|
sourceProjection,
|
|
292
|
-
|
|
292
|
+
widget,
|
|
293
293
|
usage,
|
|
294
294
|
event: binding.event || null,
|
|
295
|
-
suggestedFix: "Declare the event on the
|
|
295
|
+
suggestedFix: "Declare the event on the widget or update the projection binding."
|
|
296
296
|
}));
|
|
297
297
|
}
|
|
298
298
|
if (binding.action === "navigate") {
|
|
299
299
|
if (!screens.has(binding.target?.id)) {
|
|
300
300
|
checks.push(checkRecord({
|
|
301
|
-
code: "
|
|
301
|
+
code: "widget_event_navigation_target_missing",
|
|
302
302
|
severity: "error",
|
|
303
303
|
message: `Event '${binding.event}' navigates to missing screen '${binding.target?.id || "(missing)"}'.`,
|
|
304
304
|
projection,
|
|
305
305
|
sourceProjection,
|
|
306
|
-
|
|
306
|
+
widget,
|
|
307
307
|
usage,
|
|
308
308
|
event: binding.event || null,
|
|
309
309
|
suggestedFix: "Add the target screen or update the event navigation target."
|
|
@@ -313,24 +313,24 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
313
313
|
const target = binding.target?.id ? statements.get(binding.target.id) : null;
|
|
314
314
|
if (!target || target.kind !== "capability") {
|
|
315
315
|
checks.push(checkRecord({
|
|
316
|
-
code: "
|
|
316
|
+
code: "widget_event_action_missing",
|
|
317
317
|
severity: "error",
|
|
318
318
|
message: `Event '${binding.event}' targets missing capability '${binding.target?.id || "(missing)"}'.`,
|
|
319
319
|
projection,
|
|
320
320
|
sourceProjection,
|
|
321
|
-
|
|
321
|
+
widget,
|
|
322
322
|
usage,
|
|
323
323
|
event: binding.event || null,
|
|
324
324
|
suggestedFix: "Bind the event to an existing capability."
|
|
325
325
|
}));
|
|
326
326
|
} else if (!realizedIds.has(target.id)) {
|
|
327
327
|
checks.push(checkRecord({
|
|
328
|
-
code: "
|
|
328
|
+
code: "widget_event_action_not_in_projection",
|
|
329
329
|
severity: "error",
|
|
330
330
|
message: `Event '${binding.event}' targets capability '${target.id}', but projection '${sourceProjection.id}' does not realize it through its UI context.`,
|
|
331
331
|
projection,
|
|
332
332
|
sourceProjection,
|
|
333
|
-
|
|
333
|
+
widget,
|
|
334
334
|
usage,
|
|
335
335
|
event: binding.event || null,
|
|
336
336
|
suggestedFix: `Add '${target.id}' to projection '${sourceProjection.id}' or an inherited shared projection realizes list, or choose a capability already in this projection context.`
|
|
@@ -338,12 +338,12 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
338
338
|
}
|
|
339
339
|
} else {
|
|
340
340
|
checks.push(checkRecord({
|
|
341
|
-
code: "
|
|
341
|
+
code: "widget_event_action_unsupported",
|
|
342
342
|
severity: "error",
|
|
343
343
|
message: `Event '${binding.event}' uses unsupported action '${binding.action}'.`,
|
|
344
344
|
projection,
|
|
345
345
|
sourceProjection,
|
|
346
|
-
|
|
346
|
+
widget,
|
|
347
347
|
usage,
|
|
348
348
|
event: binding.event || null,
|
|
349
349
|
suggestedFix: "Use 'navigate' or 'action'."
|
|
@@ -355,12 +355,12 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
355
355
|
const stateProp = behavior.directives?.state;
|
|
356
356
|
if (stateProp && !propNames.has(stateProp)) {
|
|
357
357
|
checks.push(checkRecord({
|
|
358
|
-
code: "
|
|
358
|
+
code: "widget_behavior_prop_missing",
|
|
359
359
|
severity: "error",
|
|
360
360
|
message: `Behavior '${behavior.kind}' references missing prop '${stateProp}'.`,
|
|
361
361
|
projection,
|
|
362
362
|
sourceProjection,
|
|
363
|
-
|
|
363
|
+
widget,
|
|
364
364
|
usage,
|
|
365
365
|
prop: stateProp,
|
|
366
366
|
behavior: behavior.kind,
|
|
@@ -373,12 +373,12 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
373
373
|
for (const eventName of emits) {
|
|
374
374
|
if (!eventNames.has(eventName)) {
|
|
375
375
|
checks.push(checkRecord({
|
|
376
|
-
code: "
|
|
376
|
+
code: "widget_behavior_event_missing",
|
|
377
377
|
severity: "error",
|
|
378
378
|
message: `Behavior '${behavior.kind}' references missing event '${eventName}'.`,
|
|
379
379
|
projection,
|
|
380
380
|
sourceProjection,
|
|
381
|
-
|
|
381
|
+
widget,
|
|
382
382
|
usage,
|
|
383
383
|
event: eventName,
|
|
384
384
|
behavior: behavior.kind,
|
|
@@ -388,16 +388,16 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
388
388
|
}
|
|
389
389
|
if (!(usage.eventBindings || []).some((binding) => binding.event === eventName)) {
|
|
390
390
|
checks.push(checkRecord({
|
|
391
|
-
code: "
|
|
391
|
+
code: "widget_behavior_event_unbound",
|
|
392
392
|
severity: "warning",
|
|
393
393
|
message: `Behavior '${behavior.kind}' emits event '${eventName}', but this projection usage does not bind that event to navigation or an action.`,
|
|
394
394
|
projection,
|
|
395
395
|
sourceProjection,
|
|
396
|
-
|
|
396
|
+
widget,
|
|
397
397
|
usage,
|
|
398
398
|
event: eventName,
|
|
399
399
|
behavior: behavior.kind,
|
|
400
|
-
suggestedFix: `Add 'event ${eventName} navigate <screen>' or 'event ${eventName} action <capability>' to the projection
|
|
400
|
+
suggestedFix: `Add 'event ${eventName} navigate <screen>' or 'event ${eventName} action <capability>' to the projection widget_bindings entry.`
|
|
401
401
|
}));
|
|
402
402
|
}
|
|
403
403
|
}
|
|
@@ -409,16 +409,16 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
409
409
|
if (eventNames.has(actionTarget)) {
|
|
410
410
|
if (!(usage.eventBindings || []).some((binding) => binding.event === actionTarget)) {
|
|
411
411
|
checks.push(checkRecord({
|
|
412
|
-
code: "
|
|
412
|
+
code: "widget_behavior_action_unbound",
|
|
413
413
|
severity: "warning",
|
|
414
414
|
message: `Behavior '${behavior.kind}' declares action event '${actionTarget}', but this projection usage does not bind that event to navigation or an action.`,
|
|
415
415
|
projection,
|
|
416
416
|
sourceProjection,
|
|
417
|
-
|
|
417
|
+
widget,
|
|
418
418
|
usage,
|
|
419
419
|
event: actionTarget,
|
|
420
420
|
behavior: behavior.kind,
|
|
421
|
-
suggestedFix: `Add 'event ${actionTarget} action <capability>' or 'event ${actionTarget} navigate <screen>' to the projection
|
|
421
|
+
suggestedFix: `Add 'event ${actionTarget} action <capability>' or 'event ${actionTarget} navigate <screen>' to the projection widget_bindings entry.`
|
|
422
422
|
}));
|
|
423
423
|
}
|
|
424
424
|
continue;
|
|
@@ -427,12 +427,12 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
427
427
|
const target = statements.get(actionTarget);
|
|
428
428
|
if (!target || target.kind !== "capability") {
|
|
429
429
|
checks.push(checkRecord({
|
|
430
|
-
code: "
|
|
430
|
+
code: "widget_behavior_action_missing",
|
|
431
431
|
severity: "error",
|
|
432
432
|
message: `Behavior '${behavior.kind}' references missing capability action '${actionTarget}'.`,
|
|
433
433
|
projection,
|
|
434
434
|
sourceProjection,
|
|
435
|
-
|
|
435
|
+
widget,
|
|
436
436
|
usage,
|
|
437
437
|
behavior: behavior.kind,
|
|
438
438
|
suggestedFix: "Update the behavior directive or declare the referenced capability."
|
|
@@ -441,12 +441,12 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
441
441
|
}
|
|
442
442
|
if (!realizedIds.has(actionTarget)) {
|
|
443
443
|
checks.push(checkRecord({
|
|
444
|
-
code: "
|
|
444
|
+
code: "widget_behavior_action_not_in_projection",
|
|
445
445
|
severity: "error",
|
|
446
446
|
message: `Behavior '${behavior.kind}' references capability '${actionTarget}', but projection '${sourceProjection.id}' does not realize it.`,
|
|
447
447
|
projection,
|
|
448
448
|
sourceProjection,
|
|
449
|
-
|
|
449
|
+
widget,
|
|
450
450
|
usage,
|
|
451
451
|
behavior: behavior.kind,
|
|
452
452
|
suggestedFix: `Add '${actionTarget}' to projection '${sourceProjection.id}' realizes or choose a capability already in this projection context.`
|
|
@@ -458,15 +458,15 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
458
458
|
binding.target?.kind === "capability"
|
|
459
459
|
)) {
|
|
460
460
|
checks.push(checkRecord({
|
|
461
|
-
code: "
|
|
461
|
+
code: "widget_behavior_action_unbound",
|
|
462
462
|
severity: "warning",
|
|
463
|
-
message: `Behavior '${behavior.kind}' declares capability action '${actionTarget}', but this projection usage does not bind any
|
|
463
|
+
message: `Behavior '${behavior.kind}' declares capability action '${actionTarget}', but this projection usage does not bind any widget event to that capability.`,
|
|
464
464
|
projection,
|
|
465
465
|
sourceProjection,
|
|
466
|
-
|
|
466
|
+
widget,
|
|
467
467
|
usage,
|
|
468
468
|
behavior: behavior.kind,
|
|
469
|
-
suggestedFix: `Add 'event <
|
|
469
|
+
suggestedFix: `Add 'event <widget_event> action ${actionTarget}' to the projection widget_bindings entry.`
|
|
470
470
|
}));
|
|
471
471
|
}
|
|
472
472
|
}
|
|
@@ -507,35 +507,36 @@ function candidateProjections(graph, projectionId) {
|
|
|
507
507
|
return [...direct, ...inherited].sort((a, b) => a.id.localeCompare(b.id));
|
|
508
508
|
}
|
|
509
509
|
|
|
510
|
-
function relatedVerificationFiles(graph,
|
|
511
|
-
const ids = new Set([...
|
|
510
|
+
function relatedVerificationFiles(graph, widgetIds, projectionIds) {
|
|
511
|
+
const ids = new Set([...widgetIds, ...projectionIds]);
|
|
512
512
|
return stableUnique((graph.byKind.verification || [])
|
|
513
513
|
.filter((verification) => (verification.validates || []).some((ref) => ids.has(ref.id)))
|
|
514
514
|
.map((verification) => sourcePath(verification)));
|
|
515
515
|
}
|
|
516
516
|
|
|
517
|
-
export function
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
|
|
517
|
+
export function generateWidgetConformanceReport(graph, options = {}) {
|
|
518
|
+
const selectedWidgetId = options.widgetId || options.componentId || null;
|
|
519
|
+
const widgets = byId(graph.byKind.widget || []);
|
|
520
|
+
if (selectedWidgetId && !widgets.has(selectedWidgetId)) {
|
|
521
|
+
throw new Error(`No widget found with id '${selectedWidgetId}'`);
|
|
521
522
|
}
|
|
522
523
|
|
|
523
524
|
const projectionUsageRecords = [];
|
|
524
525
|
const checks = [];
|
|
525
|
-
const
|
|
526
|
+
const referencedWidgetIds = new Set();
|
|
526
527
|
const affectedProjectionIds = new Set();
|
|
527
528
|
|
|
528
529
|
for (const projection of candidateProjections(graph, options.projectionId)) {
|
|
529
530
|
for (const entry of projectionUsageEntries(graph, projection)) {
|
|
530
|
-
const
|
|
531
|
-
if (
|
|
531
|
+
const widgetId = entry.usage.widget?.id || null;
|
|
532
|
+
if (selectedWidgetId && widgetId !== selectedWidgetId) {
|
|
532
533
|
continue;
|
|
533
534
|
}
|
|
534
|
-
const
|
|
535
|
-
if (
|
|
535
|
+
const widget = widgetId ? widgets.get(widgetId) : null;
|
|
536
|
+
if (widgetId) referencedWidgetIds.add(widgetId);
|
|
536
537
|
affectedProjectionIds.add(entry.projection.id);
|
|
537
538
|
if (entry.sourceProjection?.id) affectedProjectionIds.add(entry.sourceProjection.id);
|
|
538
|
-
const usageChecks = collectUsageChecks({ graph, projection: entry.projection, sourceProjection: entry.sourceProjection, usage: entry.usage,
|
|
539
|
+
const usageChecks = collectUsageChecks({ graph, projection: entry.projection, sourceProjection: entry.sourceProjection, usage: entry.usage, widget });
|
|
539
540
|
checks.push(...usageChecks);
|
|
540
541
|
const outcome = usageChecks.some((check) => check.severity === "error")
|
|
541
542
|
? "error"
|
|
@@ -543,7 +544,7 @@ export function generateComponentConformanceReport(graph, options = {}) {
|
|
|
543
544
|
? "warning"
|
|
544
545
|
: "pass";
|
|
545
546
|
projectionUsageRecords.push({
|
|
546
|
-
key:
|
|
547
|
+
key: widgetUsageKey(entry.projection, entry.sourceProjection, entry.usage, entry.index),
|
|
547
548
|
projection: summarizeProjection(entry.projection),
|
|
548
549
|
source_projection: entry.sourceProjection.id === entry.projection.id ? null : summarizeProjection(entry.sourceProjection),
|
|
549
550
|
screen: {
|
|
@@ -552,29 +553,29 @@ export function generateComponentConformanceReport(graph, options = {}) {
|
|
|
552
553
|
title: projectionScreenMap(graph, entry.sourceProjection).get(entry.usage.screenId)?.title || null
|
|
553
554
|
},
|
|
554
555
|
region: entry.usage.region || null,
|
|
555
|
-
|
|
556
|
+
widget: summarizeWidget(widget) || { id: widgetId, name: widgetId, category: null, version: null, status: null, source_path: null },
|
|
556
557
|
data_bindings: entry.usage.dataBindings || [],
|
|
557
558
|
event_bindings: entry.usage.eventBindings || [],
|
|
558
|
-
behavior_realizations:
|
|
559
|
+
behavior_realizations: buildWidgetBehaviorRealizations(widgetContract(widget), entry.usage),
|
|
559
560
|
outcome,
|
|
560
561
|
check_codes: usageChecks.map((check) => check.code)
|
|
561
562
|
});
|
|
562
563
|
}
|
|
563
564
|
}
|
|
564
565
|
|
|
565
|
-
const
|
|
566
|
+
const widgetFiles = stableUnique([...referencedWidgetIds].map((id) => sourcePath(widgets.get(id))));
|
|
566
567
|
const projectionFiles = stableUnique(
|
|
567
568
|
[...affectedProjectionIds].map((id) => sourcePath((graph.byKind.projection || []).find((projection) => projection.id === id)))
|
|
568
569
|
);
|
|
569
|
-
const verificationFiles = relatedVerificationFiles(graph,
|
|
570
|
+
const verificationFiles = relatedVerificationFiles(graph, referencedWidgetIds, affectedProjectionIds);
|
|
570
571
|
const errors = checks.filter((check) => check.severity === "error");
|
|
571
572
|
const warnings = checks.filter((check) => check.severity === "warning");
|
|
572
573
|
|
|
573
574
|
return {
|
|
574
|
-
type: "
|
|
575
|
+
type: "widget_conformance_report",
|
|
575
576
|
filters: {
|
|
576
577
|
projection: options.projectionId || null,
|
|
577
|
-
|
|
578
|
+
widget: selectedWidgetId
|
|
578
579
|
},
|
|
579
580
|
summary: {
|
|
580
581
|
total_usages: projectionUsageRecords.length,
|
|
@@ -584,22 +585,22 @@ export function generateComponentConformanceReport(graph, options = {}) {
|
|
|
584
585
|
warnings: warnings.length,
|
|
585
586
|
errors: errors.length,
|
|
586
587
|
affected_projections: stableUnique([...affectedProjectionIds]),
|
|
587
|
-
|
|
588
|
+
affected_widgets: stableUnique([...referencedWidgetIds])
|
|
588
589
|
},
|
|
589
590
|
projection_usages: projectionUsageRecords,
|
|
590
591
|
checks,
|
|
591
|
-
|
|
592
|
-
.map((id) =>
|
|
592
|
+
widget_contracts: stableUnique([...referencedWidgetIds])
|
|
593
|
+
.map((id) => summarizeWidgetContract(widgets.get(id)))
|
|
593
594
|
.filter(Boolean),
|
|
594
595
|
write_scope: {
|
|
595
|
-
|
|
596
|
+
widget_files: widgetFiles,
|
|
596
597
|
projection_files: projectionFiles,
|
|
597
598
|
verification_files: verificationFiles,
|
|
598
|
-
paths: stableUnique([...
|
|
599
|
+
paths: stableUnique([...widgetFiles, ...projectionFiles, ...verificationFiles])
|
|
599
600
|
},
|
|
600
601
|
impact: {
|
|
601
602
|
projections: stableUnique([...affectedProjectionIds]),
|
|
602
|
-
|
|
603
|
+
widgets: stableUnique([...referencedWidgetIds])
|
|
603
604
|
}
|
|
604
605
|
};
|
|
605
606
|
}
|
|
@@ -644,9 +645,9 @@ function effectTypesFromBehavior(behavior) {
|
|
|
644
645
|
function checksForBehavior(conformanceReport, usage, behavior) {
|
|
645
646
|
return (conformanceReport.checks || [])
|
|
646
647
|
.filter((check) =>
|
|
647
|
-
check.code?.startsWith("
|
|
648
|
+
check.code?.startsWith("widget_behavior_") &&
|
|
648
649
|
check.projection === usage.projection?.id &&
|
|
649
|
-
check.
|
|
650
|
+
check.widget === usage.widget?.id &&
|
|
650
651
|
check.screen === usage.screen?.id &&
|
|
651
652
|
check.region === usage.region &&
|
|
652
653
|
(!check.behavior || check.behavior === behavior.kind)
|
|
@@ -667,29 +668,29 @@ function behaviorHighlights(behaviorRows) {
|
|
|
667
668
|
if (row.behavior.status === "partial") {
|
|
668
669
|
highlights.push({
|
|
669
670
|
severity: "warning",
|
|
670
|
-
code: "
|
|
671
|
-
message: `Behavior '${row.behavior.kind}' is partially realized for
|
|
671
|
+
code: "widget_behavior_partial",
|
|
672
|
+
message: `Behavior '${row.behavior.kind}' is partially realized for widget '${row.widget.id}' on screen '${row.screen.id}'.`,
|
|
672
673
|
projection: row.projection.id,
|
|
673
|
-
|
|
674
|
+
widget: row.widget.id,
|
|
674
675
|
screen: row.screen.id,
|
|
675
676
|
region: row.region,
|
|
676
677
|
behavior: row.behavior.kind,
|
|
677
|
-
suggested_fix: "Bind the required behavior data, events, or capability actions in the projection
|
|
678
|
+
suggested_fix: "Bind the required behavior data, events, or capability actions in the projection widget_bindings entry."
|
|
678
679
|
});
|
|
679
680
|
}
|
|
680
681
|
for (const emittedEvent of row.emits || []) {
|
|
681
682
|
if (!emittedEvent.bound) {
|
|
682
683
|
highlights.push({
|
|
683
684
|
severity: "warning",
|
|
684
|
-
code: "
|
|
685
|
-
message: `Behavior '${row.behavior.kind}' emits event '${emittedEvent.event}', but this
|
|
685
|
+
code: "widget_behavior_event_unbound",
|
|
686
|
+
message: `Behavior '${row.behavior.kind}' emits event '${emittedEvent.event}', but this widget usage does not bind it.`,
|
|
686
687
|
projection: row.projection.id,
|
|
687
|
-
|
|
688
|
+
widget: row.widget.id,
|
|
688
689
|
screen: row.screen.id,
|
|
689
690
|
region: row.region,
|
|
690
691
|
event: emittedEvent.event || null,
|
|
691
692
|
behavior: row.behavior.kind,
|
|
692
|
-
suggested_fix: `Add 'event ${emittedEvent.event} navigate <screen>' or 'event ${emittedEvent.event} action <capability>' to the projection
|
|
693
|
+
suggested_fix: `Add 'event ${emittedEvent.event} navigate <screen>' or 'event ${emittedEvent.event} action <capability>' to the projection widget_bindings entry.`
|
|
693
694
|
});
|
|
694
695
|
}
|
|
695
696
|
}
|
|
@@ -698,18 +699,18 @@ function behaviorHighlights(behaviorRows) {
|
|
|
698
699
|
const target = action.capability?.id || action.event || "(unknown)";
|
|
699
700
|
highlights.push({
|
|
700
701
|
severity: "warning",
|
|
701
|
-
code: "
|
|
702
|
-
message: `Behavior '${row.behavior.kind}' declares action '${target}', but this
|
|
702
|
+
code: "widget_behavior_action_unbound",
|
|
703
|
+
message: `Behavior '${row.behavior.kind}' declares action '${target}', but this widget usage does not bind it.`,
|
|
703
704
|
projection: row.projection.id,
|
|
704
|
-
|
|
705
|
+
widget: row.widget.id,
|
|
705
706
|
screen: row.screen.id,
|
|
706
707
|
region: row.region,
|
|
707
708
|
event: action.event || null,
|
|
708
709
|
capability: action.capability?.id || null,
|
|
709
710
|
behavior: row.behavior.kind,
|
|
710
711
|
suggested_fix: action.capability?.id
|
|
711
|
-
? `Add 'event <
|
|
712
|
-
: `Add 'event ${action.event} action <capability>' or 'event ${action.event} navigate <screen>' to the projection
|
|
712
|
+
? `Add 'event <widget_event> action ${action.capability.id}' to the projection widget_bindings entry.`
|
|
713
|
+
: `Add 'event ${action.event} action <capability>' or 'event ${action.event} navigate <screen>' to the projection widget_bindings entry.`
|
|
713
714
|
});
|
|
714
715
|
}
|
|
715
716
|
}
|
|
@@ -739,8 +740,8 @@ function groupBehaviorRows(rows, keyFn, itemFn = null) {
|
|
|
739
740
|
}));
|
|
740
741
|
}
|
|
741
742
|
|
|
742
|
-
export function
|
|
743
|
-
const conformanceReport =
|
|
743
|
+
export function generateWidgetBehaviorReport(graph, options = {}) {
|
|
744
|
+
const conformanceReport = generateWidgetConformanceReport(graph, options);
|
|
744
745
|
const behaviorRows = [];
|
|
745
746
|
|
|
746
747
|
for (const usage of conformanceReport.projection_usages || []) {
|
|
@@ -752,7 +753,7 @@ export function generateComponentBehaviorReport(graph, options = {}) {
|
|
|
752
753
|
source_projection: usage.source_projection,
|
|
753
754
|
screen: usage.screen,
|
|
754
755
|
region: usage.region,
|
|
755
|
-
|
|
756
|
+
widget: usage.widget,
|
|
756
757
|
behavior: {
|
|
757
758
|
kind: behavior.kind || null,
|
|
758
759
|
source: behavior.source || null,
|
|
@@ -775,7 +776,7 @@ export function generateComponentBehaviorReport(graph, options = {}) {
|
|
|
775
776
|
const affectedCapabilities = stableUnique(behaviorRows.flatMap((row) => row.capabilities));
|
|
776
777
|
|
|
777
778
|
return {
|
|
778
|
-
type: "
|
|
779
|
+
type: "widget_behavior_report",
|
|
779
780
|
filters: conformanceReport.filters,
|
|
780
781
|
summary: {
|
|
781
782
|
total_usages: conformanceReport.summary.total_usages,
|
|
@@ -785,14 +786,14 @@ export function generateComponentBehaviorReport(graph, options = {}) {
|
|
|
785
786
|
declared: behaviorRows.filter((row) => row.behavior.status === "declared").length,
|
|
786
787
|
warnings: conformanceReport.summary.warnings,
|
|
787
788
|
errors: conformanceReport.summary.errors,
|
|
788
|
-
|
|
789
|
+
affected_widgets: conformanceReport.summary.affected_widgets,
|
|
789
790
|
affected_projections: conformanceReport.summary.affected_projections,
|
|
790
791
|
affected_capabilities: affectedCapabilities
|
|
791
792
|
},
|
|
792
793
|
groups: {
|
|
793
|
-
|
|
794
|
+
widgets: groupBehaviorRows(
|
|
794
795
|
behaviorRows,
|
|
795
|
-
(row) => [row.
|
|
796
|
+
(row) => [row.widget.id].filter(Boolean),
|
|
796
797
|
(row) => row.key
|
|
797
798
|
),
|
|
798
799
|
screens: groupBehaviorRows(
|
|
@@ -813,11 +814,11 @@ export function generateComponentBehaviorReport(graph, options = {}) {
|
|
|
813
814
|
},
|
|
814
815
|
behaviors: behaviorRows,
|
|
815
816
|
highlights,
|
|
816
|
-
checks: conformanceReport.checks.filter((check) => check.code?.startsWith("
|
|
817
|
+
checks: conformanceReport.checks.filter((check) => check.code?.startsWith("widget_behavior_")),
|
|
817
818
|
write_scope: conformanceReport.write_scope,
|
|
818
819
|
impact: {
|
|
819
820
|
projections: conformanceReport.impact.projections,
|
|
820
|
-
|
|
821
|
+
widgets: conformanceReport.impact.widgets,
|
|
821
822
|
capabilities: affectedCapabilities
|
|
822
823
|
}
|
|
823
824
|
};
|