@topogram/cli 0.3.51 → 0.3.53
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 +71 -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 +104 -44
- 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 +509 -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
package/src/validator/index.js
CHANGED
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
UI_DESIGN_ACCESSIBILITY_VALUES,
|
|
50
50
|
FIELD_SPECS
|
|
51
51
|
} from "./kinds.js";
|
|
52
|
-
import {
|
|
52
|
+
import { validateWidget } from "./per-kind/widget.js";
|
|
53
53
|
import { validateDomain, validateDomainTag } from "./per-kind/domain.js";
|
|
54
54
|
import { validatePitch } from "./per-kind/pitch.js";
|
|
55
55
|
import { validateRequirement } from "./per-kind/requirement.js";
|
|
@@ -108,6 +108,10 @@ export function pushError(errors, message, loc) {
|
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function renameDiagnostic(oldName, newName, example) {
|
|
112
|
+
return `${oldName} was renamed to ${newName}. Example fix: ${example}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
111
115
|
export function formatLoc(loc) {
|
|
112
116
|
const line = loc?.start?.line ?? 1;
|
|
113
117
|
const column = loc?.start?.column ?? 1;
|
|
@@ -225,7 +229,48 @@ function validateFieldPresence(errors, statement, fieldMap) {
|
|
|
225
229
|
return;
|
|
226
230
|
}
|
|
227
231
|
|
|
232
|
+
const renamedFields = new Map([
|
|
233
|
+
["platform", ["type", "type web_surface"]],
|
|
234
|
+
["ui_components", ["widget_bindings", "widget_bindings { screen item_list region results widget widget_data_grid }"]],
|
|
235
|
+
["ui_design", ["design_tokens", "design_tokens { density comfortable tone operational }"]],
|
|
236
|
+
["ui_routes", ["screen_routes", "screen_routes { screen item_list path /items }"]],
|
|
237
|
+
["ui_screens", ["screens", "screens { item_list list title \"Items\" }"]],
|
|
238
|
+
["ui_screen_regions", ["screen_regions", "screen_regions { screen item_list region results pattern resource_table }"]],
|
|
239
|
+
["ui_navigation", ["navigation", "navigation { primary item_list label \"Items\" }"]],
|
|
240
|
+
["ui_app_shell", ["app_shell", "app_shell { shell sidebar }"]],
|
|
241
|
+
["ui_collections", ["collection_views", "collection_views { item_list presentation table }"]],
|
|
242
|
+
["ui_actions", ["screen_actions", "screen_actions { screen item_list action create target cap_create_item }"]],
|
|
243
|
+
["ui_visibility", ["visibility_rules", "visibility_rules { screen item_list when cap_list_items }"]],
|
|
244
|
+
["ui_lookups", ["field_lookups", "field_lookups { field owner_id source cap_list_users }"]],
|
|
245
|
+
["web_surface", ["web_hints", "web_hints { router file_based }"]],
|
|
246
|
+
["ios_surface", ["ios_hints", "ios_hints { navigation stack }"]],
|
|
247
|
+
["http", ["endpoints", "endpoints { cap_list_items method GET path /items success 200 }"]],
|
|
248
|
+
["http_errors", ["error_responses", "error_responses { cap_list_items 404 shape_error }"]],
|
|
249
|
+
["http_fields", ["wire_fields", "wire_fields { shape_item title title }"]],
|
|
250
|
+
["http_responses", ["responses", "responses { cap_list_items 200 shape_item_list }"]],
|
|
251
|
+
["http_preconditions", ["preconditions", "preconditions { cap_update_item rule_item_exists }"]],
|
|
252
|
+
["http_idempotency", ["idempotency", "idempotency { cap_create_item key request_id }"]],
|
|
253
|
+
["http_cache", ["cache", "cache { cap_list_items max_age 60 }"]],
|
|
254
|
+
["http_delete", ["delete_semantics", "delete_semantics { cap_delete_item mode soft_delete }"]],
|
|
255
|
+
["http_async", ["async_jobs", "async_jobs { cap_export_items job task_export }"]],
|
|
256
|
+
["http_status", ["async_status", "async_status { cap_export_items path /exports/{job_id} }"]],
|
|
257
|
+
["http_download", ["downloads", "downloads { cap_download_export content_type text/csv }"]],
|
|
258
|
+
["http_authz", ["authorization", "authorization { cap_update_item role editor }"]],
|
|
259
|
+
["http_callbacks", ["callbacks", "callbacks { cap_export_items event completed }"]],
|
|
260
|
+
["db_tables", ["tables", "tables { entity_item table items }"]],
|
|
261
|
+
["db_columns", ["columns", "columns { entity_item field title column title }"]],
|
|
262
|
+
["db_keys", ["keys", "keys { entity_item primary [id] }"]],
|
|
263
|
+
["db_indexes", ["indexes", "indexes { entity_item index [title] }"]],
|
|
264
|
+
["db_relations", ["relations", "relations { entity_item foreign_key owner_id references entity_user.id }"]],
|
|
265
|
+
["db_lifecycle", ["lifecycle", "lifecycle { entity_item timestamps created_at created_at updated_at updated_at }"]]
|
|
266
|
+
]);
|
|
267
|
+
|
|
228
268
|
for (const key of fieldMap.keys()) {
|
|
269
|
+
if (renamedFields.has(key)) {
|
|
270
|
+
const [newName, example] = renamedFields.get(key);
|
|
271
|
+
pushError(errors, `Field '${key}' on ${statement.kind} ${statement.id} ${renameDiagnostic(`'${key}'`, `'${newName}'`, example)}`, fieldMap.get(key)[0].loc);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
229
274
|
if (!spec.allowed.includes(key)) {
|
|
230
275
|
pushError(errors, `Field '${key}' is not allowed on ${statement.kind} ${statement.id}`, fieldMap.get(key)[0].loc);
|
|
231
276
|
}
|
|
@@ -238,6 +283,36 @@ function validateFieldPresence(errors, statement, fieldMap) {
|
|
|
238
283
|
}
|
|
239
284
|
}
|
|
240
285
|
|
|
286
|
+
function validateProjectionTypeRenames(errors, statement, fieldMap) {
|
|
287
|
+
if (statement.kind !== "projection") {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const typeField = fieldMap.get("type")?.[0];
|
|
292
|
+
const typeValue = symbolValue(typeField?.value);
|
|
293
|
+
const renamedTypes = new Map([
|
|
294
|
+
["ui_shared", "ui_contract"],
|
|
295
|
+
["ui_web", "web_surface"],
|
|
296
|
+
["ui_ios", "ios_surface"],
|
|
297
|
+
["ui_android", "android_surface"],
|
|
298
|
+
["dotnet", "api_contract"],
|
|
299
|
+
["api", "api_contract"],
|
|
300
|
+
["backend", "api_contract"],
|
|
301
|
+
["db_postgres", "db_contract"],
|
|
302
|
+
["db_sqlite", "db_contract"]
|
|
303
|
+
]);
|
|
304
|
+
if (!typeField || !renamedTypes.has(typeValue)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const nextType = renamedTypes.get(typeValue);
|
|
309
|
+
pushError(
|
|
310
|
+
errors,
|
|
311
|
+
`Projection ${statement.id} ${renameDiagnostic(`type value '${typeValue}'`, `'${nextType}'`, `type ${nextType}`)}`,
|
|
312
|
+
typeField.value.loc
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
241
316
|
function validateBlockEntryLengths(errors, statement, fieldMap, key, minimumWidth) {
|
|
242
317
|
const field = fieldMap.get(key)?.[0];
|
|
243
318
|
if (!field || field.value.type !== "block") {
|
|
@@ -255,7 +330,7 @@ function validateFieldShapes(errors, statement, fieldMap) {
|
|
|
255
330
|
ensureSingleValueField(errors, statement, fieldMap, "name", ["string"]);
|
|
256
331
|
ensureSingleValueField(errors, statement, fieldMap, "description", ["string"]);
|
|
257
332
|
ensureSingleValueField(errors, statement, fieldMap, "status", ["symbol"]);
|
|
258
|
-
ensureSingleValueField(errors, statement, fieldMap, "
|
|
333
|
+
ensureSingleValueField(errors, statement, fieldMap, "type", ["symbol"]);
|
|
259
334
|
ensureSingleValueField(errors, statement, fieldMap, "method", ["symbol"]);
|
|
260
335
|
ensureSingleValueField(errors, statement, fieldMap, "severity", ["symbol"]);
|
|
261
336
|
ensureSingleValueField(errors, statement, fieldMap, "category", ["symbol"]);
|
|
@@ -299,7 +374,7 @@ function validateFieldShapes(errors, statement, fieldMap) {
|
|
|
299
374
|
ensureSingleValueField(errors, statement, fieldMap, key, ["list"]);
|
|
300
375
|
}
|
|
301
376
|
|
|
302
|
-
for (const key of ["fields", "props", "events", "slots", "behaviors", "keys", "relations", "invariants", "rename", "overrides", "
|
|
377
|
+
for (const key of ["fields", "props", "events", "slots", "behaviors", "keys", "relations", "invariants", "rename", "overrides", "endpoints", "error_responses", "wire_fields", "responses", "preconditions", "idempotency", "cache", "delete_semantics", "async_jobs", "async_status", "downloads", "authorization", "callbacks", "screens", "collection_views", "screen_actions", "visibility_rules", "field_lookups", "screen_routes", "web_hints", "ios_hints", "app_shell", "navigation", "screen_regions", "widget_bindings", "design_tokens", "tables", "columns", "keys", "indexes", "relations", "lifecycle", "generator_defaults"]) {
|
|
303
378
|
ensureSingleValueField(errors, statement, fieldMap, key, ["block"]);
|
|
304
379
|
}
|
|
305
380
|
|
|
@@ -307,40 +382,46 @@ function validateFieldShapes(errors, statement, fieldMap) {
|
|
|
307
382
|
validateBlockEntryLengths(errors, statement, fieldMap, "props", 3);
|
|
308
383
|
validateBlockEntryLengths(errors, statement, fieldMap, "events", 2);
|
|
309
384
|
validateBlockEntryLengths(errors, statement, fieldMap, "slots", 2);
|
|
310
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "keys", 2);
|
|
311
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "relations", 3);
|
|
312
385
|
validateBlockEntryLengths(errors, statement, fieldMap, "invariants", 2);
|
|
313
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http", 7);
|
|
314
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_errors", 3);
|
|
315
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_fields", 5);
|
|
316
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_responses", 3);
|
|
317
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_preconditions", 9);
|
|
318
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_idempotency", 7);
|
|
319
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_cache", 11);
|
|
320
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_delete", 7);
|
|
321
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_async", 11);
|
|
322
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_status", 11);
|
|
323
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_download", 7);
|
|
324
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_authz", 3);
|
|
325
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "http_callbacks", 11);
|
|
326
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ui_screens", 4);
|
|
327
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ui_collections", 4);
|
|
328
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ui_actions", 6);
|
|
329
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ui_visibility", 5);
|
|
330
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ui_lookups", 8);
|
|
331
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ui_routes", 4);
|
|
332
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ui_web", 4);
|
|
333
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ui_ios", 4);
|
|
334
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ui_app_shell", 2);
|
|
335
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ui_navigation", 2);
|
|
336
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ui_screen_regions", 4);
|
|
337
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "db_tables", 3);
|
|
338
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "db_columns", 5);
|
|
339
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "db_keys", 3);
|
|
340
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "db_indexes", 3);
|
|
341
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "db_relations", 6);
|
|
342
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "db_lifecycle", 3);
|
|
343
386
|
validateBlockEntryLengths(errors, statement, fieldMap, "generator_defaults", 2);
|
|
387
|
+
|
|
388
|
+
if (statement.kind === "entity") {
|
|
389
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "keys", 2);
|
|
390
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "relations", 3);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (statement.kind === "projection") {
|
|
394
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "endpoints", 7);
|
|
395
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "error_responses", 3);
|
|
396
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "wire_fields", 5);
|
|
397
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "responses", 3);
|
|
398
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "preconditions", 9);
|
|
399
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "idempotency", 7);
|
|
400
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "cache", 11);
|
|
401
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "delete_semantics", 7);
|
|
402
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "async_jobs", 11);
|
|
403
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "async_status", 11);
|
|
404
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "downloads", 7);
|
|
405
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "authorization", 3);
|
|
406
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "callbacks", 11);
|
|
407
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "screens", 4);
|
|
408
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "collection_views", 4);
|
|
409
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "screen_actions", 6);
|
|
410
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "visibility_rules", 5);
|
|
411
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "field_lookups", 8);
|
|
412
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "screen_routes", 4);
|
|
413
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "web_hints", 4);
|
|
414
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "ios_hints", 4);
|
|
415
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "app_shell", 2);
|
|
416
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "navigation", 2);
|
|
417
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "screen_regions", 4);
|
|
418
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "tables", 3);
|
|
419
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "columns", 5);
|
|
420
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "keys", 3);
|
|
421
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "indexes", 3);
|
|
422
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "relations", 6);
|
|
423
|
+
validateBlockEntryLengths(errors, statement, fieldMap, "lifecycle", 3);
|
|
424
|
+
}
|
|
344
425
|
}
|
|
345
426
|
|
|
346
427
|
function validateStatus(errors, statement, fieldMap) {
|
|
@@ -456,7 +537,7 @@ function validateReferenceKinds(errors, statement, fieldMap, registry) {
|
|
|
456
537
|
pitch: ["pitch"],
|
|
457
538
|
requirement: null,
|
|
458
539
|
from_requirement: ["requirement"],
|
|
459
|
-
affects: ["capability", "entity", "rule", "projection", "
|
|
540
|
+
affects: ["capability", "entity", "rule", "projection", "widget", "orchestration", "operation"],
|
|
460
541
|
introduces_rules: ["rule"],
|
|
461
542
|
respects_rules: ["rule"],
|
|
462
543
|
decisions: ["decision"],
|
|
@@ -798,7 +879,7 @@ function validateProjectionHttp(errors, statement, fieldMap, registry) {
|
|
|
798
879
|
return;
|
|
799
880
|
}
|
|
800
881
|
|
|
801
|
-
const httpField = fieldMap.get("
|
|
882
|
+
const httpField = fieldMap.get("endpoints")?.[0];
|
|
802
883
|
if (!httpField || httpField.value.type !== "block") {
|
|
803
884
|
return;
|
|
804
885
|
}
|
|
@@ -879,7 +960,7 @@ function validateProjectionHttpErrors(errors, statement, fieldMap, registry) {
|
|
|
879
960
|
return;
|
|
880
961
|
}
|
|
881
962
|
|
|
882
|
-
const httpErrorsField = fieldMap.get("
|
|
963
|
+
const httpErrorsField = fieldMap.get("error_responses")?.[0];
|
|
883
964
|
if (!httpErrorsField || httpErrorsField.value.type !== "block") {
|
|
884
965
|
return;
|
|
885
966
|
}
|
|
@@ -891,20 +972,20 @@ function validateProjectionHttpErrors(errors, statement, fieldMap, registry) {
|
|
|
891
972
|
|
|
892
973
|
const target = registry.get(capabilityId);
|
|
893
974
|
if (!target) {
|
|
894
|
-
pushError(errors, `Projection ${statement.id}
|
|
975
|
+
pushError(errors, `Projection ${statement.id} error_responses references missing capability '${capabilityId}'`, entry.loc);
|
|
895
976
|
continue;
|
|
896
977
|
}
|
|
897
978
|
if (target.kind !== "capability") {
|
|
898
|
-
pushError(errors, `Projection ${statement.id}
|
|
979
|
+
pushError(errors, `Projection ${statement.id} error_responses must target a capability, found ${target.kind} '${target.id}'`, entry.loc);
|
|
899
980
|
}
|
|
900
981
|
if (!realized.has(capabilityId)) {
|
|
901
|
-
pushError(errors, `Projection ${statement.id}
|
|
982
|
+
pushError(errors, `Projection ${statement.id} error_responses for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
902
983
|
}
|
|
903
984
|
if (!/^\d{3}$/.test(status || "")) {
|
|
904
|
-
pushError(errors, `Projection ${statement.id}
|
|
985
|
+
pushError(errors, `Projection ${statement.id} error_responses for '${capabilityId}' must use a 3-digit status`, entry.loc);
|
|
905
986
|
}
|
|
906
987
|
if (!errorCode) {
|
|
907
|
-
pushError(errors, `Projection ${statement.id}
|
|
988
|
+
pushError(errors, `Projection ${statement.id} error_responses for '${capabilityId}' must include an error code`, entry.loc);
|
|
908
989
|
}
|
|
909
990
|
}
|
|
910
991
|
}
|
|
@@ -939,7 +1020,7 @@ function validateProjectionHttpFields(errors, statement, fieldMap, registry) {
|
|
|
939
1020
|
return;
|
|
940
1021
|
}
|
|
941
1022
|
|
|
942
|
-
const httpFieldsField = fieldMap.get("
|
|
1023
|
+
const httpFieldsField = fieldMap.get("wire_fields")?.[0];
|
|
943
1024
|
if (!httpFieldsField || httpFieldsField.value.type !== "block") {
|
|
944
1025
|
return;
|
|
945
1026
|
}
|
|
@@ -951,34 +1032,34 @@ function validateProjectionHttpFields(errors, statement, fieldMap, registry) {
|
|
|
951
1032
|
|
|
952
1033
|
const capability = registry.get(capabilityId);
|
|
953
1034
|
if (!capability) {
|
|
954
|
-
pushError(errors, `Projection ${statement.id}
|
|
1035
|
+
pushError(errors, `Projection ${statement.id} wire_fields references missing capability '${capabilityId}'`, entry.loc);
|
|
955
1036
|
continue;
|
|
956
1037
|
}
|
|
957
1038
|
if (capability.kind !== "capability") {
|
|
958
|
-
pushError(errors, `Projection ${statement.id}
|
|
1039
|
+
pushError(errors, `Projection ${statement.id} wire_fields must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
959
1040
|
}
|
|
960
1041
|
if (!realized.has(capabilityId)) {
|
|
961
|
-
pushError(errors, `Projection ${statement.id}
|
|
1042
|
+
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
962
1043
|
}
|
|
963
1044
|
if (!["input", "output"].includes(direction)) {
|
|
964
|
-
pushError(errors, `Projection ${statement.id}
|
|
1045
|
+
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' has invalid direction '${direction}'`, entry.loc);
|
|
965
1046
|
}
|
|
966
1047
|
if (keywordIn !== "in") {
|
|
967
|
-
pushError(errors, `Projection ${statement.id}
|
|
1048
|
+
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' must use 'in' before the location`, entry.loc);
|
|
968
1049
|
}
|
|
969
1050
|
if (!["path", "query", "header", "body"].includes(location)) {
|
|
970
|
-
pushError(errors, `Projection ${statement.id}
|
|
1051
|
+
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' has invalid location '${location}'`, entry.loc);
|
|
971
1052
|
}
|
|
972
1053
|
if (maybeAs && maybeAs !== "as") {
|
|
973
|
-
pushError(errors, `Projection ${statement.id}
|
|
1054
|
+
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' has unexpected token '${maybeAs}'`, entry.loc);
|
|
974
1055
|
}
|
|
975
1056
|
if (maybeAs === "as" && !maybeWireName) {
|
|
976
|
-
pushError(errors, `Projection ${statement.id}
|
|
1057
|
+
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' must provide a wire name after 'as'`, entry.loc);
|
|
977
1058
|
}
|
|
978
1059
|
|
|
979
1060
|
const availableFields = resolveCapabilityContractFields(registry, capabilityId, direction);
|
|
980
1061
|
if (fieldName && availableFields.size > 0 && !availableFields.has(fieldName)) {
|
|
981
|
-
pushError(errors, `Projection ${statement.id}
|
|
1062
|
+
pushError(errors, `Projection ${statement.id} wire_fields references unknown ${direction} field '${fieldName}' on ${capabilityId}`, entry.loc);
|
|
982
1063
|
}
|
|
983
1064
|
}
|
|
984
1065
|
}
|
|
@@ -988,7 +1069,7 @@ function validateProjectionHttpResponses(errors, statement, fieldMap, registry)
|
|
|
988
1069
|
return;
|
|
989
1070
|
}
|
|
990
1071
|
|
|
991
|
-
const httpResponsesField = fieldMap.get("
|
|
1072
|
+
const httpResponsesField = fieldMap.get("responses")?.[0];
|
|
992
1073
|
if (!httpResponsesField || httpResponsesField.value.type !== "block") {
|
|
993
1074
|
return;
|
|
994
1075
|
}
|
|
@@ -1000,86 +1081,86 @@ function validateProjectionHttpResponses(errors, statement, fieldMap, registry)
|
|
|
1000
1081
|
const capability = registry.get(capabilityId);
|
|
1001
1082
|
|
|
1002
1083
|
if (!capability) {
|
|
1003
|
-
pushError(errors, `Projection ${statement.id}
|
|
1084
|
+
pushError(errors, `Projection ${statement.id} responses references missing capability '${capabilityId}'`, entry.loc);
|
|
1004
1085
|
continue;
|
|
1005
1086
|
}
|
|
1006
1087
|
if (capability.kind !== "capability") {
|
|
1007
|
-
pushError(errors, `Projection ${statement.id}
|
|
1088
|
+
pushError(errors, `Projection ${statement.id} responses must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1008
1089
|
}
|
|
1009
1090
|
if (!realized.has(capabilityId)) {
|
|
1010
|
-
pushError(errors, `Projection ${statement.id}
|
|
1091
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1011
1092
|
}
|
|
1012
1093
|
|
|
1013
1094
|
const directives = parseProjectionHttpResponsesDirectives(tokens.slice(1));
|
|
1014
1095
|
for (const message of directives.errors) {
|
|
1015
|
-
pushError(errors, `Projection ${statement.id}
|
|
1096
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' ${message}`, entry.loc);
|
|
1016
1097
|
}
|
|
1017
1098
|
|
|
1018
1099
|
if (!directives.mode) {
|
|
1019
|
-
pushError(errors, `Projection ${statement.id}
|
|
1100
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'mode'`, entry.loc);
|
|
1020
1101
|
}
|
|
1021
1102
|
|
|
1022
1103
|
const mode = directives.mode;
|
|
1023
1104
|
if (mode && !["item", "collection", "paged", "cursor"].includes(mode)) {
|
|
1024
|
-
pushError(errors, `Projection ${statement.id}
|
|
1105
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid mode '${mode}'`, entry.loc);
|
|
1025
1106
|
}
|
|
1026
1107
|
|
|
1027
1108
|
const itemShapeId = directives.item;
|
|
1028
1109
|
if (mode && mode !== "item" && !itemShapeId) {
|
|
1029
|
-
pushError(errors, `Projection ${statement.id}
|
|
1110
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'item' for mode '${mode}'`, entry.loc);
|
|
1030
1111
|
}
|
|
1031
1112
|
if (itemShapeId) {
|
|
1032
1113
|
const itemShape = registry.get(itemShapeId);
|
|
1033
1114
|
if (!itemShape) {
|
|
1034
|
-
pushError(errors, `Projection ${statement.id}
|
|
1115
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' references missing shape '${itemShapeId}'`, entry.loc);
|
|
1035
1116
|
} else if (itemShape.kind !== "shape") {
|
|
1036
|
-
pushError(errors, `Projection ${statement.id}
|
|
1117
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must reference a shape for 'item', found ${itemShape.kind} '${itemShape.id}'`, entry.loc);
|
|
1037
1118
|
}
|
|
1038
1119
|
}
|
|
1039
1120
|
|
|
1040
1121
|
if (mode === "cursor") {
|
|
1041
1122
|
if (!directives.cursor?.requestAfter) {
|
|
1042
|
-
pushError(errors, `Projection ${statement.id}
|
|
1123
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'cursor request_after <field>'`, entry.loc);
|
|
1043
1124
|
}
|
|
1044
1125
|
if (!directives.cursor?.responseNext) {
|
|
1045
|
-
pushError(errors, `Projection ${statement.id}
|
|
1126
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'cursor response_next <wire_name>'`, entry.loc);
|
|
1046
1127
|
}
|
|
1047
1128
|
if (!directives.limit) {
|
|
1048
|
-
pushError(errors, `Projection ${statement.id}
|
|
1129
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'limit field <field> default <n> max <n>'`, entry.loc);
|
|
1049
1130
|
}
|
|
1050
1131
|
if (!directives.sort) {
|
|
1051
|
-
pushError(errors, `Projection ${statement.id}
|
|
1132
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'sort by <field> direction <asc|desc>'`, entry.loc);
|
|
1052
1133
|
}
|
|
1053
1134
|
}
|
|
1054
1135
|
|
|
1055
1136
|
if (directives.sort && !["asc", "desc"].includes(directives.sort.direction || "")) {
|
|
1056
|
-
pushError(errors, `Projection ${statement.id}
|
|
1137
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid sort direction '${directives.sort.direction}'`, entry.loc);
|
|
1057
1138
|
}
|
|
1058
1139
|
|
|
1059
1140
|
if (directives.total && !["true", "false"].includes(directives.total.included || "")) {
|
|
1060
|
-
pushError(errors, `Projection ${statement.id}
|
|
1141
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid total included value '${directives.total.included}'`, entry.loc);
|
|
1061
1142
|
}
|
|
1062
1143
|
|
|
1063
1144
|
if (directives.limit) {
|
|
1064
1145
|
const defaultValue = Number.parseInt(directives.limit.defaultValue || "", 10);
|
|
1065
1146
|
const maxValue = Number.parseInt(directives.limit.maxValue || "", 10);
|
|
1066
1147
|
if (!Number.isInteger(defaultValue) || !Number.isInteger(maxValue)) {
|
|
1067
|
-
pushError(errors, `Projection ${statement.id}
|
|
1148
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must use integer default/max values for 'limit'`, entry.loc);
|
|
1068
1149
|
} else if (defaultValue > maxValue) {
|
|
1069
|
-
pushError(errors, `Projection ${statement.id}
|
|
1150
|
+
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must use default <= max for 'limit'`, entry.loc);
|
|
1070
1151
|
}
|
|
1071
1152
|
}
|
|
1072
1153
|
|
|
1073
1154
|
const inputFields = resolveCapabilityContractFields(registry, capabilityId, "input");
|
|
1074
1155
|
const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
|
|
1075
1156
|
if (directives.cursor?.requestAfter && inputFields.size > 0 && !inputFields.has(directives.cursor.requestAfter)) {
|
|
1076
|
-
pushError(errors, `Projection ${statement.id}
|
|
1157
|
+
pushError(errors, `Projection ${statement.id} responses references unknown input field '${directives.cursor.requestAfter}' for cursor request_after on ${capabilityId}`, entry.loc);
|
|
1077
1158
|
}
|
|
1078
1159
|
if (directives.limit?.field && inputFields.size > 0 && !inputFields.has(directives.limit.field)) {
|
|
1079
|
-
pushError(errors, `Projection ${statement.id}
|
|
1160
|
+
pushError(errors, `Projection ${statement.id} responses references unknown input field '${directives.limit.field}' for limit on ${capabilityId}`, entry.loc);
|
|
1080
1161
|
}
|
|
1081
1162
|
if (directives.sort?.field && outputFields.size > 0 && !outputFields.has(directives.sort.field)) {
|
|
1082
|
-
pushError(errors, `Projection ${statement.id}
|
|
1163
|
+
pushError(errors, `Projection ${statement.id} responses references unknown output field '${directives.sort.field}' for sort on ${capabilityId}`, entry.loc);
|
|
1083
1164
|
}
|
|
1084
1165
|
}
|
|
1085
1166
|
}
|
|
@@ -1089,7 +1170,7 @@ function validateProjectionHttpPreconditions(errors, statement, fieldMap, regist
|
|
|
1089
1170
|
return;
|
|
1090
1171
|
}
|
|
1091
1172
|
|
|
1092
|
-
const httpPreconditionsField = fieldMap.get("
|
|
1173
|
+
const httpPreconditionsField = fieldMap.get("preconditions")?.[0];
|
|
1093
1174
|
if (!httpPreconditionsField || httpPreconditionsField.value.type !== "block") {
|
|
1094
1175
|
return;
|
|
1095
1176
|
}
|
|
@@ -1101,14 +1182,14 @@ function validateProjectionHttpPreconditions(errors, statement, fieldMap, regist
|
|
|
1101
1182
|
const capability = registry.get(capabilityId);
|
|
1102
1183
|
|
|
1103
1184
|
if (!capability) {
|
|
1104
|
-
pushError(errors, `Projection ${statement.id}
|
|
1185
|
+
pushError(errors, `Projection ${statement.id} preconditions references missing capability '${capabilityId}'`, entry.loc);
|
|
1105
1186
|
continue;
|
|
1106
1187
|
}
|
|
1107
1188
|
if (capability.kind !== "capability") {
|
|
1108
|
-
pushError(errors, `Projection ${statement.id}
|
|
1189
|
+
pushError(errors, `Projection ${statement.id} preconditions must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1109
1190
|
}
|
|
1110
1191
|
if (!realized.has(capabilityId)) {
|
|
1111
|
-
pushError(errors, `Projection ${statement.id}
|
|
1192
|
+
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1112
1193
|
}
|
|
1113
1194
|
|
|
1114
1195
|
const directives = new Map();
|
|
@@ -1116,7 +1197,7 @@ function validateProjectionHttpPreconditions(errors, statement, fieldMap, regist
|
|
|
1116
1197
|
const key = tokens[i];
|
|
1117
1198
|
const value = tokens[i + 1];
|
|
1118
1199
|
if (!value) {
|
|
1119
|
-
pushError(errors, `Projection ${statement.id}
|
|
1200
|
+
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1120
1201
|
continue;
|
|
1121
1202
|
}
|
|
1122
1203
|
directives.set(key, value);
|
|
@@ -1124,30 +1205,30 @@ function validateProjectionHttpPreconditions(errors, statement, fieldMap, regist
|
|
|
1124
1205
|
|
|
1125
1206
|
for (const requiredKey of ["header", "required", "error", "source", "code"]) {
|
|
1126
1207
|
if (!directives.has(requiredKey)) {
|
|
1127
|
-
pushError(errors, `Projection ${statement.id}
|
|
1208
|
+
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1128
1209
|
}
|
|
1129
1210
|
}
|
|
1130
1211
|
|
|
1131
1212
|
for (const key of directives.keys()) {
|
|
1132
1213
|
if (!["header", "required", "error", "source", "code"].includes(key)) {
|
|
1133
|
-
pushError(errors, `Projection ${statement.id}
|
|
1214
|
+
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1134
1215
|
}
|
|
1135
1216
|
}
|
|
1136
1217
|
|
|
1137
1218
|
const required = directives.get("required");
|
|
1138
1219
|
if (required && !["true", "false"].includes(required)) {
|
|
1139
|
-
pushError(errors, `Projection ${statement.id}
|
|
1220
|
+
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' has invalid required value '${required}'`, entry.loc);
|
|
1140
1221
|
}
|
|
1141
1222
|
|
|
1142
1223
|
const errorStatus = directives.get("error");
|
|
1143
1224
|
if (errorStatus && !/^\d{3}$/.test(errorStatus)) {
|
|
1144
|
-
pushError(errors, `Projection ${statement.id}
|
|
1225
|
+
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' must use a 3-digit error status`, entry.loc);
|
|
1145
1226
|
}
|
|
1146
1227
|
|
|
1147
1228
|
const sourceField = directives.get("source");
|
|
1148
1229
|
const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
|
|
1149
1230
|
if (sourceField && outputFields.size > 0 && !outputFields.has(sourceField)) {
|
|
1150
|
-
pushError(errors, `Projection ${statement.id}
|
|
1231
|
+
pushError(errors, `Projection ${statement.id} preconditions references unknown output field '${sourceField}' on ${capabilityId}`, entry.loc);
|
|
1151
1232
|
}
|
|
1152
1233
|
}
|
|
1153
1234
|
}
|
|
@@ -1157,7 +1238,7 @@ function validateProjectionHttpIdempotency(errors, statement, fieldMap, registry
|
|
|
1157
1238
|
return;
|
|
1158
1239
|
}
|
|
1159
1240
|
|
|
1160
|
-
const httpIdempotencyField = fieldMap.get("
|
|
1241
|
+
const httpIdempotencyField = fieldMap.get("idempotency")?.[0];
|
|
1161
1242
|
if (!httpIdempotencyField || httpIdempotencyField.value.type !== "block") {
|
|
1162
1243
|
return;
|
|
1163
1244
|
}
|
|
@@ -1169,14 +1250,14 @@ function validateProjectionHttpIdempotency(errors, statement, fieldMap, registry
|
|
|
1169
1250
|
const capability = registry.get(capabilityId);
|
|
1170
1251
|
|
|
1171
1252
|
if (!capability) {
|
|
1172
|
-
pushError(errors, `Projection ${statement.id}
|
|
1253
|
+
pushError(errors, `Projection ${statement.id} idempotency references missing capability '${capabilityId}'`, entry.loc);
|
|
1173
1254
|
continue;
|
|
1174
1255
|
}
|
|
1175
1256
|
if (capability.kind !== "capability") {
|
|
1176
|
-
pushError(errors, `Projection ${statement.id}
|
|
1257
|
+
pushError(errors, `Projection ${statement.id} idempotency must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1177
1258
|
}
|
|
1178
1259
|
if (!realized.has(capabilityId)) {
|
|
1179
|
-
pushError(errors, `Projection ${statement.id}
|
|
1260
|
+
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1180
1261
|
}
|
|
1181
1262
|
|
|
1182
1263
|
const directives = new Map();
|
|
@@ -1184,7 +1265,7 @@ function validateProjectionHttpIdempotency(errors, statement, fieldMap, registry
|
|
|
1184
1265
|
const key = tokens[i];
|
|
1185
1266
|
const value = tokens[i + 1];
|
|
1186
1267
|
if (!value) {
|
|
1187
|
-
pushError(errors, `Projection ${statement.id}
|
|
1268
|
+
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1188
1269
|
continue;
|
|
1189
1270
|
}
|
|
1190
1271
|
directives.set(key, value);
|
|
@@ -1192,24 +1273,24 @@ function validateProjectionHttpIdempotency(errors, statement, fieldMap, registry
|
|
|
1192
1273
|
|
|
1193
1274
|
for (const requiredKey of ["header", "required", "error", "code"]) {
|
|
1194
1275
|
if (!directives.has(requiredKey)) {
|
|
1195
|
-
pushError(errors, `Projection ${statement.id}
|
|
1276
|
+
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1196
1277
|
}
|
|
1197
1278
|
}
|
|
1198
1279
|
|
|
1199
1280
|
for (const key of directives.keys()) {
|
|
1200
1281
|
if (!["header", "required", "error", "code"].includes(key)) {
|
|
1201
|
-
pushError(errors, `Projection ${statement.id}
|
|
1282
|
+
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1202
1283
|
}
|
|
1203
1284
|
}
|
|
1204
1285
|
|
|
1205
1286
|
const required = directives.get("required");
|
|
1206
1287
|
if (required && !["true", "false"].includes(required)) {
|
|
1207
|
-
pushError(errors, `Projection ${statement.id}
|
|
1288
|
+
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' has invalid required value '${required}'`, entry.loc);
|
|
1208
1289
|
}
|
|
1209
1290
|
|
|
1210
1291
|
const errorStatus = directives.get("error");
|
|
1211
1292
|
if (errorStatus && !/^\d{3}$/.test(errorStatus)) {
|
|
1212
|
-
pushError(errors, `Projection ${statement.id}
|
|
1293
|
+
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' must use a 3-digit error status`, entry.loc);
|
|
1213
1294
|
}
|
|
1214
1295
|
}
|
|
1215
1296
|
}
|
|
@@ -1219,13 +1300,13 @@ function validateProjectionHttpCache(errors, statement, fieldMap, registry) {
|
|
|
1219
1300
|
return;
|
|
1220
1301
|
}
|
|
1221
1302
|
|
|
1222
|
-
const httpCacheField = fieldMap.get("
|
|
1303
|
+
const httpCacheField = fieldMap.get("cache")?.[0];
|
|
1223
1304
|
if (!httpCacheField || httpCacheField.value.type !== "block") {
|
|
1224
1305
|
return;
|
|
1225
1306
|
}
|
|
1226
1307
|
|
|
1227
1308
|
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1228
|
-
const httpEntries = blockEntries(getFieldValue(statement, "
|
|
1309
|
+
const httpEntries = blockEntries(getFieldValue(statement, "endpoints"));
|
|
1229
1310
|
const httpMethodsByCapability = new Map();
|
|
1230
1311
|
|
|
1231
1312
|
for (const entry of httpEntries) {
|
|
@@ -1245,14 +1326,14 @@ function validateProjectionHttpCache(errors, statement, fieldMap, registry) {
|
|
|
1245
1326
|
const capability = registry.get(capabilityId);
|
|
1246
1327
|
|
|
1247
1328
|
if (!capability) {
|
|
1248
|
-
pushError(errors, `Projection ${statement.id}
|
|
1329
|
+
pushError(errors, `Projection ${statement.id} cache references missing capability '${capabilityId}'`, entry.loc);
|
|
1249
1330
|
continue;
|
|
1250
1331
|
}
|
|
1251
1332
|
if (capability.kind !== "capability") {
|
|
1252
|
-
pushError(errors, `Projection ${statement.id}
|
|
1333
|
+
pushError(errors, `Projection ${statement.id} cache must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1253
1334
|
}
|
|
1254
1335
|
if (!realized.has(capabilityId)) {
|
|
1255
|
-
pushError(errors, `Projection ${statement.id}
|
|
1336
|
+
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1256
1337
|
}
|
|
1257
1338
|
|
|
1258
1339
|
const directives = new Map();
|
|
@@ -1260,7 +1341,7 @@ function validateProjectionHttpCache(errors, statement, fieldMap, registry) {
|
|
|
1260
1341
|
const key = tokens[i];
|
|
1261
1342
|
const value = tokens[i + 1];
|
|
1262
1343
|
if (!value) {
|
|
1263
|
-
pushError(errors, `Projection ${statement.id}
|
|
1344
|
+
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1264
1345
|
continue;
|
|
1265
1346
|
}
|
|
1266
1347
|
directives.set(key, value);
|
|
@@ -1268,35 +1349,35 @@ function validateProjectionHttpCache(errors, statement, fieldMap, registry) {
|
|
|
1268
1349
|
|
|
1269
1350
|
for (const requiredKey of ["response_header", "request_header", "required", "not_modified", "source", "code"]) {
|
|
1270
1351
|
if (!directives.has(requiredKey)) {
|
|
1271
|
-
pushError(errors, `Projection ${statement.id}
|
|
1352
|
+
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1272
1353
|
}
|
|
1273
1354
|
}
|
|
1274
1355
|
|
|
1275
1356
|
for (const key of directives.keys()) {
|
|
1276
1357
|
if (!["response_header", "request_header", "required", "not_modified", "source", "code"].includes(key)) {
|
|
1277
|
-
pushError(errors, `Projection ${statement.id}
|
|
1358
|
+
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1278
1359
|
}
|
|
1279
1360
|
}
|
|
1280
1361
|
|
|
1281
1362
|
const required = directives.get("required");
|
|
1282
1363
|
if (required && !["true", "false"].includes(required)) {
|
|
1283
|
-
pushError(errors, `Projection ${statement.id}
|
|
1364
|
+
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' has invalid required value '${required}'`, entry.loc);
|
|
1284
1365
|
}
|
|
1285
1366
|
|
|
1286
1367
|
const notModifiedStatus = directives.get("not_modified");
|
|
1287
1368
|
if (notModifiedStatus && notModifiedStatus !== "304") {
|
|
1288
|
-
pushError(errors, `Projection ${statement.id}
|
|
1369
|
+
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' must use 304 for 'not_modified'`, entry.loc);
|
|
1289
1370
|
}
|
|
1290
1371
|
|
|
1291
1372
|
const sourceField = directives.get("source");
|
|
1292
1373
|
const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
|
|
1293
1374
|
if (sourceField && outputFields.size > 0 && !outputFields.has(sourceField)) {
|
|
1294
|
-
pushError(errors, `Projection ${statement.id}
|
|
1375
|
+
pushError(errors, `Projection ${statement.id} cache references unknown output field '${sourceField}' on ${capabilityId}`, entry.loc);
|
|
1295
1376
|
}
|
|
1296
1377
|
|
|
1297
1378
|
const method = httpMethodsByCapability.get(capabilityId);
|
|
1298
1379
|
if (method && method !== "GET") {
|
|
1299
|
-
pushError(errors, `Projection ${statement.id}
|
|
1380
|
+
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' requires an HTTP GET realization, found '${method}'`, entry.loc);
|
|
1300
1381
|
}
|
|
1301
1382
|
}
|
|
1302
1383
|
}
|
|
@@ -1306,7 +1387,7 @@ function validateProjectionHttpDelete(errors, statement, fieldMap, registry) {
|
|
|
1306
1387
|
return;
|
|
1307
1388
|
}
|
|
1308
1389
|
|
|
1309
|
-
const httpDeleteField = fieldMap.get("
|
|
1390
|
+
const httpDeleteField = fieldMap.get("delete_semantics")?.[0];
|
|
1310
1391
|
if (!httpDeleteField || httpDeleteField.value.type !== "block") {
|
|
1311
1392
|
return;
|
|
1312
1393
|
}
|
|
@@ -1318,14 +1399,14 @@ function validateProjectionHttpDelete(errors, statement, fieldMap, registry) {
|
|
|
1318
1399
|
const capability = registry.get(capabilityId);
|
|
1319
1400
|
|
|
1320
1401
|
if (!capability) {
|
|
1321
|
-
pushError(errors, `Projection ${statement.id}
|
|
1402
|
+
pushError(errors, `Projection ${statement.id} delete_semantics references missing capability '${capabilityId}'`, entry.loc);
|
|
1322
1403
|
continue;
|
|
1323
1404
|
}
|
|
1324
1405
|
if (capability.kind !== "capability") {
|
|
1325
|
-
pushError(errors, `Projection ${statement.id}
|
|
1406
|
+
pushError(errors, `Projection ${statement.id} delete_semantics must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1326
1407
|
}
|
|
1327
1408
|
if (!realized.has(capabilityId)) {
|
|
1328
|
-
pushError(errors, `Projection ${statement.id}
|
|
1409
|
+
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1329
1410
|
}
|
|
1330
1411
|
|
|
1331
1412
|
const directives = new Map();
|
|
@@ -1333,7 +1414,7 @@ function validateProjectionHttpDelete(errors, statement, fieldMap, registry) {
|
|
|
1333
1414
|
const key = tokens[i];
|
|
1334
1415
|
const value = tokens[i + 1];
|
|
1335
1416
|
if (!value) {
|
|
1336
|
-
pushError(errors, `Projection ${statement.id}
|
|
1417
|
+
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1337
1418
|
continue;
|
|
1338
1419
|
}
|
|
1339
1420
|
directives.set(key, value);
|
|
@@ -1341,34 +1422,34 @@ function validateProjectionHttpDelete(errors, statement, fieldMap, registry) {
|
|
|
1341
1422
|
|
|
1342
1423
|
for (const requiredKey of ["mode", "response"]) {
|
|
1343
1424
|
if (!directives.has(requiredKey)) {
|
|
1344
|
-
pushError(errors, `Projection ${statement.id}
|
|
1425
|
+
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1345
1426
|
}
|
|
1346
1427
|
}
|
|
1347
1428
|
|
|
1348
1429
|
for (const key of directives.keys()) {
|
|
1349
1430
|
if (!["mode", "field", "value", "response"].includes(key)) {
|
|
1350
|
-
pushError(errors, `Projection ${statement.id}
|
|
1431
|
+
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1351
1432
|
}
|
|
1352
1433
|
}
|
|
1353
1434
|
|
|
1354
1435
|
const mode = directives.get("mode");
|
|
1355
1436
|
if (mode && !["soft", "hard"].includes(mode)) {
|
|
1356
|
-
pushError(errors, `Projection ${statement.id}
|
|
1437
|
+
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' has invalid mode '${mode}'`, entry.loc);
|
|
1357
1438
|
}
|
|
1358
1439
|
|
|
1359
1440
|
const response = directives.get("response");
|
|
1360
1441
|
if (response && !["none", "body"].includes(response)) {
|
|
1361
|
-
pushError(errors, `Projection ${statement.id}
|
|
1442
|
+
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' has invalid response '${response}'`, entry.loc);
|
|
1362
1443
|
}
|
|
1363
1444
|
|
|
1364
1445
|
if (mode === "soft") {
|
|
1365
1446
|
if (!directives.has("field") || !directives.has("value")) {
|
|
1366
|
-
pushError(errors, `Projection ${statement.id}
|
|
1447
|
+
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' must include 'field' and 'value' for soft deletes`, entry.loc);
|
|
1367
1448
|
}
|
|
1368
1449
|
const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
|
|
1369
1450
|
const fieldName = directives.get("field");
|
|
1370
1451
|
if (fieldName && outputFields.size > 0 && !outputFields.has(fieldName)) {
|
|
1371
|
-
pushError(errors, `Projection ${statement.id}
|
|
1452
|
+
pushError(errors, `Projection ${statement.id} delete_semantics references unknown output field '${fieldName}' on ${capabilityId}`, entry.loc);
|
|
1372
1453
|
}
|
|
1373
1454
|
}
|
|
1374
1455
|
}
|
|
@@ -1379,13 +1460,13 @@ function validateProjectionHttpAsync(errors, statement, fieldMap, registry) {
|
|
|
1379
1460
|
return;
|
|
1380
1461
|
}
|
|
1381
1462
|
|
|
1382
|
-
const httpAsyncField = fieldMap.get("
|
|
1463
|
+
const httpAsyncField = fieldMap.get("async_jobs")?.[0];
|
|
1383
1464
|
if (!httpAsyncField || httpAsyncField.value.type !== "block") {
|
|
1384
1465
|
return;
|
|
1385
1466
|
}
|
|
1386
1467
|
|
|
1387
1468
|
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1388
|
-
const httpEntries = blockEntries(getFieldValue(statement, "
|
|
1469
|
+
const httpEntries = blockEntries(getFieldValue(statement, "endpoints"));
|
|
1389
1470
|
const httpDirectivesByCapability = new Map();
|
|
1390
1471
|
for (const entry of httpEntries) {
|
|
1391
1472
|
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
@@ -1402,14 +1483,14 @@ function validateProjectionHttpAsync(errors, statement, fieldMap, registry) {
|
|
|
1402
1483
|
const capability = registry.get(capabilityId);
|
|
1403
1484
|
|
|
1404
1485
|
if (!capability) {
|
|
1405
|
-
pushError(errors, `Projection ${statement.id}
|
|
1486
|
+
pushError(errors, `Projection ${statement.id} async_jobs references missing capability '${capabilityId}'`, entry.loc);
|
|
1406
1487
|
continue;
|
|
1407
1488
|
}
|
|
1408
1489
|
if (capability.kind !== "capability") {
|
|
1409
|
-
pushError(errors, `Projection ${statement.id}
|
|
1490
|
+
pushError(errors, `Projection ${statement.id} async_jobs must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1410
1491
|
}
|
|
1411
1492
|
if (!realized.has(capabilityId)) {
|
|
1412
|
-
pushError(errors, `Projection ${statement.id}
|
|
1493
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1413
1494
|
}
|
|
1414
1495
|
|
|
1415
1496
|
const directives = new Map();
|
|
@@ -1417,7 +1498,7 @@ function validateProjectionHttpAsync(errors, statement, fieldMap, registry) {
|
|
|
1417
1498
|
const key = tokens[i];
|
|
1418
1499
|
const value = tokens[i + 1];
|
|
1419
1500
|
if (!value) {
|
|
1420
|
-
pushError(errors, `Projection ${statement.id}
|
|
1501
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1421
1502
|
continue;
|
|
1422
1503
|
}
|
|
1423
1504
|
directives.set(key, value);
|
|
@@ -1425,33 +1506,33 @@ function validateProjectionHttpAsync(errors, statement, fieldMap, registry) {
|
|
|
1425
1506
|
|
|
1426
1507
|
for (const requiredKey of ["mode", "accepted", "location_header", "retry_after_header", "status_path", "status_capability", "job"]) {
|
|
1427
1508
|
if (!directives.has(requiredKey)) {
|
|
1428
|
-
pushError(errors, `Projection ${statement.id}
|
|
1509
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1429
1510
|
}
|
|
1430
1511
|
}
|
|
1431
1512
|
|
|
1432
1513
|
for (const key of directives.keys()) {
|
|
1433
1514
|
if (!["mode", "accepted", "location_header", "retry_after_header", "status_path", "status_capability", "job"].includes(key)) {
|
|
1434
|
-
pushError(errors, `Projection ${statement.id}
|
|
1515
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1435
1516
|
}
|
|
1436
1517
|
}
|
|
1437
1518
|
|
|
1438
1519
|
const mode = directives.get("mode");
|
|
1439
1520
|
if (mode && mode !== "job") {
|
|
1440
|
-
pushError(errors, `Projection ${statement.id}
|
|
1521
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' has invalid mode '${mode}'`, entry.loc);
|
|
1441
1522
|
}
|
|
1442
1523
|
|
|
1443
1524
|
const accepted = directives.get("accepted");
|
|
1444
1525
|
if (accepted && accepted !== "202") {
|
|
1445
|
-
pushError(errors, `Projection ${statement.id}
|
|
1526
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must use 202 for 'accepted'`, entry.loc);
|
|
1446
1527
|
}
|
|
1447
1528
|
|
|
1448
1529
|
const jobShapeId = directives.get("job");
|
|
1449
1530
|
if (jobShapeId) {
|
|
1450
1531
|
const jobShape = registry.get(jobShapeId);
|
|
1451
1532
|
if (!jobShape) {
|
|
1452
|
-
pushError(errors, `Projection ${statement.id}
|
|
1533
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' references missing shape '${jobShapeId}'`, entry.loc);
|
|
1453
1534
|
} else if (jobShape.kind !== "shape") {
|
|
1454
|
-
pushError(errors, `Projection ${statement.id}
|
|
1535
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must reference a shape for 'job', found ${jobShape.kind} '${jobShape.id}'`, entry.loc);
|
|
1455
1536
|
}
|
|
1456
1537
|
}
|
|
1457
1538
|
|
|
@@ -1459,25 +1540,25 @@ function validateProjectionHttpAsync(errors, statement, fieldMap, registry) {
|
|
|
1459
1540
|
if (statusCapabilityId) {
|
|
1460
1541
|
const statusCapability = registry.get(statusCapabilityId);
|
|
1461
1542
|
if (!statusCapability) {
|
|
1462
|
-
pushError(errors, `Projection ${statement.id}
|
|
1543
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' references missing status capability '${statusCapabilityId}'`, entry.loc);
|
|
1463
1544
|
} else if (statusCapability.kind !== "capability") {
|
|
1464
|
-
pushError(errors, `Projection ${statement.id}
|
|
1545
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must reference a capability for 'status_capability', found ${statusCapability.kind} '${statusCapability.id}'`, entry.loc);
|
|
1465
1546
|
} else if (!realized.has(statusCapabilityId)) {
|
|
1466
|
-
pushError(errors, `Projection ${statement.id}
|
|
1547
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' status capability '${statusCapabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1467
1548
|
}
|
|
1468
1549
|
|
|
1469
1550
|
const statusHttp = httpDirectivesByCapability.get(statusCapabilityId);
|
|
1470
1551
|
if (statusHttp?.get("method") && statusHttp.get("method") !== "GET") {
|
|
1471
|
-
pushError(errors, `Projection ${statement.id}
|
|
1552
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' status capability '${statusCapabilityId}' must use HTTP GET`, entry.loc);
|
|
1472
1553
|
}
|
|
1473
1554
|
if (statusHttp?.get("path") && directives.get("status_path") && statusHttp.get("path") !== directives.get("status_path")) {
|
|
1474
|
-
pushError(errors, `Projection ${statement.id}
|
|
1555
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' status_path must match the path for '${statusCapabilityId}'`, entry.loc);
|
|
1475
1556
|
}
|
|
1476
1557
|
}
|
|
1477
1558
|
|
|
1478
1559
|
const statusPath = directives.get("status_path");
|
|
1479
1560
|
if (statusPath && !statusPath.startsWith("/")) {
|
|
1480
|
-
pushError(errors, `Projection ${statement.id}
|
|
1561
|
+
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must use an absolute path for 'status_path'`, entry.loc);
|
|
1481
1562
|
}
|
|
1482
1563
|
}
|
|
1483
1564
|
}
|
|
@@ -1487,13 +1568,13 @@ function validateProjectionHttpStatus(errors, statement, fieldMap, registry) {
|
|
|
1487
1568
|
return;
|
|
1488
1569
|
}
|
|
1489
1570
|
|
|
1490
|
-
const httpStatusField = fieldMap.get("
|
|
1571
|
+
const httpStatusField = fieldMap.get("async_status")?.[0];
|
|
1491
1572
|
if (!httpStatusField || httpStatusField.value.type !== "block") {
|
|
1492
1573
|
return;
|
|
1493
1574
|
}
|
|
1494
1575
|
|
|
1495
1576
|
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1496
|
-
const httpEntries = blockEntries(getFieldValue(statement, "
|
|
1577
|
+
const httpEntries = blockEntries(getFieldValue(statement, "endpoints"));
|
|
1497
1578
|
const httpMethodsByCapability = new Map();
|
|
1498
1579
|
for (const entry of httpEntries) {
|
|
1499
1580
|
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
@@ -1511,14 +1592,14 @@ function validateProjectionHttpStatus(errors, statement, fieldMap, registry) {
|
|
|
1511
1592
|
const capability = registry.get(capabilityId);
|
|
1512
1593
|
|
|
1513
1594
|
if (!capability) {
|
|
1514
|
-
pushError(errors, `Projection ${statement.id}
|
|
1595
|
+
pushError(errors, `Projection ${statement.id} async_status references missing capability '${capabilityId}'`, entry.loc);
|
|
1515
1596
|
continue;
|
|
1516
1597
|
}
|
|
1517
1598
|
if (capability.kind !== "capability") {
|
|
1518
|
-
pushError(errors, `Projection ${statement.id}
|
|
1599
|
+
pushError(errors, `Projection ${statement.id} async_status must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1519
1600
|
}
|
|
1520
1601
|
if (!realized.has(capabilityId)) {
|
|
1521
|
-
pushError(errors, `Projection ${statement.id}
|
|
1602
|
+
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1522
1603
|
}
|
|
1523
1604
|
|
|
1524
1605
|
const directives = new Map();
|
|
@@ -1526,7 +1607,7 @@ function validateProjectionHttpStatus(errors, statement, fieldMap, registry) {
|
|
|
1526
1607
|
const key = tokens[i];
|
|
1527
1608
|
const value = tokens[i + 1];
|
|
1528
1609
|
if (!value) {
|
|
1529
|
-
pushError(errors, `Projection ${statement.id}
|
|
1610
|
+
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1530
1611
|
continue;
|
|
1531
1612
|
}
|
|
1532
1613
|
directives.set(key, value);
|
|
@@ -1534,13 +1615,13 @@ function validateProjectionHttpStatus(errors, statement, fieldMap, registry) {
|
|
|
1534
1615
|
|
|
1535
1616
|
for (const requiredKey of ["async_for", "state_field", "completed", "failed"]) {
|
|
1536
1617
|
if (!directives.has(requiredKey)) {
|
|
1537
|
-
pushError(errors, `Projection ${statement.id}
|
|
1618
|
+
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1538
1619
|
}
|
|
1539
1620
|
}
|
|
1540
1621
|
|
|
1541
1622
|
for (const key of directives.keys()) {
|
|
1542
1623
|
if (!["async_for", "state_field", "completed", "failed", "expired", "download_capability", "download_field", "error_field"].includes(key)) {
|
|
1543
|
-
pushError(errors, `Projection ${statement.id}
|
|
1624
|
+
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1544
1625
|
}
|
|
1545
1626
|
}
|
|
1546
1627
|
|
|
@@ -1548,11 +1629,11 @@ function validateProjectionHttpStatus(errors, statement, fieldMap, registry) {
|
|
|
1548
1629
|
if (asyncCapabilityId) {
|
|
1549
1630
|
const asyncCapability = registry.get(asyncCapabilityId);
|
|
1550
1631
|
if (!asyncCapability) {
|
|
1551
|
-
pushError(errors, `Projection ${statement.id}
|
|
1632
|
+
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' references missing async capability '${asyncCapabilityId}'`, entry.loc);
|
|
1552
1633
|
} else if (asyncCapability.kind !== "capability") {
|
|
1553
|
-
pushError(errors, `Projection ${statement.id}
|
|
1634
|
+
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' must reference a capability for 'async_for', found ${asyncCapability.kind} '${asyncCapability.id}'`, entry.loc);
|
|
1554
1635
|
} else if (!realized.has(asyncCapabilityId)) {
|
|
1555
|
-
pushError(errors, `Projection ${statement.id}
|
|
1636
|
+
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' async capability '${asyncCapabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1556
1637
|
}
|
|
1557
1638
|
}
|
|
1558
1639
|
|
|
@@ -1563,7 +1644,7 @@ function validateProjectionHttpStatus(errors, statement, fieldMap, registry) {
|
|
|
1563
1644
|
["error_field", directives.get("error_field")]
|
|
1564
1645
|
]) {
|
|
1565
1646
|
if (fieldName && outputFields.size > 0 && !outputFields.has(fieldName)) {
|
|
1566
|
-
pushError(errors, `Projection ${statement.id}
|
|
1647
|
+
pushError(errors, `Projection ${statement.id} async_status references unknown output field '${fieldName}' for '${directive}' on ${capabilityId}`, entry.loc);
|
|
1567
1648
|
}
|
|
1568
1649
|
}
|
|
1569
1650
|
|
|
@@ -1571,16 +1652,16 @@ function validateProjectionHttpStatus(errors, statement, fieldMap, registry) {
|
|
|
1571
1652
|
if (downloadCapabilityId) {
|
|
1572
1653
|
const downloadCapability = registry.get(downloadCapabilityId);
|
|
1573
1654
|
if (!downloadCapability) {
|
|
1574
|
-
pushError(errors, `Projection ${statement.id}
|
|
1655
|
+
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' references missing download capability '${downloadCapabilityId}'`, entry.loc);
|
|
1575
1656
|
} else if (downloadCapability.kind !== "capability") {
|
|
1576
|
-
pushError(errors, `Projection ${statement.id}
|
|
1657
|
+
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' must reference a capability for 'download_capability', found ${downloadCapability.kind} '${downloadCapability.id}'`, entry.loc);
|
|
1577
1658
|
} else if (!realized.has(downloadCapabilityId)) {
|
|
1578
|
-
pushError(errors, `Projection ${statement.id}
|
|
1659
|
+
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' download capability '${downloadCapabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1579
1660
|
}
|
|
1580
1661
|
|
|
1581
1662
|
const method = httpMethodsByCapability.get(downloadCapabilityId);
|
|
1582
1663
|
if (method && method !== "GET") {
|
|
1583
|
-
pushError(errors, `Projection ${statement.id}
|
|
1664
|
+
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' download capability '${downloadCapabilityId}' must use HTTP GET`, entry.loc);
|
|
1584
1665
|
}
|
|
1585
1666
|
}
|
|
1586
1667
|
}
|
|
@@ -1591,7 +1672,7 @@ function validateProjectionHttpDownload(errors, statement, fieldMap, registry) {
|
|
|
1591
1672
|
return;
|
|
1592
1673
|
}
|
|
1593
1674
|
|
|
1594
|
-
const httpDownloadField = fieldMap.get("
|
|
1675
|
+
const httpDownloadField = fieldMap.get("downloads")?.[0];
|
|
1595
1676
|
if (!httpDownloadField || httpDownloadField.value.type !== "block") {
|
|
1596
1677
|
return;
|
|
1597
1678
|
}
|
|
@@ -1603,14 +1684,14 @@ function validateProjectionHttpDownload(errors, statement, fieldMap, registry) {
|
|
|
1603
1684
|
const capability = registry.get(capabilityId);
|
|
1604
1685
|
|
|
1605
1686
|
if (!capability) {
|
|
1606
|
-
pushError(errors, `Projection ${statement.id}
|
|
1687
|
+
pushError(errors, `Projection ${statement.id} downloads references missing capability '${capabilityId}'`, entry.loc);
|
|
1607
1688
|
continue;
|
|
1608
1689
|
}
|
|
1609
1690
|
if (capability.kind !== "capability") {
|
|
1610
|
-
pushError(errors, `Projection ${statement.id}
|
|
1691
|
+
pushError(errors, `Projection ${statement.id} downloads must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1611
1692
|
}
|
|
1612
1693
|
if (!realized.has(capabilityId)) {
|
|
1613
|
-
pushError(errors, `Projection ${statement.id}
|
|
1694
|
+
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1614
1695
|
}
|
|
1615
1696
|
|
|
1616
1697
|
const directives = new Map();
|
|
@@ -1618,7 +1699,7 @@ function validateProjectionHttpDownload(errors, statement, fieldMap, registry) {
|
|
|
1618
1699
|
const key = tokens[i];
|
|
1619
1700
|
const value = tokens[i + 1];
|
|
1620
1701
|
if (!value) {
|
|
1621
|
-
pushError(errors, `Projection ${statement.id}
|
|
1702
|
+
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1622
1703
|
continue;
|
|
1623
1704
|
}
|
|
1624
1705
|
directives.set(key, value);
|
|
@@ -1626,13 +1707,13 @@ function validateProjectionHttpDownload(errors, statement, fieldMap, registry) {
|
|
|
1626
1707
|
|
|
1627
1708
|
for (const requiredKey of ["async_for", "media", "disposition"]) {
|
|
1628
1709
|
if (!directives.has(requiredKey)) {
|
|
1629
|
-
pushError(errors, `Projection ${statement.id}
|
|
1710
|
+
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1630
1711
|
}
|
|
1631
1712
|
}
|
|
1632
1713
|
|
|
1633
1714
|
for (const key of directives.keys()) {
|
|
1634
1715
|
if (!["async_for", "media", "filename", "disposition"].includes(key)) {
|
|
1635
|
-
pushError(errors, `Projection ${statement.id}
|
|
1716
|
+
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1636
1717
|
}
|
|
1637
1718
|
}
|
|
1638
1719
|
|
|
@@ -1640,22 +1721,22 @@ function validateProjectionHttpDownload(errors, statement, fieldMap, registry) {
|
|
|
1640
1721
|
if (asyncCapabilityId) {
|
|
1641
1722
|
const asyncCapability = registry.get(asyncCapabilityId);
|
|
1642
1723
|
if (!asyncCapability) {
|
|
1643
|
-
pushError(errors, `Projection ${statement.id}
|
|
1724
|
+
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' references missing async capability '${asyncCapabilityId}'`, entry.loc);
|
|
1644
1725
|
} else if (asyncCapability.kind !== "capability") {
|
|
1645
|
-
pushError(errors, `Projection ${statement.id}
|
|
1726
|
+
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' must reference a capability for 'async_for', found ${asyncCapability.kind} '${asyncCapability.id}'`, entry.loc);
|
|
1646
1727
|
} else if (!realized.has(asyncCapabilityId)) {
|
|
1647
|
-
pushError(errors, `Projection ${statement.id}
|
|
1728
|
+
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' async capability '${asyncCapabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1648
1729
|
}
|
|
1649
1730
|
}
|
|
1650
1731
|
|
|
1651
1732
|
const media = directives.get("media");
|
|
1652
1733
|
if (media && !media.includes("/")) {
|
|
1653
|
-
pushError(errors, `Projection ${statement.id}
|
|
1734
|
+
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' must use a valid media type`, entry.loc);
|
|
1654
1735
|
}
|
|
1655
1736
|
|
|
1656
1737
|
const disposition = directives.get("disposition");
|
|
1657
1738
|
if (disposition && !["attachment", "inline"].includes(disposition)) {
|
|
1658
|
-
pushError(errors, `Projection ${statement.id}
|
|
1739
|
+
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' has invalid disposition '${disposition}'`, entry.loc);
|
|
1659
1740
|
}
|
|
1660
1741
|
}
|
|
1661
1742
|
}
|
|
@@ -1665,7 +1746,7 @@ function validateProjectionHttpAuthz(errors, statement, fieldMap, registry) {
|
|
|
1665
1746
|
return;
|
|
1666
1747
|
}
|
|
1667
1748
|
|
|
1668
|
-
const httpAuthzField = fieldMap.get("
|
|
1749
|
+
const httpAuthzField = fieldMap.get("authorization")?.[0];
|
|
1669
1750
|
if (!httpAuthzField || httpAuthzField.value.type !== "block") {
|
|
1670
1751
|
return;
|
|
1671
1752
|
}
|
|
@@ -1677,14 +1758,14 @@ function validateProjectionHttpAuthz(errors, statement, fieldMap, registry) {
|
|
|
1677
1758
|
const capability = registry.get(capabilityId);
|
|
1678
1759
|
|
|
1679
1760
|
if (!capability) {
|
|
1680
|
-
pushError(errors, `Projection ${statement.id}
|
|
1761
|
+
pushError(errors, `Projection ${statement.id} authorization references missing capability '${capabilityId}'`, entry.loc);
|
|
1681
1762
|
continue;
|
|
1682
1763
|
}
|
|
1683
1764
|
if (capability.kind !== "capability") {
|
|
1684
|
-
pushError(errors, `Projection ${statement.id}
|
|
1765
|
+
pushError(errors, `Projection ${statement.id} authorization must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1685
1766
|
}
|
|
1686
1767
|
if (!realized.has(capabilityId)) {
|
|
1687
|
-
pushError(errors, `Projection ${statement.id}
|
|
1768
|
+
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1688
1769
|
}
|
|
1689
1770
|
|
|
1690
1771
|
const directives = new Map();
|
|
@@ -1692,7 +1773,7 @@ function validateProjectionHttpAuthz(errors, statement, fieldMap, registry) {
|
|
|
1692
1773
|
const key = tokens[i];
|
|
1693
1774
|
const value = tokens[i + 1];
|
|
1694
1775
|
if (!value) {
|
|
1695
|
-
pushError(errors, `Projection ${statement.id}
|
|
1776
|
+
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1696
1777
|
continue;
|
|
1697
1778
|
}
|
|
1698
1779
|
directives.set(key, value);
|
|
@@ -1700,27 +1781,27 @@ function validateProjectionHttpAuthz(errors, statement, fieldMap, registry) {
|
|
|
1700
1781
|
|
|
1701
1782
|
for (const key of directives.keys()) {
|
|
1702
1783
|
if (!["role", "permission", "claim", "claim_value", "ownership", "ownership_field"].includes(key)) {
|
|
1703
|
-
pushError(errors, `Projection ${statement.id}
|
|
1784
|
+
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1704
1785
|
}
|
|
1705
1786
|
}
|
|
1706
1787
|
|
|
1707
1788
|
if (directives.size === 0) {
|
|
1708
|
-
pushError(errors, `Projection ${statement.id}
|
|
1789
|
+
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' must include at least one directive`, entry.loc);
|
|
1709
1790
|
}
|
|
1710
1791
|
|
|
1711
1792
|
const ownership = directives.get("ownership");
|
|
1712
1793
|
if (ownership && !["owner", "owner_or_admin", "project_member", "none"].includes(ownership)) {
|
|
1713
|
-
pushError(errors, `Projection ${statement.id}
|
|
1794
|
+
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' has invalid ownership '${ownership}'`, entry.loc);
|
|
1714
1795
|
}
|
|
1715
1796
|
|
|
1716
1797
|
const ownershipField = directives.get("ownership_field");
|
|
1717
1798
|
if (ownershipField && (!ownership || ownership === "none")) {
|
|
1718
|
-
pushError(errors, `Projection ${statement.id}
|
|
1799
|
+
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' cannot declare ownership_field without ownership`, entry.loc);
|
|
1719
1800
|
}
|
|
1720
1801
|
|
|
1721
1802
|
const claimValue = directives.get("claim_value");
|
|
1722
1803
|
if (claimValue && !directives.get("claim")) {
|
|
1723
|
-
pushError(errors, `Projection ${statement.id}
|
|
1804
|
+
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' cannot declare claim_value without claim`, entry.loc);
|
|
1724
1805
|
}
|
|
1725
1806
|
}
|
|
1726
1807
|
}
|
|
@@ -1730,7 +1811,7 @@ function validateProjectionHttpCallbacks(errors, statement, fieldMap, registry)
|
|
|
1730
1811
|
return;
|
|
1731
1812
|
}
|
|
1732
1813
|
|
|
1733
|
-
const httpCallbacksField = fieldMap.get("
|
|
1814
|
+
const httpCallbacksField = fieldMap.get("callbacks")?.[0];
|
|
1734
1815
|
if (!httpCallbacksField || httpCallbacksField.value.type !== "block") {
|
|
1735
1816
|
return;
|
|
1736
1817
|
}
|
|
@@ -1742,14 +1823,14 @@ function validateProjectionHttpCallbacks(errors, statement, fieldMap, registry)
|
|
|
1742
1823
|
const capability = registry.get(capabilityId);
|
|
1743
1824
|
|
|
1744
1825
|
if (!capability) {
|
|
1745
|
-
pushError(errors, `Projection ${statement.id}
|
|
1826
|
+
pushError(errors, `Projection ${statement.id} callbacks references missing capability '${capabilityId}'`, entry.loc);
|
|
1746
1827
|
continue;
|
|
1747
1828
|
}
|
|
1748
1829
|
if (capability.kind !== "capability") {
|
|
1749
|
-
pushError(errors, `Projection ${statement.id}
|
|
1830
|
+
pushError(errors, `Projection ${statement.id} callbacks must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1750
1831
|
}
|
|
1751
1832
|
if (!realized.has(capabilityId)) {
|
|
1752
|
-
pushError(errors, `Projection ${statement.id}
|
|
1833
|
+
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1753
1834
|
}
|
|
1754
1835
|
|
|
1755
1836
|
const directives = new Map();
|
|
@@ -1757,7 +1838,7 @@ function validateProjectionHttpCallbacks(errors, statement, fieldMap, registry)
|
|
|
1757
1838
|
const key = tokens[i];
|
|
1758
1839
|
const value = tokens[i + 1];
|
|
1759
1840
|
if (!value) {
|
|
1760
|
-
pushError(errors, `Projection ${statement.id}
|
|
1841
|
+
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1761
1842
|
continue;
|
|
1762
1843
|
}
|
|
1763
1844
|
directives.set(key, value);
|
|
@@ -1765,40 +1846,40 @@ function validateProjectionHttpCallbacks(errors, statement, fieldMap, registry)
|
|
|
1765
1846
|
|
|
1766
1847
|
for (const requiredKey of ["event", "target_field", "method", "payload", "success"]) {
|
|
1767
1848
|
if (!directives.has(requiredKey)) {
|
|
1768
|
-
pushError(errors, `Projection ${statement.id}
|
|
1849
|
+
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1769
1850
|
}
|
|
1770
1851
|
}
|
|
1771
1852
|
|
|
1772
1853
|
for (const key of directives.keys()) {
|
|
1773
1854
|
if (!["event", "target_field", "method", "payload", "success"].includes(key)) {
|
|
1774
|
-
pushError(errors, `Projection ${statement.id}
|
|
1855
|
+
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1775
1856
|
}
|
|
1776
1857
|
}
|
|
1777
1858
|
|
|
1778
1859
|
const method = directives.get("method");
|
|
1779
1860
|
if (method && !["POST", "PUT", "PATCH"].includes(method)) {
|
|
1780
|
-
pushError(errors, `Projection ${statement.id}
|
|
1861
|
+
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' has invalid method '${method}'`, entry.loc);
|
|
1781
1862
|
}
|
|
1782
1863
|
|
|
1783
1864
|
const success = directives.get("success");
|
|
1784
1865
|
if (success && !/^\d{3}$/.test(success)) {
|
|
1785
|
-
pushError(errors, `Projection ${statement.id}
|
|
1866
|
+
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' must use a 3-digit success status`, entry.loc);
|
|
1786
1867
|
}
|
|
1787
1868
|
|
|
1788
1869
|
const payloadShapeId = directives.get("payload");
|
|
1789
1870
|
if (payloadShapeId) {
|
|
1790
1871
|
const payloadShape = registry.get(payloadShapeId);
|
|
1791
1872
|
if (!payloadShape) {
|
|
1792
|
-
pushError(errors, `Projection ${statement.id}
|
|
1873
|
+
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' references missing shape '${payloadShapeId}'`, entry.loc);
|
|
1793
1874
|
} else if (payloadShape.kind !== "shape") {
|
|
1794
|
-
pushError(errors, `Projection ${statement.id}
|
|
1875
|
+
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' must reference a shape for 'payload', found ${payloadShape.kind} '${payloadShape.id}'`, entry.loc);
|
|
1795
1876
|
}
|
|
1796
1877
|
}
|
|
1797
1878
|
|
|
1798
1879
|
const targetField = directives.get("target_field");
|
|
1799
1880
|
const inputFields = resolveCapabilityContractFields(registry, capabilityId, "input");
|
|
1800
1881
|
if (targetField && inputFields.size > 0 && !inputFields.has(targetField)) {
|
|
1801
|
-
pushError(errors, `Projection ${statement.id}
|
|
1882
|
+
pushError(errors, `Projection ${statement.id} callbacks references unknown input field '${targetField}' on ${capabilityId}`, entry.loc);
|
|
1802
1883
|
}
|
|
1803
1884
|
}
|
|
1804
1885
|
}
|
|
@@ -1909,7 +1990,7 @@ function resolveCapabilityOutputShape(registry, capabilityId) {
|
|
|
1909
1990
|
}
|
|
1910
1991
|
|
|
1911
1992
|
function collectProjectionUiScreens(statement, fieldMap) {
|
|
1912
|
-
const screensField = fieldMap.get("
|
|
1993
|
+
const screensField = fieldMap.get("screens")?.[0];
|
|
1913
1994
|
if (!screensField || screensField.value.type !== "block") {
|
|
1914
1995
|
return new Map();
|
|
1915
1996
|
}
|
|
@@ -1954,7 +2035,7 @@ function resolveProjectionUiScreenFieldNames(registry, screenEntry, statement) {
|
|
|
1954
2035
|
|
|
1955
2036
|
function screenIdsFromProjectionStatement(statement) {
|
|
1956
2037
|
const screens = new Set();
|
|
1957
|
-
for (const entry of blockEntries(getFieldValue(statement, "
|
|
2038
|
+
for (const entry of blockEntries(getFieldValue(statement, "screens"))) {
|
|
1958
2039
|
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1959
2040
|
if (tokens[0] === "screen" && tokens[1]) {
|
|
1960
2041
|
screens.add(tokens[1]);
|
|
@@ -1978,7 +2059,7 @@ function collectAvailableUiScreenIds(statement, fieldMap, registry) {
|
|
|
1978
2059
|
|
|
1979
2060
|
function collectProjectionUiRegionKeys(statement) {
|
|
1980
2061
|
const keys = new Set();
|
|
1981
|
-
for (const entry of blockEntries(getFieldValue(statement, "
|
|
2062
|
+
for (const entry of blockEntries(getFieldValue(statement, "screen_regions"))) {
|
|
1982
2063
|
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1983
2064
|
if (tokens[0] === "screen" && tokens[1] && tokens[2] === "region" && tokens[3]) {
|
|
1984
2065
|
keys.add(`${tokens[1]}:${tokens[3]}`);
|
|
@@ -2002,7 +2083,7 @@ function collectAvailableUiRegionKeys(statement, registry) {
|
|
|
2002
2083
|
|
|
2003
2084
|
function collectProjectionUiRegionPatterns(statement) {
|
|
2004
2085
|
const patterns = new Map();
|
|
2005
|
-
for (const entry of blockEntries(getFieldValue(statement, "
|
|
2086
|
+
for (const entry of blockEntries(getFieldValue(statement, "screen_regions"))) {
|
|
2006
2087
|
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2007
2088
|
if (tokens[0] !== "screen" || !tokens[1] || tokens[2] !== "region" || !tokens[3]) {
|
|
2008
2089
|
continue;
|
|
@@ -2056,7 +2137,7 @@ function validateProjectionUiScreens(errors, statement, fieldMap, registry) {
|
|
|
2056
2137
|
return;
|
|
2057
2138
|
}
|
|
2058
2139
|
|
|
2059
|
-
const screensField = fieldMap.get("
|
|
2140
|
+
const screensField = fieldMap.get("screens")?.[0];
|
|
2060
2141
|
if (!screensField || screensField.value.type !== "block") {
|
|
2061
2142
|
return;
|
|
2062
2143
|
}
|
|
@@ -2069,33 +2150,33 @@ function validateProjectionUiScreens(errors, statement, fieldMap, registry) {
|
|
|
2069
2150
|
const [keyword, screenId] = tokens;
|
|
2070
2151
|
|
|
2071
2152
|
if (keyword !== "screen") {
|
|
2072
|
-
pushError(errors, `Projection ${statement.id}
|
|
2153
|
+
pushError(errors, `Projection ${statement.id} screens entries must start with 'screen'`, entry.loc);
|
|
2073
2154
|
continue;
|
|
2074
2155
|
}
|
|
2075
2156
|
if (!screenId) {
|
|
2076
|
-
pushError(errors, `Projection ${statement.id}
|
|
2157
|
+
pushError(errors, `Projection ${statement.id} screens entries must include a screen id`, entry.loc);
|
|
2077
2158
|
continue;
|
|
2078
2159
|
}
|
|
2079
2160
|
if (!IDENTIFIER_PATTERN.test(screenId)) {
|
|
2080
|
-
pushError(errors, `Projection ${statement.id}
|
|
2161
|
+
pushError(errors, `Projection ${statement.id} screens has invalid screen id '${screenId}'`, entry.loc);
|
|
2081
2162
|
}
|
|
2082
2163
|
if (seenScreens.has(screenId)) {
|
|
2083
|
-
pushError(errors, `Projection ${statement.id}
|
|
2164
|
+
pushError(errors, `Projection ${statement.id} screens has duplicate screen id '${screenId}'`, entry.loc);
|
|
2084
2165
|
}
|
|
2085
2166
|
seenScreens.add(screenId);
|
|
2086
2167
|
|
|
2087
|
-
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `
|
|
2168
|
+
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `screens for '${screenId}'`);
|
|
2088
2169
|
const kind = directives.get("kind");
|
|
2089
2170
|
if (!kind) {
|
|
2090
|
-
pushError(errors, `Projection ${statement.id}
|
|
2171
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' must include 'kind'`, entry.loc);
|
|
2091
2172
|
}
|
|
2092
2173
|
if (kind && !UI_SCREEN_KINDS.has(kind)) {
|
|
2093
|
-
pushError(errors, `Projection ${statement.id}
|
|
2174
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' has invalid kind '${kind}'`, entry.loc);
|
|
2094
2175
|
}
|
|
2095
2176
|
|
|
2096
2177
|
for (const key of directives.keys()) {
|
|
2097
2178
|
if (!["kind", "title", "load", "item_shape", "view_shape", "input_shape", "submit", "detail_capability", "primary_action", "secondary_action", "destructive_action", "success_navigate", "success_refresh", "empty_title", "empty_body", "terminal_action", "loading_state", "error_state", "unauthorized_state", "not_found_state", "success_state"].includes(key)) {
|
|
2098
|
-
pushError(errors, `Projection ${statement.id}
|
|
2179
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' has unknown directive '${key}'`, entry.loc);
|
|
2099
2180
|
}
|
|
2100
2181
|
}
|
|
2101
2182
|
|
|
@@ -2117,43 +2198,43 @@ function validateProjectionUiScreens(errors, statement, fieldMap, registry) {
|
|
|
2117
2198
|
}
|
|
2118
2199
|
const target = registry.get(targetId);
|
|
2119
2200
|
if (!target) {
|
|
2120
|
-
pushError(errors, `Projection ${statement.id}
|
|
2201
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' references missing ${expectedKind} '${targetId}' for '${key}'`, entry.loc);
|
|
2121
2202
|
continue;
|
|
2122
2203
|
}
|
|
2123
2204
|
if (target.kind !== expectedKind) {
|
|
2124
|
-
pushError(errors, `Projection ${statement.id}
|
|
2205
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' must reference a ${expectedKind} for '${key}', found ${target.kind} '${target.id}'`, entry.loc);
|
|
2125
2206
|
}
|
|
2126
2207
|
if (expectedKind === "capability" && !realized.has(targetId)) {
|
|
2127
|
-
pushError(errors, `Projection ${statement.id}
|
|
2208
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' capability '${targetId}' for '${key}' must also appear in 'realizes'`, entry.loc);
|
|
2128
2209
|
}
|
|
2129
2210
|
}
|
|
2130
2211
|
|
|
2131
2212
|
const successNavigate = directives.get("success_navigate");
|
|
2132
2213
|
const successRefresh = directives.get("success_refresh");
|
|
2133
2214
|
if (successNavigate && !IDENTIFIER_PATTERN.test(successNavigate)) {
|
|
2134
|
-
pushError(errors, `Projection ${statement.id}
|
|
2215
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' has invalid target '${successNavigate}' for 'success_navigate'`, entry.loc);
|
|
2135
2216
|
}
|
|
2136
2217
|
if (successRefresh && !IDENTIFIER_PATTERN.test(successRefresh)) {
|
|
2137
|
-
pushError(errors, `Projection ${statement.id}
|
|
2218
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' has invalid target '${successRefresh}' for 'success_refresh'`, entry.loc);
|
|
2138
2219
|
}
|
|
2139
2220
|
|
|
2140
2221
|
if (kind === "list" && !directives.get("load")) {
|
|
2141
|
-
pushError(errors, `Projection ${statement.id}
|
|
2222
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' kind 'list' requires 'load'`, entry.loc);
|
|
2142
2223
|
}
|
|
2143
2224
|
if (kind === "detail") {
|
|
2144
2225
|
if (!directives.get("load")) {
|
|
2145
|
-
pushError(errors, `Projection ${statement.id}
|
|
2226
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' kind 'detail' requires 'load'`, entry.loc);
|
|
2146
2227
|
}
|
|
2147
2228
|
if (!directives.get("view_shape")) {
|
|
2148
|
-
pushError(errors, `Projection ${statement.id}
|
|
2229
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' kind 'detail' requires 'view_shape'`, entry.loc);
|
|
2149
2230
|
}
|
|
2150
2231
|
}
|
|
2151
2232
|
if (kind === "form") {
|
|
2152
2233
|
if (!directives.get("input_shape")) {
|
|
2153
|
-
pushError(errors, `Projection ${statement.id}
|
|
2234
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' kind 'form' requires 'input_shape'`, entry.loc);
|
|
2154
2235
|
}
|
|
2155
2236
|
if (!directives.get("submit")) {
|
|
2156
|
-
pushError(errors, `Projection ${statement.id}
|
|
2237
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' kind 'form' requires 'submit'`, entry.loc);
|
|
2157
2238
|
}
|
|
2158
2239
|
}
|
|
2159
2240
|
}
|
|
@@ -2168,7 +2249,7 @@ function validateProjectionUiScreens(errors, statement, fieldMap, registry) {
|
|
|
2168
2249
|
for (const key of ["success_navigate", "success_refresh"]) {
|
|
2169
2250
|
const targetScreenId = directives.get(key);
|
|
2170
2251
|
if (targetScreenId && !seenScreens.has(targetScreenId)) {
|
|
2171
|
-
pushError(errors, `Projection ${statement.id}
|
|
2252
|
+
pushError(errors, `Projection ${statement.id} screens for '${screenId}' references unknown screen '${targetScreenId}' for '${key}'`, entry.loc);
|
|
2172
2253
|
}
|
|
2173
2254
|
}
|
|
2174
2255
|
}
|
|
@@ -2179,7 +2260,7 @@ function validateProjectionUiCollections(errors, statement, fieldMap, registry)
|
|
|
2179
2260
|
return;
|
|
2180
2261
|
}
|
|
2181
2262
|
|
|
2182
|
-
const collectionsField = fieldMap.get("
|
|
2263
|
+
const collectionsField = fieldMap.get("collection_views")?.[0];
|
|
2183
2264
|
if (!collectionsField || collectionsField.value.type !== "block") {
|
|
2184
2265
|
return;
|
|
2185
2266
|
}
|
|
@@ -2190,23 +2271,23 @@ function validateProjectionUiCollections(errors, statement, fieldMap, registry)
|
|
|
2190
2271
|
const [keyword, screenId, operation, value, extra] = tokens;
|
|
2191
2272
|
|
|
2192
2273
|
if (keyword !== "screen") {
|
|
2193
|
-
pushError(errors, `Projection ${statement.id}
|
|
2274
|
+
pushError(errors, `Projection ${statement.id} collection_views entries must start with 'screen'`, entry.loc);
|
|
2194
2275
|
continue;
|
|
2195
2276
|
}
|
|
2196
2277
|
const screenEntry = screens.get(screenId);
|
|
2197
2278
|
if (!screenEntry) {
|
|
2198
|
-
pushError(errors, `Projection ${statement.id}
|
|
2279
|
+
pushError(errors, `Projection ${statement.id} collection_views references unknown screen '${screenId}'`, entry.loc);
|
|
2199
2280
|
continue;
|
|
2200
2281
|
}
|
|
2201
2282
|
|
|
2202
2283
|
const screenTokens = blockSymbolItems(screenEntry).map((item) => item.value);
|
|
2203
2284
|
const screenDirectives = parseUiDirectiveMap(screenTokens, 2, [], statement, screenEntry, "");
|
|
2204
2285
|
if (screenDirectives.get("kind") !== "list") {
|
|
2205
|
-
pushError(errors, `Projection ${statement.id}
|
|
2286
|
+
pushError(errors, `Projection ${statement.id} collection_views may only target list screens, found '${screenId}'`, entry.loc);
|
|
2206
2287
|
}
|
|
2207
2288
|
|
|
2208
2289
|
if (!["filter", "search", "pagination", "sort", "group", "view", "refresh"].includes(operation)) {
|
|
2209
|
-
pushError(errors, `Projection ${statement.id}
|
|
2290
|
+
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' has invalid operation '${operation}'`, entry.loc);
|
|
2210
2291
|
continue;
|
|
2211
2292
|
}
|
|
2212
2293
|
|
|
@@ -2219,43 +2300,43 @@ function validateProjectionUiCollections(errors, statement, fieldMap, registry)
|
|
|
2219
2300
|
|
|
2220
2301
|
if (operation === "filter" || operation === "search") {
|
|
2221
2302
|
if (!value) {
|
|
2222
|
-
pushError(errors, `Projection ${statement.id}
|
|
2303
|
+
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' must include a field for '${operation}'`, entry.loc);
|
|
2223
2304
|
} else if (inputFields.size > 0 && !inputFields.has(value)) {
|
|
2224
|
-
pushError(errors, `Projection ${statement.id}
|
|
2305
|
+
pushError(errors, `Projection ${statement.id} collection_views references unknown input field '${value}' for '${operation}' on '${screenId}'`, entry.loc);
|
|
2225
2306
|
}
|
|
2226
2307
|
}
|
|
2227
2308
|
|
|
2228
2309
|
if (operation === "pagination" && !["cursor", "paged", "none"].includes(value || "")) {
|
|
2229
|
-
pushError(errors, `Projection ${statement.id}
|
|
2310
|
+
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' has invalid pagination '${value}'`, entry.loc);
|
|
2230
2311
|
}
|
|
2231
2312
|
|
|
2232
2313
|
if (operation === "sort") {
|
|
2233
2314
|
if (!value || !extra) {
|
|
2234
|
-
pushError(errors, `Projection ${statement.id}
|
|
2315
|
+
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' must use 'sort <field> <asc|desc>'`, entry.loc);
|
|
2235
2316
|
} else {
|
|
2236
2317
|
if (!["asc", "desc"].includes(extra)) {
|
|
2237
|
-
pushError(errors, `Projection ${statement.id}
|
|
2318
|
+
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' has invalid sort direction '${extra}'`, entry.loc);
|
|
2238
2319
|
}
|
|
2239
2320
|
if (outputFields.size > 0 && !outputFields.has(value)) {
|
|
2240
|
-
pushError(errors, `Projection ${statement.id}
|
|
2321
|
+
pushError(errors, `Projection ${statement.id} collection_views references unknown output field '${value}' for sort on '${screenId}'`, entry.loc);
|
|
2241
2322
|
}
|
|
2242
2323
|
}
|
|
2243
2324
|
}
|
|
2244
2325
|
|
|
2245
2326
|
if (operation === "group") {
|
|
2246
2327
|
if (!value) {
|
|
2247
|
-
pushError(errors, `Projection ${statement.id}
|
|
2328
|
+
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' must include a field for 'group'`, entry.loc);
|
|
2248
2329
|
} else if (outputFields.size > 0 && !outputFields.has(value)) {
|
|
2249
|
-
pushError(errors, `Projection ${statement.id}
|
|
2330
|
+
pushError(errors, `Projection ${statement.id} collection_views references unknown output field '${value}' for group on '${screenId}'`, entry.loc);
|
|
2250
2331
|
}
|
|
2251
2332
|
}
|
|
2252
2333
|
|
|
2253
2334
|
if (operation === "view" && !UI_COLLECTION_PRESENTATIONS.has(value || "")) {
|
|
2254
|
-
pushError(errors, `Projection ${statement.id}
|
|
2335
|
+
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' has invalid view '${value}'`, entry.loc);
|
|
2255
2336
|
}
|
|
2256
2337
|
|
|
2257
2338
|
if (operation === "refresh" && !["manual", "pull_to_refresh", "auto"].includes(value || "")) {
|
|
2258
|
-
pushError(errors, `Projection ${statement.id}
|
|
2339
|
+
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' has invalid refresh '${value}'`, entry.loc);
|
|
2259
2340
|
}
|
|
2260
2341
|
}
|
|
2261
2342
|
}
|
|
@@ -2265,7 +2346,7 @@ function validateProjectionUiActions(errors, statement, fieldMap, registry) {
|
|
|
2265
2346
|
return;
|
|
2266
2347
|
}
|
|
2267
2348
|
|
|
2268
|
-
const actionsField = fieldMap.get("
|
|
2349
|
+
const actionsField = fieldMap.get("screen_actions")?.[0];
|
|
2269
2350
|
if (!actionsField || actionsField.value.type !== "block") {
|
|
2270
2351
|
return;
|
|
2271
2352
|
}
|
|
@@ -2278,34 +2359,34 @@ function validateProjectionUiActions(errors, statement, fieldMap, registry) {
|
|
|
2278
2359
|
const [keyword, screenId, actionKeyword, capabilityId, prominenceKeyword, prominence, placementKeyword, placement] = tokens;
|
|
2279
2360
|
|
|
2280
2361
|
if (keyword !== "screen") {
|
|
2281
|
-
pushError(errors, `Projection ${statement.id}
|
|
2362
|
+
pushError(errors, `Projection ${statement.id} screen_actions entries must start with 'screen'`, entry.loc);
|
|
2282
2363
|
continue;
|
|
2283
2364
|
}
|
|
2284
2365
|
if (!screens.has(screenId)) {
|
|
2285
|
-
pushError(errors, `Projection ${statement.id}
|
|
2366
|
+
pushError(errors, `Projection ${statement.id} screen_actions references unknown screen '${screenId}'`, entry.loc);
|
|
2286
2367
|
}
|
|
2287
2368
|
if (actionKeyword !== "action") {
|
|
2288
|
-
pushError(errors, `Projection ${statement.id}
|
|
2369
|
+
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' must use 'action'`, entry.loc);
|
|
2289
2370
|
}
|
|
2290
2371
|
const capability = registry.get(capabilityId);
|
|
2291
2372
|
if (!capability) {
|
|
2292
|
-
pushError(errors, `Projection ${statement.id}
|
|
2373
|
+
pushError(errors, `Projection ${statement.id} screen_actions references missing capability '${capabilityId}'`, entry.loc);
|
|
2293
2374
|
} else if (capability.kind !== "capability") {
|
|
2294
|
-
pushError(errors, `Projection ${statement.id}
|
|
2375
|
+
pushError(errors, `Projection ${statement.id} screen_actions must reference a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
2295
2376
|
} else if (!realized.has(capabilityId)) {
|
|
2296
|
-
pushError(errors, `Projection ${statement.id}
|
|
2377
|
+
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' capability '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
2297
2378
|
}
|
|
2298
2379
|
if (prominenceKeyword !== "prominence") {
|
|
2299
|
-
pushError(errors, `Projection ${statement.id}
|
|
2380
|
+
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' must use 'prominence'`, entry.loc);
|
|
2300
2381
|
}
|
|
2301
2382
|
if (!["primary", "secondary", "destructive", "contextual"].includes(prominence || "")) {
|
|
2302
|
-
pushError(errors, `Projection ${statement.id}
|
|
2383
|
+
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' has invalid prominence '${prominence}'`, entry.loc);
|
|
2303
2384
|
}
|
|
2304
2385
|
if (placementKeyword && placementKeyword !== "placement") {
|
|
2305
|
-
pushError(errors, `Projection ${statement.id}
|
|
2386
|
+
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' has unknown directive '${placementKeyword}'`, entry.loc);
|
|
2306
2387
|
}
|
|
2307
2388
|
if (placementKeyword === "placement" && !["toolbar", "menu", "bulk", "inline", "footer"].includes(placement || "")) {
|
|
2308
|
-
pushError(errors, `Projection ${statement.id}
|
|
2389
|
+
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' has invalid placement '${placement}'`, entry.loc);
|
|
2309
2390
|
}
|
|
2310
2391
|
}
|
|
2311
2392
|
}
|
|
@@ -2315,7 +2396,7 @@ function validateProjectionUiAppShell(errors, statement, fieldMap) {
|
|
|
2315
2396
|
return;
|
|
2316
2397
|
}
|
|
2317
2398
|
|
|
2318
|
-
const shellField = fieldMap.get("
|
|
2399
|
+
const shellField = fieldMap.get("app_shell")?.[0];
|
|
2319
2400
|
if (!shellField || shellField.value.type !== "block") {
|
|
2320
2401
|
return;
|
|
2321
2402
|
}
|
|
@@ -2325,42 +2406,42 @@ function validateProjectionUiAppShell(errors, statement, fieldMap) {
|
|
|
2325
2406
|
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2326
2407
|
const [key, value, extra] = tokens;
|
|
2327
2408
|
if (!["brand", "shell", "primary_nav", "secondary_nav", "utility_nav", "footer", "global_search", "notifications", "account_menu", "workspace_switcher", "windowing"].includes(key || "")) {
|
|
2328
|
-
pushError(errors, `Projection ${statement.id}
|
|
2409
|
+
pushError(errors, `Projection ${statement.id} app_shell has unknown key '${key}'`, entry.loc);
|
|
2329
2410
|
continue;
|
|
2330
2411
|
}
|
|
2331
2412
|
if (!value) {
|
|
2332
|
-
pushError(errors, `Projection ${statement.id}
|
|
2413
|
+
pushError(errors, `Projection ${statement.id} app_shell is missing a value for '${key}'`, entry.loc);
|
|
2333
2414
|
continue;
|
|
2334
2415
|
}
|
|
2335
2416
|
if (extra) {
|
|
2336
|
-
pushError(errors, `Projection ${statement.id}
|
|
2417
|
+
pushError(errors, `Projection ${statement.id} app_shell '${key}' accepts exactly one value`, entry.loc);
|
|
2337
2418
|
}
|
|
2338
2419
|
if (seenKeys.has(key)) {
|
|
2339
|
-
pushError(errors, `Projection ${statement.id}
|
|
2420
|
+
pushError(errors, `Projection ${statement.id} app_shell has duplicate key '${key}'`, entry.loc);
|
|
2340
2421
|
}
|
|
2341
2422
|
seenKeys.add(key);
|
|
2342
2423
|
|
|
2343
2424
|
if (key === "shell" && !UI_APP_SHELL_KINDS.has(value)) {
|
|
2344
|
-
pushError(errors, `Projection ${statement.id}
|
|
2425
|
+
pushError(errors, `Projection ${statement.id} app_shell has invalid shell '${value}'`, entry.loc);
|
|
2345
2426
|
}
|
|
2346
2427
|
if (["global_search", "notifications", "account_menu", "workspace_switcher"].includes(key) && !["true", "false"].includes(value)) {
|
|
2347
|
-
pushError(errors, `Projection ${statement.id}
|
|
2428
|
+
pushError(errors, `Projection ${statement.id} app_shell '${key}' must be true or false`, entry.loc);
|
|
2348
2429
|
}
|
|
2349
2430
|
if (key === "windowing" && !UI_WINDOWING_MODES.has(value)) {
|
|
2350
|
-
pushError(errors, `Projection ${statement.id}
|
|
2431
|
+
pushError(errors, `Projection ${statement.id} app_shell has invalid windowing '${value}'`, entry.loc);
|
|
2351
2432
|
}
|
|
2352
2433
|
}
|
|
2353
2434
|
}
|
|
2354
2435
|
|
|
2355
2436
|
const SHARED_UI_SEMANTIC_BLOCKS = [
|
|
2356
|
-
"
|
|
2357
|
-
"
|
|
2358
|
-
"
|
|
2359
|
-
"
|
|
2360
|
-
"
|
|
2361
|
-
"
|
|
2362
|
-
"
|
|
2363
|
-
"
|
|
2437
|
+
"screens",
|
|
2438
|
+
"collection_views",
|
|
2439
|
+
"screen_actions",
|
|
2440
|
+
"visibility_rules",
|
|
2441
|
+
"field_lookups",
|
|
2442
|
+
"app_shell",
|
|
2443
|
+
"navigation",
|
|
2444
|
+
"screen_regions"
|
|
2364
2445
|
];
|
|
2365
2446
|
|
|
2366
2447
|
function validateProjectionUiOwnership(errors, statement, fieldMap) {
|
|
@@ -2368,26 +2449,26 @@ function validateProjectionUiOwnership(errors, statement, fieldMap) {
|
|
|
2368
2449
|
return;
|
|
2369
2450
|
}
|
|
2370
2451
|
|
|
2371
|
-
const platform = symbolValue(getFieldValue(statement, "
|
|
2452
|
+
const platform = symbolValue(getFieldValue(statement, "type"));
|
|
2372
2453
|
for (const key of SHARED_UI_SEMANTIC_BLOCKS) {
|
|
2373
2454
|
const field = fieldMap.get(key)?.[0];
|
|
2374
2455
|
if (!field || field.value.type !== "block") {
|
|
2375
2456
|
continue;
|
|
2376
2457
|
}
|
|
2377
|
-
if (platform !== "
|
|
2458
|
+
if (platform !== "ui_contract") {
|
|
2378
2459
|
pushError(
|
|
2379
2460
|
errors,
|
|
2380
|
-
`Projection ${statement.id} ${key} belongs on shared UI projections; concrete UI projections may define
|
|
2461
|
+
`Projection ${statement.id} ${key} belongs on shared UI projections; concrete UI projections may define screen_routes and platform surface hints only`,
|
|
2381
2462
|
field.loc
|
|
2382
2463
|
);
|
|
2383
2464
|
}
|
|
2384
2465
|
}
|
|
2385
2466
|
|
|
2386
|
-
const routesField = fieldMap.get("
|
|
2387
|
-
if (routesField?.value.type === "block" && !["
|
|
2467
|
+
const routesField = fieldMap.get("screen_routes")?.[0];
|
|
2468
|
+
if (routesField?.value.type === "block" && !["web_surface", "ios_surface"].includes(platform || "")) {
|
|
2388
2469
|
pushError(
|
|
2389
2470
|
errors,
|
|
2390
|
-
`Projection ${statement.id}
|
|
2471
|
+
`Projection ${statement.id} screen_routes belongs on concrete UI projections; shared UI projections own semantic screens and regions`,
|
|
2391
2472
|
routesField.loc
|
|
2392
2473
|
);
|
|
2393
2474
|
}
|
|
@@ -2398,13 +2479,13 @@ function validateProjectionUiDesign(errors, statement, fieldMap) {
|
|
|
2398
2479
|
return;
|
|
2399
2480
|
}
|
|
2400
2481
|
|
|
2401
|
-
const designField = fieldMap.get("
|
|
2482
|
+
const designField = fieldMap.get("design_tokens")?.[0];
|
|
2402
2483
|
if (!designField || designField.value.type !== "block") {
|
|
2403
2484
|
return;
|
|
2404
2485
|
}
|
|
2405
2486
|
|
|
2406
|
-
if (symbolValue(getFieldValue(statement, "
|
|
2407
|
-
pushError(errors, `Projection ${statement.id}
|
|
2487
|
+
if (symbolValue(getFieldValue(statement, "type")) !== "ui_contract") {
|
|
2488
|
+
pushError(errors, `Projection ${statement.id} design_tokens belongs on shared UI projections; concrete UI projections inherit semantic design intent through 'realizes'`, designField.loc);
|
|
2408
2489
|
}
|
|
2409
2490
|
|
|
2410
2491
|
for (const entry of designField.value.entries) {
|
|
@@ -2413,60 +2494,60 @@ function validateProjectionUiDesign(errors, statement, fieldMap) {
|
|
|
2413
2494
|
|
|
2414
2495
|
if (key === "density") {
|
|
2415
2496
|
if (!UI_DESIGN_DENSITIES.has(value || "")) {
|
|
2416
|
-
pushError(errors, `Projection ${statement.id}
|
|
2497
|
+
pushError(errors, `Projection ${statement.id} design_tokens density has invalid value '${value}'`, entry.loc);
|
|
2417
2498
|
}
|
|
2418
2499
|
if (tokens.length !== 2) {
|
|
2419
|
-
pushError(errors, `Projection ${statement.id}
|
|
2500
|
+
pushError(errors, `Projection ${statement.id} design_tokens density accepts exactly one value`, entry.loc);
|
|
2420
2501
|
}
|
|
2421
2502
|
continue;
|
|
2422
2503
|
}
|
|
2423
2504
|
|
|
2424
2505
|
if (key === "tone") {
|
|
2425
2506
|
if (!UI_DESIGN_TONES.has(value || "")) {
|
|
2426
|
-
pushError(errors, `Projection ${statement.id}
|
|
2507
|
+
pushError(errors, `Projection ${statement.id} design_tokens tone has invalid value '${value}'`, entry.loc);
|
|
2427
2508
|
}
|
|
2428
2509
|
if (tokens.length !== 2) {
|
|
2429
|
-
pushError(errors, `Projection ${statement.id}
|
|
2510
|
+
pushError(errors, `Projection ${statement.id} design_tokens tone accepts exactly one value`, entry.loc);
|
|
2430
2511
|
}
|
|
2431
2512
|
continue;
|
|
2432
2513
|
}
|
|
2433
2514
|
|
|
2434
2515
|
if (key === "radius_scale") {
|
|
2435
2516
|
if (!UI_DESIGN_RADIUS_SCALES.has(value || "")) {
|
|
2436
|
-
pushError(errors, `Projection ${statement.id}
|
|
2517
|
+
pushError(errors, `Projection ${statement.id} design_tokens radius_scale has invalid value '${value}'`, entry.loc);
|
|
2437
2518
|
}
|
|
2438
2519
|
if (tokens.length !== 2) {
|
|
2439
|
-
pushError(errors, `Projection ${statement.id}
|
|
2520
|
+
pushError(errors, `Projection ${statement.id} design_tokens radius_scale accepts exactly one value`, entry.loc);
|
|
2440
2521
|
}
|
|
2441
2522
|
continue;
|
|
2442
2523
|
}
|
|
2443
2524
|
|
|
2444
2525
|
if (key === "color_role") {
|
|
2445
2526
|
if (!UI_DESIGN_COLOR_ROLES.has(value || "")) {
|
|
2446
|
-
pushError(errors, `Projection ${statement.id}
|
|
2527
|
+
pushError(errors, `Projection ${statement.id} design_tokens color_role has invalid role '${value}'`, entry.loc);
|
|
2447
2528
|
}
|
|
2448
2529
|
if (tokens.length !== 3) {
|
|
2449
|
-
pushError(errors, `Projection ${statement.id}
|
|
2530
|
+
pushError(errors, `Projection ${statement.id} design_tokens color_role must use 'color_role <role> <semantic-token>'`, entry.loc);
|
|
2450
2531
|
}
|
|
2451
2532
|
continue;
|
|
2452
2533
|
}
|
|
2453
2534
|
|
|
2454
2535
|
if (key === "typography_role") {
|
|
2455
2536
|
if (!UI_DESIGN_TYPOGRAPHY_ROLES.has(value || "")) {
|
|
2456
|
-
pushError(errors, `Projection ${statement.id}
|
|
2537
|
+
pushError(errors, `Projection ${statement.id} design_tokens typography_role has invalid role '${value}'`, entry.loc);
|
|
2457
2538
|
}
|
|
2458
2539
|
if (tokens.length !== 3) {
|
|
2459
|
-
pushError(errors, `Projection ${statement.id}
|
|
2540
|
+
pushError(errors, `Projection ${statement.id} design_tokens typography_role must use 'typography_role <role> <semantic-token>'`, entry.loc);
|
|
2460
2541
|
}
|
|
2461
2542
|
continue;
|
|
2462
2543
|
}
|
|
2463
2544
|
|
|
2464
2545
|
if (key === "action_role") {
|
|
2465
2546
|
if (!UI_DESIGN_ACTION_ROLES.has(value || "")) {
|
|
2466
|
-
pushError(errors, `Projection ${statement.id}
|
|
2547
|
+
pushError(errors, `Projection ${statement.id} design_tokens action_role has invalid role '${value}'`, entry.loc);
|
|
2467
2548
|
}
|
|
2468
2549
|
if (tokens.length !== 3) {
|
|
2469
|
-
pushError(errors, `Projection ${statement.id}
|
|
2550
|
+
pushError(errors, `Projection ${statement.id} design_tokens action_role must use 'action_role <role> <semantic-token>'`, entry.loc);
|
|
2470
2551
|
}
|
|
2471
2552
|
continue;
|
|
2472
2553
|
}
|
|
@@ -2474,17 +2555,17 @@ function validateProjectionUiDesign(errors, statement, fieldMap) {
|
|
|
2474
2555
|
if (key === "accessibility") {
|
|
2475
2556
|
const values = UI_DESIGN_ACCESSIBILITY_VALUES[value];
|
|
2476
2557
|
if (tokens.length !== 3) {
|
|
2477
|
-
pushError(errors, `Projection ${statement.id}
|
|
2558
|
+
pushError(errors, `Projection ${statement.id} design_tokens accessibility must use 'accessibility <setting> <value>'`, entry.loc);
|
|
2478
2559
|
}
|
|
2479
2560
|
if (!values) {
|
|
2480
|
-
pushError(errors, `Projection ${statement.id}
|
|
2561
|
+
pushError(errors, `Projection ${statement.id} design_tokens accessibility has invalid setting '${value}'`, entry.loc);
|
|
2481
2562
|
} else if (!values.has(extra || "")) {
|
|
2482
|
-
pushError(errors, `Projection ${statement.id}
|
|
2563
|
+
pushError(errors, `Projection ${statement.id} design_tokens accessibility '${value}' has invalid value '${extra}'`, entry.loc);
|
|
2483
2564
|
}
|
|
2484
2565
|
continue;
|
|
2485
2566
|
}
|
|
2486
2567
|
|
|
2487
|
-
pushError(errors, `Projection ${statement.id}
|
|
2568
|
+
pushError(errors, `Projection ${statement.id} design_tokens has unknown key '${key}'`, entry.loc);
|
|
2488
2569
|
}
|
|
2489
2570
|
}
|
|
2490
2571
|
|
|
@@ -2493,7 +2574,7 @@ function validateProjectionUiNavigation(errors, statement, fieldMap, registry) {
|
|
|
2493
2574
|
return;
|
|
2494
2575
|
}
|
|
2495
2576
|
|
|
2496
|
-
const navigationField = fieldMap.get("
|
|
2577
|
+
const navigationField = fieldMap.get("navigation")?.[0];
|
|
2497
2578
|
if (!navigationField || navigationField.value.type !== "block") {
|
|
2498
2579
|
return;
|
|
2499
2580
|
}
|
|
@@ -2507,58 +2588,58 @@ function validateProjectionUiNavigation(errors, statement, fieldMap, registry) {
|
|
|
2507
2588
|
|
|
2508
2589
|
if (targetKind === "group") {
|
|
2509
2590
|
if (!targetId || !IDENTIFIER_PATTERN.test(targetId)) {
|
|
2510
|
-
pushError(errors, `Projection ${statement.id}
|
|
2591
|
+
pushError(errors, `Projection ${statement.id} navigation group entries must include a valid group id`, entry.loc);
|
|
2511
2592
|
continue;
|
|
2512
2593
|
}
|
|
2513
2594
|
groups.add(targetId);
|
|
2514
|
-
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `
|
|
2595
|
+
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `navigation group '${targetId}'`);
|
|
2515
2596
|
for (const key of directives.keys()) {
|
|
2516
2597
|
if (!["label", "placement", "icon", "order", "pattern"].includes(key)) {
|
|
2517
|
-
pushError(errors, `Projection ${statement.id}
|
|
2598
|
+
pushError(errors, `Projection ${statement.id} navigation group '${targetId}' has unknown directive '${key}'`, entry.loc);
|
|
2518
2599
|
}
|
|
2519
2600
|
}
|
|
2520
2601
|
if (directives.has("placement") && !["primary", "secondary", "utility"].includes(directives.get("placement"))) {
|
|
2521
|
-
pushError(errors, `Projection ${statement.id}
|
|
2602
|
+
pushError(errors, `Projection ${statement.id} navigation group '${targetId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
|
|
2522
2603
|
}
|
|
2523
2604
|
if (directives.has("pattern") && !UI_NAVIGATION_PATTERNS.has(directives.get("pattern"))) {
|
|
2524
|
-
pushError(errors, `Projection ${statement.id}
|
|
2605
|
+
pushError(errors, `Projection ${statement.id} navigation group '${targetId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
|
|
2525
2606
|
}
|
|
2526
2607
|
continue;
|
|
2527
2608
|
}
|
|
2528
2609
|
|
|
2529
2610
|
if (targetKind === "screen") {
|
|
2530
2611
|
if (!availableScreens.has(targetId)) {
|
|
2531
|
-
pushError(errors, `Projection ${statement.id}
|
|
2612
|
+
pushError(errors, `Projection ${statement.id} navigation references unknown screen '${targetId}'`, entry.loc);
|
|
2532
2613
|
}
|
|
2533
|
-
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `
|
|
2614
|
+
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `navigation screen '${targetId}'`);
|
|
2534
2615
|
for (const key of directives.keys()) {
|
|
2535
2616
|
if (!["group", "label", "order", "visible", "default", "breadcrumb", "sitemap", "placement", "pattern"].includes(key)) {
|
|
2536
|
-
pushError(errors, `Projection ${statement.id}
|
|
2617
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has unknown directive '${key}'`, entry.loc);
|
|
2537
2618
|
}
|
|
2538
2619
|
}
|
|
2539
2620
|
if (directives.has("visible") && !["true", "false"].includes(directives.get("visible"))) {
|
|
2540
|
-
pushError(errors, `Projection ${statement.id}
|
|
2621
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid visible '${directives.get("visible")}'`, entry.loc);
|
|
2541
2622
|
}
|
|
2542
2623
|
if (directives.has("default") && !["true", "false"].includes(directives.get("default"))) {
|
|
2543
|
-
pushError(errors, `Projection ${statement.id}
|
|
2624
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid default '${directives.get("default")}'`, entry.loc);
|
|
2544
2625
|
}
|
|
2545
2626
|
if (directives.has("placement") && !["primary", "secondary", "utility"].includes(directives.get("placement"))) {
|
|
2546
|
-
pushError(errors, `Projection ${statement.id}
|
|
2627
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
|
|
2547
2628
|
}
|
|
2548
2629
|
if (directives.has("sitemap") && !["include", "exclude"].includes(directives.get("sitemap"))) {
|
|
2549
|
-
pushError(errors, `Projection ${statement.id}
|
|
2630
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid sitemap '${directives.get("sitemap")}'`, entry.loc);
|
|
2550
2631
|
}
|
|
2551
2632
|
if (directives.has("pattern") && !UI_NAVIGATION_PATTERNS.has(directives.get("pattern"))) {
|
|
2552
|
-
pushError(errors, `Projection ${statement.id}
|
|
2633
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
|
|
2553
2634
|
}
|
|
2554
2635
|
const breadcrumb = directives.get("breadcrumb");
|
|
2555
2636
|
if (breadcrumb && breadcrumb !== "none" && !availableScreens.has(breadcrumb)) {
|
|
2556
|
-
pushError(errors, `Projection ${statement.id}
|
|
2637
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' references unknown breadcrumb screen '${breadcrumb}'`, entry.loc);
|
|
2557
2638
|
}
|
|
2558
2639
|
continue;
|
|
2559
2640
|
}
|
|
2560
2641
|
|
|
2561
|
-
pushError(errors, `Projection ${statement.id}
|
|
2642
|
+
pushError(errors, `Projection ${statement.id} navigation entries must start with 'group' or 'screen'`, entry.loc);
|
|
2562
2643
|
}
|
|
2563
2644
|
|
|
2564
2645
|
for (const entry of navigationField.value.entries) {
|
|
@@ -2568,7 +2649,7 @@ function validateProjectionUiNavigation(errors, statement, fieldMap, registry) {
|
|
|
2568
2649
|
}
|
|
2569
2650
|
const directives = parseUiDirectiveMap(tokens, 2, [], statement, entry, "");
|
|
2570
2651
|
if (directives.has("group") && !groups.has(directives.get("group"))) {
|
|
2571
|
-
pushError(errors, `Projection ${statement.id}
|
|
2652
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${tokens[1]}' references unknown group '${directives.get("group")}'`, entry.loc);
|
|
2572
2653
|
}
|
|
2573
2654
|
}
|
|
2574
2655
|
}
|
|
@@ -2578,7 +2659,7 @@ function validateProjectionUiScreenRegions(errors, statement, fieldMap, registry
|
|
|
2578
2659
|
return;
|
|
2579
2660
|
}
|
|
2580
2661
|
|
|
2581
|
-
const regionField = fieldMap.get("
|
|
2662
|
+
const regionField = fieldMap.get("screen_regions")?.[0];
|
|
2582
2663
|
if (!regionField || regionField.value.type !== "block") {
|
|
2583
2664
|
return;
|
|
2584
2665
|
}
|
|
@@ -2589,33 +2670,33 @@ function validateProjectionUiScreenRegions(errors, statement, fieldMap, registry
|
|
|
2589
2670
|
const [keyword, screenId, regionKeyword, regionName] = tokens;
|
|
2590
2671
|
|
|
2591
2672
|
if (keyword !== "screen") {
|
|
2592
|
-
pushError(errors, `Projection ${statement.id}
|
|
2673
|
+
pushError(errors, `Projection ${statement.id} screen_regions entries must start with 'screen'`, entry.loc);
|
|
2593
2674
|
continue;
|
|
2594
2675
|
}
|
|
2595
2676
|
if (!availableScreens.has(screenId)) {
|
|
2596
|
-
pushError(errors, `Projection ${statement.id}
|
|
2677
|
+
pushError(errors, `Projection ${statement.id} screen_regions references unknown screen '${screenId}'`, entry.loc);
|
|
2597
2678
|
}
|
|
2598
2679
|
if (regionKeyword !== "region") {
|
|
2599
|
-
pushError(errors, `Projection ${statement.id}
|
|
2680
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' must use 'region'`, entry.loc);
|
|
2600
2681
|
}
|
|
2601
2682
|
if (!UI_REGION_KINDS.has(regionName || "")) {
|
|
2602
|
-
pushError(errors, `Projection ${statement.id}
|
|
2683
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid region '${regionName}'`, entry.loc);
|
|
2603
2684
|
}
|
|
2604
2685
|
|
|
2605
|
-
const directives = parseUiDirectiveMap(tokens, 4, errors, statement, entry, `
|
|
2686
|
+
const directives = parseUiDirectiveMap(tokens, 4, errors, statement, entry, `screen_regions for '${screenId}'`);
|
|
2606
2687
|
for (const key of directives.keys()) {
|
|
2607
2688
|
if (!["pattern", "placement", "title", "state", "variant"].includes(key)) {
|
|
2608
|
-
pushError(errors, `Projection ${statement.id}
|
|
2689
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has unknown directive '${key}'`, entry.loc);
|
|
2609
2690
|
}
|
|
2610
2691
|
}
|
|
2611
2692
|
if (directives.has("pattern") && !UI_PATTERN_KINDS.has(directives.get("pattern"))) {
|
|
2612
|
-
pushError(errors, `Projection ${statement.id}
|
|
2693
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
|
|
2613
2694
|
}
|
|
2614
2695
|
if (directives.has("placement") && !["primary", "secondary", "supporting"].includes(directives.get("placement"))) {
|
|
2615
|
-
pushError(errors, `Projection ${statement.id}
|
|
2696
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
|
|
2616
2697
|
}
|
|
2617
2698
|
if (directives.has("state") && !UI_STATE_KINDS.has(directives.get("state"))) {
|
|
2618
|
-
pushError(errors, `Projection ${statement.id}
|
|
2699
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid state '${directives.get("state")}'`, entry.loc);
|
|
2619
2700
|
}
|
|
2620
2701
|
}
|
|
2621
2702
|
}
|
|
@@ -2625,13 +2706,13 @@ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
|
2625
2706
|
return;
|
|
2626
2707
|
}
|
|
2627
2708
|
|
|
2628
|
-
const componentsField = fieldMap.get("
|
|
2709
|
+
const componentsField = fieldMap.get("widget_bindings")?.[0];
|
|
2629
2710
|
if (!componentsField || componentsField.value.type !== "block") {
|
|
2630
2711
|
return;
|
|
2631
2712
|
}
|
|
2632
2713
|
|
|
2633
|
-
if (symbolValue(getFieldValue(statement, "
|
|
2634
|
-
pushError(errors, `Projection ${statement.id}
|
|
2714
|
+
if (symbolValue(getFieldValue(statement, "type")) !== "ui_contract") {
|
|
2715
|
+
pushError(errors, `Projection ${statement.id} widget_bindings belongs on shared UI projections; concrete UI projections inherit widget placement through 'realizes'`, componentsField.loc);
|
|
2635
2716
|
}
|
|
2636
2717
|
|
|
2637
2718
|
const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
|
|
@@ -2643,48 +2724,48 @@ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
|
2643
2724
|
const [screenKeyword, screenId, regionKeyword, regionName, componentKeyword, componentId] = tokens;
|
|
2644
2725
|
|
|
2645
2726
|
if (screenKeyword !== "screen") {
|
|
2646
|
-
pushError(errors, `Projection ${statement.id}
|
|
2727
|
+
pushError(errors, `Projection ${statement.id} widget_bindings entries must start with 'screen'`, entry.loc);
|
|
2647
2728
|
continue;
|
|
2648
2729
|
}
|
|
2649
2730
|
if (!availableScreens.has(screenId)) {
|
|
2650
|
-
pushError(errors, `Projection ${statement.id}
|
|
2731
|
+
pushError(errors, `Projection ${statement.id} widget_bindings references unknown screen '${screenId}'`, entry.loc);
|
|
2651
2732
|
}
|
|
2652
2733
|
if (regionKeyword !== "region") {
|
|
2653
|
-
pushError(errors, `Projection ${statement.id}
|
|
2734
|
+
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' must use 'region'`, entry.loc);
|
|
2654
2735
|
}
|
|
2655
2736
|
if (!UI_REGION_KINDS.has(regionName || "")) {
|
|
2656
|
-
pushError(errors, `Projection ${statement.id}
|
|
2737
|
+
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' has invalid region '${regionName}'`, entry.loc);
|
|
2657
2738
|
} else if (!availableRegions.has(`${screenId}:${regionName}`)) {
|
|
2658
|
-
pushError(errors, `Projection ${statement.id}
|
|
2739
|
+
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' references undeclared region '${regionName}'`, entry.loc);
|
|
2659
2740
|
}
|
|
2660
|
-
if (componentKeyword !== "
|
|
2661
|
-
pushError(errors, `Projection ${statement.id}
|
|
2741
|
+
if (componentKeyword !== "widget") {
|
|
2742
|
+
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' must use 'widget'`, entry.loc);
|
|
2662
2743
|
}
|
|
2663
2744
|
|
|
2664
|
-
const
|
|
2665
|
-
if (!
|
|
2666
|
-
pushError(errors, `Projection ${statement.id}
|
|
2745
|
+
const widget = registry.get(componentId);
|
|
2746
|
+
if (!widget) {
|
|
2747
|
+
pushError(errors, `Projection ${statement.id} widget_bindings references missing widget '${componentId}'`, entry.loc);
|
|
2667
2748
|
continue;
|
|
2668
2749
|
}
|
|
2669
|
-
if (
|
|
2670
|
-
pushError(errors, `Projection ${statement.id}
|
|
2750
|
+
if (widget.kind !== "widget") {
|
|
2751
|
+
pushError(errors, `Projection ${statement.id} widget_bindings must reference a widget, found ${widget.kind} '${widget.id}'`, entry.loc);
|
|
2671
2752
|
continue;
|
|
2672
2753
|
}
|
|
2673
2754
|
|
|
2674
|
-
const propNames = new Set(blockEntries(getFieldValue(
|
|
2755
|
+
const propNames = new Set(blockEntries(getFieldValue(widget, "props"))
|
|
2675
2756
|
.map((propEntry) => propEntry.items[0])
|
|
2676
2757
|
.filter((item) => item?.type === "symbol")
|
|
2677
2758
|
.map((item) => item.value));
|
|
2678
|
-
const eventNames = new Set(blockEntries(getFieldValue(
|
|
2759
|
+
const eventNames = new Set(blockEntries(getFieldValue(widget, "events"))
|
|
2679
2760
|
.map((eventEntry) => eventEntry.items[0])
|
|
2680
2761
|
.filter((item) => item?.type === "symbol")
|
|
2681
2762
|
.map((item) => item.value));
|
|
2682
|
-
const componentRegions = symbolValues(getFieldValue(
|
|
2683
|
-
const componentPatterns = symbolValues(getFieldValue(
|
|
2763
|
+
const componentRegions = symbolValues(getFieldValue(widget, "regions"));
|
|
2764
|
+
const componentPatterns = symbolValues(getFieldValue(widget, "patterns"));
|
|
2684
2765
|
if (componentRegions.length > 0 && !componentRegions.includes(regionName)) {
|
|
2685
2766
|
pushError(
|
|
2686
2767
|
errors,
|
|
2687
|
-
`Projection ${statement.id}
|
|
2768
|
+
`Projection ${statement.id} widget_bindings uses widget '${componentId}' in region '${regionName}', but the widget supports regions [${componentRegions.join(", ")}]`,
|
|
2688
2769
|
entry.loc
|
|
2689
2770
|
);
|
|
2690
2771
|
}
|
|
@@ -2692,7 +2773,7 @@ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
|
2692
2773
|
if (regionPattern && componentPatterns.length > 0 && !componentPatterns.includes(regionPattern)) {
|
|
2693
2774
|
pushError(
|
|
2694
2775
|
errors,
|
|
2695
|
-
`Projection ${statement.id}
|
|
2776
|
+
`Projection ${statement.id} widget_bindings uses widget '${componentId}' in '${screenId}:${regionName}' with pattern '${regionPattern}', but the widget supports patterns [${componentPatterns.join(", ")}]`,
|
|
2696
2777
|
entry.loc
|
|
2697
2778
|
);
|
|
2698
2779
|
}
|
|
@@ -2704,15 +2785,15 @@ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
|
2704
2785
|
const fromKeyword = tokens[i + 2];
|
|
2705
2786
|
const sourceId = tokens[i + 3];
|
|
2706
2787
|
if (!propName || fromKeyword !== "from" || !sourceId) {
|
|
2707
|
-
pushError(errors, `Projection ${statement.id}
|
|
2788
|
+
pushError(errors, `Projection ${statement.id} widget_bindings data bindings must use 'data <prop> from <source>'`, entry.loc);
|
|
2708
2789
|
break;
|
|
2709
2790
|
}
|
|
2710
2791
|
if (!propNames.has(propName)) {
|
|
2711
|
-
pushError(errors, `Projection ${statement.id}
|
|
2792
|
+
pushError(errors, `Projection ${statement.id} widget_bindings references unknown prop '${propName}' on widget '${componentId}'`, entry.loc);
|
|
2712
2793
|
}
|
|
2713
2794
|
const source = registry.get(sourceId);
|
|
2714
2795
|
if (!source || !["capability", "projection", "shape", "entity"].includes(source.kind)) {
|
|
2715
|
-
pushError(errors, `Projection ${statement.id}
|
|
2796
|
+
pushError(errors, `Projection ${statement.id} widget_bindings data binding for '${propName}' references missing source '${sourceId}'`, entry.loc);
|
|
2716
2797
|
}
|
|
2717
2798
|
i += 4;
|
|
2718
2799
|
continue;
|
|
@@ -2723,29 +2804,29 @@ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
|
2723
2804
|
const action = tokens[i + 2];
|
|
2724
2805
|
const targetId = tokens[i + 3];
|
|
2725
2806
|
if (!eventName || !action || !targetId) {
|
|
2726
|
-
pushError(errors, `Projection ${statement.id}
|
|
2807
|
+
pushError(errors, `Projection ${statement.id} widget_bindings event bindings must use 'event <event> <navigate|action> <target>'`, entry.loc);
|
|
2727
2808
|
break;
|
|
2728
2809
|
}
|
|
2729
2810
|
if (!eventNames.has(eventName)) {
|
|
2730
|
-
pushError(errors, `Projection ${statement.id}
|
|
2811
|
+
pushError(errors, `Projection ${statement.id} widget_bindings references unknown event '${eventName}' on widget '${componentId}'`, entry.loc);
|
|
2731
2812
|
}
|
|
2732
2813
|
if (action === "navigate") {
|
|
2733
2814
|
if (!availableScreens.has(targetId)) {
|
|
2734
|
-
pushError(errors, `Projection ${statement.id}
|
|
2815
|
+
pushError(errors, `Projection ${statement.id} widget_bindings event '${eventName}' references unknown navigation target '${targetId}'`, entry.loc);
|
|
2735
2816
|
}
|
|
2736
2817
|
} else if (action === "action") {
|
|
2737
2818
|
const target = registry.get(targetId);
|
|
2738
2819
|
if (!target || target.kind !== "capability") {
|
|
2739
|
-
pushError(errors, `Projection ${statement.id}
|
|
2820
|
+
pushError(errors, `Projection ${statement.id} widget_bindings event '${eventName}' references missing capability action '${targetId}'`, entry.loc);
|
|
2740
2821
|
}
|
|
2741
2822
|
} else {
|
|
2742
|
-
pushError(errors, `Projection ${statement.id}
|
|
2823
|
+
pushError(errors, `Projection ${statement.id} widget_bindings event '${eventName}' has unsupported action '${action}'`, entry.loc);
|
|
2743
2824
|
}
|
|
2744
2825
|
i += 4;
|
|
2745
2826
|
continue;
|
|
2746
2827
|
}
|
|
2747
2828
|
|
|
2748
|
-
pushError(errors, `Projection ${statement.id}
|
|
2829
|
+
pushError(errors, `Projection ${statement.id} widget_bindings has unknown directive '${directive}'`, entry.loc);
|
|
2749
2830
|
break;
|
|
2750
2831
|
}
|
|
2751
2832
|
}
|
|
@@ -2756,7 +2837,7 @@ function validateProjectionUiVisibility(errors, statement, fieldMap, registry) {
|
|
|
2756
2837
|
return;
|
|
2757
2838
|
}
|
|
2758
2839
|
|
|
2759
|
-
const visibilityField = fieldMap.get("
|
|
2840
|
+
const visibilityField = fieldMap.get("visibility_rules")?.[0];
|
|
2760
2841
|
if (!visibilityField || visibilityField.value.type !== "block") {
|
|
2761
2842
|
return;
|
|
2762
2843
|
}
|
|
@@ -2767,30 +2848,30 @@ function validateProjectionUiVisibility(errors, statement, fieldMap, registry) {
|
|
|
2767
2848
|
const [keyword, capabilityId, predicateKeyword, predicateType, predicateValue] = tokens;
|
|
2768
2849
|
|
|
2769
2850
|
if (keyword !== "action") {
|
|
2770
|
-
pushError(errors, `Projection ${statement.id}
|
|
2851
|
+
pushError(errors, `Projection ${statement.id} visibility_rules entries must start with 'action'`, entry.loc);
|
|
2771
2852
|
continue;
|
|
2772
2853
|
}
|
|
2773
2854
|
|
|
2774
2855
|
const capability = registry.get(capabilityId);
|
|
2775
2856
|
if (!capability) {
|
|
2776
|
-
pushError(errors, `Projection ${statement.id}
|
|
2857
|
+
pushError(errors, `Projection ${statement.id} visibility_rules references missing capability '${capabilityId}'`, entry.loc);
|
|
2777
2858
|
} else if (capability.kind !== "capability") {
|
|
2778
|
-
pushError(errors, `Projection ${statement.id}
|
|
2859
|
+
pushError(errors, `Projection ${statement.id} visibility_rules must reference a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
2779
2860
|
} else if (!realized.has(capabilityId)) {
|
|
2780
|
-
pushError(errors, `Projection ${statement.id}
|
|
2861
|
+
pushError(errors, `Projection ${statement.id} visibility_rules action '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
2781
2862
|
}
|
|
2782
2863
|
|
|
2783
2864
|
if (predicateKeyword !== "visible_if") {
|
|
2784
|
-
pushError(errors, `Projection ${statement.id}
|
|
2865
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' must use 'visible_if'`, entry.loc);
|
|
2785
2866
|
}
|
|
2786
2867
|
if (!["permission", "ownership", "claim"].includes(predicateType || "")) {
|
|
2787
|
-
pushError(errors, `Projection ${statement.id}
|
|
2868
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' has invalid predicate '${predicateType}'`, entry.loc);
|
|
2788
2869
|
}
|
|
2789
2870
|
if (!predicateValue) {
|
|
2790
|
-
pushError(errors, `Projection ${statement.id}
|
|
2871
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' must include a predicate value`, entry.loc);
|
|
2791
2872
|
}
|
|
2792
2873
|
if (predicateType === "ownership" && !["owner", "owner_or_admin", "project_member", "none"].includes(predicateValue || "")) {
|
|
2793
|
-
pushError(errors, `Projection ${statement.id}
|
|
2874
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' has invalid ownership '${predicateValue}'`, entry.loc);
|
|
2794
2875
|
}
|
|
2795
2876
|
const directiveTokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2796
2877
|
const directives = new Map();
|
|
@@ -2798,18 +2879,18 @@ function validateProjectionUiVisibility(errors, statement, fieldMap, registry) {
|
|
|
2798
2879
|
const key = directiveTokens[i];
|
|
2799
2880
|
const value = directiveTokens[i + 1];
|
|
2800
2881
|
if (!value) {
|
|
2801
|
-
pushError(errors, `Projection ${statement.id}
|
|
2882
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
2802
2883
|
continue;
|
|
2803
2884
|
}
|
|
2804
2885
|
directives.set(key, value);
|
|
2805
2886
|
}
|
|
2806
2887
|
for (const key of directives.keys()) {
|
|
2807
2888
|
if (!["claim_value"].includes(key)) {
|
|
2808
|
-
pushError(errors, `Projection ${statement.id}
|
|
2889
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
2809
2890
|
}
|
|
2810
2891
|
}
|
|
2811
2892
|
if (directives.get("claim_value") && predicateType !== "claim") {
|
|
2812
|
-
pushError(errors, `Projection ${statement.id}
|
|
2893
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' cannot declare claim_value without claim`, entry.loc);
|
|
2813
2894
|
}
|
|
2814
2895
|
}
|
|
2815
2896
|
}
|
|
@@ -2819,7 +2900,7 @@ function validateProjectionUiLookups(errors, statement, fieldMap, registry) {
|
|
|
2819
2900
|
return;
|
|
2820
2901
|
}
|
|
2821
2902
|
|
|
2822
|
-
const lookupsField = fieldMap.get("
|
|
2903
|
+
const lookupsField = fieldMap.get("field_lookups")?.[0];
|
|
2823
2904
|
if (!lookupsField || lookupsField.value.type !== "block") {
|
|
2824
2905
|
return;
|
|
2825
2906
|
}
|
|
@@ -2831,56 +2912,56 @@ function validateProjectionUiLookups(errors, statement, fieldMap, registry) {
|
|
|
2831
2912
|
const [keyword, screenId, fieldKeyword, fieldName, entityKeyword, entityId, labelKeyword, labelField, maybeEmptyKeyword, maybeEmptyLabel] = tokens;
|
|
2832
2913
|
|
|
2833
2914
|
if (keyword !== "screen") {
|
|
2834
|
-
pushError(errors, `Projection ${statement.id}
|
|
2915
|
+
pushError(errors, `Projection ${statement.id} field_lookups entries must start with 'screen'`, entry.loc);
|
|
2835
2916
|
continue;
|
|
2836
2917
|
}
|
|
2837
2918
|
|
|
2838
2919
|
const screenEntry = screens.get(screenId);
|
|
2839
2920
|
if (!screenEntry) {
|
|
2840
|
-
pushError(errors, `Projection ${statement.id}
|
|
2921
|
+
pushError(errors, `Projection ${statement.id} field_lookups references unknown screen '${screenId}'`, entry.loc);
|
|
2841
2922
|
continue;
|
|
2842
2923
|
}
|
|
2843
2924
|
|
|
2844
2925
|
if (fieldKeyword !== "field") {
|
|
2845
|
-
pushError(errors, `Projection ${statement.id}
|
|
2926
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must use 'field'`, entry.loc);
|
|
2846
2927
|
}
|
|
2847
2928
|
if (!fieldName) {
|
|
2848
|
-
pushError(errors, `Projection ${statement.id}
|
|
2929
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must include a field name`, entry.loc);
|
|
2849
2930
|
}
|
|
2850
2931
|
|
|
2851
2932
|
if (entityKeyword !== "entity") {
|
|
2852
|
-
pushError(errors, `Projection ${statement.id}
|
|
2933
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must use 'entity'`, entry.loc);
|
|
2853
2934
|
}
|
|
2854
2935
|
const entity = entityId ? registry.get(entityId) : null;
|
|
2855
2936
|
if (!entity) {
|
|
2856
|
-
pushError(errors, `Projection ${statement.id}
|
|
2937
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' references missing entity '${entityId}'`, entry.loc);
|
|
2857
2938
|
} else if (entity.kind !== "entity") {
|
|
2858
|
-
pushError(errors, `Projection ${statement.id}
|
|
2939
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must reference an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
2859
2940
|
}
|
|
2860
2941
|
|
|
2861
2942
|
if (labelKeyword !== "label_field") {
|
|
2862
|
-
pushError(errors, `Projection ${statement.id}
|
|
2943
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must use 'label_field'`, entry.loc);
|
|
2863
2944
|
}
|
|
2864
2945
|
if (!labelField) {
|
|
2865
|
-
pushError(errors, `Projection ${statement.id}
|
|
2946
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must include a label_field`, entry.loc);
|
|
2866
2947
|
}
|
|
2867
2948
|
|
|
2868
2949
|
if (maybeEmptyKeyword && maybeEmptyKeyword !== "empty_label") {
|
|
2869
|
-
pushError(errors, `Projection ${statement.id}
|
|
2950
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' has unknown directive '${maybeEmptyKeyword}'`, entry.loc);
|
|
2870
2951
|
}
|
|
2871
2952
|
if (maybeEmptyKeyword === "empty_label" && !maybeEmptyLabel) {
|
|
2872
|
-
pushError(errors, `Projection ${statement.id}
|
|
2953
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must include a value for 'empty_label'`, entry.loc);
|
|
2873
2954
|
}
|
|
2874
2955
|
|
|
2875
2956
|
const availableFields = resolveProjectionUiScreenFieldNames(registry, screenEntry, statement);
|
|
2876
2957
|
if (fieldName && availableFields.size > 0 && !availableFields.has(fieldName)) {
|
|
2877
|
-
pushError(errors, `Projection ${statement.id}
|
|
2958
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' references unknown screen field '${fieldName}'`, entry.loc);
|
|
2878
2959
|
}
|
|
2879
2960
|
|
|
2880
2961
|
if (entity?.kind === "entity") {
|
|
2881
2962
|
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
2882
2963
|
if (labelField && !entityFieldNames.has(labelField)) {
|
|
2883
|
-
pushError(errors, `Projection ${statement.id}
|
|
2964
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' references unknown entity field '${labelField}' on '${entity.id}'`, entry.loc);
|
|
2884
2965
|
}
|
|
2885
2966
|
}
|
|
2886
2967
|
}
|
|
@@ -2891,38 +2972,38 @@ function validateProjectionUiRoutes(errors, statement, fieldMap, registry) {
|
|
|
2891
2972
|
return;
|
|
2892
2973
|
}
|
|
2893
2974
|
|
|
2894
|
-
const routesField = fieldMap.get("
|
|
2975
|
+
const routesField = fieldMap.get("screen_routes")?.[0];
|
|
2895
2976
|
if (!routesField || routesField.value.type !== "block") {
|
|
2896
2977
|
return;
|
|
2897
2978
|
}
|
|
2898
2979
|
|
|
2899
2980
|
const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
|
|
2900
2981
|
const seenPaths = new Set();
|
|
2901
|
-
const platform = symbolValue(getFieldValue(statement, "
|
|
2982
|
+
const platform = symbolValue(getFieldValue(statement, "type"));
|
|
2902
2983
|
|
|
2903
2984
|
for (const entry of routesField.value.entries) {
|
|
2904
2985
|
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2905
2986
|
const [keyword, screenId, pathKeyword, routePath] = tokens;
|
|
2906
2987
|
|
|
2907
2988
|
if (keyword !== "screen") {
|
|
2908
|
-
pushError(errors, `Projection ${statement.id}
|
|
2989
|
+
pushError(errors, `Projection ${statement.id} screen_routes entries must start with 'screen'`, entry.loc);
|
|
2909
2990
|
continue;
|
|
2910
2991
|
}
|
|
2911
2992
|
if (!availableScreens.has(screenId)) {
|
|
2912
|
-
pushError(errors, `Projection ${statement.id}
|
|
2993
|
+
pushError(errors, `Projection ${statement.id} screen_routes references unknown screen '${screenId}'`, entry.loc);
|
|
2913
2994
|
}
|
|
2914
2995
|
if (pathKeyword !== "path") {
|
|
2915
|
-
pushError(errors, `Projection ${statement.id}
|
|
2996
|
+
pushError(errors, `Projection ${statement.id} screen_routes for '${screenId}' must use 'path'`, entry.loc);
|
|
2916
2997
|
}
|
|
2917
2998
|
if (!routePath) {
|
|
2918
|
-
pushError(errors, `Projection ${statement.id}
|
|
2999
|
+
pushError(errors, `Projection ${statement.id} screen_routes for '${screenId}' must include a path`, entry.loc);
|
|
2919
3000
|
continue;
|
|
2920
3001
|
}
|
|
2921
|
-
if ((platform === "
|
|
2922
|
-
pushError(errors, `Projection ${statement.id}
|
|
3002
|
+
if ((platform === "web_surface" || platform === "ios_surface") && !routePath.startsWith("/")) {
|
|
3003
|
+
pushError(errors, `Projection ${statement.id} screen_routes for '${screenId}' must use an absolute path`, entry.loc);
|
|
2923
3004
|
}
|
|
2924
3005
|
if (seenPaths.has(routePath)) {
|
|
2925
|
-
pushError(errors, `Projection ${statement.id}
|
|
3006
|
+
pushError(errors, `Projection ${statement.id} screen_routes has duplicate path '${routePath}'`, entry.loc);
|
|
2926
3007
|
}
|
|
2927
3008
|
seenPaths.add(routePath);
|
|
2928
3009
|
}
|
|
@@ -2938,7 +3019,7 @@ function validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry,
|
|
|
2938
3019
|
return;
|
|
2939
3020
|
}
|
|
2940
3021
|
|
|
2941
|
-
const platform = symbolValue(getFieldValue(statement, "
|
|
3022
|
+
const platform = symbolValue(getFieldValue(statement, "type"));
|
|
2942
3023
|
if (platform !== expectedPlatform) {
|
|
2943
3024
|
pushError(errors, `Projection ${statement.id} may only use '${surfaceBlockKey}' when platform is '${expectedPlatform}'`, surfaceField.loc);
|
|
2944
3025
|
return;
|
|
@@ -3010,11 +3091,11 @@ function validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry,
|
|
|
3010
3091
|
}
|
|
3011
3092
|
|
|
3012
3093
|
function validateProjectionUiWeb(errors, statement, fieldMap, registry) {
|
|
3013
|
-
validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "
|
|
3094
|
+
validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "web_hints", "web_surface");
|
|
3014
3095
|
}
|
|
3015
3096
|
|
|
3016
3097
|
function validateProjectionUiIos(errors, statement, fieldMap, registry) {
|
|
3017
|
-
validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "
|
|
3098
|
+
validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "ios_hints", "ios_surface");
|
|
3018
3099
|
}
|
|
3019
3100
|
|
|
3020
3101
|
function validateProjectionGeneratorDefaults(errors, statement, fieldMap) {
|
|
@@ -3055,7 +3136,7 @@ function validateProjectionDbTables(errors, statement, fieldMap, registry) {
|
|
|
3055
3136
|
return;
|
|
3056
3137
|
}
|
|
3057
3138
|
|
|
3058
|
-
const dbTablesField = fieldMap.get("
|
|
3139
|
+
const dbTablesField = fieldMap.get("tables")?.[0];
|
|
3059
3140
|
if (!dbTablesField || dbTablesField.value.type !== "block") {
|
|
3060
3141
|
return;
|
|
3061
3142
|
}
|
|
@@ -3068,22 +3149,22 @@ function validateProjectionDbTables(errors, statement, fieldMap, registry) {
|
|
|
3068
3149
|
const entity = registry.get(entityId);
|
|
3069
3150
|
|
|
3070
3151
|
if (!entity) {
|
|
3071
|
-
pushError(errors, `Projection ${statement.id}
|
|
3152
|
+
pushError(errors, `Projection ${statement.id} tables references missing entity '${entityId}'`, entry.loc);
|
|
3072
3153
|
continue;
|
|
3073
3154
|
}
|
|
3074
3155
|
if (entity.kind !== "entity") {
|
|
3075
|
-
pushError(errors, `Projection ${statement.id}
|
|
3156
|
+
pushError(errors, `Projection ${statement.id} tables must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3076
3157
|
}
|
|
3077
3158
|
if (!realized.has(entityId)) {
|
|
3078
|
-
pushError(errors, `Projection ${statement.id}
|
|
3159
|
+
pushError(errors, `Projection ${statement.id} tables entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3079
3160
|
}
|
|
3080
3161
|
if (tableKeyword !== "table") {
|
|
3081
|
-
pushError(errors, `Projection ${statement.id}
|
|
3162
|
+
pushError(errors, `Projection ${statement.id} tables for '${entityId}' must use 'table'`, entry.loc);
|
|
3082
3163
|
}
|
|
3083
3164
|
if (!tableName) {
|
|
3084
|
-
pushError(errors, `Projection ${statement.id}
|
|
3165
|
+
pushError(errors, `Projection ${statement.id} tables for '${entityId}' must include a table name`, entry.loc);
|
|
3085
3166
|
} else if (seenTables.has(tableName)) {
|
|
3086
|
-
pushError(errors, `Projection ${statement.id}
|
|
3167
|
+
pushError(errors, `Projection ${statement.id} tables has duplicate table name '${tableName}'`, entry.loc);
|
|
3087
3168
|
}
|
|
3088
3169
|
seenTables.add(tableName);
|
|
3089
3170
|
}
|
|
@@ -3094,7 +3175,7 @@ function validateProjectionDbColumns(errors, statement, fieldMap, registry) {
|
|
|
3094
3175
|
return;
|
|
3095
3176
|
}
|
|
3096
3177
|
|
|
3097
|
-
const dbColumnsField = fieldMap.get("
|
|
3178
|
+
const dbColumnsField = fieldMap.get("columns")?.[0];
|
|
3098
3179
|
if (!dbColumnsField || dbColumnsField.value.type !== "block") {
|
|
3099
3180
|
return;
|
|
3100
3181
|
}
|
|
@@ -3106,27 +3187,27 @@ function validateProjectionDbColumns(errors, statement, fieldMap, registry) {
|
|
|
3106
3187
|
const entity = registry.get(entityId);
|
|
3107
3188
|
|
|
3108
3189
|
if (!entity) {
|
|
3109
|
-
pushError(errors, `Projection ${statement.id}
|
|
3190
|
+
pushError(errors, `Projection ${statement.id} columns references missing entity '${entityId}'`, entry.loc);
|
|
3110
3191
|
continue;
|
|
3111
3192
|
}
|
|
3112
3193
|
if (entity.kind !== "entity") {
|
|
3113
|
-
pushError(errors, `Projection ${statement.id}
|
|
3194
|
+
pushError(errors, `Projection ${statement.id} columns must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3114
3195
|
}
|
|
3115
3196
|
if (!realized.has(entityId)) {
|
|
3116
|
-
pushError(errors, `Projection ${statement.id}
|
|
3197
|
+
pushError(errors, `Projection ${statement.id} columns entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3117
3198
|
}
|
|
3118
3199
|
if (fieldKeyword !== "field") {
|
|
3119
|
-
pushError(errors, `Projection ${statement.id}
|
|
3200
|
+
pushError(errors, `Projection ${statement.id} columns for '${entityId}' must use 'field'`, entry.loc);
|
|
3120
3201
|
}
|
|
3121
3202
|
if (columnKeyword !== "column") {
|
|
3122
|
-
pushError(errors, `Projection ${statement.id}
|
|
3203
|
+
pushError(errors, `Projection ${statement.id} columns for '${entityId}' must use 'column'`, entry.loc);
|
|
3123
3204
|
}
|
|
3124
3205
|
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3125
3206
|
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3126
|
-
pushError(errors, `Projection ${statement.id}
|
|
3207
|
+
pushError(errors, `Projection ${statement.id} columns references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3127
3208
|
}
|
|
3128
3209
|
if (!columnName) {
|
|
3129
|
-
pushError(errors, `Projection ${statement.id}
|
|
3210
|
+
pushError(errors, `Projection ${statement.id} columns for '${entityId}.${fieldName}' must include a column name`, entry.loc);
|
|
3130
3211
|
}
|
|
3131
3212
|
}
|
|
3132
3213
|
}
|
|
@@ -3136,7 +3217,7 @@ function validateProjectionDbKeys(errors, statement, fieldMap, registry) {
|
|
|
3136
3217
|
return;
|
|
3137
3218
|
}
|
|
3138
3219
|
|
|
3139
|
-
const dbKeysField = fieldMap.get("
|
|
3220
|
+
const dbKeysField = fieldMap.get("keys")?.[0];
|
|
3140
3221
|
if (!dbKeysField || dbKeysField.value.type !== "block") {
|
|
3141
3222
|
return;
|
|
3142
3223
|
}
|
|
@@ -3148,27 +3229,27 @@ function validateProjectionDbKeys(errors, statement, fieldMap, registry) {
|
|
|
3148
3229
|
const entity = registry.get(entityId);
|
|
3149
3230
|
|
|
3150
3231
|
if (!entity) {
|
|
3151
|
-
pushError(errors, `Projection ${statement.id}
|
|
3232
|
+
pushError(errors, `Projection ${statement.id} keys references missing entity '${entityId}'`, entry.loc);
|
|
3152
3233
|
continue;
|
|
3153
3234
|
}
|
|
3154
3235
|
if (entity.kind !== "entity") {
|
|
3155
|
-
pushError(errors, `Projection ${statement.id}
|
|
3236
|
+
pushError(errors, `Projection ${statement.id} keys must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3156
3237
|
}
|
|
3157
3238
|
if (!realized.has(entityId)) {
|
|
3158
|
-
pushError(errors, `Projection ${statement.id}
|
|
3239
|
+
pushError(errors, `Projection ${statement.id} keys entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3159
3240
|
}
|
|
3160
3241
|
if (!["primary", "unique"].includes(keyType || "")) {
|
|
3161
|
-
pushError(errors, `Projection ${statement.id}
|
|
3242
|
+
pushError(errors, `Projection ${statement.id} keys for '${entityId}' has invalid key type '${keyType}'`, entry.loc);
|
|
3162
3243
|
}
|
|
3163
3244
|
const fieldList = entry.items[2];
|
|
3164
3245
|
if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
|
|
3165
|
-
pushError(errors, `Projection ${statement.id}
|
|
3246
|
+
pushError(errors, `Projection ${statement.id} keys for '${entityId}' must include a non-empty field list`, entry.loc);
|
|
3166
3247
|
continue;
|
|
3167
3248
|
}
|
|
3168
3249
|
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3169
3250
|
for (const item of fieldList.items) {
|
|
3170
3251
|
if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
|
|
3171
|
-
pushError(errors, `Projection ${statement.id}
|
|
3252
|
+
pushError(errors, `Projection ${statement.id} keys references unknown field '${item.value}' on ${entityId}`, item.loc);
|
|
3172
3253
|
}
|
|
3173
3254
|
}
|
|
3174
3255
|
}
|
|
@@ -3179,7 +3260,7 @@ function validateProjectionDbIndexes(errors, statement, fieldMap, registry) {
|
|
|
3179
3260
|
return;
|
|
3180
3261
|
}
|
|
3181
3262
|
|
|
3182
|
-
const dbIndexesField = fieldMap.get("
|
|
3263
|
+
const dbIndexesField = fieldMap.get("indexes")?.[0];
|
|
3183
3264
|
if (!dbIndexesField || dbIndexesField.value.type !== "block") {
|
|
3184
3265
|
return;
|
|
3185
3266
|
}
|
|
@@ -3191,27 +3272,27 @@ function validateProjectionDbIndexes(errors, statement, fieldMap, registry) {
|
|
|
3191
3272
|
const entity = registry.get(entityId);
|
|
3192
3273
|
|
|
3193
3274
|
if (!entity) {
|
|
3194
|
-
pushError(errors, `Projection ${statement.id}
|
|
3275
|
+
pushError(errors, `Projection ${statement.id} indexes references missing entity '${entityId}'`, entry.loc);
|
|
3195
3276
|
continue;
|
|
3196
3277
|
}
|
|
3197
3278
|
if (entity.kind !== "entity") {
|
|
3198
|
-
pushError(errors, `Projection ${statement.id}
|
|
3279
|
+
pushError(errors, `Projection ${statement.id} indexes must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3199
3280
|
}
|
|
3200
3281
|
if (!realized.has(entityId)) {
|
|
3201
|
-
pushError(errors, `Projection ${statement.id}
|
|
3282
|
+
pushError(errors, `Projection ${statement.id} indexes entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3202
3283
|
}
|
|
3203
3284
|
if (!["index", "unique"].includes(indexType || "")) {
|
|
3204
|
-
pushError(errors, `Projection ${statement.id}
|
|
3285
|
+
pushError(errors, `Projection ${statement.id} indexes for '${entityId}' has invalid index type '${indexType}'`, entry.loc);
|
|
3205
3286
|
}
|
|
3206
3287
|
const fieldList = entry.items[2];
|
|
3207
3288
|
if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
|
|
3208
|
-
pushError(errors, `Projection ${statement.id}
|
|
3289
|
+
pushError(errors, `Projection ${statement.id} indexes for '${entityId}' must include a non-empty field list`, entry.loc);
|
|
3209
3290
|
continue;
|
|
3210
3291
|
}
|
|
3211
3292
|
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3212
3293
|
for (const item of fieldList.items) {
|
|
3213
3294
|
if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
|
|
3214
|
-
pushError(errors, `Projection ${statement.id}
|
|
3295
|
+
pushError(errors, `Projection ${statement.id} indexes references unknown field '${item.value}' on ${entityId}`, item.loc);
|
|
3215
3296
|
}
|
|
3216
3297
|
}
|
|
3217
3298
|
}
|
|
@@ -3222,7 +3303,7 @@ function validateProjectionDbRelations(errors, statement, fieldMap, registry) {
|
|
|
3222
3303
|
return;
|
|
3223
3304
|
}
|
|
3224
3305
|
|
|
3225
|
-
const dbRelationsField = fieldMap.get("
|
|
3306
|
+
const dbRelationsField = fieldMap.get("relations")?.[0];
|
|
3226
3307
|
if (!dbRelationsField || dbRelationsField.value.type !== "block") {
|
|
3227
3308
|
return;
|
|
3228
3309
|
}
|
|
@@ -3234,43 +3315,43 @@ function validateProjectionDbRelations(errors, statement, fieldMap, registry) {
|
|
|
3234
3315
|
const entity = registry.get(entityId);
|
|
3235
3316
|
|
|
3236
3317
|
if (!entity) {
|
|
3237
|
-
pushError(errors, `Projection ${statement.id}
|
|
3318
|
+
pushError(errors, `Projection ${statement.id} relations references missing entity '${entityId}'`, entry.loc);
|
|
3238
3319
|
continue;
|
|
3239
3320
|
}
|
|
3240
3321
|
if (entity.kind !== "entity") {
|
|
3241
|
-
pushError(errors, `Projection ${statement.id}
|
|
3322
|
+
pushError(errors, `Projection ${statement.id} relations must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3242
3323
|
}
|
|
3243
3324
|
if (!realized.has(entityId)) {
|
|
3244
|
-
pushError(errors, `Projection ${statement.id}
|
|
3325
|
+
pushError(errors, `Projection ${statement.id} relations entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3245
3326
|
}
|
|
3246
3327
|
if (relationType !== "foreign_key") {
|
|
3247
|
-
pushError(errors, `Projection ${statement.id}
|
|
3328
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' must use 'foreign_key'`, entry.loc);
|
|
3248
3329
|
}
|
|
3249
3330
|
if (referencesKeyword !== "references") {
|
|
3250
|
-
pushError(errors, `Projection ${statement.id}
|
|
3331
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' must use 'references'`, entry.loc);
|
|
3251
3332
|
}
|
|
3252
3333
|
if (onDeleteKeyword && onDeleteKeyword !== "on_delete") {
|
|
3253
|
-
pushError(errors, `Projection ${statement.id}
|
|
3334
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' has unexpected token '${onDeleteKeyword}'`, entry.loc);
|
|
3254
3335
|
}
|
|
3255
3336
|
if (onDeleteValue && !["cascade", "restrict", "set_null", "no_action"].includes(onDeleteValue)) {
|
|
3256
|
-
pushError(errors, `Projection ${statement.id}
|
|
3337
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' has invalid on_delete '${onDeleteValue}'`, entry.loc);
|
|
3257
3338
|
}
|
|
3258
3339
|
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3259
3340
|
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3260
|
-
pushError(errors, `Projection ${statement.id}
|
|
3341
|
+
pushError(errors, `Projection ${statement.id} relations references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3261
3342
|
}
|
|
3262
3343
|
const [targetEntityId, targetFieldName] = (targetRef || "").split(".");
|
|
3263
3344
|
const targetEntity = registry.get(targetEntityId);
|
|
3264
3345
|
if (!targetEntity) {
|
|
3265
|
-
pushError(errors, `Projection ${statement.id}
|
|
3346
|
+
pushError(errors, `Projection ${statement.id} relations references missing target entity '${targetEntityId}'`, entry.loc);
|
|
3266
3347
|
continue;
|
|
3267
3348
|
}
|
|
3268
3349
|
if (targetEntity.kind !== "entity") {
|
|
3269
|
-
pushError(errors, `Projection ${statement.id}
|
|
3350
|
+
pushError(errors, `Projection ${statement.id} relations must reference an entity target, found ${targetEntity.kind} '${targetEntity.id}'`, entry.loc);
|
|
3270
3351
|
}
|
|
3271
3352
|
const targetFieldNames = new Set(statementFieldNames(targetEntity));
|
|
3272
3353
|
if (targetFieldName && targetFieldNames.size > 0 && !targetFieldNames.has(targetFieldName)) {
|
|
3273
|
-
pushError(errors, `Projection ${statement.id}
|
|
3354
|
+
pushError(errors, `Projection ${statement.id} relations references unknown target field '${targetFieldName}' on ${targetEntityId}`, entry.loc);
|
|
3274
3355
|
}
|
|
3275
3356
|
}
|
|
3276
3357
|
}
|
|
@@ -3280,7 +3361,7 @@ function validateProjectionDbLifecycle(errors, statement, fieldMap, registry) {
|
|
|
3280
3361
|
return;
|
|
3281
3362
|
}
|
|
3282
3363
|
|
|
3283
|
-
const dbLifecycleField = fieldMap.get("
|
|
3364
|
+
const dbLifecycleField = fieldMap.get("lifecycle")?.[0];
|
|
3284
3365
|
if (!dbLifecycleField || dbLifecycleField.value.type !== "block") {
|
|
3285
3366
|
return;
|
|
3286
3367
|
}
|
|
@@ -3292,19 +3373,19 @@ function validateProjectionDbLifecycle(errors, statement, fieldMap, registry) {
|
|
|
3292
3373
|
const entity = registry.get(entityId);
|
|
3293
3374
|
|
|
3294
3375
|
if (!entity) {
|
|
3295
|
-
pushError(errors, `Projection ${statement.id}
|
|
3376
|
+
pushError(errors, `Projection ${statement.id} lifecycle references missing entity '${entityId}'`, entry.loc);
|
|
3296
3377
|
continue;
|
|
3297
3378
|
}
|
|
3298
3379
|
if (entity.kind !== "entity") {
|
|
3299
|
-
pushError(errors, `Projection ${statement.id}
|
|
3380
|
+
pushError(errors, `Projection ${statement.id} lifecycle must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3300
3381
|
}
|
|
3301
3382
|
if (!realized.has(entityId)) {
|
|
3302
|
-
pushError(errors, `Projection ${statement.id}
|
|
3383
|
+
pushError(errors, `Projection ${statement.id} lifecycle entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3303
3384
|
}
|
|
3304
3385
|
|
|
3305
|
-
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `
|
|
3386
|
+
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `lifecycle for '${entityId}'`);
|
|
3306
3387
|
if (!["soft_delete", "timestamps"].includes(lifecycleType || "")) {
|
|
3307
|
-
pushError(errors, `Projection ${statement.id}
|
|
3388
|
+
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' has invalid lifecycle '${lifecycleType}'`, entry.loc);
|
|
3308
3389
|
continue;
|
|
3309
3390
|
}
|
|
3310
3391
|
|
|
@@ -3312,23 +3393,23 @@ function validateProjectionDbLifecycle(errors, statement, fieldMap, registry) {
|
|
|
3312
3393
|
if (lifecycleType === "soft_delete") {
|
|
3313
3394
|
for (const requiredKey of ["field", "value"]) {
|
|
3314
3395
|
if (!directives.has(requiredKey)) {
|
|
3315
|
-
pushError(errors, `Projection ${statement.id}
|
|
3396
|
+
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' must include '${requiredKey}' for soft_delete`, entry.loc);
|
|
3316
3397
|
}
|
|
3317
3398
|
}
|
|
3318
3399
|
const fieldName = directives.get("field");
|
|
3319
3400
|
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3320
|
-
pushError(errors, `Projection ${statement.id}
|
|
3401
|
+
pushError(errors, `Projection ${statement.id} lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3321
3402
|
}
|
|
3322
3403
|
}
|
|
3323
3404
|
|
|
3324
3405
|
if (lifecycleType === "timestamps") {
|
|
3325
3406
|
for (const requiredKey of ["created_at", "updated_at"]) {
|
|
3326
3407
|
if (!directives.has(requiredKey)) {
|
|
3327
|
-
pushError(errors, `Projection ${statement.id}
|
|
3408
|
+
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' must include '${requiredKey}' for timestamps`, entry.loc);
|
|
3328
3409
|
}
|
|
3329
3410
|
const fieldName = directives.get(requiredKey);
|
|
3330
3411
|
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3331
|
-
pushError(errors, `Projection ${statement.id}
|
|
3412
|
+
pushError(errors, `Projection ${statement.id} lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3332
3413
|
}
|
|
3333
3414
|
}
|
|
3334
3415
|
}
|
|
@@ -3341,7 +3422,11 @@ export function buildRegistry(workspaceAst, errors) {
|
|
|
3341
3422
|
for (const file of workspaceAst.files) {
|
|
3342
3423
|
for (const statement of file.statements) {
|
|
3343
3424
|
if (!STATEMENT_KINDS.has(statement.kind)) {
|
|
3344
|
-
|
|
3425
|
+
if (statement.kind === "component") {
|
|
3426
|
+
pushError(errors, `Statement kind ${renameDiagnostic("'component'", "'widget'", "widget widget_data_grid { ... }")}`, statement.loc);
|
|
3427
|
+
} else {
|
|
3428
|
+
pushError(errors, `Unknown statement kind '${statement.kind}'`, statement.loc);
|
|
3429
|
+
}
|
|
3345
3430
|
}
|
|
3346
3431
|
|
|
3347
3432
|
if (!IDENTIFIER_PATTERN.test(statement.id)) {
|
|
@@ -3519,6 +3604,7 @@ export function validateWorkspace(workspaceAst) {
|
|
|
3519
3604
|
for (const statement of file.statements) {
|
|
3520
3605
|
const fieldMap = collectFieldMap(statement);
|
|
3521
3606
|
validateFieldPresence(errors, statement, fieldMap);
|
|
3607
|
+
validateProjectionTypeRenames(errors, statement, fieldMap);
|
|
3522
3608
|
validateFieldShapes(errors, statement, fieldMap);
|
|
3523
3609
|
validateStatus(errors, statement, fieldMap);
|
|
3524
3610
|
validateRuleSeverity(errors, statement, fieldMap);
|
|
@@ -3561,7 +3647,7 @@ export function validateWorkspace(workspaceAst) {
|
|
|
3561
3647
|
validateProjectionDbRelations(errors, statement, fieldMap, registry);
|
|
3562
3648
|
validateProjectionDbLifecycle(errors, statement, fieldMap, registry);
|
|
3563
3649
|
validateProjectionGeneratorDefaults(errors, statement, fieldMap);
|
|
3564
|
-
|
|
3650
|
+
validateWidget(errors, statement, fieldMap, registry);
|
|
3565
3651
|
validateDomain(errors, statement, fieldMap, registry);
|
|
3566
3652
|
validateDomainTag(errors, statement, fieldMap, registry);
|
|
3567
3653
|
validatePitch(errors, statement, fieldMap, registry);
|