@topogram/cli 0.3.50 → 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 +501 -409
- 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,31 +2371,72 @@ 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);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
const SHARED_UI_SEMANTIC_BLOCKS = [
|
|
2402
|
+
"screens",
|
|
2403
|
+
"collection_views",
|
|
2404
|
+
"screen_actions",
|
|
2405
|
+
"visibility_rules",
|
|
2406
|
+
"field_lookups",
|
|
2407
|
+
"app_shell",
|
|
2408
|
+
"navigation",
|
|
2409
|
+
"screen_regions"
|
|
2410
|
+
];
|
|
2411
|
+
|
|
2412
|
+
function validateProjectionUiOwnership(errors, statement, fieldMap) {
|
|
2413
|
+
if (statement.kind !== "projection") {
|
|
2414
|
+
return;
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
const platform = symbolValue(getFieldValue(statement, "type"));
|
|
2418
|
+
for (const key of SHARED_UI_SEMANTIC_BLOCKS) {
|
|
2419
|
+
const field = fieldMap.get(key)?.[0];
|
|
2420
|
+
if (!field || field.value.type !== "block") {
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
if (platform !== "ui_contract") {
|
|
2424
|
+
pushError(
|
|
2425
|
+
errors,
|
|
2426
|
+
`Projection ${statement.id} ${key} belongs on shared UI projections; concrete UI projections may define screen_routes and platform surface hints only`,
|
|
2427
|
+
field.loc
|
|
2428
|
+
);
|
|
2351
2429
|
}
|
|
2352
2430
|
}
|
|
2431
|
+
|
|
2432
|
+
const routesField = fieldMap.get("screen_routes")?.[0];
|
|
2433
|
+
if (routesField?.value.type === "block" && !["web_surface", "ios_surface"].includes(platform || "")) {
|
|
2434
|
+
pushError(
|
|
2435
|
+
errors,
|
|
2436
|
+
`Projection ${statement.id} screen_routes belongs on concrete UI projections; shared UI projections own semantic screens and regions`,
|
|
2437
|
+
routesField.loc
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2353
2440
|
}
|
|
2354
2441
|
|
|
2355
2442
|
function validateProjectionUiDesign(errors, statement, fieldMap) {
|
|
@@ -2357,13 +2444,13 @@ function validateProjectionUiDesign(errors, statement, fieldMap) {
|
|
|
2357
2444
|
return;
|
|
2358
2445
|
}
|
|
2359
2446
|
|
|
2360
|
-
const designField = fieldMap.get("
|
|
2447
|
+
const designField = fieldMap.get("design_tokens")?.[0];
|
|
2361
2448
|
if (!designField || designField.value.type !== "block") {
|
|
2362
2449
|
return;
|
|
2363
2450
|
}
|
|
2364
2451
|
|
|
2365
|
-
if (symbolValue(getFieldValue(statement, "
|
|
2366
|
-
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);
|
|
2367
2454
|
}
|
|
2368
2455
|
|
|
2369
2456
|
for (const entry of designField.value.entries) {
|
|
@@ -2372,60 +2459,60 @@ function validateProjectionUiDesign(errors, statement, fieldMap) {
|
|
|
2372
2459
|
|
|
2373
2460
|
if (key === "density") {
|
|
2374
2461
|
if (!UI_DESIGN_DENSITIES.has(value || "")) {
|
|
2375
|
-
pushError(errors, `Projection ${statement.id}
|
|
2462
|
+
pushError(errors, `Projection ${statement.id} design_tokens density has invalid value '${value}'`, entry.loc);
|
|
2376
2463
|
}
|
|
2377
2464
|
if (tokens.length !== 2) {
|
|
2378
|
-
pushError(errors, `Projection ${statement.id}
|
|
2465
|
+
pushError(errors, `Projection ${statement.id} design_tokens density accepts exactly one value`, entry.loc);
|
|
2379
2466
|
}
|
|
2380
2467
|
continue;
|
|
2381
2468
|
}
|
|
2382
2469
|
|
|
2383
2470
|
if (key === "tone") {
|
|
2384
2471
|
if (!UI_DESIGN_TONES.has(value || "")) {
|
|
2385
|
-
pushError(errors, `Projection ${statement.id}
|
|
2472
|
+
pushError(errors, `Projection ${statement.id} design_tokens tone has invalid value '${value}'`, entry.loc);
|
|
2386
2473
|
}
|
|
2387
2474
|
if (tokens.length !== 2) {
|
|
2388
|
-
pushError(errors, `Projection ${statement.id}
|
|
2475
|
+
pushError(errors, `Projection ${statement.id} design_tokens tone accepts exactly one value`, entry.loc);
|
|
2389
2476
|
}
|
|
2390
2477
|
continue;
|
|
2391
2478
|
}
|
|
2392
2479
|
|
|
2393
2480
|
if (key === "radius_scale") {
|
|
2394
2481
|
if (!UI_DESIGN_RADIUS_SCALES.has(value || "")) {
|
|
2395
|
-
pushError(errors, `Projection ${statement.id}
|
|
2482
|
+
pushError(errors, `Projection ${statement.id} design_tokens radius_scale has invalid value '${value}'`, entry.loc);
|
|
2396
2483
|
}
|
|
2397
2484
|
if (tokens.length !== 2) {
|
|
2398
|
-
pushError(errors, `Projection ${statement.id}
|
|
2485
|
+
pushError(errors, `Projection ${statement.id} design_tokens radius_scale accepts exactly one value`, entry.loc);
|
|
2399
2486
|
}
|
|
2400
2487
|
continue;
|
|
2401
2488
|
}
|
|
2402
2489
|
|
|
2403
2490
|
if (key === "color_role") {
|
|
2404
2491
|
if (!UI_DESIGN_COLOR_ROLES.has(value || "")) {
|
|
2405
|
-
pushError(errors, `Projection ${statement.id}
|
|
2492
|
+
pushError(errors, `Projection ${statement.id} design_tokens color_role has invalid role '${value}'`, entry.loc);
|
|
2406
2493
|
}
|
|
2407
2494
|
if (tokens.length !== 3) {
|
|
2408
|
-
pushError(errors, `Projection ${statement.id}
|
|
2495
|
+
pushError(errors, `Projection ${statement.id} design_tokens color_role must use 'color_role <role> <semantic-token>'`, entry.loc);
|
|
2409
2496
|
}
|
|
2410
2497
|
continue;
|
|
2411
2498
|
}
|
|
2412
2499
|
|
|
2413
2500
|
if (key === "typography_role") {
|
|
2414
2501
|
if (!UI_DESIGN_TYPOGRAPHY_ROLES.has(value || "")) {
|
|
2415
|
-
pushError(errors, `Projection ${statement.id}
|
|
2502
|
+
pushError(errors, `Projection ${statement.id} design_tokens typography_role has invalid role '${value}'`, entry.loc);
|
|
2416
2503
|
}
|
|
2417
2504
|
if (tokens.length !== 3) {
|
|
2418
|
-
pushError(errors, `Projection ${statement.id}
|
|
2505
|
+
pushError(errors, `Projection ${statement.id} design_tokens typography_role must use 'typography_role <role> <semantic-token>'`, entry.loc);
|
|
2419
2506
|
}
|
|
2420
2507
|
continue;
|
|
2421
2508
|
}
|
|
2422
2509
|
|
|
2423
2510
|
if (key === "action_role") {
|
|
2424
2511
|
if (!UI_DESIGN_ACTION_ROLES.has(value || "")) {
|
|
2425
|
-
pushError(errors, `Projection ${statement.id}
|
|
2512
|
+
pushError(errors, `Projection ${statement.id} design_tokens action_role has invalid role '${value}'`, entry.loc);
|
|
2426
2513
|
}
|
|
2427
2514
|
if (tokens.length !== 3) {
|
|
2428
|
-
pushError(errors, `Projection ${statement.id}
|
|
2515
|
+
pushError(errors, `Projection ${statement.id} design_tokens action_role must use 'action_role <role> <semantic-token>'`, entry.loc);
|
|
2429
2516
|
}
|
|
2430
2517
|
continue;
|
|
2431
2518
|
}
|
|
@@ -2433,17 +2520,17 @@ function validateProjectionUiDesign(errors, statement, fieldMap) {
|
|
|
2433
2520
|
if (key === "accessibility") {
|
|
2434
2521
|
const values = UI_DESIGN_ACCESSIBILITY_VALUES[value];
|
|
2435
2522
|
if (tokens.length !== 3) {
|
|
2436
|
-
pushError(errors, `Projection ${statement.id}
|
|
2523
|
+
pushError(errors, `Projection ${statement.id} design_tokens accessibility must use 'accessibility <setting> <value>'`, entry.loc);
|
|
2437
2524
|
}
|
|
2438
2525
|
if (!values) {
|
|
2439
|
-
pushError(errors, `Projection ${statement.id}
|
|
2526
|
+
pushError(errors, `Projection ${statement.id} design_tokens accessibility has invalid setting '${value}'`, entry.loc);
|
|
2440
2527
|
} else if (!values.has(extra || "")) {
|
|
2441
|
-
pushError(errors, `Projection ${statement.id}
|
|
2528
|
+
pushError(errors, `Projection ${statement.id} design_tokens accessibility '${value}' has invalid value '${extra}'`, entry.loc);
|
|
2442
2529
|
}
|
|
2443
2530
|
continue;
|
|
2444
2531
|
}
|
|
2445
2532
|
|
|
2446
|
-
pushError(errors, `Projection ${statement.id}
|
|
2533
|
+
pushError(errors, `Projection ${statement.id} design_tokens has unknown key '${key}'`, entry.loc);
|
|
2447
2534
|
}
|
|
2448
2535
|
}
|
|
2449
2536
|
|
|
@@ -2452,7 +2539,7 @@ function validateProjectionUiNavigation(errors, statement, fieldMap, registry) {
|
|
|
2452
2539
|
return;
|
|
2453
2540
|
}
|
|
2454
2541
|
|
|
2455
|
-
const navigationField = fieldMap.get("
|
|
2542
|
+
const navigationField = fieldMap.get("navigation")?.[0];
|
|
2456
2543
|
if (!navigationField || navigationField.value.type !== "block") {
|
|
2457
2544
|
return;
|
|
2458
2545
|
}
|
|
@@ -2466,58 +2553,58 @@ function validateProjectionUiNavigation(errors, statement, fieldMap, registry) {
|
|
|
2466
2553
|
|
|
2467
2554
|
if (targetKind === "group") {
|
|
2468
2555
|
if (!targetId || !IDENTIFIER_PATTERN.test(targetId)) {
|
|
2469
|
-
pushError(errors, `Projection ${statement.id}
|
|
2556
|
+
pushError(errors, `Projection ${statement.id} navigation group entries must include a valid group id`, entry.loc);
|
|
2470
2557
|
continue;
|
|
2471
2558
|
}
|
|
2472
2559
|
groups.add(targetId);
|
|
2473
|
-
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `
|
|
2560
|
+
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `navigation group '${targetId}'`);
|
|
2474
2561
|
for (const key of directives.keys()) {
|
|
2475
2562
|
if (!["label", "placement", "icon", "order", "pattern"].includes(key)) {
|
|
2476
|
-
pushError(errors, `Projection ${statement.id}
|
|
2563
|
+
pushError(errors, `Projection ${statement.id} navigation group '${targetId}' has unknown directive '${key}'`, entry.loc);
|
|
2477
2564
|
}
|
|
2478
2565
|
}
|
|
2479
2566
|
if (directives.has("placement") && !["primary", "secondary", "utility"].includes(directives.get("placement"))) {
|
|
2480
|
-
pushError(errors, `Projection ${statement.id}
|
|
2567
|
+
pushError(errors, `Projection ${statement.id} navigation group '${targetId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
|
|
2481
2568
|
}
|
|
2482
2569
|
if (directives.has("pattern") && !UI_NAVIGATION_PATTERNS.has(directives.get("pattern"))) {
|
|
2483
|
-
pushError(errors, `Projection ${statement.id}
|
|
2570
|
+
pushError(errors, `Projection ${statement.id} navigation group '${targetId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
|
|
2484
2571
|
}
|
|
2485
2572
|
continue;
|
|
2486
2573
|
}
|
|
2487
2574
|
|
|
2488
2575
|
if (targetKind === "screen") {
|
|
2489
2576
|
if (!availableScreens.has(targetId)) {
|
|
2490
|
-
pushError(errors, `Projection ${statement.id}
|
|
2577
|
+
pushError(errors, `Projection ${statement.id} navigation references unknown screen '${targetId}'`, entry.loc);
|
|
2491
2578
|
}
|
|
2492
|
-
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `
|
|
2579
|
+
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `navigation screen '${targetId}'`);
|
|
2493
2580
|
for (const key of directives.keys()) {
|
|
2494
2581
|
if (!["group", "label", "order", "visible", "default", "breadcrumb", "sitemap", "placement", "pattern"].includes(key)) {
|
|
2495
|
-
pushError(errors, `Projection ${statement.id}
|
|
2582
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has unknown directive '${key}'`, entry.loc);
|
|
2496
2583
|
}
|
|
2497
2584
|
}
|
|
2498
2585
|
if (directives.has("visible") && !["true", "false"].includes(directives.get("visible"))) {
|
|
2499
|
-
pushError(errors, `Projection ${statement.id}
|
|
2586
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid visible '${directives.get("visible")}'`, entry.loc);
|
|
2500
2587
|
}
|
|
2501
2588
|
if (directives.has("default") && !["true", "false"].includes(directives.get("default"))) {
|
|
2502
|
-
pushError(errors, `Projection ${statement.id}
|
|
2589
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid default '${directives.get("default")}'`, entry.loc);
|
|
2503
2590
|
}
|
|
2504
2591
|
if (directives.has("placement") && !["primary", "secondary", "utility"].includes(directives.get("placement"))) {
|
|
2505
|
-
pushError(errors, `Projection ${statement.id}
|
|
2592
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
|
|
2506
2593
|
}
|
|
2507
2594
|
if (directives.has("sitemap") && !["include", "exclude"].includes(directives.get("sitemap"))) {
|
|
2508
|
-
pushError(errors, `Projection ${statement.id}
|
|
2595
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid sitemap '${directives.get("sitemap")}'`, entry.loc);
|
|
2509
2596
|
}
|
|
2510
2597
|
if (directives.has("pattern") && !UI_NAVIGATION_PATTERNS.has(directives.get("pattern"))) {
|
|
2511
|
-
pushError(errors, `Projection ${statement.id}
|
|
2598
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
|
|
2512
2599
|
}
|
|
2513
2600
|
const breadcrumb = directives.get("breadcrumb");
|
|
2514
2601
|
if (breadcrumb && breadcrumb !== "none" && !availableScreens.has(breadcrumb)) {
|
|
2515
|
-
pushError(errors, `Projection ${statement.id}
|
|
2602
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' references unknown breadcrumb screen '${breadcrumb}'`, entry.loc);
|
|
2516
2603
|
}
|
|
2517
2604
|
continue;
|
|
2518
2605
|
}
|
|
2519
2606
|
|
|
2520
|
-
pushError(errors, `Projection ${statement.id}
|
|
2607
|
+
pushError(errors, `Projection ${statement.id} navigation entries must start with 'group' or 'screen'`, entry.loc);
|
|
2521
2608
|
}
|
|
2522
2609
|
|
|
2523
2610
|
for (const entry of navigationField.value.entries) {
|
|
@@ -2527,7 +2614,7 @@ function validateProjectionUiNavigation(errors, statement, fieldMap, registry) {
|
|
|
2527
2614
|
}
|
|
2528
2615
|
const directives = parseUiDirectiveMap(tokens, 2, [], statement, entry, "");
|
|
2529
2616
|
if (directives.has("group") && !groups.has(directives.get("group"))) {
|
|
2530
|
-
pushError(errors, `Projection ${statement.id}
|
|
2617
|
+
pushError(errors, `Projection ${statement.id} navigation screen '${tokens[1]}' references unknown group '${directives.get("group")}'`, entry.loc);
|
|
2531
2618
|
}
|
|
2532
2619
|
}
|
|
2533
2620
|
}
|
|
@@ -2537,7 +2624,7 @@ function validateProjectionUiScreenRegions(errors, statement, fieldMap, registry
|
|
|
2537
2624
|
return;
|
|
2538
2625
|
}
|
|
2539
2626
|
|
|
2540
|
-
const regionField = fieldMap.get("
|
|
2627
|
+
const regionField = fieldMap.get("screen_regions")?.[0];
|
|
2541
2628
|
if (!regionField || regionField.value.type !== "block") {
|
|
2542
2629
|
return;
|
|
2543
2630
|
}
|
|
@@ -2548,33 +2635,33 @@ function validateProjectionUiScreenRegions(errors, statement, fieldMap, registry
|
|
|
2548
2635
|
const [keyword, screenId, regionKeyword, regionName] = tokens;
|
|
2549
2636
|
|
|
2550
2637
|
if (keyword !== "screen") {
|
|
2551
|
-
pushError(errors, `Projection ${statement.id}
|
|
2638
|
+
pushError(errors, `Projection ${statement.id} screen_regions entries must start with 'screen'`, entry.loc);
|
|
2552
2639
|
continue;
|
|
2553
2640
|
}
|
|
2554
2641
|
if (!availableScreens.has(screenId)) {
|
|
2555
|
-
pushError(errors, `Projection ${statement.id}
|
|
2642
|
+
pushError(errors, `Projection ${statement.id} screen_regions references unknown screen '${screenId}'`, entry.loc);
|
|
2556
2643
|
}
|
|
2557
2644
|
if (regionKeyword !== "region") {
|
|
2558
|
-
pushError(errors, `Projection ${statement.id}
|
|
2645
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' must use 'region'`, entry.loc);
|
|
2559
2646
|
}
|
|
2560
2647
|
if (!UI_REGION_KINDS.has(regionName || "")) {
|
|
2561
|
-
pushError(errors, `Projection ${statement.id}
|
|
2648
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid region '${regionName}'`, entry.loc);
|
|
2562
2649
|
}
|
|
2563
2650
|
|
|
2564
|
-
const directives = parseUiDirectiveMap(tokens, 4, errors, statement, entry, `
|
|
2651
|
+
const directives = parseUiDirectiveMap(tokens, 4, errors, statement, entry, `screen_regions for '${screenId}'`);
|
|
2565
2652
|
for (const key of directives.keys()) {
|
|
2566
2653
|
if (!["pattern", "placement", "title", "state", "variant"].includes(key)) {
|
|
2567
|
-
pushError(errors, `Projection ${statement.id}
|
|
2654
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has unknown directive '${key}'`, entry.loc);
|
|
2568
2655
|
}
|
|
2569
2656
|
}
|
|
2570
2657
|
if (directives.has("pattern") && !UI_PATTERN_KINDS.has(directives.get("pattern"))) {
|
|
2571
|
-
pushError(errors, `Projection ${statement.id}
|
|
2658
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
|
|
2572
2659
|
}
|
|
2573
2660
|
if (directives.has("placement") && !["primary", "secondary", "supporting"].includes(directives.get("placement"))) {
|
|
2574
|
-
pushError(errors, `Projection ${statement.id}
|
|
2661
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
|
|
2575
2662
|
}
|
|
2576
2663
|
if (directives.has("state") && !UI_STATE_KINDS.has(directives.get("state"))) {
|
|
2577
|
-
pushError(errors, `Projection ${statement.id}
|
|
2664
|
+
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid state '${directives.get("state")}'`, entry.loc);
|
|
2578
2665
|
}
|
|
2579
2666
|
}
|
|
2580
2667
|
}
|
|
@@ -2584,13 +2671,13 @@ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
|
2584
2671
|
return;
|
|
2585
2672
|
}
|
|
2586
2673
|
|
|
2587
|
-
const componentsField = fieldMap.get("
|
|
2674
|
+
const componentsField = fieldMap.get("widget_bindings")?.[0];
|
|
2588
2675
|
if (!componentsField || componentsField.value.type !== "block") {
|
|
2589
2676
|
return;
|
|
2590
2677
|
}
|
|
2591
2678
|
|
|
2592
|
-
if (symbolValue(getFieldValue(statement, "
|
|
2593
|
-
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);
|
|
2594
2681
|
}
|
|
2595
2682
|
|
|
2596
2683
|
const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
|
|
@@ -2602,48 +2689,48 @@ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
|
2602
2689
|
const [screenKeyword, screenId, regionKeyword, regionName, componentKeyword, componentId] = tokens;
|
|
2603
2690
|
|
|
2604
2691
|
if (screenKeyword !== "screen") {
|
|
2605
|
-
pushError(errors, `Projection ${statement.id}
|
|
2692
|
+
pushError(errors, `Projection ${statement.id} widget_bindings entries must start with 'screen'`, entry.loc);
|
|
2606
2693
|
continue;
|
|
2607
2694
|
}
|
|
2608
2695
|
if (!availableScreens.has(screenId)) {
|
|
2609
|
-
pushError(errors, `Projection ${statement.id}
|
|
2696
|
+
pushError(errors, `Projection ${statement.id} widget_bindings references unknown screen '${screenId}'`, entry.loc);
|
|
2610
2697
|
}
|
|
2611
2698
|
if (regionKeyword !== "region") {
|
|
2612
|
-
pushError(errors, `Projection ${statement.id}
|
|
2699
|
+
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' must use 'region'`, entry.loc);
|
|
2613
2700
|
}
|
|
2614
2701
|
if (!UI_REGION_KINDS.has(regionName || "")) {
|
|
2615
|
-
pushError(errors, `Projection ${statement.id}
|
|
2702
|
+
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' has invalid region '${regionName}'`, entry.loc);
|
|
2616
2703
|
} else if (!availableRegions.has(`${screenId}:${regionName}`)) {
|
|
2617
|
-
pushError(errors, `Projection ${statement.id}
|
|
2704
|
+
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' references undeclared region '${regionName}'`, entry.loc);
|
|
2618
2705
|
}
|
|
2619
|
-
if (componentKeyword !== "
|
|
2620
|
-
pushError(errors, `Projection ${statement.id}
|
|
2706
|
+
if (componentKeyword !== "widget") {
|
|
2707
|
+
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' must use 'widget'`, entry.loc);
|
|
2621
2708
|
}
|
|
2622
2709
|
|
|
2623
|
-
const
|
|
2624
|
-
if (!
|
|
2625
|
-
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);
|
|
2626
2713
|
continue;
|
|
2627
2714
|
}
|
|
2628
|
-
if (
|
|
2629
|
-
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);
|
|
2630
2717
|
continue;
|
|
2631
2718
|
}
|
|
2632
2719
|
|
|
2633
|
-
const propNames = new Set(blockEntries(getFieldValue(
|
|
2720
|
+
const propNames = new Set(blockEntries(getFieldValue(widget, "props"))
|
|
2634
2721
|
.map((propEntry) => propEntry.items[0])
|
|
2635
2722
|
.filter((item) => item?.type === "symbol")
|
|
2636
2723
|
.map((item) => item.value));
|
|
2637
|
-
const eventNames = new Set(blockEntries(getFieldValue(
|
|
2724
|
+
const eventNames = new Set(blockEntries(getFieldValue(widget, "events"))
|
|
2638
2725
|
.map((eventEntry) => eventEntry.items[0])
|
|
2639
2726
|
.filter((item) => item?.type === "symbol")
|
|
2640
2727
|
.map((item) => item.value));
|
|
2641
|
-
const componentRegions = symbolValues(getFieldValue(
|
|
2642
|
-
const componentPatterns = symbolValues(getFieldValue(
|
|
2728
|
+
const componentRegions = symbolValues(getFieldValue(widget, "regions"));
|
|
2729
|
+
const componentPatterns = symbolValues(getFieldValue(widget, "patterns"));
|
|
2643
2730
|
if (componentRegions.length > 0 && !componentRegions.includes(regionName)) {
|
|
2644
2731
|
pushError(
|
|
2645
2732
|
errors,
|
|
2646
|
-
`Projection ${statement.id}
|
|
2733
|
+
`Projection ${statement.id} widget_bindings uses widget '${componentId}' in region '${regionName}', but the widget supports regions [${componentRegions.join(", ")}]`,
|
|
2647
2734
|
entry.loc
|
|
2648
2735
|
);
|
|
2649
2736
|
}
|
|
@@ -2651,7 +2738,7 @@ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
|
2651
2738
|
if (regionPattern && componentPatterns.length > 0 && !componentPatterns.includes(regionPattern)) {
|
|
2652
2739
|
pushError(
|
|
2653
2740
|
errors,
|
|
2654
|
-
`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(", ")}]`,
|
|
2655
2742
|
entry.loc
|
|
2656
2743
|
);
|
|
2657
2744
|
}
|
|
@@ -2663,15 +2750,15 @@ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
|
2663
2750
|
const fromKeyword = tokens[i + 2];
|
|
2664
2751
|
const sourceId = tokens[i + 3];
|
|
2665
2752
|
if (!propName || fromKeyword !== "from" || !sourceId) {
|
|
2666
|
-
pushError(errors, `Projection ${statement.id}
|
|
2753
|
+
pushError(errors, `Projection ${statement.id} widget_bindings data bindings must use 'data <prop> from <source>'`, entry.loc);
|
|
2667
2754
|
break;
|
|
2668
2755
|
}
|
|
2669
2756
|
if (!propNames.has(propName)) {
|
|
2670
|
-
pushError(errors, `Projection ${statement.id}
|
|
2757
|
+
pushError(errors, `Projection ${statement.id} widget_bindings references unknown prop '${propName}' on widget '${componentId}'`, entry.loc);
|
|
2671
2758
|
}
|
|
2672
2759
|
const source = registry.get(sourceId);
|
|
2673
2760
|
if (!source || !["capability", "projection", "shape", "entity"].includes(source.kind)) {
|
|
2674
|
-
pushError(errors, `Projection ${statement.id}
|
|
2761
|
+
pushError(errors, `Projection ${statement.id} widget_bindings data binding for '${propName}' references missing source '${sourceId}'`, entry.loc);
|
|
2675
2762
|
}
|
|
2676
2763
|
i += 4;
|
|
2677
2764
|
continue;
|
|
@@ -2682,29 +2769,29 @@ function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
|
2682
2769
|
const action = tokens[i + 2];
|
|
2683
2770
|
const targetId = tokens[i + 3];
|
|
2684
2771
|
if (!eventName || !action || !targetId) {
|
|
2685
|
-
pushError(errors, `Projection ${statement.id}
|
|
2772
|
+
pushError(errors, `Projection ${statement.id} widget_bindings event bindings must use 'event <event> <navigate|action> <target>'`, entry.loc);
|
|
2686
2773
|
break;
|
|
2687
2774
|
}
|
|
2688
2775
|
if (!eventNames.has(eventName)) {
|
|
2689
|
-
pushError(errors, `Projection ${statement.id}
|
|
2776
|
+
pushError(errors, `Projection ${statement.id} widget_bindings references unknown event '${eventName}' on widget '${componentId}'`, entry.loc);
|
|
2690
2777
|
}
|
|
2691
2778
|
if (action === "navigate") {
|
|
2692
2779
|
if (!availableScreens.has(targetId)) {
|
|
2693
|
-
pushError(errors, `Projection ${statement.id}
|
|
2780
|
+
pushError(errors, `Projection ${statement.id} widget_bindings event '${eventName}' references unknown navigation target '${targetId}'`, entry.loc);
|
|
2694
2781
|
}
|
|
2695
2782
|
} else if (action === "action") {
|
|
2696
2783
|
const target = registry.get(targetId);
|
|
2697
2784
|
if (!target || target.kind !== "capability") {
|
|
2698
|
-
pushError(errors, `Projection ${statement.id}
|
|
2785
|
+
pushError(errors, `Projection ${statement.id} widget_bindings event '${eventName}' references missing capability action '${targetId}'`, entry.loc);
|
|
2699
2786
|
}
|
|
2700
2787
|
} else {
|
|
2701
|
-
pushError(errors, `Projection ${statement.id}
|
|
2788
|
+
pushError(errors, `Projection ${statement.id} widget_bindings event '${eventName}' has unsupported action '${action}'`, entry.loc);
|
|
2702
2789
|
}
|
|
2703
2790
|
i += 4;
|
|
2704
2791
|
continue;
|
|
2705
2792
|
}
|
|
2706
2793
|
|
|
2707
|
-
pushError(errors, `Projection ${statement.id}
|
|
2794
|
+
pushError(errors, `Projection ${statement.id} widget_bindings has unknown directive '${directive}'`, entry.loc);
|
|
2708
2795
|
break;
|
|
2709
2796
|
}
|
|
2710
2797
|
}
|
|
@@ -2715,7 +2802,7 @@ function validateProjectionUiVisibility(errors, statement, fieldMap, registry) {
|
|
|
2715
2802
|
return;
|
|
2716
2803
|
}
|
|
2717
2804
|
|
|
2718
|
-
const visibilityField = fieldMap.get("
|
|
2805
|
+
const visibilityField = fieldMap.get("visibility_rules")?.[0];
|
|
2719
2806
|
if (!visibilityField || visibilityField.value.type !== "block") {
|
|
2720
2807
|
return;
|
|
2721
2808
|
}
|
|
@@ -2726,30 +2813,30 @@ function validateProjectionUiVisibility(errors, statement, fieldMap, registry) {
|
|
|
2726
2813
|
const [keyword, capabilityId, predicateKeyword, predicateType, predicateValue] = tokens;
|
|
2727
2814
|
|
|
2728
2815
|
if (keyword !== "action") {
|
|
2729
|
-
pushError(errors, `Projection ${statement.id}
|
|
2816
|
+
pushError(errors, `Projection ${statement.id} visibility_rules entries must start with 'action'`, entry.loc);
|
|
2730
2817
|
continue;
|
|
2731
2818
|
}
|
|
2732
2819
|
|
|
2733
2820
|
const capability = registry.get(capabilityId);
|
|
2734
2821
|
if (!capability) {
|
|
2735
|
-
pushError(errors, `Projection ${statement.id}
|
|
2822
|
+
pushError(errors, `Projection ${statement.id} visibility_rules references missing capability '${capabilityId}'`, entry.loc);
|
|
2736
2823
|
} else if (capability.kind !== "capability") {
|
|
2737
|
-
pushError(errors, `Projection ${statement.id}
|
|
2824
|
+
pushError(errors, `Projection ${statement.id} visibility_rules must reference a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
2738
2825
|
} else if (!realized.has(capabilityId)) {
|
|
2739
|
-
pushError(errors, `Projection ${statement.id}
|
|
2826
|
+
pushError(errors, `Projection ${statement.id} visibility_rules action '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
2740
2827
|
}
|
|
2741
2828
|
|
|
2742
2829
|
if (predicateKeyword !== "visible_if") {
|
|
2743
|
-
pushError(errors, `Projection ${statement.id}
|
|
2830
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' must use 'visible_if'`, entry.loc);
|
|
2744
2831
|
}
|
|
2745
2832
|
if (!["permission", "ownership", "claim"].includes(predicateType || "")) {
|
|
2746
|
-
pushError(errors, `Projection ${statement.id}
|
|
2833
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' has invalid predicate '${predicateType}'`, entry.loc);
|
|
2747
2834
|
}
|
|
2748
2835
|
if (!predicateValue) {
|
|
2749
|
-
pushError(errors, `Projection ${statement.id}
|
|
2836
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' must include a predicate value`, entry.loc);
|
|
2750
2837
|
}
|
|
2751
2838
|
if (predicateType === "ownership" && !["owner", "owner_or_admin", "project_member", "none"].includes(predicateValue || "")) {
|
|
2752
|
-
pushError(errors, `Projection ${statement.id}
|
|
2839
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' has invalid ownership '${predicateValue}'`, entry.loc);
|
|
2753
2840
|
}
|
|
2754
2841
|
const directiveTokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2755
2842
|
const directives = new Map();
|
|
@@ -2757,18 +2844,18 @@ function validateProjectionUiVisibility(errors, statement, fieldMap, registry) {
|
|
|
2757
2844
|
const key = directiveTokens[i];
|
|
2758
2845
|
const value = directiveTokens[i + 1];
|
|
2759
2846
|
if (!value) {
|
|
2760
|
-
pushError(errors, `Projection ${statement.id}
|
|
2847
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
2761
2848
|
continue;
|
|
2762
2849
|
}
|
|
2763
2850
|
directives.set(key, value);
|
|
2764
2851
|
}
|
|
2765
2852
|
for (const key of directives.keys()) {
|
|
2766
2853
|
if (!["claim_value"].includes(key)) {
|
|
2767
|
-
pushError(errors, `Projection ${statement.id}
|
|
2854
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
2768
2855
|
}
|
|
2769
2856
|
}
|
|
2770
2857
|
if (directives.get("claim_value") && predicateType !== "claim") {
|
|
2771
|
-
pushError(errors, `Projection ${statement.id}
|
|
2858
|
+
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' cannot declare claim_value without claim`, entry.loc);
|
|
2772
2859
|
}
|
|
2773
2860
|
}
|
|
2774
2861
|
}
|
|
@@ -2778,7 +2865,7 @@ function validateProjectionUiLookups(errors, statement, fieldMap, registry) {
|
|
|
2778
2865
|
return;
|
|
2779
2866
|
}
|
|
2780
2867
|
|
|
2781
|
-
const lookupsField = fieldMap.get("
|
|
2868
|
+
const lookupsField = fieldMap.get("field_lookups")?.[0];
|
|
2782
2869
|
if (!lookupsField || lookupsField.value.type !== "block") {
|
|
2783
2870
|
return;
|
|
2784
2871
|
}
|
|
@@ -2790,56 +2877,56 @@ function validateProjectionUiLookups(errors, statement, fieldMap, registry) {
|
|
|
2790
2877
|
const [keyword, screenId, fieldKeyword, fieldName, entityKeyword, entityId, labelKeyword, labelField, maybeEmptyKeyword, maybeEmptyLabel] = tokens;
|
|
2791
2878
|
|
|
2792
2879
|
if (keyword !== "screen") {
|
|
2793
|
-
pushError(errors, `Projection ${statement.id}
|
|
2880
|
+
pushError(errors, `Projection ${statement.id} field_lookups entries must start with 'screen'`, entry.loc);
|
|
2794
2881
|
continue;
|
|
2795
2882
|
}
|
|
2796
2883
|
|
|
2797
2884
|
const screenEntry = screens.get(screenId);
|
|
2798
2885
|
if (!screenEntry) {
|
|
2799
|
-
pushError(errors, `Projection ${statement.id}
|
|
2886
|
+
pushError(errors, `Projection ${statement.id} field_lookups references unknown screen '${screenId}'`, entry.loc);
|
|
2800
2887
|
continue;
|
|
2801
2888
|
}
|
|
2802
2889
|
|
|
2803
2890
|
if (fieldKeyword !== "field") {
|
|
2804
|
-
pushError(errors, `Projection ${statement.id}
|
|
2891
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must use 'field'`, entry.loc);
|
|
2805
2892
|
}
|
|
2806
2893
|
if (!fieldName) {
|
|
2807
|
-
pushError(errors, `Projection ${statement.id}
|
|
2894
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must include a field name`, entry.loc);
|
|
2808
2895
|
}
|
|
2809
2896
|
|
|
2810
2897
|
if (entityKeyword !== "entity") {
|
|
2811
|
-
pushError(errors, `Projection ${statement.id}
|
|
2898
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must use 'entity'`, entry.loc);
|
|
2812
2899
|
}
|
|
2813
2900
|
const entity = entityId ? registry.get(entityId) : null;
|
|
2814
2901
|
if (!entity) {
|
|
2815
|
-
pushError(errors, `Projection ${statement.id}
|
|
2902
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' references missing entity '${entityId}'`, entry.loc);
|
|
2816
2903
|
} else if (entity.kind !== "entity") {
|
|
2817
|
-
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);
|
|
2818
2905
|
}
|
|
2819
2906
|
|
|
2820
2907
|
if (labelKeyword !== "label_field") {
|
|
2821
|
-
pushError(errors, `Projection ${statement.id}
|
|
2908
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must use 'label_field'`, entry.loc);
|
|
2822
2909
|
}
|
|
2823
2910
|
if (!labelField) {
|
|
2824
|
-
pushError(errors, `Projection ${statement.id}
|
|
2911
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must include a label_field`, entry.loc);
|
|
2825
2912
|
}
|
|
2826
2913
|
|
|
2827
2914
|
if (maybeEmptyKeyword && maybeEmptyKeyword !== "empty_label") {
|
|
2828
|
-
pushError(errors, `Projection ${statement.id}
|
|
2915
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' has unknown directive '${maybeEmptyKeyword}'`, entry.loc);
|
|
2829
2916
|
}
|
|
2830
2917
|
if (maybeEmptyKeyword === "empty_label" && !maybeEmptyLabel) {
|
|
2831
|
-
pushError(errors, `Projection ${statement.id}
|
|
2918
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must include a value for 'empty_label'`, entry.loc);
|
|
2832
2919
|
}
|
|
2833
2920
|
|
|
2834
2921
|
const availableFields = resolveProjectionUiScreenFieldNames(registry, screenEntry, statement);
|
|
2835
2922
|
if (fieldName && availableFields.size > 0 && !availableFields.has(fieldName)) {
|
|
2836
|
-
pushError(errors, `Projection ${statement.id}
|
|
2923
|
+
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' references unknown screen field '${fieldName}'`, entry.loc);
|
|
2837
2924
|
}
|
|
2838
2925
|
|
|
2839
2926
|
if (entity?.kind === "entity") {
|
|
2840
2927
|
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
2841
2928
|
if (labelField && !entityFieldNames.has(labelField)) {
|
|
2842
|
-
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);
|
|
2843
2930
|
}
|
|
2844
2931
|
}
|
|
2845
2932
|
}
|
|
@@ -2850,38 +2937,38 @@ function validateProjectionUiRoutes(errors, statement, fieldMap, registry) {
|
|
|
2850
2937
|
return;
|
|
2851
2938
|
}
|
|
2852
2939
|
|
|
2853
|
-
const routesField = fieldMap.get("
|
|
2940
|
+
const routesField = fieldMap.get("screen_routes")?.[0];
|
|
2854
2941
|
if (!routesField || routesField.value.type !== "block") {
|
|
2855
2942
|
return;
|
|
2856
2943
|
}
|
|
2857
2944
|
|
|
2858
2945
|
const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
|
|
2859
2946
|
const seenPaths = new Set();
|
|
2860
|
-
const platform = symbolValue(getFieldValue(statement, "
|
|
2947
|
+
const platform = symbolValue(getFieldValue(statement, "type"));
|
|
2861
2948
|
|
|
2862
2949
|
for (const entry of routesField.value.entries) {
|
|
2863
2950
|
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2864
2951
|
const [keyword, screenId, pathKeyword, routePath] = tokens;
|
|
2865
2952
|
|
|
2866
2953
|
if (keyword !== "screen") {
|
|
2867
|
-
pushError(errors, `Projection ${statement.id}
|
|
2954
|
+
pushError(errors, `Projection ${statement.id} screen_routes entries must start with 'screen'`, entry.loc);
|
|
2868
2955
|
continue;
|
|
2869
2956
|
}
|
|
2870
2957
|
if (!availableScreens.has(screenId)) {
|
|
2871
|
-
pushError(errors, `Projection ${statement.id}
|
|
2958
|
+
pushError(errors, `Projection ${statement.id} screen_routes references unknown screen '${screenId}'`, entry.loc);
|
|
2872
2959
|
}
|
|
2873
2960
|
if (pathKeyword !== "path") {
|
|
2874
|
-
pushError(errors, `Projection ${statement.id}
|
|
2961
|
+
pushError(errors, `Projection ${statement.id} screen_routes for '${screenId}' must use 'path'`, entry.loc);
|
|
2875
2962
|
}
|
|
2876
2963
|
if (!routePath) {
|
|
2877
|
-
pushError(errors, `Projection ${statement.id}
|
|
2964
|
+
pushError(errors, `Projection ${statement.id} screen_routes for '${screenId}' must include a path`, entry.loc);
|
|
2878
2965
|
continue;
|
|
2879
2966
|
}
|
|
2880
|
-
if ((platform === "
|
|
2881
|
-
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);
|
|
2882
2969
|
}
|
|
2883
2970
|
if (seenPaths.has(routePath)) {
|
|
2884
|
-
pushError(errors, `Projection ${statement.id}
|
|
2971
|
+
pushError(errors, `Projection ${statement.id} screen_routes has duplicate path '${routePath}'`, entry.loc);
|
|
2885
2972
|
}
|
|
2886
2973
|
seenPaths.add(routePath);
|
|
2887
2974
|
}
|
|
@@ -2897,7 +2984,7 @@ function validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry,
|
|
|
2897
2984
|
return;
|
|
2898
2985
|
}
|
|
2899
2986
|
|
|
2900
|
-
const platform = symbolValue(getFieldValue(statement, "
|
|
2987
|
+
const platform = symbolValue(getFieldValue(statement, "type"));
|
|
2901
2988
|
if (platform !== expectedPlatform) {
|
|
2902
2989
|
pushError(errors, `Projection ${statement.id} may only use '${surfaceBlockKey}' when platform is '${expectedPlatform}'`, surfaceField.loc);
|
|
2903
2990
|
return;
|
|
@@ -2969,11 +3056,11 @@ function validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry,
|
|
|
2969
3056
|
}
|
|
2970
3057
|
|
|
2971
3058
|
function validateProjectionUiWeb(errors, statement, fieldMap, registry) {
|
|
2972
|
-
validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "
|
|
3059
|
+
validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "web_hints", "web_surface");
|
|
2973
3060
|
}
|
|
2974
3061
|
|
|
2975
3062
|
function validateProjectionUiIos(errors, statement, fieldMap, registry) {
|
|
2976
|
-
validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "
|
|
3063
|
+
validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "ios_hints", "ios_surface");
|
|
2977
3064
|
}
|
|
2978
3065
|
|
|
2979
3066
|
function validateProjectionGeneratorDefaults(errors, statement, fieldMap) {
|
|
@@ -3014,7 +3101,7 @@ function validateProjectionDbTables(errors, statement, fieldMap, registry) {
|
|
|
3014
3101
|
return;
|
|
3015
3102
|
}
|
|
3016
3103
|
|
|
3017
|
-
const dbTablesField = fieldMap.get("
|
|
3104
|
+
const dbTablesField = fieldMap.get("tables")?.[0];
|
|
3018
3105
|
if (!dbTablesField || dbTablesField.value.type !== "block") {
|
|
3019
3106
|
return;
|
|
3020
3107
|
}
|
|
@@ -3027,22 +3114,22 @@ function validateProjectionDbTables(errors, statement, fieldMap, registry) {
|
|
|
3027
3114
|
const entity = registry.get(entityId);
|
|
3028
3115
|
|
|
3029
3116
|
if (!entity) {
|
|
3030
|
-
pushError(errors, `Projection ${statement.id}
|
|
3117
|
+
pushError(errors, `Projection ${statement.id} tables references missing entity '${entityId}'`, entry.loc);
|
|
3031
3118
|
continue;
|
|
3032
3119
|
}
|
|
3033
3120
|
if (entity.kind !== "entity") {
|
|
3034
|
-
pushError(errors, `Projection ${statement.id}
|
|
3121
|
+
pushError(errors, `Projection ${statement.id} tables must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3035
3122
|
}
|
|
3036
3123
|
if (!realized.has(entityId)) {
|
|
3037
|
-
pushError(errors, `Projection ${statement.id}
|
|
3124
|
+
pushError(errors, `Projection ${statement.id} tables entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3038
3125
|
}
|
|
3039
3126
|
if (tableKeyword !== "table") {
|
|
3040
|
-
pushError(errors, `Projection ${statement.id}
|
|
3127
|
+
pushError(errors, `Projection ${statement.id} tables for '${entityId}' must use 'table'`, entry.loc);
|
|
3041
3128
|
}
|
|
3042
3129
|
if (!tableName) {
|
|
3043
|
-
pushError(errors, `Projection ${statement.id}
|
|
3130
|
+
pushError(errors, `Projection ${statement.id} tables for '${entityId}' must include a table name`, entry.loc);
|
|
3044
3131
|
} else if (seenTables.has(tableName)) {
|
|
3045
|
-
pushError(errors, `Projection ${statement.id}
|
|
3132
|
+
pushError(errors, `Projection ${statement.id} tables has duplicate table name '${tableName}'`, entry.loc);
|
|
3046
3133
|
}
|
|
3047
3134
|
seenTables.add(tableName);
|
|
3048
3135
|
}
|
|
@@ -3053,7 +3140,7 @@ function validateProjectionDbColumns(errors, statement, fieldMap, registry) {
|
|
|
3053
3140
|
return;
|
|
3054
3141
|
}
|
|
3055
3142
|
|
|
3056
|
-
const dbColumnsField = fieldMap.get("
|
|
3143
|
+
const dbColumnsField = fieldMap.get("columns")?.[0];
|
|
3057
3144
|
if (!dbColumnsField || dbColumnsField.value.type !== "block") {
|
|
3058
3145
|
return;
|
|
3059
3146
|
}
|
|
@@ -3065,27 +3152,27 @@ function validateProjectionDbColumns(errors, statement, fieldMap, registry) {
|
|
|
3065
3152
|
const entity = registry.get(entityId);
|
|
3066
3153
|
|
|
3067
3154
|
if (!entity) {
|
|
3068
|
-
pushError(errors, `Projection ${statement.id}
|
|
3155
|
+
pushError(errors, `Projection ${statement.id} columns references missing entity '${entityId}'`, entry.loc);
|
|
3069
3156
|
continue;
|
|
3070
3157
|
}
|
|
3071
3158
|
if (entity.kind !== "entity") {
|
|
3072
|
-
pushError(errors, `Projection ${statement.id}
|
|
3159
|
+
pushError(errors, `Projection ${statement.id} columns must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3073
3160
|
}
|
|
3074
3161
|
if (!realized.has(entityId)) {
|
|
3075
|
-
pushError(errors, `Projection ${statement.id}
|
|
3162
|
+
pushError(errors, `Projection ${statement.id} columns entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3076
3163
|
}
|
|
3077
3164
|
if (fieldKeyword !== "field") {
|
|
3078
|
-
pushError(errors, `Projection ${statement.id}
|
|
3165
|
+
pushError(errors, `Projection ${statement.id} columns for '${entityId}' must use 'field'`, entry.loc);
|
|
3079
3166
|
}
|
|
3080
3167
|
if (columnKeyword !== "column") {
|
|
3081
|
-
pushError(errors, `Projection ${statement.id}
|
|
3168
|
+
pushError(errors, `Projection ${statement.id} columns for '${entityId}' must use 'column'`, entry.loc);
|
|
3082
3169
|
}
|
|
3083
3170
|
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3084
3171
|
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3085
|
-
pushError(errors, `Projection ${statement.id}
|
|
3172
|
+
pushError(errors, `Projection ${statement.id} columns references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3086
3173
|
}
|
|
3087
3174
|
if (!columnName) {
|
|
3088
|
-
pushError(errors, `Projection ${statement.id}
|
|
3175
|
+
pushError(errors, `Projection ${statement.id} columns for '${entityId}.${fieldName}' must include a column name`, entry.loc);
|
|
3089
3176
|
}
|
|
3090
3177
|
}
|
|
3091
3178
|
}
|
|
@@ -3095,7 +3182,7 @@ function validateProjectionDbKeys(errors, statement, fieldMap, registry) {
|
|
|
3095
3182
|
return;
|
|
3096
3183
|
}
|
|
3097
3184
|
|
|
3098
|
-
const dbKeysField = fieldMap.get("
|
|
3185
|
+
const dbKeysField = fieldMap.get("keys")?.[0];
|
|
3099
3186
|
if (!dbKeysField || dbKeysField.value.type !== "block") {
|
|
3100
3187
|
return;
|
|
3101
3188
|
}
|
|
@@ -3107,27 +3194,27 @@ function validateProjectionDbKeys(errors, statement, fieldMap, registry) {
|
|
|
3107
3194
|
const entity = registry.get(entityId);
|
|
3108
3195
|
|
|
3109
3196
|
if (!entity) {
|
|
3110
|
-
pushError(errors, `Projection ${statement.id}
|
|
3197
|
+
pushError(errors, `Projection ${statement.id} keys references missing entity '${entityId}'`, entry.loc);
|
|
3111
3198
|
continue;
|
|
3112
3199
|
}
|
|
3113
3200
|
if (entity.kind !== "entity") {
|
|
3114
|
-
pushError(errors, `Projection ${statement.id}
|
|
3201
|
+
pushError(errors, `Projection ${statement.id} keys must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3115
3202
|
}
|
|
3116
3203
|
if (!realized.has(entityId)) {
|
|
3117
|
-
pushError(errors, `Projection ${statement.id}
|
|
3204
|
+
pushError(errors, `Projection ${statement.id} keys entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3118
3205
|
}
|
|
3119
3206
|
if (!["primary", "unique"].includes(keyType || "")) {
|
|
3120
|
-
pushError(errors, `Projection ${statement.id}
|
|
3207
|
+
pushError(errors, `Projection ${statement.id} keys for '${entityId}' has invalid key type '${keyType}'`, entry.loc);
|
|
3121
3208
|
}
|
|
3122
3209
|
const fieldList = entry.items[2];
|
|
3123
3210
|
if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
|
|
3124
|
-
pushError(errors, `Projection ${statement.id}
|
|
3211
|
+
pushError(errors, `Projection ${statement.id} keys for '${entityId}' must include a non-empty field list`, entry.loc);
|
|
3125
3212
|
continue;
|
|
3126
3213
|
}
|
|
3127
3214
|
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3128
3215
|
for (const item of fieldList.items) {
|
|
3129
3216
|
if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
|
|
3130
|
-
pushError(errors, `Projection ${statement.id}
|
|
3217
|
+
pushError(errors, `Projection ${statement.id} keys references unknown field '${item.value}' on ${entityId}`, item.loc);
|
|
3131
3218
|
}
|
|
3132
3219
|
}
|
|
3133
3220
|
}
|
|
@@ -3138,7 +3225,7 @@ function validateProjectionDbIndexes(errors, statement, fieldMap, registry) {
|
|
|
3138
3225
|
return;
|
|
3139
3226
|
}
|
|
3140
3227
|
|
|
3141
|
-
const dbIndexesField = fieldMap.get("
|
|
3228
|
+
const dbIndexesField = fieldMap.get("indexes")?.[0];
|
|
3142
3229
|
if (!dbIndexesField || dbIndexesField.value.type !== "block") {
|
|
3143
3230
|
return;
|
|
3144
3231
|
}
|
|
@@ -3150,27 +3237,27 @@ function validateProjectionDbIndexes(errors, statement, fieldMap, registry) {
|
|
|
3150
3237
|
const entity = registry.get(entityId);
|
|
3151
3238
|
|
|
3152
3239
|
if (!entity) {
|
|
3153
|
-
pushError(errors, `Projection ${statement.id}
|
|
3240
|
+
pushError(errors, `Projection ${statement.id} indexes references missing entity '${entityId}'`, entry.loc);
|
|
3154
3241
|
continue;
|
|
3155
3242
|
}
|
|
3156
3243
|
if (entity.kind !== "entity") {
|
|
3157
|
-
pushError(errors, `Projection ${statement.id}
|
|
3244
|
+
pushError(errors, `Projection ${statement.id} indexes must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3158
3245
|
}
|
|
3159
3246
|
if (!realized.has(entityId)) {
|
|
3160
|
-
pushError(errors, `Projection ${statement.id}
|
|
3247
|
+
pushError(errors, `Projection ${statement.id} indexes entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3161
3248
|
}
|
|
3162
3249
|
if (!["index", "unique"].includes(indexType || "")) {
|
|
3163
|
-
pushError(errors, `Projection ${statement.id}
|
|
3250
|
+
pushError(errors, `Projection ${statement.id} indexes for '${entityId}' has invalid index type '${indexType}'`, entry.loc);
|
|
3164
3251
|
}
|
|
3165
3252
|
const fieldList = entry.items[2];
|
|
3166
3253
|
if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
|
|
3167
|
-
pushError(errors, `Projection ${statement.id}
|
|
3254
|
+
pushError(errors, `Projection ${statement.id} indexes for '${entityId}' must include a non-empty field list`, entry.loc);
|
|
3168
3255
|
continue;
|
|
3169
3256
|
}
|
|
3170
3257
|
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3171
3258
|
for (const item of fieldList.items) {
|
|
3172
3259
|
if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
|
|
3173
|
-
pushError(errors, `Projection ${statement.id}
|
|
3260
|
+
pushError(errors, `Projection ${statement.id} indexes references unknown field '${item.value}' on ${entityId}`, item.loc);
|
|
3174
3261
|
}
|
|
3175
3262
|
}
|
|
3176
3263
|
}
|
|
@@ -3181,7 +3268,7 @@ function validateProjectionDbRelations(errors, statement, fieldMap, registry) {
|
|
|
3181
3268
|
return;
|
|
3182
3269
|
}
|
|
3183
3270
|
|
|
3184
|
-
const dbRelationsField = fieldMap.get("
|
|
3271
|
+
const dbRelationsField = fieldMap.get("relations")?.[0];
|
|
3185
3272
|
if (!dbRelationsField || dbRelationsField.value.type !== "block") {
|
|
3186
3273
|
return;
|
|
3187
3274
|
}
|
|
@@ -3193,43 +3280,43 @@ function validateProjectionDbRelations(errors, statement, fieldMap, registry) {
|
|
|
3193
3280
|
const entity = registry.get(entityId);
|
|
3194
3281
|
|
|
3195
3282
|
if (!entity) {
|
|
3196
|
-
pushError(errors, `Projection ${statement.id}
|
|
3283
|
+
pushError(errors, `Projection ${statement.id} relations references missing entity '${entityId}'`, entry.loc);
|
|
3197
3284
|
continue;
|
|
3198
3285
|
}
|
|
3199
3286
|
if (entity.kind !== "entity") {
|
|
3200
|
-
pushError(errors, `Projection ${statement.id}
|
|
3287
|
+
pushError(errors, `Projection ${statement.id} relations must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3201
3288
|
}
|
|
3202
3289
|
if (!realized.has(entityId)) {
|
|
3203
|
-
pushError(errors, `Projection ${statement.id}
|
|
3290
|
+
pushError(errors, `Projection ${statement.id} relations entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3204
3291
|
}
|
|
3205
3292
|
if (relationType !== "foreign_key") {
|
|
3206
|
-
pushError(errors, `Projection ${statement.id}
|
|
3293
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' must use 'foreign_key'`, entry.loc);
|
|
3207
3294
|
}
|
|
3208
3295
|
if (referencesKeyword !== "references") {
|
|
3209
|
-
pushError(errors, `Projection ${statement.id}
|
|
3296
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' must use 'references'`, entry.loc);
|
|
3210
3297
|
}
|
|
3211
3298
|
if (onDeleteKeyword && onDeleteKeyword !== "on_delete") {
|
|
3212
|
-
pushError(errors, `Projection ${statement.id}
|
|
3299
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' has unexpected token '${onDeleteKeyword}'`, entry.loc);
|
|
3213
3300
|
}
|
|
3214
3301
|
if (onDeleteValue && !["cascade", "restrict", "set_null", "no_action"].includes(onDeleteValue)) {
|
|
3215
|
-
pushError(errors, `Projection ${statement.id}
|
|
3302
|
+
pushError(errors, `Projection ${statement.id} relations for '${entityId}' has invalid on_delete '${onDeleteValue}'`, entry.loc);
|
|
3216
3303
|
}
|
|
3217
3304
|
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3218
3305
|
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3219
|
-
pushError(errors, `Projection ${statement.id}
|
|
3306
|
+
pushError(errors, `Projection ${statement.id} relations references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3220
3307
|
}
|
|
3221
3308
|
const [targetEntityId, targetFieldName] = (targetRef || "").split(".");
|
|
3222
3309
|
const targetEntity = registry.get(targetEntityId);
|
|
3223
3310
|
if (!targetEntity) {
|
|
3224
|
-
pushError(errors, `Projection ${statement.id}
|
|
3311
|
+
pushError(errors, `Projection ${statement.id} relations references missing target entity '${targetEntityId}'`, entry.loc);
|
|
3225
3312
|
continue;
|
|
3226
3313
|
}
|
|
3227
3314
|
if (targetEntity.kind !== "entity") {
|
|
3228
|
-
pushError(errors, `Projection ${statement.id}
|
|
3315
|
+
pushError(errors, `Projection ${statement.id} relations must reference an entity target, found ${targetEntity.kind} '${targetEntity.id}'`, entry.loc);
|
|
3229
3316
|
}
|
|
3230
3317
|
const targetFieldNames = new Set(statementFieldNames(targetEntity));
|
|
3231
3318
|
if (targetFieldName && targetFieldNames.size > 0 && !targetFieldNames.has(targetFieldName)) {
|
|
3232
|
-
pushError(errors, `Projection ${statement.id}
|
|
3319
|
+
pushError(errors, `Projection ${statement.id} relations references unknown target field '${targetFieldName}' on ${targetEntityId}`, entry.loc);
|
|
3233
3320
|
}
|
|
3234
3321
|
}
|
|
3235
3322
|
}
|
|
@@ -3239,7 +3326,7 @@ function validateProjectionDbLifecycle(errors, statement, fieldMap, registry) {
|
|
|
3239
3326
|
return;
|
|
3240
3327
|
}
|
|
3241
3328
|
|
|
3242
|
-
const dbLifecycleField = fieldMap.get("
|
|
3329
|
+
const dbLifecycleField = fieldMap.get("lifecycle")?.[0];
|
|
3243
3330
|
if (!dbLifecycleField || dbLifecycleField.value.type !== "block") {
|
|
3244
3331
|
return;
|
|
3245
3332
|
}
|
|
@@ -3251,19 +3338,19 @@ function validateProjectionDbLifecycle(errors, statement, fieldMap, registry) {
|
|
|
3251
3338
|
const entity = registry.get(entityId);
|
|
3252
3339
|
|
|
3253
3340
|
if (!entity) {
|
|
3254
|
-
pushError(errors, `Projection ${statement.id}
|
|
3341
|
+
pushError(errors, `Projection ${statement.id} lifecycle references missing entity '${entityId}'`, entry.loc);
|
|
3255
3342
|
continue;
|
|
3256
3343
|
}
|
|
3257
3344
|
if (entity.kind !== "entity") {
|
|
3258
|
-
pushError(errors, `Projection ${statement.id}
|
|
3345
|
+
pushError(errors, `Projection ${statement.id} lifecycle must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3259
3346
|
}
|
|
3260
3347
|
if (!realized.has(entityId)) {
|
|
3261
|
-
pushError(errors, `Projection ${statement.id}
|
|
3348
|
+
pushError(errors, `Projection ${statement.id} lifecycle entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3262
3349
|
}
|
|
3263
3350
|
|
|
3264
|
-
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `
|
|
3351
|
+
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `lifecycle for '${entityId}'`);
|
|
3265
3352
|
if (!["soft_delete", "timestamps"].includes(lifecycleType || "")) {
|
|
3266
|
-
pushError(errors, `Projection ${statement.id}
|
|
3353
|
+
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' has invalid lifecycle '${lifecycleType}'`, entry.loc);
|
|
3267
3354
|
continue;
|
|
3268
3355
|
}
|
|
3269
3356
|
|
|
@@ -3271,23 +3358,23 @@ function validateProjectionDbLifecycle(errors, statement, fieldMap, registry) {
|
|
|
3271
3358
|
if (lifecycleType === "soft_delete") {
|
|
3272
3359
|
for (const requiredKey of ["field", "value"]) {
|
|
3273
3360
|
if (!directives.has(requiredKey)) {
|
|
3274
|
-
pushError(errors, `Projection ${statement.id}
|
|
3361
|
+
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' must include '${requiredKey}' for soft_delete`, entry.loc);
|
|
3275
3362
|
}
|
|
3276
3363
|
}
|
|
3277
3364
|
const fieldName = directives.get("field");
|
|
3278
3365
|
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3279
|
-
pushError(errors, `Projection ${statement.id}
|
|
3366
|
+
pushError(errors, `Projection ${statement.id} lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3280
3367
|
}
|
|
3281
3368
|
}
|
|
3282
3369
|
|
|
3283
3370
|
if (lifecycleType === "timestamps") {
|
|
3284
3371
|
for (const requiredKey of ["created_at", "updated_at"]) {
|
|
3285
3372
|
if (!directives.has(requiredKey)) {
|
|
3286
|
-
pushError(errors, `Projection ${statement.id}
|
|
3373
|
+
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' must include '${requiredKey}' for timestamps`, entry.loc);
|
|
3287
3374
|
}
|
|
3288
3375
|
const fieldName = directives.get(requiredKey);
|
|
3289
3376
|
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3290
|
-
pushError(errors, `Projection ${statement.id}
|
|
3377
|
+
pushError(errors, `Projection ${statement.id} lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3291
3378
|
}
|
|
3292
3379
|
}
|
|
3293
3380
|
}
|
|
@@ -3300,7 +3387,11 @@ export function buildRegistry(workspaceAst, errors) {
|
|
|
3300
3387
|
for (const file of workspaceAst.files) {
|
|
3301
3388
|
for (const statement of file.statements) {
|
|
3302
3389
|
if (!STATEMENT_KINDS.has(statement.kind)) {
|
|
3303
|
-
|
|
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
|
+
}
|
|
3304
3395
|
}
|
|
3305
3396
|
|
|
3306
3397
|
if (!IDENTIFIER_PATTERN.test(statement.id)) {
|
|
@@ -3499,6 +3590,7 @@ export function validateWorkspace(workspaceAst) {
|
|
|
3499
3590
|
validateProjectionHttpDownload(errors, statement, fieldMap, registry);
|
|
3500
3591
|
validateProjectionHttpAuthz(errors, statement, fieldMap, registry);
|
|
3501
3592
|
validateProjectionHttpCallbacks(errors, statement, fieldMap, registry);
|
|
3593
|
+
validateProjectionUiOwnership(errors, statement, fieldMap);
|
|
3502
3594
|
validateProjectionUiScreens(errors, statement, fieldMap, registry);
|
|
3503
3595
|
validateProjectionUiCollections(errors, statement, fieldMap, registry);
|
|
3504
3596
|
validateProjectionUiActions(errors, statement, fieldMap, registry);
|
|
@@ -3519,7 +3611,7 @@ export function validateWorkspace(workspaceAst) {
|
|
|
3519
3611
|
validateProjectionDbRelations(errors, statement, fieldMap, registry);
|
|
3520
3612
|
validateProjectionDbLifecycle(errors, statement, fieldMap, registry);
|
|
3521
3613
|
validateProjectionGeneratorDefaults(errors, statement, fieldMap);
|
|
3522
|
-
|
|
3614
|
+
validateWidget(errors, statement, fieldMap, registry);
|
|
3523
3615
|
validateDomain(errors, statement, fieldMap, registry);
|
|
3524
3616
|
validateDomainTag(errors, statement, fieldMap, registry);
|
|
3525
3617
|
validatePitch(errors, statement, fieldMap, registry);
|