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