@topogram/cli 0.3.64 → 0.3.65
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/package.json +1 -1
- package/src/adoption/plan/index.js +703 -0
- package/src/adoption/plan.js +12 -703
- package/src/agent-ops/query-builders/auth.js +375 -0
- package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
- package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
- package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
- package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
- package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
- package/src/agent-ops/query-builders/change-risk.js +25 -0
- package/src/agent-ops/query-builders/common.js +149 -0
- package/src/agent-ops/query-builders/maintained-risk.js +539 -0
- package/src/agent-ops/query-builders/maintained-shared.js +120 -0
- package/src/agent-ops/query-builders/multi-agent.js +547 -0
- package/src/agent-ops/query-builders/projection-impacts.js +514 -0
- package/src/agent-ops/query-builders/work-packets.js +417 -0
- package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
- package/src/agent-ops/query-builders/workflow-context.js +398 -0
- package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
- package/src/agent-ops/query-builders/workflow-presets.js +341 -0
- package/src/agent-ops/query-builders.d.ts +26 -26
- package/src/agent-ops/query-builders.js +42 -5021
- package/src/catalog/constants.js +10 -0
- package/src/catalog/copy.js +60 -0
- package/src/catalog/diagnostics.js +15 -0
- package/src/catalog/entries.js +42 -0
- package/src/catalog/files.js +67 -0
- package/src/catalog/provenance.js +122 -0
- package/src/catalog/source.js +150 -0
- package/src/catalog/validation.js +252 -0
- package/src/catalog.d.ts +2 -0
- package/src/catalog.js +18 -746
- package/src/cli/commands/catalog/check.js +31 -0
- package/src/cli/commands/catalog/copy.js +59 -0
- package/src/cli/commands/catalog/doctor.js +248 -0
- package/src/cli/commands/catalog/help.js +21 -0
- package/src/cli/commands/catalog/list.js +52 -0
- package/src/cli/commands/catalog/runner.js +92 -0
- package/src/cli/commands/catalog/shared.js +17 -0
- package/src/cli/commands/catalog/show.js +134 -0
- package/src/cli/commands/catalog.js +30 -615
- package/src/cli/commands/generator-policy/package-info.js +162 -0
- package/src/cli/commands/generator-policy/payloads.js +372 -0
- package/src/cli/commands/generator-policy/printers.js +159 -0
- package/src/cli/commands/generator-policy/runner.js +81 -0
- package/src/cli/commands/generator-policy/shared.js +39 -0
- package/src/cli/commands/generator-policy.js +15 -783
- package/src/cli/commands/import/adopt.js +170 -0
- package/src/cli/commands/import/check.js +91 -0
- package/src/cli/commands/import/diff.js +84 -0
- package/src/cli/commands/import/help.js +47 -0
- package/src/cli/commands/import/paths.js +277 -0
- package/src/cli/commands/import/plan.js +284 -0
- package/src/cli/commands/import/refresh.js +470 -0
- package/src/cli/commands/import/status-history.js +196 -0
- package/src/cli/commands/import/workspace.js +230 -0
- package/src/cli/commands/import.js +33 -1732
- package/src/cli/commands/package/constants.js +17 -0
- package/src/cli/commands/package/doctor.js +240 -0
- package/src/cli/commands/package/help.js +27 -0
- package/src/cli/commands/package/lockfile.js +135 -0
- package/src/cli/commands/package/npm.js +97 -0
- package/src/cli/commands/package/reporting.js +35 -0
- package/src/cli/commands/package/runner.js +33 -0
- package/src/cli/commands/package/shared.js +9 -0
- package/src/cli/commands/package/update-cli.js +252 -0
- package/src/cli/commands/package/versions.js +35 -0
- package/src/cli/commands/package.js +29 -813
- package/src/cli/commands/query/change-plan.js +68 -0
- package/src/cli/commands/query/definitions.js +202 -0
- package/src/cli/commands/query/import-adopt.js +121 -0
- package/src/cli/commands/query/runner/artifacts.js +102 -0
- package/src/cli/commands/query/runner/boundaries.js +211 -0
- package/src/cli/commands/query/runner/change.js +182 -0
- package/src/cli/commands/query/runner/import-adopt.js +111 -0
- package/src/cli/commands/query/runner/index.js +31 -0
- package/src/cli/commands/query/runner/output.js +12 -0
- package/src/cli/commands/query/runner/workflow.js +241 -0
- package/src/cli/commands/query/runner.js +3 -0
- package/src/cli/commands/query/workflow-context.js +5 -0
- package/src/cli/commands/query/workspace.js +274 -0
- package/src/cli/commands/query.js +9 -1300
- package/src/cli/commands/template/baseline.js +100 -0
- package/src/cli/commands/template/check.js +466 -0
- package/src/cli/commands/template/constants.js +8 -0
- package/src/cli/commands/template/diagnostics.js +26 -0
- package/src/cli/commands/template/help.js +28 -0
- package/src/cli/commands/template/lifecycle.js +404 -0
- package/src/cli/commands/template/list-show.js +287 -0
- package/src/cli/commands/template/policy.js +422 -0
- package/src/cli/commands/template/shared.js +127 -0
- package/src/cli/commands/template/updates.js +352 -0
- package/src/cli/commands/template.js +41 -2143
- package/src/generator/api/contracts.js +497 -0
- package/src/generator/api/metadata.js +221 -0
- package/src/generator/api/openapi.js +559 -0
- package/src/generator/api/schema.js +124 -0
- package/src/generator/api/types.d.ts +98 -0
- package/src/generator/api.js +3 -1195
- package/src/generator/context/shared/domain-sdlc.js +282 -0
- package/src/generator/context/shared/maintained-boundary.js +665 -0
- package/src/generator/context/shared/metrics.js +85 -0
- package/src/generator/context/shared/primitives.js +64 -0
- package/src/generator/context/shared/relationships.js +453 -0
- package/src/generator/context/shared/summaries.js +263 -0
- package/src/generator/context/shared/types.d.ts +207 -0
- package/src/generator/context/shared.d.ts +42 -0
- package/src/generator/context/shared.js +80 -1390
- package/src/generator/context/slice/core.js +397 -0
- package/src/generator/context/slice/sdlc.js +417 -0
- package/src/generator/context/slice/ui-packets.js +183 -0
- package/src/generator/context/slice.js +2 -859
- package/src/generator/registry/index.js +507 -0
- package/src/generator/registry.js +18 -504
- package/src/generator/runtime/environment/index.js +666 -0
- package/src/generator/runtime/environment.js +4 -666
- package/src/generator/runtime/runtime-check/index.js +554 -0
- package/src/generator/runtime/runtime-check.js +4 -554
- package/src/generator/runtime/shared/index.js +572 -0
- package/src/generator/runtime/shared.js +19 -570
- package/src/generator/shared.d.ts +2 -0
- package/src/generator/surfaces/shared.d.ts +3 -0
- package/src/generator/widget-conformance/behavior-report.js +258 -0
- package/src/generator/widget-conformance/checks.js +371 -0
- package/src/generator/widget-conformance/projection-context.js +200 -0
- package/src/generator/widget-conformance/report.js +166 -0
- package/src/generator/widget-conformance/types.d.ts +121 -0
- package/src/generator/widget-conformance.js +3 -824
- package/src/import/core/context.d.ts +3 -0
- package/src/import/core/contracts.d.ts +1 -0
- package/src/import/core/registry.d.ts +4 -0
- package/src/import/core/runner/candidates.js +217 -0
- package/src/import/core/runner/options.js +22 -0
- package/src/import/core/runner/reports.js +50 -0
- package/src/import/core/runner/run.js +79 -0
- package/src/import/core/runner/tracks.js +150 -0
- package/src/import/core/runner/ui-drafts.js +337 -0
- package/src/import/core/runner.js +3 -698
- package/src/import/core/shared/api-routes.js +221 -0
- package/src/import/core/shared/candidates.js +97 -0
- package/src/import/core/shared/files.js +177 -0
- package/src/import/core/shared/next-app.js +389 -0
- package/src/import/core/shared/types.d.ts +51 -0
- package/src/import/core/shared/ui-routes.js +230 -0
- package/src/import/core/shared.js +60 -861
- package/src/new-project/constants.js +128 -0
- package/src/new-project/create.js +83 -0
- package/src/new-project/json.js +28 -0
- package/src/new-project/metadata.js +96 -0
- package/src/new-project/package-spec.js +161 -0
- package/src/new-project/project-files.js +348 -0
- package/src/new-project/template-policy.js +269 -0
- package/src/new-project/template-resolution.js +368 -0
- package/src/new-project/template-snapshots.js +430 -0
- package/src/new-project/template-updates.js +512 -0
- package/src/new-project/types.d.ts +83 -0
- package/src/new-project.js +6 -2277
- package/src/parser.d.ts +87 -1
- package/src/parser.js +118 -0
- package/src/policy/review-boundaries.d.ts +15 -0
- package/src/project-config/index.js +564 -0
- package/src/project-config.js +19 -561
- package/src/resolver/enrich/acceptance-criterion.js +2 -0
- package/src/resolver/enrich/bug.js +2 -0
- package/src/resolver/enrich/pitch.js +2 -0
- package/src/resolver/enrich/requirement.js +2 -0
- package/src/resolver/enrich/task.js +2 -0
- package/src/resolver/index.js +19 -2089
- package/src/resolver/normalize.js +384 -1
- package/src/resolver/plans.js +168 -0
- package/src/resolver/projections-api.js +494 -0
- package/src/resolver/projections-db.js +133 -0
- package/src/resolver/projections-ui.js +317 -0
- package/src/resolver/shapes.js +251 -0
- package/src/resolver/shared.js +278 -0
- package/src/resolver/widgets.js +132 -0
- package/src/template-trust/constants.js +62 -0
- package/src/template-trust/content.js +258 -0
- package/src/template-trust/diff.js +92 -0
- package/src/template-trust/policy.js +61 -0
- package/src/template-trust/record.js +90 -0
- package/src/template-trust/status.js +182 -0
- package/src/template-trust.js +24 -687
- package/src/text-helpers.d.ts +1 -0
- package/src/topogram-types.d.ts +69 -0
- package/src/validator/common.js +488 -0
- package/src/validator/data-model.js +237 -0
- package/src/validator/docs.js +167 -0
- package/src/validator/expressions.js +146 -1
- package/src/validator/index.d.ts +23 -0
- package/src/validator/index.js +32 -3585
- package/src/validator/kinds.d.ts +41 -0
- package/src/validator/kinds.js +2 -0
- package/src/validator/model-helpers.js +46 -0
- package/src/validator/per-kind/acceptance-criterion.js +5 -0
- package/src/validator/per-kind/bug.js +6 -0
- package/src/validator/per-kind/domain.js +15 -2
- package/src/validator/per-kind/pitch.js +7 -0
- package/src/validator/per-kind/requirement.js +5 -0
- package/src/validator/per-kind/task.js +7 -0
- package/src/validator/per-kind/widget.js +14 -0
- package/src/validator/projections/api-http-async.js +410 -0
- package/src/validator/projections/api-http-authz.js +88 -0
- package/src/validator/projections/api-http-core.js +205 -0
- package/src/validator/projections/api-http-policies.js +339 -0
- package/src/validator/projections/api-http-responses.js +233 -0
- package/src/validator/projections/api-http.js +44 -0
- package/src/validator/projections/db.js +353 -0
- package/src/validator/projections/generator-defaults.js +45 -0
- package/src/validator/projections/helpers.js +87 -0
- package/src/validator/projections/ui-helpers.js +214 -0
- package/src/validator/projections/ui-navigation.js +344 -0
- package/src/validator/projections/ui-structure.js +364 -0
- package/src/validator/projections/ui-widgets.js +493 -0
- package/src/validator/projections/ui.js +46 -0
- package/src/validator/registry.js +48 -1
- package/src/validator/utils.d.ts +20 -0
- package/src/validator/utils.js +115 -12
- package/src/widget-behavior.d.ts +1 -0
- package/src/workflows/import-app/api/collect.js +221 -0
- package/src/workflows/import-app/api/openapi.js +257 -0
- package/src/workflows/import-app/api/routes.js +327 -0
- package/src/workflows/import-app/api/sources.js +22 -0
- package/src/workflows/import-app/api.js +2 -797
- package/src/workflows/reconcile/adoption-plan/build.js +208 -0
- package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
- package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
- package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
- package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
- package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
- package/src/workflows/reconcile/adoption-plan.js +30 -740
- package/src/workflows/reconcile/auth/closures.js +115 -0
- package/src/workflows/reconcile/auth/formatters.js +142 -0
- package/src/workflows/reconcile/auth/inference.js +330 -0
- package/src/workflows/reconcile/auth/roles.js +122 -0
- package/src/workflows/reconcile/auth.js +35 -690
- package/src/workflows/reconcile/bundle-core/index.js +600 -0
- package/src/workflows/reconcile/bundle-core.js +12 -598
- package/src/workflows/reconcile/canonical-surface.js +1 -1
- package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
- package/src/workflows/reconcile/impacts/indexes.js +101 -0
- package/src/workflows/reconcile/impacts/patches.js +252 -0
- package/src/workflows/reconcile/impacts/reports.js +80 -0
- package/src/workflows/reconcile/impacts.js +14 -623
- package/src/workspace-docs.d.ts +29 -0
package/src/validator/index.js
CHANGED
|
@@ -1,54 +1,16 @@
|
|
|
1
|
+
import { validateCoreStatement, validateReferenceRules } from "./common.js";
|
|
2
|
+
import { validateDataModelStatement, validateShapeFrom } from "./data-model.js";
|
|
3
|
+
import { validateDocs } from "./docs.js";
|
|
4
|
+
import { validateExpressions } from "./expressions.js";
|
|
5
|
+
import { validateApiHttpProjection } from "./projections/api-http.js";
|
|
6
|
+
import { validateDbProjection } from "./projections/db.js";
|
|
7
|
+
import { validateProjectionGeneratorDefaults } from "./projections/generator-defaults.js";
|
|
8
|
+
import { validateUiProjection } from "./projections/ui.js";
|
|
9
|
+
import { buildRegistry } from "./registry.js";
|
|
1
10
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
DOC_REFERENCE_FIELDS,
|
|
6
|
-
DOC_STATUSES
|
|
7
|
-
} from "../workspace-docs.js";
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
STATEMENT_KINDS,
|
|
11
|
-
IDENTIFIER_PATTERN,
|
|
12
|
-
DOMAIN_IDENTIFIER_PATTERN,
|
|
13
|
-
DOMAIN_TAGGABLE_KINDS,
|
|
14
|
-
PITCH_IDENTIFIER_PATTERN,
|
|
15
|
-
REQUIREMENT_IDENTIFIER_PATTERN,
|
|
16
|
-
ACCEPTANCE_CRITERION_IDENTIFIER_PATTERN,
|
|
17
|
-
TASK_IDENTIFIER_PATTERN,
|
|
18
|
-
BUG_IDENTIFIER_PATTERN,
|
|
19
|
-
DOCUMENT_IDENTIFIER_PATTERN,
|
|
20
|
-
GLOBAL_STATUSES,
|
|
21
|
-
DECISION_STATUSES,
|
|
22
|
-
RULE_SEVERITIES,
|
|
23
|
-
VERIFICATION_METHODS,
|
|
24
|
-
STATUS_SETS_BY_KIND,
|
|
25
|
-
PITCH_STATUSES,
|
|
26
|
-
REQUIREMENT_STATUSES,
|
|
27
|
-
ACCEPTANCE_CRITERION_STATUSES,
|
|
28
|
-
TASK_STATUSES,
|
|
29
|
-
BUG_STATUSES,
|
|
30
|
-
PRIORITY_VALUES,
|
|
31
|
-
WORK_TYPES,
|
|
32
|
-
BUG_SEVERITIES,
|
|
33
|
-
DOC_TYPES,
|
|
34
|
-
AUDIENCES,
|
|
35
|
-
UI_SCREEN_KINDS,
|
|
36
|
-
UI_COLLECTION_PRESENTATIONS,
|
|
37
|
-
UI_NAVIGATION_PATTERNS,
|
|
38
|
-
UI_REGION_KINDS,
|
|
39
|
-
UI_PATTERN_KINDS,
|
|
40
|
-
UI_APP_SHELL_KINDS,
|
|
41
|
-
UI_WINDOWING_MODES,
|
|
42
|
-
UI_STATE_KINDS,
|
|
43
|
-
UI_DESIGN_DENSITIES,
|
|
44
|
-
UI_DESIGN_TONES,
|
|
45
|
-
UI_DESIGN_RADIUS_SCALES,
|
|
46
|
-
UI_DESIGN_COLOR_ROLES,
|
|
47
|
-
UI_DESIGN_TYPOGRAPHY_ROLES,
|
|
48
|
-
UI_DESIGN_ACTION_ROLES,
|
|
49
|
-
UI_DESIGN_ACCESSIBILITY_VALUES,
|
|
50
|
-
FIELD_SPECS
|
|
51
|
-
} from "./kinds.js";
|
|
11
|
+
collectFieldMap,
|
|
12
|
+
formatLoc
|
|
13
|
+
} from "./utils.js";
|
|
52
14
|
import { validateWidget } from "./per-kind/widget.js";
|
|
53
15
|
import { validateDomain, validateDomainTag } from "./per-kind/domain.js";
|
|
54
16
|
import { validatePitch } from "./per-kind/pitch.js";
|
|
@@ -101,3499 +63,20 @@ export {
|
|
|
101
63
|
FIELD_SPECS
|
|
102
64
|
} from "./kinds.js";
|
|
103
65
|
|
|
104
|
-
export
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const file = loc?.file ?? "<unknown>";
|
|
119
|
-
return `${file}:${line}:${column}`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function valueAsArray(value) {
|
|
123
|
-
if (!value) {
|
|
124
|
-
return [];
|
|
125
|
-
}
|
|
126
|
-
if (value.type === "list") {
|
|
127
|
-
return value.items;
|
|
128
|
-
}
|
|
129
|
-
if (value.type === "sequence") {
|
|
130
|
-
return value.items;
|
|
131
|
-
}
|
|
132
|
-
return [value];
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function symbolValues(value) {
|
|
136
|
-
return valueAsArray(value).filter((item) => item.type === "symbol").map((item) => item.value);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function collectFieldMap(statement) {
|
|
140
|
-
const map = new Map();
|
|
141
|
-
for (const field of statement.fields) {
|
|
142
|
-
if (!map.has(field.key)) {
|
|
143
|
-
map.set(field.key, []);
|
|
144
|
-
}
|
|
145
|
-
map.get(field.key).push(field);
|
|
146
|
-
}
|
|
147
|
-
return map;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function getField(statement, key) {
|
|
151
|
-
return collectFieldMap(statement).get(key)?.[0] || null;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function getFieldValue(statement, key) {
|
|
155
|
-
return getField(statement, key)?.value || null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function stringValue(value) {
|
|
159
|
-
return value?.type === "string" ? value.value : null;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export function symbolValue(value) {
|
|
163
|
-
return value?.type === "symbol" ? value.value : null;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function blockEntries(value) {
|
|
167
|
-
return value?.type === "block" ? value.entries : [];
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function blockSymbolItems(entry) {
|
|
171
|
-
return entry.items.filter((item) => item.type === "symbol" || item.type === "string");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function statementFieldNames(statement) {
|
|
175
|
-
return blockEntries(getFieldValue(statement, "fields"))
|
|
176
|
-
.map((entry) => entry.items[0])
|
|
177
|
-
.filter((item) => item?.type === "symbol")
|
|
178
|
-
.map((item) => item.value);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function resolveShapeBaseFieldNames(statement, registry) {
|
|
182
|
-
const explicitFieldNames = statementFieldNames(statement);
|
|
183
|
-
if (explicitFieldNames.length > 0) {
|
|
184
|
-
return explicitFieldNames;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (!statement.from) {
|
|
188
|
-
return [];
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const source = registry.get(statement.from.value);
|
|
192
|
-
if (!source || source.kind !== "entity") {
|
|
193
|
-
return [];
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const sourceFieldNames = statementFieldNames(source);
|
|
197
|
-
const includeNames = symbolValues(getFieldValue(statement, "include"));
|
|
198
|
-
const excludeNames = new Set(symbolValues(getFieldValue(statement, "exclude")));
|
|
199
|
-
const selectedNames = includeNames.length > 0 ? includeNames.filter((name) => sourceFieldNames.includes(name)) : sourceFieldNames;
|
|
200
|
-
|
|
201
|
-
return selectedNames.filter((fieldName) => !excludeNames.has(fieldName));
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function ensureSingleValueField(errors, statement, fieldMap, key, expectedTypes) {
|
|
205
|
-
const fields = fieldMap.get(key) || [];
|
|
206
|
-
if (fields.length > 1) {
|
|
207
|
-
for (const field of fields.slice(1)) {
|
|
208
|
-
pushError(errors, `Duplicate field '${key}' on ${statement.kind} ${statement.id}`, field.loc);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const field = fields[0];
|
|
213
|
-
if (!field) {
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (!expectedTypes.includes(field.value.type)) {
|
|
218
|
-
pushError(
|
|
219
|
-
errors,
|
|
220
|
-
`Field '${key}' on ${statement.kind} ${statement.id} must be ${expectedTypes.join(" or ")}, found ${field.value.type}`,
|
|
221
|
-
field.loc
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function validateFieldPresence(errors, statement, fieldMap) {
|
|
227
|
-
const spec = FIELD_SPECS[statement.kind];
|
|
228
|
-
if (!spec) {
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const renamedFields = new Map([
|
|
233
|
-
["platform", ["type", "type web_surface"]],
|
|
234
|
-
["ui_components", ["widget_bindings", "widget_bindings { screen item_list region results widget widget_data_grid }"]],
|
|
235
|
-
["ui_design", ["design_tokens", "design_tokens { density comfortable tone operational }"]],
|
|
236
|
-
["ui_routes", ["screen_routes", "screen_routes { screen item_list path /items }"]],
|
|
237
|
-
["ui_screens", ["screens", "screens { item_list list title \"Items\" }"]],
|
|
238
|
-
["ui_screen_regions", ["screen_regions", "screen_regions { screen item_list region results pattern resource_table }"]],
|
|
239
|
-
["ui_navigation", ["navigation", "navigation { primary item_list label \"Items\" }"]],
|
|
240
|
-
["ui_app_shell", ["app_shell", "app_shell { shell sidebar }"]],
|
|
241
|
-
["ui_collections", ["collection_views", "collection_views { item_list presentation table }"]],
|
|
242
|
-
["ui_actions", ["screen_actions", "screen_actions { screen item_list action create target cap_create_item }"]],
|
|
243
|
-
["ui_visibility", ["visibility_rules", "visibility_rules { screen item_list when cap_list_items }"]],
|
|
244
|
-
["ui_lookups", ["field_lookups", "field_lookups { field owner_id source cap_list_users }"]],
|
|
245
|
-
["web_surface", ["web_hints", "web_hints { router file_based }"]],
|
|
246
|
-
["ios_surface", ["ios_hints", "ios_hints { navigation stack }"]],
|
|
247
|
-
["http", ["endpoints", "endpoints { cap_list_items method GET path /items success 200 }"]],
|
|
248
|
-
["http_errors", ["error_responses", "error_responses { cap_list_items 404 shape_error }"]],
|
|
249
|
-
["http_fields", ["wire_fields", "wire_fields { shape_item title title }"]],
|
|
250
|
-
["http_responses", ["responses", "responses { cap_list_items 200 shape_item_list }"]],
|
|
251
|
-
["http_preconditions", ["preconditions", "preconditions { cap_update_item rule_item_exists }"]],
|
|
252
|
-
["http_idempotency", ["idempotency", "idempotency { cap_create_item key request_id }"]],
|
|
253
|
-
["http_cache", ["cache", "cache { cap_list_items max_age 60 }"]],
|
|
254
|
-
["http_delete", ["delete_semantics", "delete_semantics { cap_delete_item mode soft_delete }"]],
|
|
255
|
-
["http_async", ["async_jobs", "async_jobs { cap_export_items job task_export }"]],
|
|
256
|
-
["http_status", ["async_status", "async_status { cap_export_items path /exports/{job_id} }"]],
|
|
257
|
-
["http_download", ["downloads", "downloads { cap_download_export content_type text/csv }"]],
|
|
258
|
-
["http_authz", ["authorization", "authorization { cap_update_item role editor }"]],
|
|
259
|
-
["http_callbacks", ["callbacks", "callbacks { cap_export_items event completed }"]],
|
|
260
|
-
["db_tables", ["tables", "tables { entity_item table items }"]],
|
|
261
|
-
["db_columns", ["columns", "columns { entity_item field title column title }"]],
|
|
262
|
-
["db_keys", ["keys", "keys { entity_item primary [id] }"]],
|
|
263
|
-
["db_indexes", ["indexes", "indexes { entity_item index [title] }"]],
|
|
264
|
-
["db_relations", ["relations", "relations { entity_item foreign_key owner_id references entity_user.id }"]],
|
|
265
|
-
["db_lifecycle", ["lifecycle", "lifecycle { entity_item timestamps created_at created_at updated_at updated_at }"]]
|
|
266
|
-
]);
|
|
267
|
-
|
|
268
|
-
for (const key of fieldMap.keys()) {
|
|
269
|
-
if (renamedFields.has(key)) {
|
|
270
|
-
const [newName, example] = renamedFields.get(key);
|
|
271
|
-
pushError(errors, `Field '${key}' on ${statement.kind} ${statement.id} ${renameDiagnostic(`'${key}'`, `'${newName}'`, example)}`, fieldMap.get(key)[0].loc);
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
if (!spec.allowed.includes(key)) {
|
|
275
|
-
pushError(errors, `Field '${key}' is not allowed on ${statement.kind} ${statement.id}`, fieldMap.get(key)[0].loc);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
for (const key of spec.required) {
|
|
280
|
-
if (!fieldMap.has(key)) {
|
|
281
|
-
pushError(errors, `Missing required field '${key}' on ${statement.kind} ${statement.id}`, statement.loc);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function validateProjectionTypeRenames(errors, statement, fieldMap) {
|
|
287
|
-
if (statement.kind !== "projection") {
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const typeField = fieldMap.get("type")?.[0];
|
|
292
|
-
const typeValue = symbolValue(typeField?.value);
|
|
293
|
-
const renamedTypes = new Map([
|
|
294
|
-
["ui_shared", "ui_contract"],
|
|
295
|
-
["ui_web", "web_surface"],
|
|
296
|
-
["ui_ios", "ios_surface"],
|
|
297
|
-
["ui_android", "android_surface"],
|
|
298
|
-
["dotnet", "api_contract"],
|
|
299
|
-
["api", "api_contract"],
|
|
300
|
-
["backend", "api_contract"],
|
|
301
|
-
["db_postgres", "db_contract"],
|
|
302
|
-
["db_sqlite", "db_contract"]
|
|
303
|
-
]);
|
|
304
|
-
if (!typeField || !renamedTypes.has(typeValue)) {
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
const nextType = renamedTypes.get(typeValue);
|
|
309
|
-
pushError(
|
|
310
|
-
errors,
|
|
311
|
-
`Projection ${statement.id} ${renameDiagnostic(`type value '${typeValue}'`, `'${nextType}'`, `type ${nextType}`)}`,
|
|
312
|
-
typeField.value.loc
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
function validateBlockEntryLengths(errors, statement, fieldMap, key, minimumWidth) {
|
|
317
|
-
const field = fieldMap.get(key)?.[0];
|
|
318
|
-
if (!field || field.value.type !== "block") {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
for (const entry of field.value.entries) {
|
|
323
|
-
if (entry.items.length < minimumWidth) {
|
|
324
|
-
pushError(errors, `Each '${key}' entry on ${statement.kind} ${statement.id} must have at least ${minimumWidth} token(s)`, entry.loc);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function validateFieldShapes(errors, statement, fieldMap) {
|
|
330
|
-
ensureSingleValueField(errors, statement, fieldMap, "name", ["string"]);
|
|
331
|
-
ensureSingleValueField(errors, statement, fieldMap, "description", ["string"]);
|
|
332
|
-
ensureSingleValueField(errors, statement, fieldMap, "status", ["symbol"]);
|
|
333
|
-
ensureSingleValueField(errors, statement, fieldMap, "type", ["symbol"]);
|
|
334
|
-
ensureSingleValueField(errors, statement, fieldMap, "method", ["symbol"]);
|
|
335
|
-
ensureSingleValueField(errors, statement, fieldMap, "severity", ["symbol"]);
|
|
336
|
-
ensureSingleValueField(errors, statement, fieldMap, "category", ["symbol"]);
|
|
337
|
-
ensureSingleValueField(errors, statement, fieldMap, "version", ["string"]);
|
|
338
|
-
|
|
339
|
-
for (const key of [
|
|
340
|
-
"aliases",
|
|
341
|
-
"excludes",
|
|
342
|
-
"uses_terms",
|
|
343
|
-
"include",
|
|
344
|
-
"exclude",
|
|
345
|
-
"derived_from",
|
|
346
|
-
"applies_to",
|
|
347
|
-
"actors",
|
|
348
|
-
"roles",
|
|
349
|
-
"reads",
|
|
350
|
-
"creates",
|
|
351
|
-
"updates",
|
|
352
|
-
"deletes",
|
|
353
|
-
"input",
|
|
354
|
-
"output",
|
|
355
|
-
"context",
|
|
356
|
-
"consequences",
|
|
357
|
-
"realizes",
|
|
358
|
-
"outputs",
|
|
359
|
-
"inputs",
|
|
360
|
-
"steps",
|
|
361
|
-
"validates",
|
|
362
|
-
"scenarios",
|
|
363
|
-
"observes",
|
|
364
|
-
"metrics",
|
|
365
|
-
"alerts",
|
|
366
|
-
"source_of_truth",
|
|
367
|
-
"behavior",
|
|
368
|
-
"patterns",
|
|
369
|
-
"regions",
|
|
370
|
-
"lookups",
|
|
371
|
-
"dependencies",
|
|
372
|
-
"approvals"
|
|
373
|
-
]) {
|
|
374
|
-
ensureSingleValueField(errors, statement, fieldMap, key, ["list"]);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
for (const key of ["fields", "props", "events", "slots", "behaviors", "keys", "relations", "invariants", "rename", "overrides", "endpoints", "error_responses", "wire_fields", "responses", "preconditions", "idempotency", "cache", "delete_semantics", "async_jobs", "async_status", "downloads", "authorization", "callbacks", "screens", "collection_views", "screen_actions", "visibility_rules", "field_lookups", "screen_routes", "web_hints", "ios_hints", "app_shell", "navigation", "screen_regions", "widget_bindings", "design_tokens", "tables", "columns", "keys", "indexes", "relations", "lifecycle", "generator_defaults"]) {
|
|
378
|
-
ensureSingleValueField(errors, statement, fieldMap, key, ["block"]);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "fields", 2);
|
|
382
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "props", 3);
|
|
383
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "events", 2);
|
|
384
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "slots", 2);
|
|
385
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "invariants", 2);
|
|
386
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "generator_defaults", 2);
|
|
387
|
-
|
|
388
|
-
if (statement.kind === "entity") {
|
|
389
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "keys", 2);
|
|
390
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "relations", 3);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (statement.kind === "projection") {
|
|
394
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "endpoints", 7);
|
|
395
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "error_responses", 3);
|
|
396
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "wire_fields", 5);
|
|
397
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "responses", 3);
|
|
398
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "preconditions", 9);
|
|
399
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "idempotency", 7);
|
|
400
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "cache", 11);
|
|
401
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "delete_semantics", 7);
|
|
402
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "async_jobs", 11);
|
|
403
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "async_status", 11);
|
|
404
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "downloads", 7);
|
|
405
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "authorization", 3);
|
|
406
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "callbacks", 11);
|
|
407
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "screens", 4);
|
|
408
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "collection_views", 4);
|
|
409
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "screen_actions", 6);
|
|
410
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "visibility_rules", 5);
|
|
411
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "field_lookups", 8);
|
|
412
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "screen_routes", 4);
|
|
413
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "web_hints", 4);
|
|
414
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "ios_hints", 4);
|
|
415
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "app_shell", 2);
|
|
416
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "navigation", 2);
|
|
417
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "screen_regions", 4);
|
|
418
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "tables", 3);
|
|
419
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "columns", 5);
|
|
420
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "keys", 3);
|
|
421
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "indexes", 3);
|
|
422
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "relations", 6);
|
|
423
|
-
validateBlockEntryLengths(errors, statement, fieldMap, "lifecycle", 3);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function validateStatus(errors, statement, fieldMap) {
|
|
428
|
-
const field = fieldMap.get("status")?.[0];
|
|
429
|
-
if (!field || field.value.type !== "symbol") {
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Per-kind status table takes precedence (decision and SDLC kinds), with
|
|
434
|
-
// GLOBAL_STATUSES as the default.
|
|
435
|
-
const allowed = STATUS_SETS_BY_KIND[statement.kind] || GLOBAL_STATUSES;
|
|
436
|
-
if (!allowed.has(field.value.value)) {
|
|
437
|
-
pushError(errors, `Invalid status '${field.value.value}' on ${statement.kind} ${statement.id}`, field.loc);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function validateRuleSeverity(errors, statement, fieldMap) {
|
|
442
|
-
if (statement.kind !== "rule") {
|
|
443
|
-
return;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const field = fieldMap.get("severity")?.[0];
|
|
447
|
-
if (!field) {
|
|
448
|
-
return;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
if (field.value.type === "symbol" && !RULE_SEVERITIES.has(field.value.value)) {
|
|
452
|
-
pushError(errors, `Invalid severity '${field.value.value}' on rule ${statement.id}`, field.loc);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function validateVerification(errors, statement, fieldMap) {
|
|
457
|
-
if (statement.kind !== "verification") {
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
const methodField = fieldMap.get("method")?.[0];
|
|
462
|
-
if (methodField?.value.type === "symbol" && !VERIFICATION_METHODS.has(methodField.value.value)) {
|
|
463
|
-
pushError(
|
|
464
|
-
errors,
|
|
465
|
-
`Invalid verification method '${methodField.value.value}' on verification ${statement.id}`,
|
|
466
|
-
methodField.loc
|
|
467
|
-
);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
const scenariosField = fieldMap.get("scenarios")?.[0];
|
|
471
|
-
if (!scenariosField || scenariosField.value.type !== "list") {
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (scenariosField.value.items.length === 0) {
|
|
476
|
-
pushError(errors, `Verification ${statement.id} must include at least one scenario`, scenariosField.loc);
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
for (const item of scenariosField.value.items) {
|
|
481
|
-
if (item.type !== "symbol" && item.type !== "string") {
|
|
482
|
-
pushError(errors, `Verification ${statement.id} scenarios must use symbols or strings`, item.loc);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function validateShapeFrom(errors, statement, registry) {
|
|
488
|
-
if (statement.kind !== "shape" || !statement.from) {
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
const target = registry.get(statement.from.value);
|
|
493
|
-
if (!target) {
|
|
494
|
-
pushError(errors, `Shape ${statement.id} derives from missing statement '${statement.from.value}'`, statement.from.loc);
|
|
495
|
-
return;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
if (target.kind !== "entity") {
|
|
499
|
-
pushError(errors, `Shape ${statement.id} can only derive from an entity, found ${target.kind} '${target.id}'`, statement.from.loc);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
function validateReferenceKinds(errors, statement, fieldMap, registry) {
|
|
504
|
-
// Phase 2: SDLC kinds add several reference fields. The `affects` field is
|
|
505
|
-
// polymorphic — pitches/requirements/tasks/bugs all use it, so we keep the
|
|
506
|
-
// target set wide. `pitch` is single-id but lives in the same map for
|
|
507
|
-
// uniform validation.
|
|
508
|
-
const expectedByField = {
|
|
509
|
-
uses_terms: ["term"],
|
|
510
|
-
derived_from: ["entity"],
|
|
511
|
-
applies_to: ["capability"],
|
|
512
|
-
source_of_truth: ["decision"],
|
|
513
|
-
actors: ["actor"],
|
|
514
|
-
roles: ["role"],
|
|
515
|
-
reads: ["entity"],
|
|
516
|
-
creates: ["entity"],
|
|
517
|
-
updates: ["entity"],
|
|
518
|
-
deletes: ["entity"],
|
|
519
|
-
input: ["shape"],
|
|
520
|
-
output: ["shape"],
|
|
521
|
-
dependencies: [...STATEMENT_KINDS],
|
|
522
|
-
realizes: ["capability", "projection", "entity"],
|
|
523
|
-
validates: [...STATEMENT_KINDS],
|
|
524
|
-
observes: [...STATEMENT_KINDS],
|
|
525
|
-
inputs: [...STATEMENT_KINDS],
|
|
526
|
-
outputs: null,
|
|
527
|
-
steps: null,
|
|
528
|
-
scenarios: null,
|
|
529
|
-
metrics: null,
|
|
530
|
-
alerts: null,
|
|
531
|
-
aliases: null,
|
|
532
|
-
excludes: null,
|
|
533
|
-
include: null,
|
|
534
|
-
exclude: null,
|
|
535
|
-
context: null,
|
|
536
|
-
consequences: null,
|
|
537
|
-
pitch: ["pitch"],
|
|
538
|
-
requirement: null,
|
|
539
|
-
from_requirement: ["requirement"],
|
|
540
|
-
affects: ["capability", "entity", "rule", "projection", "widget", "orchestration", "operation"],
|
|
541
|
-
introduces_rules: ["rule"],
|
|
542
|
-
respects_rules: ["rule"],
|
|
543
|
-
decisions: ["decision"],
|
|
544
|
-
introduces_decisions: ["decision"],
|
|
545
|
-
satisfies: ["requirement", "acceptance_criterion"],
|
|
546
|
-
acceptance_refs: ["acceptance_criterion"],
|
|
547
|
-
requirement_refs: ["requirement"],
|
|
548
|
-
fixes_bugs: ["bug"],
|
|
549
|
-
blocks: ["task"],
|
|
550
|
-
blocked_by: ["task"],
|
|
551
|
-
claimed_by: ["actor", "role"],
|
|
552
|
-
violates: ["rule"],
|
|
553
|
-
surfaces_rule: ["rule"],
|
|
554
|
-
introduced_in: ["task", "bug"],
|
|
555
|
-
fixed_in: ["task"],
|
|
556
|
-
fixed_in_verification: ["verification"],
|
|
557
|
-
supersedes: null,
|
|
558
|
-
modifies: [...STATEMENT_KINDS],
|
|
559
|
-
introduces: [...STATEMENT_KINDS],
|
|
560
|
-
removes: [...STATEMENT_KINDS]
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
for (const [key, allowedKinds] of Object.entries(expectedByField)) {
|
|
564
|
-
const field = fieldMap.get(key)?.[0];
|
|
565
|
-
if (!field || !allowedKinds) {
|
|
566
|
-
continue;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
for (const item of valueAsArray(field.value)) {
|
|
570
|
-
if (item.type !== "symbol") {
|
|
571
|
-
pushError(errors, `Field '${key}' on ${statement.kind} ${statement.id} must only contain symbols`, item.loc);
|
|
572
|
-
continue;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const target = registry.get(item.value);
|
|
576
|
-
if (!target) {
|
|
577
|
-
pushError(errors, `Missing reference '${item.value}' in field '${key}' on ${statement.kind} ${statement.id}`, item.loc);
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
if (!allowedKinds.includes(target.kind)) {
|
|
582
|
-
pushError(
|
|
583
|
-
errors,
|
|
584
|
-
`Field '${key}' on ${statement.kind} ${statement.id} must reference ${allowedKinds.join(" or ")}, found ${target.kind} '${target.id}'`,
|
|
585
|
-
item.loc
|
|
586
|
-
);
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
function validateEntityRelations(errors, statement, fieldMap, registry) {
|
|
593
|
-
if (statement.kind !== "entity") {
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const field = fieldMap.get("relations")?.[0];
|
|
598
|
-
if (!field || field.value.type !== "block") {
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
for (const entry of field.value.entries) {
|
|
603
|
-
const [left, operator, target] = entry.items;
|
|
604
|
-
if (!left || !operator || !target) {
|
|
605
|
-
continue;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
if (left.type !== "symbol" || operator.type !== "symbol" || target.type !== "symbol") {
|
|
609
|
-
pushError(errors, `Relation entries on entity ${statement.id} must use symbols`, entry.loc);
|
|
610
|
-
continue;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
if (operator.value !== "references") {
|
|
614
|
-
pushError(errors, `Relation entries on entity ${statement.id} must use 'references'`, operator.loc);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
const [entityId] = target.value.split(".");
|
|
618
|
-
const related = registry.get(entityId);
|
|
619
|
-
if (!related) {
|
|
620
|
-
pushError(errors, `Relation on entity ${statement.id} references missing entity '${entityId}'`, target.loc);
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (related.kind !== "entity") {
|
|
625
|
-
pushError(errors, `Relation on entity ${statement.id} must target an entity, found ${related.kind} '${related.id}'`, target.loc);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function isIdentifierLike(token) {
|
|
631
|
-
return typeof token === "string" && token.length > 0;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
function isComparator(token) {
|
|
635
|
-
return ["==", "!=", "<", "<=", ">", ">="].includes(token);
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
function validateInvariantEntry(errors, statement, entry) {
|
|
639
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
640
|
-
if (tokens.length < 2) {
|
|
641
|
-
pushError(errors, `Invariant on ${statement.kind} ${statement.id} is too short`, entry.loc);
|
|
642
|
-
return;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
const [left, op, ...rest] = tokens;
|
|
646
|
-
if (!isIdentifierLike(left)) {
|
|
647
|
-
pushError(errors, `Invariant on ${statement.kind} ${statement.id} must start with a field or expression target`, entry.loc);
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (op === "requires") {
|
|
652
|
-
if (rest.length < 3) {
|
|
653
|
-
pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} must be '<field> requires <field> <op> <value>'`, entry.loc);
|
|
654
|
-
} else if (!isComparator(rest[1])) {
|
|
655
|
-
pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} uses an invalid comparator '${rest[1]}'`, entry.loc);
|
|
656
|
-
}
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
if (op === "length") {
|
|
661
|
-
if (rest.length !== 2 || !["<", "<=", ">", ">=", "=="].includes(rest[0])) {
|
|
662
|
-
pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} must be '<field> length <op> <number>'`, entry.loc);
|
|
663
|
-
}
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (op === "format") {
|
|
668
|
-
if (rest.length !== 2 || rest[0] !== "==") {
|
|
669
|
-
pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} must be '<field> format == <format>'`, entry.loc);
|
|
670
|
-
}
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if (isComparator(op)) {
|
|
675
|
-
if (rest.length < 1) {
|
|
676
|
-
pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} is missing a right-hand value`, entry.loc);
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
if (rest[1] === "implies") {
|
|
681
|
-
const [, , impliedField, impliedOperator, impliedValue] = rest;
|
|
682
|
-
if (!impliedField || !impliedOperator || !impliedValue) {
|
|
683
|
-
pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} must fully specify the implied clause`, entry.loc);
|
|
684
|
-
} else if (!(impliedOperator === "is" || isComparator(impliedOperator))) {
|
|
685
|
-
pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} has invalid implied operator '${impliedOperator}'`, entry.loc);
|
|
686
|
-
}
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
pushError(errors, `Invariant '${tokens.join(" ")}' on ${statement.kind} ${statement.id} uses unsupported form`, entry.loc);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
function validateRuleExpressionValue(errors, statement, field, label) {
|
|
697
|
-
if (!field) {
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
const items = valueAsArray(field.value);
|
|
702
|
-
if (items.length !== 1) {
|
|
703
|
-
pushError(errors, `Field '${label}' on rule ${statement.id} must contain a single expression`, field.loc);
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const item = items[0];
|
|
708
|
-
if (item.type !== "string" && item.type !== "symbol") {
|
|
709
|
-
pushError(errors, `Field '${label}' on rule ${statement.id} must be a string or symbol expression`, field.loc);
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
const text = item.value.trim();
|
|
714
|
-
if (text.length === 0) {
|
|
715
|
-
pushError(errors, `Field '${label}' on rule ${statement.id} must not be empty`, field.loc);
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
if (label === "requirement" || label === "condition") {
|
|
720
|
-
if (!/(==|!=|<=|>=|<|>)/.test(text)) {
|
|
721
|
-
pushError(errors, `Field '${label}' on rule ${statement.id} must include a comparison operator`, field.loc);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function validateExpressions(errors, statement, fieldMap) {
|
|
727
|
-
if (statement.kind === "entity") {
|
|
728
|
-
const invariantsField = fieldMap.get("invariants")?.[0];
|
|
729
|
-
if (invariantsField?.value.type === "block") {
|
|
730
|
-
for (const entry of invariantsField.value.entries) {
|
|
731
|
-
validateInvariantEntry(errors, statement, entry);
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
if (statement.kind === "rule") {
|
|
737
|
-
validateRuleExpressionValue(errors, statement, fieldMap.get("condition")?.[0], "condition");
|
|
738
|
-
validateRuleExpressionValue(errors, statement, fieldMap.get("requirement")?.[0], "requirement");
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function validateShapeTransforms(errors, statement, fieldMap, registry) {
|
|
743
|
-
if (statement.kind !== "shape") {
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
const baseFieldNames = resolveShapeBaseFieldNames(statement, registry);
|
|
748
|
-
const baseFieldSet = new Set(baseFieldNames);
|
|
749
|
-
const source = statement.from ? registry.get(statement.from.value) : null;
|
|
750
|
-
const sourceFieldSet = new Set(source ? statementFieldNames(source) : []);
|
|
751
|
-
const includeField = fieldMap.get("include")?.[0];
|
|
752
|
-
const excludeField = fieldMap.get("exclude")?.[0];
|
|
753
|
-
|
|
754
|
-
for (const [fieldKey, field] of [
|
|
755
|
-
["include", includeField],
|
|
756
|
-
["exclude", excludeField]
|
|
757
|
-
]) {
|
|
758
|
-
if (!field) {
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
for (const item of valueAsArray(field.value)) {
|
|
763
|
-
if (item.type !== "symbol") {
|
|
764
|
-
continue;
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
if (statement.from && !sourceFieldSet.has(item.value) && fieldKey === "include") {
|
|
768
|
-
pushError(errors, `Shape ${statement.id} includes unknown field '${item.value}' from ${statement.from.value}`, item.loc);
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
if (statement.from && fieldKey === "exclude") {
|
|
772
|
-
if (!sourceFieldSet.has(item.value)) {
|
|
773
|
-
pushError(errors, `Shape ${statement.id} excludes unknown field '${item.value}' from ${statement.from.value}`, item.loc);
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
const renameEntries = blockEntries(getFieldValue(statement, "rename"));
|
|
780
|
-
const renameFrom = new Map();
|
|
781
|
-
const renameTo = new Map();
|
|
782
|
-
|
|
783
|
-
for (const entry of renameEntries) {
|
|
784
|
-
const items = blockSymbolItems(entry);
|
|
785
|
-
if (items.length !== 2) {
|
|
786
|
-
pushError(errors, `Each 'rename' entry on shape ${statement.id} must be exactly '<from> <to>'`, entry.loc);
|
|
787
|
-
continue;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
const [fromItem, toItem] = items;
|
|
791
|
-
if (!baseFieldSet.has(fromItem.value)) {
|
|
792
|
-
pushError(errors, `Shape ${statement.id} renames unknown field '${fromItem.value}'`, fromItem.loc);
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
if (renameFrom.has(fromItem.value)) {
|
|
796
|
-
pushError(errors, `Shape ${statement.id} renames field '${fromItem.value}' more than once`, fromItem.loc);
|
|
797
|
-
} else {
|
|
798
|
-
renameFrom.set(fromItem.value, toItem.value);
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
if (renameTo.has(toItem.value)) {
|
|
802
|
-
pushError(errors, `Shape ${statement.id} renames multiple fields to '${toItem.value}'`, toItem.loc);
|
|
803
|
-
} else {
|
|
804
|
-
renameTo.set(toItem.value, fromItem.value);
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
const finalFieldNames = baseFieldNames.map((fieldName) => renameFrom.get(fieldName) || fieldName);
|
|
809
|
-
const finalFieldSet = new Set();
|
|
810
|
-
for (const fieldName of finalFieldNames) {
|
|
811
|
-
if (finalFieldSet.has(fieldName)) {
|
|
812
|
-
pushError(errors, `Shape ${statement.id} produces duplicate projected field '${fieldName}'`, statement.loc);
|
|
813
|
-
continue;
|
|
814
|
-
}
|
|
815
|
-
finalFieldSet.add(fieldName);
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
const sourceNameSet = new Set(baseFieldNames);
|
|
819
|
-
const overrideEntries = blockEntries(getFieldValue(statement, "overrides"));
|
|
820
|
-
const seenOverrides = new Set();
|
|
821
|
-
|
|
822
|
-
for (const entry of overrideEntries) {
|
|
823
|
-
const items = blockSymbolItems(entry);
|
|
824
|
-
if (items.length < 2) {
|
|
825
|
-
pushError(errors, `Each 'overrides' entry on shape ${statement.id} must include a field and at least one override`, entry.loc);
|
|
826
|
-
continue;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
const [fieldItem, ...rest] = items;
|
|
830
|
-
if (!finalFieldSet.has(fieldItem.value) && !sourceNameSet.has(fieldItem.value)) {
|
|
831
|
-
pushError(errors, `Shape ${statement.id} overrides unknown field '${fieldItem.value}'`, fieldItem.loc);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
if (seenOverrides.has(fieldItem.value)) {
|
|
835
|
-
pushError(errors, `Shape ${statement.id} overrides field '${fieldItem.value}' more than once`, fieldItem.loc);
|
|
836
|
-
} else {
|
|
837
|
-
seenOverrides.add(fieldItem.value);
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
let sawChange = false;
|
|
841
|
-
for (let i = 0; i < rest.length; i += 1) {
|
|
842
|
-
const token = rest[i];
|
|
843
|
-
if (token.value === "required" || token.value === "optional") {
|
|
844
|
-
sawChange = true;
|
|
845
|
-
continue;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
if (token.value === "type") {
|
|
849
|
-
sawChange = true;
|
|
850
|
-
if (!rest[i + 1]) {
|
|
851
|
-
pushError(errors, `Shape ${statement.id} override for '${fieldItem.value}' is missing a type value`, token.loc);
|
|
852
|
-
} else {
|
|
853
|
-
i += 1;
|
|
854
|
-
}
|
|
855
|
-
continue;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
if (token.value === "default") {
|
|
859
|
-
sawChange = true;
|
|
860
|
-
if (!rest[i + 1]) {
|
|
861
|
-
pushError(errors, `Shape ${statement.id} override for '${fieldItem.value}' is missing a default value`, token.loc);
|
|
862
|
-
} else {
|
|
863
|
-
i += 1;
|
|
864
|
-
}
|
|
865
|
-
continue;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
pushError(errors, `Shape ${statement.id} override for '${fieldItem.value}' has unknown directive '${token.value}'`, token.loc);
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
if (!sawChange) {
|
|
872
|
-
pushError(errors, `Shape ${statement.id} override for '${fieldItem.value}' must specify at least one valid directive`, entry.loc);
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
}
|
|
876
|
-
|
|
877
|
-
function validateProjectionHttp(errors, statement, fieldMap, registry) {
|
|
878
|
-
if (statement.kind !== "projection") {
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
const httpField = fieldMap.get("endpoints")?.[0];
|
|
883
|
-
if (!httpField || httpField.value.type !== "block") {
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
888
|
-
|
|
889
|
-
for (const entry of httpField.value.entries) {
|
|
890
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
891
|
-
const capabilityId = tokens[0];
|
|
892
|
-
if (!capabilityId) {
|
|
893
|
-
continue;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const target = registry.get(capabilityId);
|
|
897
|
-
if (!target) {
|
|
898
|
-
pushError(errors, `Projection ${statement.id} http metadata references missing capability '${capabilityId}'`, entry.loc);
|
|
899
|
-
continue;
|
|
900
|
-
}
|
|
901
|
-
if (target.kind !== "capability") {
|
|
902
|
-
pushError(errors, `Projection ${statement.id} http metadata must target a capability, found ${target.kind} '${target.id}'`, entry.loc);
|
|
903
|
-
}
|
|
904
|
-
if (!realized.has(capabilityId)) {
|
|
905
|
-
pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
const directives = new Map();
|
|
909
|
-
for (let i = 1; i < tokens.length; i += 2) {
|
|
910
|
-
const key = tokens[i];
|
|
911
|
-
const value = tokens[i + 1];
|
|
912
|
-
if (!value) {
|
|
913
|
-
pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
914
|
-
continue;
|
|
915
|
-
}
|
|
916
|
-
directives.set(key, value);
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
for (const requiredKey of ["method", "path", "success"]) {
|
|
920
|
-
if (!directives.has(requiredKey)) {
|
|
921
|
-
pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
for (const key of directives.keys()) {
|
|
926
|
-
if (!["method", "path", "success", "auth", "request"].includes(key)) {
|
|
927
|
-
pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const method = directives.get("method");
|
|
932
|
-
if (method && !["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
|
933
|
-
pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' has invalid method '${method}'`, entry.loc);
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
const path = directives.get("path");
|
|
937
|
-
if (path && !path.startsWith("/")) {
|
|
938
|
-
pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' must use an absolute path`, entry.loc);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
const success = directives.get("success");
|
|
942
|
-
if (success && !/^\d{3}$/.test(success)) {
|
|
943
|
-
pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' must use a 3-digit success status`, entry.loc);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
const auth = directives.get("auth");
|
|
947
|
-
if (auth && !["none", "user", "manager", "admin"].includes(auth)) {
|
|
948
|
-
pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' has invalid auth mode '${auth}'`, entry.loc);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
const request = directives.get("request");
|
|
952
|
-
if (request && !["body", "query", "path", "none"].includes(request)) {
|
|
953
|
-
pushError(errors, `Projection ${statement.id} http metadata for '${capabilityId}' has invalid request placement '${request}'`, entry.loc);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
function validateProjectionHttpErrors(errors, statement, fieldMap, registry) {
|
|
959
|
-
if (statement.kind !== "projection") {
|
|
960
|
-
return;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
const httpErrorsField = fieldMap.get("error_responses")?.[0];
|
|
964
|
-
if (!httpErrorsField || httpErrorsField.value.type !== "block") {
|
|
965
|
-
return;
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
969
|
-
for (const entry of httpErrorsField.value.entries) {
|
|
970
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
971
|
-
const [capabilityId, errorCode, status] = tokens;
|
|
972
|
-
|
|
973
|
-
const target = registry.get(capabilityId);
|
|
974
|
-
if (!target) {
|
|
975
|
-
pushError(errors, `Projection ${statement.id} error_responses references missing capability '${capabilityId}'`, entry.loc);
|
|
976
|
-
continue;
|
|
977
|
-
}
|
|
978
|
-
if (target.kind !== "capability") {
|
|
979
|
-
pushError(errors, `Projection ${statement.id} error_responses must target a capability, found ${target.kind} '${target.id}'`, entry.loc);
|
|
980
|
-
}
|
|
981
|
-
if (!realized.has(capabilityId)) {
|
|
982
|
-
pushError(errors, `Projection ${statement.id} error_responses for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
983
|
-
}
|
|
984
|
-
if (!/^\d{3}$/.test(status || "")) {
|
|
985
|
-
pushError(errors, `Projection ${statement.id} error_responses for '${capabilityId}' must use a 3-digit status`, entry.loc);
|
|
986
|
-
}
|
|
987
|
-
if (!errorCode) {
|
|
988
|
-
pushError(errors, `Projection ${statement.id} error_responses for '${capabilityId}' must include an error code`, entry.loc);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
function resolveCapabilityContractFields(registry, capabilityId, direction) {
|
|
994
|
-
const capability = registry.get(capabilityId);
|
|
995
|
-
if (!capability || capability.kind !== "capability") {
|
|
996
|
-
return new Set();
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
const refsField = direction === "input" ? getFieldValue(capability, "input") : getFieldValue(capability, "output");
|
|
1000
|
-
const shapeId = symbolValues(refsField)[0];
|
|
1001
|
-
if (!shapeId) {
|
|
1002
|
-
return new Set();
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const shape = registry.get(shapeId);
|
|
1006
|
-
if (!shape || shape.kind !== "shape") {
|
|
1007
|
-
return new Set();
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
const explicitFields = statementFieldNames(shape);
|
|
1011
|
-
if (explicitFields.length > 0) {
|
|
1012
|
-
return new Set(explicitFields);
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
return new Set(resolveShapeBaseFieldNames(shape, registry));
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
function validateProjectionHttpFields(errors, statement, fieldMap, registry) {
|
|
1019
|
-
if (statement.kind !== "projection") {
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
const httpFieldsField = fieldMap.get("wire_fields")?.[0];
|
|
1024
|
-
if (!httpFieldsField || httpFieldsField.value.type !== "block") {
|
|
1025
|
-
return;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1029
|
-
for (const entry of httpFieldsField.value.entries) {
|
|
1030
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1031
|
-
const [capabilityId, direction, fieldName, keywordIn, location, maybeAs, maybeWireName] = tokens;
|
|
1032
|
-
|
|
1033
|
-
const capability = registry.get(capabilityId);
|
|
1034
|
-
if (!capability) {
|
|
1035
|
-
pushError(errors, `Projection ${statement.id} wire_fields references missing capability '${capabilityId}'`, entry.loc);
|
|
1036
|
-
continue;
|
|
1037
|
-
}
|
|
1038
|
-
if (capability.kind !== "capability") {
|
|
1039
|
-
pushError(errors, `Projection ${statement.id} wire_fields must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1040
|
-
}
|
|
1041
|
-
if (!realized.has(capabilityId)) {
|
|
1042
|
-
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1043
|
-
}
|
|
1044
|
-
if (!["input", "output"].includes(direction)) {
|
|
1045
|
-
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' has invalid direction '${direction}'`, entry.loc);
|
|
1046
|
-
}
|
|
1047
|
-
if (keywordIn !== "in") {
|
|
1048
|
-
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' must use 'in' before the location`, entry.loc);
|
|
1049
|
-
}
|
|
1050
|
-
if (!["path", "query", "header", "body"].includes(location)) {
|
|
1051
|
-
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' has invalid location '${location}'`, entry.loc);
|
|
1052
|
-
}
|
|
1053
|
-
if (maybeAs && maybeAs !== "as") {
|
|
1054
|
-
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' has unexpected token '${maybeAs}'`, entry.loc);
|
|
1055
|
-
}
|
|
1056
|
-
if (maybeAs === "as" && !maybeWireName) {
|
|
1057
|
-
pushError(errors, `Projection ${statement.id} wire_fields for '${capabilityId}' must provide a wire name after 'as'`, entry.loc);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
const availableFields = resolveCapabilityContractFields(registry, capabilityId, direction);
|
|
1061
|
-
if (fieldName && availableFields.size > 0 && !availableFields.has(fieldName)) {
|
|
1062
|
-
pushError(errors, `Projection ${statement.id} wire_fields references unknown ${direction} field '${fieldName}' on ${capabilityId}`, entry.loc);
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
function validateProjectionHttpResponses(errors, statement, fieldMap, registry) {
|
|
1068
|
-
if (statement.kind !== "projection") {
|
|
1069
|
-
return;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
const httpResponsesField = fieldMap.get("responses")?.[0];
|
|
1073
|
-
if (!httpResponsesField || httpResponsesField.value.type !== "block") {
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1078
|
-
for (const entry of httpResponsesField.value.entries) {
|
|
1079
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1080
|
-
const capabilityId = tokens[0];
|
|
1081
|
-
const capability = registry.get(capabilityId);
|
|
1082
|
-
|
|
1083
|
-
if (!capability) {
|
|
1084
|
-
pushError(errors, `Projection ${statement.id} responses references missing capability '${capabilityId}'`, entry.loc);
|
|
1085
|
-
continue;
|
|
1086
|
-
}
|
|
1087
|
-
if (capability.kind !== "capability") {
|
|
1088
|
-
pushError(errors, `Projection ${statement.id} responses must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1089
|
-
}
|
|
1090
|
-
if (!realized.has(capabilityId)) {
|
|
1091
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
const directives = parseProjectionHttpResponsesDirectives(tokens.slice(1));
|
|
1095
|
-
for (const message of directives.errors) {
|
|
1096
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' ${message}`, entry.loc);
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
if (!directives.mode) {
|
|
1100
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'mode'`, entry.loc);
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
const mode = directives.mode;
|
|
1104
|
-
if (mode && !["item", "collection", "paged", "cursor"].includes(mode)) {
|
|
1105
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid mode '${mode}'`, entry.loc);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
const itemShapeId = directives.item;
|
|
1109
|
-
if (mode && mode !== "item" && !itemShapeId) {
|
|
1110
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'item' for mode '${mode}'`, entry.loc);
|
|
1111
|
-
}
|
|
1112
|
-
if (itemShapeId) {
|
|
1113
|
-
const itemShape = registry.get(itemShapeId);
|
|
1114
|
-
if (!itemShape) {
|
|
1115
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' references missing shape '${itemShapeId}'`, entry.loc);
|
|
1116
|
-
} else if (itemShape.kind !== "shape") {
|
|
1117
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must reference a shape for 'item', found ${itemShape.kind} '${itemShape.id}'`, entry.loc);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
if (mode === "cursor") {
|
|
1122
|
-
if (!directives.cursor?.requestAfter) {
|
|
1123
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'cursor request_after <field>'`, entry.loc);
|
|
1124
|
-
}
|
|
1125
|
-
if (!directives.cursor?.responseNext) {
|
|
1126
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'cursor response_next <wire_name>'`, entry.loc);
|
|
1127
|
-
}
|
|
1128
|
-
if (!directives.limit) {
|
|
1129
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'limit field <field> default <n> max <n>'`, entry.loc);
|
|
1130
|
-
}
|
|
1131
|
-
if (!directives.sort) {
|
|
1132
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must include 'sort by <field> direction <asc|desc>'`, entry.loc);
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
if (directives.sort && !["asc", "desc"].includes(directives.sort.direction || "")) {
|
|
1137
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid sort direction '${directives.sort.direction}'`, entry.loc);
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
if (directives.total && !["true", "false"].includes(directives.total.included || "")) {
|
|
1141
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' has invalid total included value '${directives.total.included}'`, entry.loc);
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
if (directives.limit) {
|
|
1145
|
-
const defaultValue = Number.parseInt(directives.limit.defaultValue || "", 10);
|
|
1146
|
-
const maxValue = Number.parseInt(directives.limit.maxValue || "", 10);
|
|
1147
|
-
if (!Number.isInteger(defaultValue) || !Number.isInteger(maxValue)) {
|
|
1148
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must use integer default/max values for 'limit'`, entry.loc);
|
|
1149
|
-
} else if (defaultValue > maxValue) {
|
|
1150
|
-
pushError(errors, `Projection ${statement.id} responses for '${capabilityId}' must use default <= max for 'limit'`, entry.loc);
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const inputFields = resolveCapabilityContractFields(registry, capabilityId, "input");
|
|
1155
|
-
const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
|
|
1156
|
-
if (directives.cursor?.requestAfter && inputFields.size > 0 && !inputFields.has(directives.cursor.requestAfter)) {
|
|
1157
|
-
pushError(errors, `Projection ${statement.id} responses references unknown input field '${directives.cursor.requestAfter}' for cursor request_after on ${capabilityId}`, entry.loc);
|
|
1158
|
-
}
|
|
1159
|
-
if (directives.limit?.field && inputFields.size > 0 && !inputFields.has(directives.limit.field)) {
|
|
1160
|
-
pushError(errors, `Projection ${statement.id} responses references unknown input field '${directives.limit.field}' for limit on ${capabilityId}`, entry.loc);
|
|
1161
|
-
}
|
|
1162
|
-
if (directives.sort?.field && outputFields.size > 0 && !outputFields.has(directives.sort.field)) {
|
|
1163
|
-
pushError(errors, `Projection ${statement.id} responses references unknown output field '${directives.sort.field}' for sort on ${capabilityId}`, entry.loc);
|
|
1164
|
-
}
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
function validateProjectionHttpPreconditions(errors, statement, fieldMap, registry) {
|
|
1169
|
-
if (statement.kind !== "projection") {
|
|
1170
|
-
return;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
const httpPreconditionsField = fieldMap.get("preconditions")?.[0];
|
|
1174
|
-
if (!httpPreconditionsField || httpPreconditionsField.value.type !== "block") {
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1179
|
-
for (const entry of httpPreconditionsField.value.entries) {
|
|
1180
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1181
|
-
const [capabilityId] = tokens;
|
|
1182
|
-
const capability = registry.get(capabilityId);
|
|
1183
|
-
|
|
1184
|
-
if (!capability) {
|
|
1185
|
-
pushError(errors, `Projection ${statement.id} preconditions references missing capability '${capabilityId}'`, entry.loc);
|
|
1186
|
-
continue;
|
|
1187
|
-
}
|
|
1188
|
-
if (capability.kind !== "capability") {
|
|
1189
|
-
pushError(errors, `Projection ${statement.id} preconditions must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1190
|
-
}
|
|
1191
|
-
if (!realized.has(capabilityId)) {
|
|
1192
|
-
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
const directives = new Map();
|
|
1196
|
-
for (let i = 1; i < tokens.length; i += 2) {
|
|
1197
|
-
const key = tokens[i];
|
|
1198
|
-
const value = tokens[i + 1];
|
|
1199
|
-
if (!value) {
|
|
1200
|
-
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
directives.set(key, value);
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
for (const requiredKey of ["header", "required", "error", "source", "code"]) {
|
|
1207
|
-
if (!directives.has(requiredKey)) {
|
|
1208
|
-
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
for (const key of directives.keys()) {
|
|
1213
|
-
if (!["header", "required", "error", "source", "code"].includes(key)) {
|
|
1214
|
-
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1215
|
-
}
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
const required = directives.get("required");
|
|
1219
|
-
if (required && !["true", "false"].includes(required)) {
|
|
1220
|
-
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' has invalid required value '${required}'`, entry.loc);
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
const errorStatus = directives.get("error");
|
|
1224
|
-
if (errorStatus && !/^\d{3}$/.test(errorStatus)) {
|
|
1225
|
-
pushError(errors, `Projection ${statement.id} preconditions for '${capabilityId}' must use a 3-digit error status`, entry.loc);
|
|
1226
|
-
}
|
|
1227
|
-
|
|
1228
|
-
const sourceField = directives.get("source");
|
|
1229
|
-
const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
|
|
1230
|
-
if (sourceField && outputFields.size > 0 && !outputFields.has(sourceField)) {
|
|
1231
|
-
pushError(errors, `Projection ${statement.id} preconditions references unknown output field '${sourceField}' on ${capabilityId}`, entry.loc);
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
function validateProjectionHttpIdempotency(errors, statement, fieldMap, registry) {
|
|
1237
|
-
if (statement.kind !== "projection") {
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
const httpIdempotencyField = fieldMap.get("idempotency")?.[0];
|
|
1242
|
-
if (!httpIdempotencyField || httpIdempotencyField.value.type !== "block") {
|
|
1243
|
-
return;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1247
|
-
for (const entry of httpIdempotencyField.value.entries) {
|
|
1248
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1249
|
-
const [capabilityId] = tokens;
|
|
1250
|
-
const capability = registry.get(capabilityId);
|
|
1251
|
-
|
|
1252
|
-
if (!capability) {
|
|
1253
|
-
pushError(errors, `Projection ${statement.id} idempotency references missing capability '${capabilityId}'`, entry.loc);
|
|
1254
|
-
continue;
|
|
1255
|
-
}
|
|
1256
|
-
if (capability.kind !== "capability") {
|
|
1257
|
-
pushError(errors, `Projection ${statement.id} idempotency must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1258
|
-
}
|
|
1259
|
-
if (!realized.has(capabilityId)) {
|
|
1260
|
-
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
const directives = new Map();
|
|
1264
|
-
for (let i = 1; i < tokens.length; i += 2) {
|
|
1265
|
-
const key = tokens[i];
|
|
1266
|
-
const value = tokens[i + 1];
|
|
1267
|
-
if (!value) {
|
|
1268
|
-
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1269
|
-
continue;
|
|
1270
|
-
}
|
|
1271
|
-
directives.set(key, value);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
for (const requiredKey of ["header", "required", "error", "code"]) {
|
|
1275
|
-
if (!directives.has(requiredKey)) {
|
|
1276
|
-
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
for (const key of directives.keys()) {
|
|
1281
|
-
if (!["header", "required", "error", "code"].includes(key)) {
|
|
1282
|
-
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
const required = directives.get("required");
|
|
1287
|
-
if (required && !["true", "false"].includes(required)) {
|
|
1288
|
-
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' has invalid required value '${required}'`, entry.loc);
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
const errorStatus = directives.get("error");
|
|
1292
|
-
if (errorStatus && !/^\d{3}$/.test(errorStatus)) {
|
|
1293
|
-
pushError(errors, `Projection ${statement.id} idempotency for '${capabilityId}' must use a 3-digit error status`, entry.loc);
|
|
1294
|
-
}
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
function validateProjectionHttpCache(errors, statement, fieldMap, registry) {
|
|
1299
|
-
if (statement.kind !== "projection") {
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
const httpCacheField = fieldMap.get("cache")?.[0];
|
|
1304
|
-
if (!httpCacheField || httpCacheField.value.type !== "block") {
|
|
1305
|
-
return;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1309
|
-
const httpEntries = blockEntries(getFieldValue(statement, "endpoints"));
|
|
1310
|
-
const httpMethodsByCapability = new Map();
|
|
1311
|
-
|
|
1312
|
-
for (const entry of httpEntries) {
|
|
1313
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1314
|
-
const capabilityId = tokens[0];
|
|
1315
|
-
for (let i = 1; i < tokens.length - 1; i += 1) {
|
|
1316
|
-
if (tokens[i] === "method") {
|
|
1317
|
-
httpMethodsByCapability.set(capabilityId, tokens[i + 1]);
|
|
1318
|
-
break;
|
|
1319
|
-
}
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
for (const entry of httpCacheField.value.entries) {
|
|
1324
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1325
|
-
const [capabilityId] = tokens;
|
|
1326
|
-
const capability = registry.get(capabilityId);
|
|
1327
|
-
|
|
1328
|
-
if (!capability) {
|
|
1329
|
-
pushError(errors, `Projection ${statement.id} cache references missing capability '${capabilityId}'`, entry.loc);
|
|
1330
|
-
continue;
|
|
1331
|
-
}
|
|
1332
|
-
if (capability.kind !== "capability") {
|
|
1333
|
-
pushError(errors, `Projection ${statement.id} cache must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1334
|
-
}
|
|
1335
|
-
if (!realized.has(capabilityId)) {
|
|
1336
|
-
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
const directives = new Map();
|
|
1340
|
-
for (let i = 1; i < tokens.length; i += 2) {
|
|
1341
|
-
const key = tokens[i];
|
|
1342
|
-
const value = tokens[i + 1];
|
|
1343
|
-
if (!value) {
|
|
1344
|
-
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1345
|
-
continue;
|
|
1346
|
-
}
|
|
1347
|
-
directives.set(key, value);
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
for (const requiredKey of ["response_header", "request_header", "required", "not_modified", "source", "code"]) {
|
|
1351
|
-
if (!directives.has(requiredKey)) {
|
|
1352
|
-
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
for (const key of directives.keys()) {
|
|
1357
|
-
if (!["response_header", "request_header", "required", "not_modified", "source", "code"].includes(key)) {
|
|
1358
|
-
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
const required = directives.get("required");
|
|
1363
|
-
if (required && !["true", "false"].includes(required)) {
|
|
1364
|
-
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' has invalid required value '${required}'`, entry.loc);
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
const notModifiedStatus = directives.get("not_modified");
|
|
1368
|
-
if (notModifiedStatus && notModifiedStatus !== "304") {
|
|
1369
|
-
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' must use 304 for 'not_modified'`, entry.loc);
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
const sourceField = directives.get("source");
|
|
1373
|
-
const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
|
|
1374
|
-
if (sourceField && outputFields.size > 0 && !outputFields.has(sourceField)) {
|
|
1375
|
-
pushError(errors, `Projection ${statement.id} cache references unknown output field '${sourceField}' on ${capabilityId}`, entry.loc);
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
const method = httpMethodsByCapability.get(capabilityId);
|
|
1379
|
-
if (method && method !== "GET") {
|
|
1380
|
-
pushError(errors, `Projection ${statement.id} cache for '${capabilityId}' requires an HTTP GET realization, found '${method}'`, entry.loc);
|
|
1381
|
-
}
|
|
1382
|
-
}
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
function validateProjectionHttpDelete(errors, statement, fieldMap, registry) {
|
|
1386
|
-
if (statement.kind !== "projection") {
|
|
1387
|
-
return;
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
const httpDeleteField = fieldMap.get("delete_semantics")?.[0];
|
|
1391
|
-
if (!httpDeleteField || httpDeleteField.value.type !== "block") {
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1396
|
-
for (const entry of httpDeleteField.value.entries) {
|
|
1397
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1398
|
-
const [capabilityId] = tokens;
|
|
1399
|
-
const capability = registry.get(capabilityId);
|
|
1400
|
-
|
|
1401
|
-
if (!capability) {
|
|
1402
|
-
pushError(errors, `Projection ${statement.id} delete_semantics references missing capability '${capabilityId}'`, entry.loc);
|
|
1403
|
-
continue;
|
|
1404
|
-
}
|
|
1405
|
-
if (capability.kind !== "capability") {
|
|
1406
|
-
pushError(errors, `Projection ${statement.id} delete_semantics must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1407
|
-
}
|
|
1408
|
-
if (!realized.has(capabilityId)) {
|
|
1409
|
-
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
|
-
const directives = new Map();
|
|
1413
|
-
for (let i = 1; i < tokens.length; i += 2) {
|
|
1414
|
-
const key = tokens[i];
|
|
1415
|
-
const value = tokens[i + 1];
|
|
1416
|
-
if (!value) {
|
|
1417
|
-
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1418
|
-
continue;
|
|
1419
|
-
}
|
|
1420
|
-
directives.set(key, value);
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
for (const requiredKey of ["mode", "response"]) {
|
|
1424
|
-
if (!directives.has(requiredKey)) {
|
|
1425
|
-
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
for (const key of directives.keys()) {
|
|
1430
|
-
if (!["mode", "field", "value", "response"].includes(key)) {
|
|
1431
|
-
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
const mode = directives.get("mode");
|
|
1436
|
-
if (mode && !["soft", "hard"].includes(mode)) {
|
|
1437
|
-
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' has invalid mode '${mode}'`, entry.loc);
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
const response = directives.get("response");
|
|
1441
|
-
if (response && !["none", "body"].includes(response)) {
|
|
1442
|
-
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' has invalid response '${response}'`, entry.loc);
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
if (mode === "soft") {
|
|
1446
|
-
if (!directives.has("field") || !directives.has("value")) {
|
|
1447
|
-
pushError(errors, `Projection ${statement.id} delete_semantics for '${capabilityId}' must include 'field' and 'value' for soft deletes`, entry.loc);
|
|
1448
|
-
}
|
|
1449
|
-
const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
|
|
1450
|
-
const fieldName = directives.get("field");
|
|
1451
|
-
if (fieldName && outputFields.size > 0 && !outputFields.has(fieldName)) {
|
|
1452
|
-
pushError(errors, `Projection ${statement.id} delete_semantics references unknown output field '${fieldName}' on ${capabilityId}`, entry.loc);
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
function validateProjectionHttpAsync(errors, statement, fieldMap, registry) {
|
|
1459
|
-
if (statement.kind !== "projection") {
|
|
1460
|
-
return;
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
const httpAsyncField = fieldMap.get("async_jobs")?.[0];
|
|
1464
|
-
if (!httpAsyncField || httpAsyncField.value.type !== "block") {
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1469
|
-
const httpEntries = blockEntries(getFieldValue(statement, "endpoints"));
|
|
1470
|
-
const httpDirectivesByCapability = new Map();
|
|
1471
|
-
for (const entry of httpEntries) {
|
|
1472
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1473
|
-
const capabilityId = tokens[0];
|
|
1474
|
-
const directives = new Map();
|
|
1475
|
-
for (let i = 1; i < tokens.length - 1; i += 2) {
|
|
1476
|
-
directives.set(tokens[i], tokens[i + 1]);
|
|
1477
|
-
}
|
|
1478
|
-
httpDirectivesByCapability.set(capabilityId, directives);
|
|
1479
|
-
}
|
|
1480
|
-
for (const entry of httpAsyncField.value.entries) {
|
|
1481
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1482
|
-
const [capabilityId] = tokens;
|
|
1483
|
-
const capability = registry.get(capabilityId);
|
|
1484
|
-
|
|
1485
|
-
if (!capability) {
|
|
1486
|
-
pushError(errors, `Projection ${statement.id} async_jobs references missing capability '${capabilityId}'`, entry.loc);
|
|
1487
|
-
continue;
|
|
1488
|
-
}
|
|
1489
|
-
if (capability.kind !== "capability") {
|
|
1490
|
-
pushError(errors, `Projection ${statement.id} async_jobs must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1491
|
-
}
|
|
1492
|
-
if (!realized.has(capabilityId)) {
|
|
1493
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
const directives = new Map();
|
|
1497
|
-
for (let i = 1; i < tokens.length; i += 2) {
|
|
1498
|
-
const key = tokens[i];
|
|
1499
|
-
const value = tokens[i + 1];
|
|
1500
|
-
if (!value) {
|
|
1501
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1502
|
-
continue;
|
|
1503
|
-
}
|
|
1504
|
-
directives.set(key, value);
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
for (const requiredKey of ["mode", "accepted", "location_header", "retry_after_header", "status_path", "status_capability", "job"]) {
|
|
1508
|
-
if (!directives.has(requiredKey)) {
|
|
1509
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
for (const key of directives.keys()) {
|
|
1514
|
-
if (!["mode", "accepted", "location_header", "retry_after_header", "status_path", "status_capability", "job"].includes(key)) {
|
|
1515
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1516
|
-
}
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
const mode = directives.get("mode");
|
|
1520
|
-
if (mode && mode !== "job") {
|
|
1521
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' has invalid mode '${mode}'`, entry.loc);
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
const accepted = directives.get("accepted");
|
|
1525
|
-
if (accepted && accepted !== "202") {
|
|
1526
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must use 202 for 'accepted'`, entry.loc);
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
const jobShapeId = directives.get("job");
|
|
1530
|
-
if (jobShapeId) {
|
|
1531
|
-
const jobShape = registry.get(jobShapeId);
|
|
1532
|
-
if (!jobShape) {
|
|
1533
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' references missing shape '${jobShapeId}'`, entry.loc);
|
|
1534
|
-
} else if (jobShape.kind !== "shape") {
|
|
1535
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must reference a shape for 'job', found ${jobShape.kind} '${jobShape.id}'`, entry.loc);
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
const statusCapabilityId = directives.get("status_capability");
|
|
1540
|
-
if (statusCapabilityId) {
|
|
1541
|
-
const statusCapability = registry.get(statusCapabilityId);
|
|
1542
|
-
if (!statusCapability) {
|
|
1543
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' references missing status capability '${statusCapabilityId}'`, entry.loc);
|
|
1544
|
-
} else if (statusCapability.kind !== "capability") {
|
|
1545
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must reference a capability for 'status_capability', found ${statusCapability.kind} '${statusCapability.id}'`, entry.loc);
|
|
1546
|
-
} else if (!realized.has(statusCapabilityId)) {
|
|
1547
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' status capability '${statusCapabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
const statusHttp = httpDirectivesByCapability.get(statusCapabilityId);
|
|
1551
|
-
if (statusHttp?.get("method") && statusHttp.get("method") !== "GET") {
|
|
1552
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' status capability '${statusCapabilityId}' must use HTTP GET`, entry.loc);
|
|
1553
|
-
}
|
|
1554
|
-
if (statusHttp?.get("path") && directives.get("status_path") && statusHttp.get("path") !== directives.get("status_path")) {
|
|
1555
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' status_path must match the path for '${statusCapabilityId}'`, entry.loc);
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
const statusPath = directives.get("status_path");
|
|
1560
|
-
if (statusPath && !statusPath.startsWith("/")) {
|
|
1561
|
-
pushError(errors, `Projection ${statement.id} async_jobs for '${capabilityId}' must use an absolute path for 'status_path'`, entry.loc);
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
function validateProjectionHttpStatus(errors, statement, fieldMap, registry) {
|
|
1567
|
-
if (statement.kind !== "projection") {
|
|
1568
|
-
return;
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
const httpStatusField = fieldMap.get("async_status")?.[0];
|
|
1572
|
-
if (!httpStatusField || httpStatusField.value.type !== "block") {
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1577
|
-
const httpEntries = blockEntries(getFieldValue(statement, "endpoints"));
|
|
1578
|
-
const httpMethodsByCapability = new Map();
|
|
1579
|
-
for (const entry of httpEntries) {
|
|
1580
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1581
|
-
const capabilityId = tokens[0];
|
|
1582
|
-
for (let i = 1; i < tokens.length - 1; i += 2) {
|
|
1583
|
-
if (tokens[i] === "method") {
|
|
1584
|
-
httpMethodsByCapability.set(capabilityId, tokens[i + 1]);
|
|
1585
|
-
break;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
for (const entry of httpStatusField.value.entries) {
|
|
1590
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1591
|
-
const [capabilityId] = tokens;
|
|
1592
|
-
const capability = registry.get(capabilityId);
|
|
1593
|
-
|
|
1594
|
-
if (!capability) {
|
|
1595
|
-
pushError(errors, `Projection ${statement.id} async_status references missing capability '${capabilityId}'`, entry.loc);
|
|
1596
|
-
continue;
|
|
1597
|
-
}
|
|
1598
|
-
if (capability.kind !== "capability") {
|
|
1599
|
-
pushError(errors, `Projection ${statement.id} async_status must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1600
|
-
}
|
|
1601
|
-
if (!realized.has(capabilityId)) {
|
|
1602
|
-
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
const directives = new Map();
|
|
1606
|
-
for (let i = 1; i < tokens.length; i += 2) {
|
|
1607
|
-
const key = tokens[i];
|
|
1608
|
-
const value = tokens[i + 1];
|
|
1609
|
-
if (!value) {
|
|
1610
|
-
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1611
|
-
continue;
|
|
1612
|
-
}
|
|
1613
|
-
directives.set(key, value);
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
for (const requiredKey of ["async_for", "state_field", "completed", "failed"]) {
|
|
1617
|
-
if (!directives.has(requiredKey)) {
|
|
1618
|
-
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
for (const key of directives.keys()) {
|
|
1623
|
-
if (!["async_for", "state_field", "completed", "failed", "expired", "download_capability", "download_field", "error_field"].includes(key)) {
|
|
1624
|
-
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
|
|
1628
|
-
const asyncCapabilityId = directives.get("async_for");
|
|
1629
|
-
if (asyncCapabilityId) {
|
|
1630
|
-
const asyncCapability = registry.get(asyncCapabilityId);
|
|
1631
|
-
if (!asyncCapability) {
|
|
1632
|
-
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' references missing async capability '${asyncCapabilityId}'`, entry.loc);
|
|
1633
|
-
} else if (asyncCapability.kind !== "capability") {
|
|
1634
|
-
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' must reference a capability for 'async_for', found ${asyncCapability.kind} '${asyncCapability.id}'`, entry.loc);
|
|
1635
|
-
} else if (!realized.has(asyncCapabilityId)) {
|
|
1636
|
-
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' async capability '${asyncCapabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
const outputFields = resolveCapabilityContractFields(registry, capabilityId, "output");
|
|
1641
|
-
for (const [directive, fieldName] of [
|
|
1642
|
-
["state_field", directives.get("state_field")],
|
|
1643
|
-
["download_field", directives.get("download_field")],
|
|
1644
|
-
["error_field", directives.get("error_field")]
|
|
1645
|
-
]) {
|
|
1646
|
-
if (fieldName && outputFields.size > 0 && !outputFields.has(fieldName)) {
|
|
1647
|
-
pushError(errors, `Projection ${statement.id} async_status references unknown output field '${fieldName}' for '${directive}' on ${capabilityId}`, entry.loc);
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
|
|
1651
|
-
const downloadCapabilityId = directives.get("download_capability");
|
|
1652
|
-
if (downloadCapabilityId) {
|
|
1653
|
-
const downloadCapability = registry.get(downloadCapabilityId);
|
|
1654
|
-
if (!downloadCapability) {
|
|
1655
|
-
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' references missing download capability '${downloadCapabilityId}'`, entry.loc);
|
|
1656
|
-
} else if (downloadCapability.kind !== "capability") {
|
|
1657
|
-
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' must reference a capability for 'download_capability', found ${downloadCapability.kind} '${downloadCapability.id}'`, entry.loc);
|
|
1658
|
-
} else if (!realized.has(downloadCapabilityId)) {
|
|
1659
|
-
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' download capability '${downloadCapabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
const method = httpMethodsByCapability.get(downloadCapabilityId);
|
|
1663
|
-
if (method && method !== "GET") {
|
|
1664
|
-
pushError(errors, `Projection ${statement.id} async_status for '${capabilityId}' download capability '${downloadCapabilityId}' must use HTTP GET`, entry.loc);
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
function validateProjectionHttpDownload(errors, statement, fieldMap, registry) {
|
|
1671
|
-
if (statement.kind !== "projection") {
|
|
1672
|
-
return;
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
const httpDownloadField = fieldMap.get("downloads")?.[0];
|
|
1676
|
-
if (!httpDownloadField || httpDownloadField.value.type !== "block") {
|
|
1677
|
-
return;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1681
|
-
for (const entry of httpDownloadField.value.entries) {
|
|
1682
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1683
|
-
const [capabilityId] = tokens;
|
|
1684
|
-
const capability = registry.get(capabilityId);
|
|
1685
|
-
|
|
1686
|
-
if (!capability) {
|
|
1687
|
-
pushError(errors, `Projection ${statement.id} downloads references missing capability '${capabilityId}'`, entry.loc);
|
|
1688
|
-
continue;
|
|
1689
|
-
}
|
|
1690
|
-
if (capability.kind !== "capability") {
|
|
1691
|
-
pushError(errors, `Projection ${statement.id} downloads must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1692
|
-
}
|
|
1693
|
-
if (!realized.has(capabilityId)) {
|
|
1694
|
-
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1695
|
-
}
|
|
1696
|
-
|
|
1697
|
-
const directives = new Map();
|
|
1698
|
-
for (let i = 1; i < tokens.length; i += 2) {
|
|
1699
|
-
const key = tokens[i];
|
|
1700
|
-
const value = tokens[i + 1];
|
|
1701
|
-
if (!value) {
|
|
1702
|
-
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1703
|
-
continue;
|
|
1704
|
-
}
|
|
1705
|
-
directives.set(key, value);
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
for (const requiredKey of ["async_for", "media", "disposition"]) {
|
|
1709
|
-
if (!directives.has(requiredKey)) {
|
|
1710
|
-
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
for (const key of directives.keys()) {
|
|
1715
|
-
if (!["async_for", "media", "filename", "disposition"].includes(key)) {
|
|
1716
|
-
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
const asyncCapabilityId = directives.get("async_for");
|
|
1721
|
-
if (asyncCapabilityId) {
|
|
1722
|
-
const asyncCapability = registry.get(asyncCapabilityId);
|
|
1723
|
-
if (!asyncCapability) {
|
|
1724
|
-
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' references missing async capability '${asyncCapabilityId}'`, entry.loc);
|
|
1725
|
-
} else if (asyncCapability.kind !== "capability") {
|
|
1726
|
-
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' must reference a capability for 'async_for', found ${asyncCapability.kind} '${asyncCapability.id}'`, entry.loc);
|
|
1727
|
-
} else if (!realized.has(asyncCapabilityId)) {
|
|
1728
|
-
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' async capability '${asyncCapabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
|
|
1732
|
-
const media = directives.get("media");
|
|
1733
|
-
if (media && !media.includes("/")) {
|
|
1734
|
-
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' must use a valid media type`, entry.loc);
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
const disposition = directives.get("disposition");
|
|
1738
|
-
if (disposition && !["attachment", "inline"].includes(disposition)) {
|
|
1739
|
-
pushError(errors, `Projection ${statement.id} downloads for '${capabilityId}' has invalid disposition '${disposition}'`, entry.loc);
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
function validateProjectionHttpAuthz(errors, statement, fieldMap, registry) {
|
|
1745
|
-
if (statement.kind !== "projection") {
|
|
1746
|
-
return;
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
const httpAuthzField = fieldMap.get("authorization")?.[0];
|
|
1750
|
-
if (!httpAuthzField || httpAuthzField.value.type !== "block") {
|
|
1751
|
-
return;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1755
|
-
for (const entry of httpAuthzField.value.entries) {
|
|
1756
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1757
|
-
const [capabilityId] = tokens;
|
|
1758
|
-
const capability = registry.get(capabilityId);
|
|
1759
|
-
|
|
1760
|
-
if (!capability) {
|
|
1761
|
-
pushError(errors, `Projection ${statement.id} authorization references missing capability '${capabilityId}'`, entry.loc);
|
|
1762
|
-
continue;
|
|
1763
|
-
}
|
|
1764
|
-
if (capability.kind !== "capability") {
|
|
1765
|
-
pushError(errors, `Projection ${statement.id} authorization must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1766
|
-
}
|
|
1767
|
-
if (!realized.has(capabilityId)) {
|
|
1768
|
-
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
const directives = new Map();
|
|
1772
|
-
for (let i = 1; i < tokens.length; i += 2) {
|
|
1773
|
-
const key = tokens[i];
|
|
1774
|
-
const value = tokens[i + 1];
|
|
1775
|
-
if (!value) {
|
|
1776
|
-
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1777
|
-
continue;
|
|
1778
|
-
}
|
|
1779
|
-
directives.set(key, value);
|
|
1780
|
-
}
|
|
1781
|
-
|
|
1782
|
-
for (const key of directives.keys()) {
|
|
1783
|
-
if (!["role", "permission", "claim", "claim_value", "ownership", "ownership_field"].includes(key)) {
|
|
1784
|
-
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
if (directives.size === 0) {
|
|
1789
|
-
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' must include at least one directive`, entry.loc);
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
const ownership = directives.get("ownership");
|
|
1793
|
-
if (ownership && !["owner", "owner_or_admin", "project_member", "none"].includes(ownership)) {
|
|
1794
|
-
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' has invalid ownership '${ownership}'`, entry.loc);
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
const ownershipField = directives.get("ownership_field");
|
|
1798
|
-
if (ownershipField && (!ownership || ownership === "none")) {
|
|
1799
|
-
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' cannot declare ownership_field without ownership`, entry.loc);
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
const claimValue = directives.get("claim_value");
|
|
1803
|
-
if (claimValue && !directives.get("claim")) {
|
|
1804
|
-
pushError(errors, `Projection ${statement.id} authorization for '${capabilityId}' cannot declare claim_value without claim`, entry.loc);
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
function validateProjectionHttpCallbacks(errors, statement, fieldMap, registry) {
|
|
1810
|
-
if (statement.kind !== "projection") {
|
|
1811
|
-
return;
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
const httpCallbacksField = fieldMap.get("callbacks")?.[0];
|
|
1815
|
-
if (!httpCallbacksField || httpCallbacksField.value.type !== "block") {
|
|
1816
|
-
return;
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
1820
|
-
for (const entry of httpCallbacksField.value.entries) {
|
|
1821
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
1822
|
-
const [capabilityId] = tokens;
|
|
1823
|
-
const capability = registry.get(capabilityId);
|
|
1824
|
-
|
|
1825
|
-
if (!capability) {
|
|
1826
|
-
pushError(errors, `Projection ${statement.id} callbacks references missing capability '${capabilityId}'`, entry.loc);
|
|
1827
|
-
continue;
|
|
1828
|
-
}
|
|
1829
|
-
if (capability.kind !== "capability") {
|
|
1830
|
-
pushError(errors, `Projection ${statement.id} callbacks must target a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
1831
|
-
}
|
|
1832
|
-
if (!realized.has(capabilityId)) {
|
|
1833
|
-
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
const directives = new Map();
|
|
1837
|
-
for (let i = 1; i < tokens.length; i += 2) {
|
|
1838
|
-
const key = tokens[i];
|
|
1839
|
-
const value = tokens[i + 1];
|
|
1840
|
-
if (!value) {
|
|
1841
|
-
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
1842
|
-
continue;
|
|
1843
|
-
}
|
|
1844
|
-
directives.set(key, value);
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
for (const requiredKey of ["event", "target_field", "method", "payload", "success"]) {
|
|
1848
|
-
if (!directives.has(requiredKey)) {
|
|
1849
|
-
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' must include '${requiredKey}'`, entry.loc);
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
for (const key of directives.keys()) {
|
|
1854
|
-
if (!["event", "target_field", "method", "payload", "success"].includes(key)) {
|
|
1855
|
-
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
1856
|
-
}
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
const method = directives.get("method");
|
|
1860
|
-
if (method && !["POST", "PUT", "PATCH"].includes(method)) {
|
|
1861
|
-
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' has invalid method '${method}'`, entry.loc);
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
const success = directives.get("success");
|
|
1865
|
-
if (success && !/^\d{3}$/.test(success)) {
|
|
1866
|
-
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' must use a 3-digit success status`, entry.loc);
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
const payloadShapeId = directives.get("payload");
|
|
1870
|
-
if (payloadShapeId) {
|
|
1871
|
-
const payloadShape = registry.get(payloadShapeId);
|
|
1872
|
-
if (!payloadShape) {
|
|
1873
|
-
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' references missing shape '${payloadShapeId}'`, entry.loc);
|
|
1874
|
-
} else if (payloadShape.kind !== "shape") {
|
|
1875
|
-
pushError(errors, `Projection ${statement.id} callbacks for '${capabilityId}' must reference a shape for 'payload', found ${payloadShape.kind} '${payloadShape.id}'`, entry.loc);
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
|
|
1879
|
-
const targetField = directives.get("target_field");
|
|
1880
|
-
const inputFields = resolveCapabilityContractFields(registry, capabilityId, "input");
|
|
1881
|
-
if (targetField && inputFields.size > 0 && !inputFields.has(targetField)) {
|
|
1882
|
-
pushError(errors, `Projection ${statement.id} callbacks references unknown input field '${targetField}' on ${capabilityId}`, entry.loc);
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
function parseProjectionHttpResponsesDirectives(tokens) {
|
|
1888
|
-
const result = {
|
|
1889
|
-
mode: null,
|
|
1890
|
-
item: null,
|
|
1891
|
-
cursor: null,
|
|
1892
|
-
limit: null,
|
|
1893
|
-
sort: null,
|
|
1894
|
-
total: null,
|
|
1895
|
-
errors: []
|
|
1896
|
-
};
|
|
1897
|
-
|
|
1898
|
-
for (let i = 0; i < tokens.length; i += 1) {
|
|
1899
|
-
const token = tokens[i];
|
|
1900
|
-
if (token === "mode") {
|
|
1901
|
-
result.mode = tokens[i + 1] || null;
|
|
1902
|
-
if (!tokens[i + 1]) {
|
|
1903
|
-
result.errors.push("is missing a value for 'mode'");
|
|
1904
|
-
}
|
|
1905
|
-
i += 1;
|
|
1906
|
-
continue;
|
|
1907
|
-
}
|
|
1908
|
-
if (token === "item") {
|
|
1909
|
-
result.item = tokens[i + 1] || null;
|
|
1910
|
-
if (!tokens[i + 1]) {
|
|
1911
|
-
result.errors.push("is missing a value for 'item'");
|
|
1912
|
-
}
|
|
1913
|
-
i += 1;
|
|
1914
|
-
continue;
|
|
1915
|
-
}
|
|
1916
|
-
if (token === "cursor") {
|
|
1917
|
-
const requestKeyword = tokens[i + 1];
|
|
1918
|
-
const requestField = tokens[i + 2];
|
|
1919
|
-
const responseKeyword = tokens[i + 3];
|
|
1920
|
-
const responseNext = tokens[i + 4];
|
|
1921
|
-
let responsePrev = null;
|
|
1922
|
-
let consumed = 4;
|
|
1923
|
-
if (tokens[i + 5] === "response_prev") {
|
|
1924
|
-
responsePrev = tokens[i + 6] || null;
|
|
1925
|
-
consumed = 6;
|
|
1926
|
-
}
|
|
1927
|
-
result.cursor = {
|
|
1928
|
-
requestAfter: requestKeyword === "request_after" ? requestField : null,
|
|
1929
|
-
responseNext: responseKeyword === "response_next" ? responseNext : null,
|
|
1930
|
-
responsePrev
|
|
1931
|
-
};
|
|
1932
|
-
if (requestKeyword !== "request_after") {
|
|
1933
|
-
result.errors.push("must use 'cursor request_after <field>'");
|
|
1934
|
-
}
|
|
1935
|
-
if (responseKeyword !== "response_next") {
|
|
1936
|
-
result.errors.push("must use 'cursor response_next <wire_name>'");
|
|
1937
|
-
}
|
|
1938
|
-
i += consumed;
|
|
1939
|
-
continue;
|
|
1940
|
-
}
|
|
1941
|
-
if (token === "limit") {
|
|
1942
|
-
result.limit = {
|
|
1943
|
-
field: tokens[i + 1] === "field" ? tokens[i + 2] || null : null,
|
|
1944
|
-
defaultValue: tokens[i + 3] === "default" ? tokens[i + 4] || null : null,
|
|
1945
|
-
maxValue: tokens[i + 5] === "max" ? tokens[i + 6] || null : null
|
|
1946
|
-
};
|
|
1947
|
-
if (tokens[i + 1] !== "field" || tokens[i + 3] !== "default" || tokens[i + 5] !== "max") {
|
|
1948
|
-
result.errors.push("must use 'limit field <field> default <n> max <n>'");
|
|
1949
|
-
}
|
|
1950
|
-
i += 6;
|
|
1951
|
-
continue;
|
|
1952
|
-
}
|
|
1953
|
-
if (token === "sort") {
|
|
1954
|
-
result.sort = {
|
|
1955
|
-
field: tokens[i + 1] === "by" ? tokens[i + 2] || null : null,
|
|
1956
|
-
direction: tokens[i + 3] === "direction" ? tokens[i + 4] || null : null
|
|
1957
|
-
};
|
|
1958
|
-
if (tokens[i + 1] !== "by" || tokens[i + 3] !== "direction") {
|
|
1959
|
-
result.errors.push("must use 'sort by <field> direction <asc|desc>'");
|
|
1960
|
-
}
|
|
1961
|
-
i += 4;
|
|
1962
|
-
continue;
|
|
1963
|
-
}
|
|
1964
|
-
if (token === "total") {
|
|
1965
|
-
result.total = {
|
|
1966
|
-
included: tokens[i + 1] === "included" ? tokens[i + 2] || null : null
|
|
1967
|
-
};
|
|
1968
|
-
if (tokens[i + 1] !== "included") {
|
|
1969
|
-
result.errors.push("must use 'total included <true|false>'");
|
|
1970
|
-
}
|
|
1971
|
-
i += 2;
|
|
1972
|
-
continue;
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
result.errors.push(`has unknown directive '${token}'`);
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
return result;
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
function resolveCapabilityOutputShape(registry, capabilityId) {
|
|
1982
|
-
const capability = registry.get(capabilityId);
|
|
1983
|
-
if (!capability || capability.kind !== "capability") {
|
|
1984
|
-
return null;
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
const shapeId = symbolValues(getFieldValue(capability, "output"))[0];
|
|
1988
|
-
const shape = shapeId ? registry.get(shapeId) : null;
|
|
1989
|
-
return shape?.kind === "shape" ? shape : null;
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
function collectProjectionUiScreens(statement, fieldMap) {
|
|
1993
|
-
const screensField = fieldMap.get("screens")?.[0];
|
|
1994
|
-
if (!screensField || screensField.value.type !== "block") {
|
|
1995
|
-
return new Map();
|
|
1996
|
-
}
|
|
1997
|
-
|
|
1998
|
-
const screens = new Map();
|
|
1999
|
-
for (const entry of screensField.value.entries) {
|
|
2000
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2001
|
-
if (tokens[0] === "screen" && tokens[1]) {
|
|
2002
|
-
screens.set(tokens[1], entry);
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
return screens;
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
function resolveProjectionUiScreenFieldNames(registry, screenEntry, statement) {
|
|
2009
|
-
const tokens = blockSymbolItems(screenEntry).map((item) => item.value);
|
|
2010
|
-
const directives = parseUiDirectiveMap(tokens, 2, [], statement, screenEntry, "");
|
|
2011
|
-
const kind = directives.get("kind");
|
|
2012
|
-
|
|
2013
|
-
if (kind === "form") {
|
|
2014
|
-
const shapeId = directives.get("input_shape");
|
|
2015
|
-
const shape = shapeId ? registry.get(shapeId) : null;
|
|
2016
|
-
if (!shape || shape.kind !== "shape") {
|
|
2017
|
-
return new Set();
|
|
2018
|
-
}
|
|
2019
|
-
const explicitFields = statementFieldNames(shape);
|
|
2020
|
-
return new Set(explicitFields.length > 0 ? explicitFields : resolveShapeBaseFieldNames(shape, registry));
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
|
-
if (kind === "list") {
|
|
2024
|
-
const loadCapabilityId = directives.get("load");
|
|
2025
|
-
return loadCapabilityId ? resolveCapabilityContractFields(registry, loadCapabilityId, "input") : new Set();
|
|
2026
|
-
}
|
|
2027
|
-
|
|
2028
|
-
if (kind === "detail" || kind === "job_status") {
|
|
2029
|
-
const loadCapabilityId = directives.get("load");
|
|
2030
|
-
return loadCapabilityId ? resolveCapabilityContractFields(registry, loadCapabilityId, "output") : new Set();
|
|
2031
|
-
}
|
|
2032
|
-
|
|
2033
|
-
return new Set();
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
function screenIdsFromProjectionStatement(statement) {
|
|
2037
|
-
const screens = new Set();
|
|
2038
|
-
for (const entry of blockEntries(getFieldValue(statement, "screens"))) {
|
|
2039
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2040
|
-
if (tokens[0] === "screen" && tokens[1]) {
|
|
2041
|
-
screens.add(tokens[1]);
|
|
2042
|
-
}
|
|
2043
|
-
}
|
|
2044
|
-
return screens;
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
function collectAvailableUiScreenIds(statement, fieldMap, registry) {
|
|
2048
|
-
const available = new Set(collectProjectionUiScreens(statement, fieldMap).keys());
|
|
2049
|
-
for (const targetId of symbolValues(getFieldValue(statement, "realizes"))) {
|
|
2050
|
-
const target = registry.get(targetId);
|
|
2051
|
-
if (target?.kind === "projection") {
|
|
2052
|
-
for (const screenId of screenIdsFromProjectionStatement(target)) {
|
|
2053
|
-
available.add(screenId);
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
return available;
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
function collectProjectionUiRegionKeys(statement) {
|
|
2061
|
-
const keys = new Set();
|
|
2062
|
-
for (const entry of blockEntries(getFieldValue(statement, "screen_regions"))) {
|
|
2063
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2064
|
-
if (tokens[0] === "screen" && tokens[1] && tokens[2] === "region" && tokens[3]) {
|
|
2065
|
-
keys.add(`${tokens[1]}:${tokens[3]}`);
|
|
2066
|
-
}
|
|
2067
|
-
}
|
|
2068
|
-
return keys;
|
|
2069
|
-
}
|
|
2070
|
-
|
|
2071
|
-
function collectAvailableUiRegionKeys(statement, registry) {
|
|
2072
|
-
const available = collectProjectionUiRegionKeys(statement);
|
|
2073
|
-
for (const targetId of symbolValues(getFieldValue(statement, "realizes"))) {
|
|
2074
|
-
const target = registry.get(targetId);
|
|
2075
|
-
if (target?.kind === "projection") {
|
|
2076
|
-
for (const key of collectProjectionUiRegionKeys(target)) {
|
|
2077
|
-
available.add(key);
|
|
2078
|
-
}
|
|
2079
|
-
}
|
|
2080
|
-
}
|
|
2081
|
-
return available;
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
function collectProjectionUiRegionPatterns(statement) {
|
|
2085
|
-
const patterns = new Map();
|
|
2086
|
-
for (const entry of blockEntries(getFieldValue(statement, "screen_regions"))) {
|
|
2087
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2088
|
-
if (tokens[0] !== "screen" || !tokens[1] || tokens[2] !== "region" || !tokens[3]) {
|
|
2089
|
-
continue;
|
|
2090
|
-
}
|
|
2091
|
-
for (let i = 4; i < tokens.length; i += 2) {
|
|
2092
|
-
if (tokens[i] === "pattern" && tokens[i + 1]) {
|
|
2093
|
-
patterns.set(`${tokens[1]}:${tokens[3]}`, tokens[i + 1]);
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
}
|
|
2097
|
-
return patterns;
|
|
2098
|
-
}
|
|
2099
|
-
|
|
2100
|
-
function collectAvailableUiRegionPatterns(statement, registry) {
|
|
2101
|
-
const patterns = collectProjectionUiRegionPatterns(statement);
|
|
2102
|
-
for (const targetId of symbolValues(getFieldValue(statement, "realizes"))) {
|
|
2103
|
-
const target = registry.get(targetId);
|
|
2104
|
-
if (target?.kind !== "projection") {
|
|
2105
|
-
continue;
|
|
2106
|
-
}
|
|
2107
|
-
for (const [key, pattern] of collectProjectionUiRegionPatterns(target)) {
|
|
2108
|
-
if (!patterns.has(key)) {
|
|
2109
|
-
patterns.set(key, pattern);
|
|
2110
|
-
}
|
|
2111
|
-
}
|
|
2112
|
-
}
|
|
2113
|
-
return patterns;
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
function parseUiDirectiveMap(tokens, startIndex, errors, statement, entry, context) {
|
|
2117
|
-
const directives = new Map();
|
|
2118
|
-
|
|
2119
|
-
for (let i = startIndex; i < tokens.length; i += 2) {
|
|
2120
|
-
const key = tokens[i];
|
|
2121
|
-
const value = tokens[i + 1];
|
|
2122
|
-
if (!key) {
|
|
2123
|
-
continue;
|
|
2124
|
-
}
|
|
2125
|
-
if (!value) {
|
|
2126
|
-
pushError(errors, `Projection ${statement.id} ${context} is missing a value for '${key}'`, entry.loc);
|
|
2127
|
-
continue;
|
|
2128
|
-
}
|
|
2129
|
-
directives.set(key, value);
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
return directives;
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
function validateProjectionUiScreens(errors, statement, fieldMap, registry) {
|
|
2136
|
-
if (statement.kind !== "projection") {
|
|
2137
|
-
return;
|
|
2138
|
-
}
|
|
2139
|
-
|
|
2140
|
-
const screensField = fieldMap.get("screens")?.[0];
|
|
2141
|
-
if (!screensField || screensField.value.type !== "block") {
|
|
2142
|
-
return;
|
|
2143
|
-
}
|
|
2144
|
-
|
|
2145
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
2146
|
-
const seenScreens = new Set();
|
|
2147
|
-
|
|
2148
|
-
for (const entry of screensField.value.entries) {
|
|
2149
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2150
|
-
const [keyword, screenId] = tokens;
|
|
2151
|
-
|
|
2152
|
-
if (keyword !== "screen") {
|
|
2153
|
-
pushError(errors, `Projection ${statement.id} screens entries must start with 'screen'`, entry.loc);
|
|
2154
|
-
continue;
|
|
2155
|
-
}
|
|
2156
|
-
if (!screenId) {
|
|
2157
|
-
pushError(errors, `Projection ${statement.id} screens entries must include a screen id`, entry.loc);
|
|
2158
|
-
continue;
|
|
2159
|
-
}
|
|
2160
|
-
if (!IDENTIFIER_PATTERN.test(screenId)) {
|
|
2161
|
-
pushError(errors, `Projection ${statement.id} screens has invalid screen id '${screenId}'`, entry.loc);
|
|
2162
|
-
}
|
|
2163
|
-
if (seenScreens.has(screenId)) {
|
|
2164
|
-
pushError(errors, `Projection ${statement.id} screens has duplicate screen id '${screenId}'`, entry.loc);
|
|
2165
|
-
}
|
|
2166
|
-
seenScreens.add(screenId);
|
|
2167
|
-
|
|
2168
|
-
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `screens for '${screenId}'`);
|
|
2169
|
-
const kind = directives.get("kind");
|
|
2170
|
-
if (!kind) {
|
|
2171
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' must include 'kind'`, entry.loc);
|
|
2172
|
-
}
|
|
2173
|
-
if (kind && !UI_SCREEN_KINDS.has(kind)) {
|
|
2174
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' has invalid kind '${kind}'`, entry.loc);
|
|
2175
|
-
}
|
|
2176
|
-
|
|
2177
|
-
for (const key of directives.keys()) {
|
|
2178
|
-
if (!["kind", "title", "load", "item_shape", "view_shape", "input_shape", "submit", "detail_capability", "primary_action", "secondary_action", "destructive_action", "success_navigate", "success_refresh", "empty_title", "empty_body", "terminal_action", "loading_state", "error_state", "unauthorized_state", "not_found_state", "success_state"].includes(key)) {
|
|
2179
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' has unknown directive '${key}'`, entry.loc);
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
for (const [key, expectedKind] of [
|
|
2184
|
-
["load", "capability"],
|
|
2185
|
-
["submit", "capability"],
|
|
2186
|
-
["detail_capability", "capability"],
|
|
2187
|
-
["primary_action", "capability"],
|
|
2188
|
-
["secondary_action", "capability"],
|
|
2189
|
-
["destructive_action", "capability"],
|
|
2190
|
-
["terminal_action", "capability"],
|
|
2191
|
-
["item_shape", "shape"],
|
|
2192
|
-
["view_shape", "shape"],
|
|
2193
|
-
["input_shape", "shape"]
|
|
2194
|
-
]) {
|
|
2195
|
-
const targetId = directives.get(key);
|
|
2196
|
-
if (!targetId) {
|
|
2197
|
-
continue;
|
|
2198
|
-
}
|
|
2199
|
-
const target = registry.get(targetId);
|
|
2200
|
-
if (!target) {
|
|
2201
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' references missing ${expectedKind} '${targetId}' for '${key}'`, entry.loc);
|
|
2202
|
-
continue;
|
|
2203
|
-
}
|
|
2204
|
-
if (target.kind !== expectedKind) {
|
|
2205
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' must reference a ${expectedKind} for '${key}', found ${target.kind} '${target.id}'`, entry.loc);
|
|
2206
|
-
}
|
|
2207
|
-
if (expectedKind === "capability" && !realized.has(targetId)) {
|
|
2208
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' capability '${targetId}' for '${key}' must also appear in 'realizes'`, entry.loc);
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
|
|
2212
|
-
const successNavigate = directives.get("success_navigate");
|
|
2213
|
-
const successRefresh = directives.get("success_refresh");
|
|
2214
|
-
if (successNavigate && !IDENTIFIER_PATTERN.test(successNavigate)) {
|
|
2215
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' has invalid target '${successNavigate}' for 'success_navigate'`, entry.loc);
|
|
2216
|
-
}
|
|
2217
|
-
if (successRefresh && !IDENTIFIER_PATTERN.test(successRefresh)) {
|
|
2218
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' has invalid target '${successRefresh}' for 'success_refresh'`, entry.loc);
|
|
2219
|
-
}
|
|
2220
|
-
|
|
2221
|
-
if (kind === "list" && !directives.get("load")) {
|
|
2222
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' kind 'list' requires 'load'`, entry.loc);
|
|
2223
|
-
}
|
|
2224
|
-
if (kind === "detail") {
|
|
2225
|
-
if (!directives.get("load")) {
|
|
2226
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' kind 'detail' requires 'load'`, entry.loc);
|
|
2227
|
-
}
|
|
2228
|
-
if (!directives.get("view_shape")) {
|
|
2229
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' kind 'detail' requires 'view_shape'`, entry.loc);
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
if (kind === "form") {
|
|
2233
|
-
if (!directives.get("input_shape")) {
|
|
2234
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' kind 'form' requires 'input_shape'`, entry.loc);
|
|
2235
|
-
}
|
|
2236
|
-
if (!directives.get("submit")) {
|
|
2237
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' kind 'form' requires 'submit'`, entry.loc);
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
}
|
|
2241
|
-
|
|
2242
|
-
for (const entry of screensField.value.entries) {
|
|
2243
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2244
|
-
const screenId = tokens[1];
|
|
2245
|
-
if (!screenId) {
|
|
2246
|
-
continue;
|
|
2247
|
-
}
|
|
2248
|
-
const directives = parseUiDirectiveMap(tokens, 2, [], statement, entry, "");
|
|
2249
|
-
for (const key of ["success_navigate", "success_refresh"]) {
|
|
2250
|
-
const targetScreenId = directives.get(key);
|
|
2251
|
-
if (targetScreenId && !seenScreens.has(targetScreenId)) {
|
|
2252
|
-
pushError(errors, `Projection ${statement.id} screens for '${screenId}' references unknown screen '${targetScreenId}' for '${key}'`, entry.loc);
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
|
-
function validateProjectionUiCollections(errors, statement, fieldMap, registry) {
|
|
2259
|
-
if (statement.kind !== "projection") {
|
|
2260
|
-
return;
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
const collectionsField = fieldMap.get("collection_views")?.[0];
|
|
2264
|
-
if (!collectionsField || collectionsField.value.type !== "block") {
|
|
2265
|
-
return;
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
const screens = collectProjectionUiScreens(statement, fieldMap);
|
|
2269
|
-
for (const entry of collectionsField.value.entries) {
|
|
2270
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2271
|
-
const [keyword, screenId, operation, value, extra] = tokens;
|
|
2272
|
-
|
|
2273
|
-
if (keyword !== "screen") {
|
|
2274
|
-
pushError(errors, `Projection ${statement.id} collection_views entries must start with 'screen'`, entry.loc);
|
|
2275
|
-
continue;
|
|
2276
|
-
}
|
|
2277
|
-
const screenEntry = screens.get(screenId);
|
|
2278
|
-
if (!screenEntry) {
|
|
2279
|
-
pushError(errors, `Projection ${statement.id} collection_views references unknown screen '${screenId}'`, entry.loc);
|
|
2280
|
-
continue;
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
const screenTokens = blockSymbolItems(screenEntry).map((item) => item.value);
|
|
2284
|
-
const screenDirectives = parseUiDirectiveMap(screenTokens, 2, [], statement, screenEntry, "");
|
|
2285
|
-
if (screenDirectives.get("kind") !== "list") {
|
|
2286
|
-
pushError(errors, `Projection ${statement.id} collection_views may only target list screens, found '${screenId}'`, entry.loc);
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
if (!["filter", "search", "pagination", "sort", "group", "view", "refresh"].includes(operation)) {
|
|
2290
|
-
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' has invalid operation '${operation}'`, entry.loc);
|
|
2291
|
-
continue;
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
const loadCapabilityId = screenDirectives.get("load");
|
|
2295
|
-
const inputFields = loadCapabilityId ? resolveCapabilityContractFields(registry, loadCapabilityId, "input") : new Set();
|
|
2296
|
-
const outputShape = loadCapabilityId ? resolveCapabilityOutputShape(registry, loadCapabilityId) : null;
|
|
2297
|
-
const outputFields = outputShape
|
|
2298
|
-
? new Set((statementFieldNames(outputShape).length > 0 ? statementFieldNames(outputShape) : resolveShapeBaseFieldNames(outputShape, registry)))
|
|
2299
|
-
: new Set();
|
|
2300
|
-
|
|
2301
|
-
if (operation === "filter" || operation === "search") {
|
|
2302
|
-
if (!value) {
|
|
2303
|
-
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' must include a field for '${operation}'`, entry.loc);
|
|
2304
|
-
} else if (inputFields.size > 0 && !inputFields.has(value)) {
|
|
2305
|
-
pushError(errors, `Projection ${statement.id} collection_views references unknown input field '${value}' for '${operation}' on '${screenId}'`, entry.loc);
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
if (operation === "pagination" && !["cursor", "paged", "none"].includes(value || "")) {
|
|
2310
|
-
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' has invalid pagination '${value}'`, entry.loc);
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
if (operation === "sort") {
|
|
2314
|
-
if (!value || !extra) {
|
|
2315
|
-
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' must use 'sort <field> <asc|desc>'`, entry.loc);
|
|
2316
|
-
} else {
|
|
2317
|
-
if (!["asc", "desc"].includes(extra)) {
|
|
2318
|
-
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' has invalid sort direction '${extra}'`, entry.loc);
|
|
2319
|
-
}
|
|
2320
|
-
if (outputFields.size > 0 && !outputFields.has(value)) {
|
|
2321
|
-
pushError(errors, `Projection ${statement.id} collection_views references unknown output field '${value}' for sort on '${screenId}'`, entry.loc);
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
}
|
|
2325
|
-
|
|
2326
|
-
if (operation === "group") {
|
|
2327
|
-
if (!value) {
|
|
2328
|
-
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' must include a field for 'group'`, entry.loc);
|
|
2329
|
-
} else if (outputFields.size > 0 && !outputFields.has(value)) {
|
|
2330
|
-
pushError(errors, `Projection ${statement.id} collection_views references unknown output field '${value}' for group on '${screenId}'`, entry.loc);
|
|
2331
|
-
}
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
if (operation === "view" && !UI_COLLECTION_PRESENTATIONS.has(value || "")) {
|
|
2335
|
-
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' has invalid view '${value}'`, entry.loc);
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
if (operation === "refresh" && !["manual", "pull_to_refresh", "auto"].includes(value || "")) {
|
|
2339
|
-
pushError(errors, `Projection ${statement.id} collection_views for '${screenId}' has invalid refresh '${value}'`, entry.loc);
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
|
|
2344
|
-
function validateProjectionUiActions(errors, statement, fieldMap, registry) {
|
|
2345
|
-
if (statement.kind !== "projection") {
|
|
2346
|
-
return;
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
|
-
const actionsField = fieldMap.get("screen_actions")?.[0];
|
|
2350
|
-
if (!actionsField || actionsField.value.type !== "block") {
|
|
2351
|
-
return;
|
|
2352
|
-
}
|
|
2353
|
-
|
|
2354
|
-
const screens = collectProjectionUiScreens(statement, fieldMap);
|
|
2355
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
2356
|
-
|
|
2357
|
-
for (const entry of actionsField.value.entries) {
|
|
2358
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2359
|
-
const [keyword, screenId, actionKeyword, capabilityId, prominenceKeyword, prominence, placementKeyword, placement] = tokens;
|
|
2360
|
-
|
|
2361
|
-
if (keyword !== "screen") {
|
|
2362
|
-
pushError(errors, `Projection ${statement.id} screen_actions entries must start with 'screen'`, entry.loc);
|
|
2363
|
-
continue;
|
|
2364
|
-
}
|
|
2365
|
-
if (!screens.has(screenId)) {
|
|
2366
|
-
pushError(errors, `Projection ${statement.id} screen_actions references unknown screen '${screenId}'`, entry.loc);
|
|
2367
|
-
}
|
|
2368
|
-
if (actionKeyword !== "action") {
|
|
2369
|
-
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' must use 'action'`, entry.loc);
|
|
2370
|
-
}
|
|
2371
|
-
const capability = registry.get(capabilityId);
|
|
2372
|
-
if (!capability) {
|
|
2373
|
-
pushError(errors, `Projection ${statement.id} screen_actions references missing capability '${capabilityId}'`, entry.loc);
|
|
2374
|
-
} else if (capability.kind !== "capability") {
|
|
2375
|
-
pushError(errors, `Projection ${statement.id} screen_actions must reference a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
2376
|
-
} else if (!realized.has(capabilityId)) {
|
|
2377
|
-
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' capability '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
2378
|
-
}
|
|
2379
|
-
if (prominenceKeyword !== "prominence") {
|
|
2380
|
-
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' must use 'prominence'`, entry.loc);
|
|
2381
|
-
}
|
|
2382
|
-
if (!["primary", "secondary", "destructive", "contextual"].includes(prominence || "")) {
|
|
2383
|
-
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' has invalid prominence '${prominence}'`, entry.loc);
|
|
2384
|
-
}
|
|
2385
|
-
if (placementKeyword && placementKeyword !== "placement") {
|
|
2386
|
-
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' has unknown directive '${placementKeyword}'`, entry.loc);
|
|
2387
|
-
}
|
|
2388
|
-
if (placementKeyword === "placement" && !["toolbar", "menu", "bulk", "inline", "footer"].includes(placement || "")) {
|
|
2389
|
-
pushError(errors, `Projection ${statement.id} screen_actions for '${screenId}' has invalid placement '${placement}'`, entry.loc);
|
|
2390
|
-
}
|
|
2391
|
-
}
|
|
2392
|
-
}
|
|
2393
|
-
|
|
2394
|
-
function validateProjectionUiAppShell(errors, statement, fieldMap) {
|
|
2395
|
-
if (statement.kind !== "projection") {
|
|
2396
|
-
return;
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
const shellField = fieldMap.get("app_shell")?.[0];
|
|
2400
|
-
if (!shellField || shellField.value.type !== "block") {
|
|
2401
|
-
return;
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
const seenKeys = new Set();
|
|
2405
|
-
for (const entry of shellField.value.entries) {
|
|
2406
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2407
|
-
const [key, value, extra] = tokens;
|
|
2408
|
-
if (!["brand", "shell", "primary_nav", "secondary_nav", "utility_nav", "footer", "global_search", "notifications", "account_menu", "workspace_switcher", "windowing"].includes(key || "")) {
|
|
2409
|
-
pushError(errors, `Projection ${statement.id} app_shell has unknown key '${key}'`, entry.loc);
|
|
2410
|
-
continue;
|
|
2411
|
-
}
|
|
2412
|
-
if (!value) {
|
|
2413
|
-
pushError(errors, `Projection ${statement.id} app_shell is missing a value for '${key}'`, entry.loc);
|
|
2414
|
-
continue;
|
|
2415
|
-
}
|
|
2416
|
-
if (extra) {
|
|
2417
|
-
pushError(errors, `Projection ${statement.id} app_shell '${key}' accepts exactly one value`, entry.loc);
|
|
2418
|
-
}
|
|
2419
|
-
if (seenKeys.has(key)) {
|
|
2420
|
-
pushError(errors, `Projection ${statement.id} app_shell has duplicate key '${key}'`, entry.loc);
|
|
2421
|
-
}
|
|
2422
|
-
seenKeys.add(key);
|
|
2423
|
-
|
|
2424
|
-
if (key === "shell" && !UI_APP_SHELL_KINDS.has(value)) {
|
|
2425
|
-
pushError(errors, `Projection ${statement.id} app_shell has invalid shell '${value}'`, entry.loc);
|
|
2426
|
-
}
|
|
2427
|
-
if (["global_search", "notifications", "account_menu", "workspace_switcher"].includes(key) && !["true", "false"].includes(value)) {
|
|
2428
|
-
pushError(errors, `Projection ${statement.id} app_shell '${key}' must be true or false`, entry.loc);
|
|
2429
|
-
}
|
|
2430
|
-
if (key === "windowing" && !UI_WINDOWING_MODES.has(value)) {
|
|
2431
|
-
pushError(errors, `Projection ${statement.id} app_shell has invalid windowing '${value}'`, entry.loc);
|
|
2432
|
-
}
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
const SHARED_UI_SEMANTIC_BLOCKS = [
|
|
2437
|
-
"screens",
|
|
2438
|
-
"collection_views",
|
|
2439
|
-
"screen_actions",
|
|
2440
|
-
"visibility_rules",
|
|
2441
|
-
"field_lookups",
|
|
2442
|
-
"app_shell",
|
|
2443
|
-
"navigation",
|
|
2444
|
-
"screen_regions"
|
|
2445
|
-
];
|
|
2446
|
-
|
|
2447
|
-
function validateProjectionUiOwnership(errors, statement, fieldMap) {
|
|
2448
|
-
if (statement.kind !== "projection") {
|
|
2449
|
-
return;
|
|
2450
|
-
}
|
|
2451
|
-
|
|
2452
|
-
const projectionType = symbolValue(getFieldValue(statement, "type"));
|
|
2453
|
-
for (const key of SHARED_UI_SEMANTIC_BLOCKS) {
|
|
2454
|
-
const field = fieldMap.get(key)?.[0];
|
|
2455
|
-
if (!field || field.value.type !== "block") {
|
|
2456
|
-
continue;
|
|
2457
|
-
}
|
|
2458
|
-
if (projectionType !== "ui_contract") {
|
|
2459
|
-
pushError(
|
|
2460
|
-
errors,
|
|
2461
|
-
`Projection ${statement.id} ${key} belongs on shared UI projections; concrete UI projections may define screen_routes and surface hints only`,
|
|
2462
|
-
field.loc
|
|
2463
|
-
);
|
|
2464
|
-
}
|
|
2465
|
-
}
|
|
2466
|
-
|
|
2467
|
-
const routesField = fieldMap.get("screen_routes")?.[0];
|
|
2468
|
-
if (routesField?.value.type === "block" && !["web_surface", "ios_surface"].includes(projectionType || "")) {
|
|
2469
|
-
pushError(
|
|
2470
|
-
errors,
|
|
2471
|
-
`Projection ${statement.id} screen_routes belongs on concrete UI projections; shared UI projections own semantic screens and regions`,
|
|
2472
|
-
routesField.loc
|
|
2473
|
-
);
|
|
2474
|
-
}
|
|
2475
|
-
}
|
|
2476
|
-
|
|
2477
|
-
function validateProjectionUiDesign(errors, statement, fieldMap) {
|
|
2478
|
-
if (statement.kind !== "projection") {
|
|
2479
|
-
return;
|
|
2480
|
-
}
|
|
2481
|
-
|
|
2482
|
-
const designField = fieldMap.get("design_tokens")?.[0];
|
|
2483
|
-
if (!designField || designField.value.type !== "block") {
|
|
2484
|
-
return;
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
|
-
if (symbolValue(getFieldValue(statement, "type")) !== "ui_contract") {
|
|
2488
|
-
pushError(errors, `Projection ${statement.id} design_tokens belongs on shared UI projections; concrete UI projections inherit semantic design intent through 'realizes'`, designField.loc);
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
for (const entry of designField.value.entries) {
|
|
2492
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2493
|
-
const [key, value, extra] = tokens;
|
|
2494
|
-
|
|
2495
|
-
if (key === "density") {
|
|
2496
|
-
if (!UI_DESIGN_DENSITIES.has(value || "")) {
|
|
2497
|
-
pushError(errors, `Projection ${statement.id} design_tokens density has invalid value '${value}'`, entry.loc);
|
|
2498
|
-
}
|
|
2499
|
-
if (tokens.length !== 2) {
|
|
2500
|
-
pushError(errors, `Projection ${statement.id} design_tokens density accepts exactly one value`, entry.loc);
|
|
2501
|
-
}
|
|
2502
|
-
continue;
|
|
2503
|
-
}
|
|
2504
|
-
|
|
2505
|
-
if (key === "tone") {
|
|
2506
|
-
if (!UI_DESIGN_TONES.has(value || "")) {
|
|
2507
|
-
pushError(errors, `Projection ${statement.id} design_tokens tone has invalid value '${value}'`, entry.loc);
|
|
2508
|
-
}
|
|
2509
|
-
if (tokens.length !== 2) {
|
|
2510
|
-
pushError(errors, `Projection ${statement.id} design_tokens tone accepts exactly one value`, entry.loc);
|
|
2511
|
-
}
|
|
2512
|
-
continue;
|
|
2513
|
-
}
|
|
2514
|
-
|
|
2515
|
-
if (key === "radius_scale") {
|
|
2516
|
-
if (!UI_DESIGN_RADIUS_SCALES.has(value || "")) {
|
|
2517
|
-
pushError(errors, `Projection ${statement.id} design_tokens radius_scale has invalid value '${value}'`, entry.loc);
|
|
2518
|
-
}
|
|
2519
|
-
if (tokens.length !== 2) {
|
|
2520
|
-
pushError(errors, `Projection ${statement.id} design_tokens radius_scale accepts exactly one value`, entry.loc);
|
|
2521
|
-
}
|
|
2522
|
-
continue;
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
|
-
if (key === "color_role") {
|
|
2526
|
-
if (!UI_DESIGN_COLOR_ROLES.has(value || "")) {
|
|
2527
|
-
pushError(errors, `Projection ${statement.id} design_tokens color_role has invalid role '${value}'`, entry.loc);
|
|
2528
|
-
}
|
|
2529
|
-
if (tokens.length !== 3) {
|
|
2530
|
-
pushError(errors, `Projection ${statement.id} design_tokens color_role must use 'color_role <role> <semantic-token>'`, entry.loc);
|
|
2531
|
-
}
|
|
2532
|
-
continue;
|
|
2533
|
-
}
|
|
2534
|
-
|
|
2535
|
-
if (key === "typography_role") {
|
|
2536
|
-
if (!UI_DESIGN_TYPOGRAPHY_ROLES.has(value || "")) {
|
|
2537
|
-
pushError(errors, `Projection ${statement.id} design_tokens typography_role has invalid role '${value}'`, entry.loc);
|
|
2538
|
-
}
|
|
2539
|
-
if (tokens.length !== 3) {
|
|
2540
|
-
pushError(errors, `Projection ${statement.id} design_tokens typography_role must use 'typography_role <role> <semantic-token>'`, entry.loc);
|
|
2541
|
-
}
|
|
2542
|
-
continue;
|
|
2543
|
-
}
|
|
2544
|
-
|
|
2545
|
-
if (key === "action_role") {
|
|
2546
|
-
if (!UI_DESIGN_ACTION_ROLES.has(value || "")) {
|
|
2547
|
-
pushError(errors, `Projection ${statement.id} design_tokens action_role has invalid role '${value}'`, entry.loc);
|
|
2548
|
-
}
|
|
2549
|
-
if (tokens.length !== 3) {
|
|
2550
|
-
pushError(errors, `Projection ${statement.id} design_tokens action_role must use 'action_role <role> <semantic-token>'`, entry.loc);
|
|
2551
|
-
}
|
|
2552
|
-
continue;
|
|
2553
|
-
}
|
|
2554
|
-
|
|
2555
|
-
if (key === "accessibility") {
|
|
2556
|
-
const values = UI_DESIGN_ACCESSIBILITY_VALUES[value];
|
|
2557
|
-
if (tokens.length !== 3) {
|
|
2558
|
-
pushError(errors, `Projection ${statement.id} design_tokens accessibility must use 'accessibility <setting> <value>'`, entry.loc);
|
|
2559
|
-
}
|
|
2560
|
-
if (!values) {
|
|
2561
|
-
pushError(errors, `Projection ${statement.id} design_tokens accessibility has invalid setting '${value}'`, entry.loc);
|
|
2562
|
-
} else if (!values.has(extra || "")) {
|
|
2563
|
-
pushError(errors, `Projection ${statement.id} design_tokens accessibility '${value}' has invalid value '${extra}'`, entry.loc);
|
|
2564
|
-
}
|
|
2565
|
-
continue;
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
pushError(errors, `Projection ${statement.id} design_tokens has unknown key '${key}'`, entry.loc);
|
|
2569
|
-
}
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
function validateProjectionUiNavigation(errors, statement, fieldMap, registry) {
|
|
2573
|
-
if (statement.kind !== "projection") {
|
|
2574
|
-
return;
|
|
2575
|
-
}
|
|
2576
|
-
|
|
2577
|
-
const navigationField = fieldMap.get("navigation")?.[0];
|
|
2578
|
-
if (!navigationField || navigationField.value.type !== "block") {
|
|
2579
|
-
return;
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
|
|
2583
|
-
const groups = new Set();
|
|
2584
|
-
|
|
2585
|
-
for (const entry of navigationField.value.entries) {
|
|
2586
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2587
|
-
const [targetKind, targetId] = tokens;
|
|
2588
|
-
|
|
2589
|
-
if (targetKind === "group") {
|
|
2590
|
-
if (!targetId || !IDENTIFIER_PATTERN.test(targetId)) {
|
|
2591
|
-
pushError(errors, `Projection ${statement.id} navigation group entries must include a valid group id`, entry.loc);
|
|
2592
|
-
continue;
|
|
2593
|
-
}
|
|
2594
|
-
groups.add(targetId);
|
|
2595
|
-
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `navigation group '${targetId}'`);
|
|
2596
|
-
for (const key of directives.keys()) {
|
|
2597
|
-
if (!["label", "placement", "icon", "order", "pattern"].includes(key)) {
|
|
2598
|
-
pushError(errors, `Projection ${statement.id} navigation group '${targetId}' has unknown directive '${key}'`, entry.loc);
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
|
-
if (directives.has("placement") && !["primary", "secondary", "utility"].includes(directives.get("placement"))) {
|
|
2602
|
-
pushError(errors, `Projection ${statement.id} navigation group '${targetId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
|
|
2603
|
-
}
|
|
2604
|
-
if (directives.has("pattern") && !UI_NAVIGATION_PATTERNS.has(directives.get("pattern"))) {
|
|
2605
|
-
pushError(errors, `Projection ${statement.id} navigation group '${targetId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
|
|
2606
|
-
}
|
|
2607
|
-
continue;
|
|
2608
|
-
}
|
|
2609
|
-
|
|
2610
|
-
if (targetKind === "screen") {
|
|
2611
|
-
if (!availableScreens.has(targetId)) {
|
|
2612
|
-
pushError(errors, `Projection ${statement.id} navigation references unknown screen '${targetId}'`, entry.loc);
|
|
2613
|
-
}
|
|
2614
|
-
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `navigation screen '${targetId}'`);
|
|
2615
|
-
for (const key of directives.keys()) {
|
|
2616
|
-
if (!["group", "label", "order", "visible", "default", "breadcrumb", "sitemap", "placement", "pattern"].includes(key)) {
|
|
2617
|
-
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has unknown directive '${key}'`, entry.loc);
|
|
2618
|
-
}
|
|
2619
|
-
}
|
|
2620
|
-
if (directives.has("visible") && !["true", "false"].includes(directives.get("visible"))) {
|
|
2621
|
-
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid visible '${directives.get("visible")}'`, entry.loc);
|
|
2622
|
-
}
|
|
2623
|
-
if (directives.has("default") && !["true", "false"].includes(directives.get("default"))) {
|
|
2624
|
-
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid default '${directives.get("default")}'`, entry.loc);
|
|
2625
|
-
}
|
|
2626
|
-
if (directives.has("placement") && !["primary", "secondary", "utility"].includes(directives.get("placement"))) {
|
|
2627
|
-
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
|
|
2628
|
-
}
|
|
2629
|
-
if (directives.has("sitemap") && !["include", "exclude"].includes(directives.get("sitemap"))) {
|
|
2630
|
-
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid sitemap '${directives.get("sitemap")}'`, entry.loc);
|
|
2631
|
-
}
|
|
2632
|
-
if (directives.has("pattern") && !UI_NAVIGATION_PATTERNS.has(directives.get("pattern"))) {
|
|
2633
|
-
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
|
|
2634
|
-
}
|
|
2635
|
-
const breadcrumb = directives.get("breadcrumb");
|
|
2636
|
-
if (breadcrumb && breadcrumb !== "none" && !availableScreens.has(breadcrumb)) {
|
|
2637
|
-
pushError(errors, `Projection ${statement.id} navigation screen '${targetId}' references unknown breadcrumb screen '${breadcrumb}'`, entry.loc);
|
|
2638
|
-
}
|
|
2639
|
-
continue;
|
|
2640
|
-
}
|
|
2641
|
-
|
|
2642
|
-
pushError(errors, `Projection ${statement.id} navigation entries must start with 'group' or 'screen'`, entry.loc);
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
for (const entry of navigationField.value.entries) {
|
|
2646
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2647
|
-
if (tokens[0] !== "screen") {
|
|
2648
|
-
continue;
|
|
2649
|
-
}
|
|
2650
|
-
const directives = parseUiDirectiveMap(tokens, 2, [], statement, entry, "");
|
|
2651
|
-
if (directives.has("group") && !groups.has(directives.get("group"))) {
|
|
2652
|
-
pushError(errors, `Projection ${statement.id} navigation screen '${tokens[1]}' references unknown group '${directives.get("group")}'`, entry.loc);
|
|
2653
|
-
}
|
|
2654
|
-
}
|
|
2655
|
-
}
|
|
2656
|
-
|
|
2657
|
-
function validateProjectionUiScreenRegions(errors, statement, fieldMap, registry) {
|
|
2658
|
-
if (statement.kind !== "projection") {
|
|
2659
|
-
return;
|
|
2660
|
-
}
|
|
2661
|
-
|
|
2662
|
-
const regionField = fieldMap.get("screen_regions")?.[0];
|
|
2663
|
-
if (!regionField || regionField.value.type !== "block") {
|
|
2664
|
-
return;
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
|
|
2668
|
-
for (const entry of regionField.value.entries) {
|
|
2669
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2670
|
-
const [keyword, screenId, regionKeyword, regionName] = tokens;
|
|
2671
|
-
|
|
2672
|
-
if (keyword !== "screen") {
|
|
2673
|
-
pushError(errors, `Projection ${statement.id} screen_regions entries must start with 'screen'`, entry.loc);
|
|
2674
|
-
continue;
|
|
2675
|
-
}
|
|
2676
|
-
if (!availableScreens.has(screenId)) {
|
|
2677
|
-
pushError(errors, `Projection ${statement.id} screen_regions references unknown screen '${screenId}'`, entry.loc);
|
|
2678
|
-
}
|
|
2679
|
-
if (regionKeyword !== "region") {
|
|
2680
|
-
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' must use 'region'`, entry.loc);
|
|
2681
|
-
}
|
|
2682
|
-
if (!UI_REGION_KINDS.has(regionName || "")) {
|
|
2683
|
-
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid region '${regionName}'`, entry.loc);
|
|
2684
|
-
}
|
|
2685
|
-
|
|
2686
|
-
const directives = parseUiDirectiveMap(tokens, 4, errors, statement, entry, `screen_regions for '${screenId}'`);
|
|
2687
|
-
for (const key of directives.keys()) {
|
|
2688
|
-
if (!["pattern", "placement", "title", "state", "variant"].includes(key)) {
|
|
2689
|
-
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has unknown directive '${key}'`, entry.loc);
|
|
2690
|
-
}
|
|
2691
|
-
}
|
|
2692
|
-
if (directives.has("pattern") && !UI_PATTERN_KINDS.has(directives.get("pattern"))) {
|
|
2693
|
-
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid pattern '${directives.get("pattern")}'`, entry.loc);
|
|
2694
|
-
}
|
|
2695
|
-
if (directives.has("placement") && !["primary", "secondary", "supporting"].includes(directives.get("placement"))) {
|
|
2696
|
-
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid placement '${directives.get("placement")}'`, entry.loc);
|
|
2697
|
-
}
|
|
2698
|
-
if (directives.has("state") && !UI_STATE_KINDS.has(directives.get("state"))) {
|
|
2699
|
-
pushError(errors, `Projection ${statement.id} screen_regions for '${screenId}' has invalid state '${directives.get("state")}'`, entry.loc);
|
|
2700
|
-
}
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
|
|
2704
|
-
function validateProjectionUiComponents(errors, statement, fieldMap, registry) {
|
|
2705
|
-
if (statement.kind !== "projection") {
|
|
2706
|
-
return;
|
|
2707
|
-
}
|
|
2708
|
-
|
|
2709
|
-
const componentsField = fieldMap.get("widget_bindings")?.[0];
|
|
2710
|
-
if (!componentsField || componentsField.value.type !== "block") {
|
|
2711
|
-
return;
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
if (symbolValue(getFieldValue(statement, "type")) !== "ui_contract") {
|
|
2715
|
-
pushError(errors, `Projection ${statement.id} widget_bindings belongs on shared UI projections; concrete UI projections inherit widget placement through 'realizes'`, componentsField.loc);
|
|
2716
|
-
}
|
|
2717
|
-
|
|
2718
|
-
const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
|
|
2719
|
-
const availableRegions = collectAvailableUiRegionKeys(statement, registry);
|
|
2720
|
-
const availableRegionPatterns = collectAvailableUiRegionPatterns(statement, registry);
|
|
2721
|
-
|
|
2722
|
-
for (const entry of componentsField.value.entries) {
|
|
2723
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2724
|
-
const [screenKeyword, screenId, regionKeyword, regionName, componentKeyword, componentId] = tokens;
|
|
2725
|
-
|
|
2726
|
-
if (screenKeyword !== "screen") {
|
|
2727
|
-
pushError(errors, `Projection ${statement.id} widget_bindings entries must start with 'screen'`, entry.loc);
|
|
2728
|
-
continue;
|
|
2729
|
-
}
|
|
2730
|
-
if (!availableScreens.has(screenId)) {
|
|
2731
|
-
pushError(errors, `Projection ${statement.id} widget_bindings references unknown screen '${screenId}'`, entry.loc);
|
|
2732
|
-
}
|
|
2733
|
-
if (regionKeyword !== "region") {
|
|
2734
|
-
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' must use 'region'`, entry.loc);
|
|
2735
|
-
}
|
|
2736
|
-
if (!UI_REGION_KINDS.has(regionName || "")) {
|
|
2737
|
-
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' has invalid region '${regionName}'`, entry.loc);
|
|
2738
|
-
} else if (!availableRegions.has(`${screenId}:${regionName}`)) {
|
|
2739
|
-
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' references undeclared region '${regionName}'`, entry.loc);
|
|
2740
|
-
}
|
|
2741
|
-
if (componentKeyword !== "widget") {
|
|
2742
|
-
pushError(errors, `Projection ${statement.id} widget_bindings for '${screenId}' must use 'widget'`, entry.loc);
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
const widget = registry.get(componentId);
|
|
2746
|
-
if (!widget) {
|
|
2747
|
-
pushError(errors, `Projection ${statement.id} widget_bindings references missing widget '${componentId}'`, entry.loc);
|
|
2748
|
-
continue;
|
|
2749
|
-
}
|
|
2750
|
-
if (widget.kind !== "widget") {
|
|
2751
|
-
pushError(errors, `Projection ${statement.id} widget_bindings must reference a widget, found ${widget.kind} '${widget.id}'`, entry.loc);
|
|
2752
|
-
continue;
|
|
2753
|
-
}
|
|
2754
|
-
|
|
2755
|
-
const propNames = new Set(blockEntries(getFieldValue(widget, "props"))
|
|
2756
|
-
.map((propEntry) => propEntry.items[0])
|
|
2757
|
-
.filter((item) => item?.type === "symbol")
|
|
2758
|
-
.map((item) => item.value));
|
|
2759
|
-
const eventNames = new Set(blockEntries(getFieldValue(widget, "events"))
|
|
2760
|
-
.map((eventEntry) => eventEntry.items[0])
|
|
2761
|
-
.filter((item) => item?.type === "symbol")
|
|
2762
|
-
.map((item) => item.value));
|
|
2763
|
-
const componentRegions = symbolValues(getFieldValue(widget, "regions"));
|
|
2764
|
-
const componentPatterns = symbolValues(getFieldValue(widget, "patterns"));
|
|
2765
|
-
if (componentRegions.length > 0 && !componentRegions.includes(regionName)) {
|
|
2766
|
-
pushError(
|
|
2767
|
-
errors,
|
|
2768
|
-
`Projection ${statement.id} widget_bindings uses widget '${componentId}' in region '${regionName}', but the widget supports regions [${componentRegions.join(", ")}]`,
|
|
2769
|
-
entry.loc
|
|
2770
|
-
);
|
|
2771
|
-
}
|
|
2772
|
-
const regionPattern = availableRegionPatterns.get(`${screenId}:${regionName}`) || null;
|
|
2773
|
-
if (regionPattern && componentPatterns.length > 0 && !componentPatterns.includes(regionPattern)) {
|
|
2774
|
-
pushError(
|
|
2775
|
-
errors,
|
|
2776
|
-
`Projection ${statement.id} widget_bindings uses widget '${componentId}' in '${screenId}:${regionName}' with pattern '${regionPattern}', but the widget supports patterns [${componentPatterns.join(", ")}]`,
|
|
2777
|
-
entry.loc
|
|
2778
|
-
);
|
|
2779
|
-
}
|
|
2780
|
-
|
|
2781
|
-
for (let i = 6; i < tokens.length;) {
|
|
2782
|
-
const directive = tokens[i];
|
|
2783
|
-
if (directive === "data") {
|
|
2784
|
-
const propName = tokens[i + 1];
|
|
2785
|
-
const fromKeyword = tokens[i + 2];
|
|
2786
|
-
const sourceId = tokens[i + 3];
|
|
2787
|
-
if (!propName || fromKeyword !== "from" || !sourceId) {
|
|
2788
|
-
pushError(errors, `Projection ${statement.id} widget_bindings data bindings must use 'data <prop> from <source>'`, entry.loc);
|
|
2789
|
-
break;
|
|
2790
|
-
}
|
|
2791
|
-
if (!propNames.has(propName)) {
|
|
2792
|
-
pushError(errors, `Projection ${statement.id} widget_bindings references unknown prop '${propName}' on widget '${componentId}'`, entry.loc);
|
|
2793
|
-
}
|
|
2794
|
-
const source = registry.get(sourceId);
|
|
2795
|
-
if (!source || !["capability", "projection", "shape", "entity"].includes(source.kind)) {
|
|
2796
|
-
pushError(errors, `Projection ${statement.id} widget_bindings data binding for '${propName}' references missing source '${sourceId}'`, entry.loc);
|
|
2797
|
-
}
|
|
2798
|
-
i += 4;
|
|
2799
|
-
continue;
|
|
2800
|
-
}
|
|
2801
|
-
|
|
2802
|
-
if (directive === "event") {
|
|
2803
|
-
const eventName = tokens[i + 1];
|
|
2804
|
-
const action = tokens[i + 2];
|
|
2805
|
-
const targetId = tokens[i + 3];
|
|
2806
|
-
if (!eventName || !action || !targetId) {
|
|
2807
|
-
pushError(errors, `Projection ${statement.id} widget_bindings event bindings must use 'event <event> <navigate|action> <target>'`, entry.loc);
|
|
2808
|
-
break;
|
|
2809
|
-
}
|
|
2810
|
-
if (!eventNames.has(eventName)) {
|
|
2811
|
-
pushError(errors, `Projection ${statement.id} widget_bindings references unknown event '${eventName}' on widget '${componentId}'`, entry.loc);
|
|
2812
|
-
}
|
|
2813
|
-
if (action === "navigate") {
|
|
2814
|
-
if (!availableScreens.has(targetId)) {
|
|
2815
|
-
pushError(errors, `Projection ${statement.id} widget_bindings event '${eventName}' references unknown navigation target '${targetId}'`, entry.loc);
|
|
2816
|
-
}
|
|
2817
|
-
} else if (action === "action") {
|
|
2818
|
-
const target = registry.get(targetId);
|
|
2819
|
-
if (!target || target.kind !== "capability") {
|
|
2820
|
-
pushError(errors, `Projection ${statement.id} widget_bindings event '${eventName}' references missing capability action '${targetId}'`, entry.loc);
|
|
2821
|
-
}
|
|
2822
|
-
} else {
|
|
2823
|
-
pushError(errors, `Projection ${statement.id} widget_bindings event '${eventName}' has unsupported action '${action}'`, entry.loc);
|
|
2824
|
-
}
|
|
2825
|
-
i += 4;
|
|
2826
|
-
continue;
|
|
2827
|
-
}
|
|
2828
|
-
|
|
2829
|
-
pushError(errors, `Projection ${statement.id} widget_bindings has unknown directive '${directive}'`, entry.loc);
|
|
2830
|
-
break;
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
}
|
|
2834
|
-
|
|
2835
|
-
function validateProjectionUiVisibility(errors, statement, fieldMap, registry) {
|
|
2836
|
-
if (statement.kind !== "projection") {
|
|
2837
|
-
return;
|
|
2838
|
-
}
|
|
2839
|
-
|
|
2840
|
-
const visibilityField = fieldMap.get("visibility_rules")?.[0];
|
|
2841
|
-
if (!visibilityField || visibilityField.value.type !== "block") {
|
|
2842
|
-
return;
|
|
2843
|
-
}
|
|
2844
|
-
|
|
2845
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
2846
|
-
for (const entry of visibilityField.value.entries) {
|
|
2847
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2848
|
-
const [keyword, capabilityId, predicateKeyword, predicateType, predicateValue] = tokens;
|
|
2849
|
-
|
|
2850
|
-
if (keyword !== "action") {
|
|
2851
|
-
pushError(errors, `Projection ${statement.id} visibility_rules entries must start with 'action'`, entry.loc);
|
|
2852
|
-
continue;
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
const capability = registry.get(capabilityId);
|
|
2856
|
-
if (!capability) {
|
|
2857
|
-
pushError(errors, `Projection ${statement.id} visibility_rules references missing capability '${capabilityId}'`, entry.loc);
|
|
2858
|
-
} else if (capability.kind !== "capability") {
|
|
2859
|
-
pushError(errors, `Projection ${statement.id} visibility_rules must reference a capability, found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
2860
|
-
} else if (!realized.has(capabilityId)) {
|
|
2861
|
-
pushError(errors, `Projection ${statement.id} visibility_rules action '${capabilityId}' must also appear in 'realizes'`, entry.loc);
|
|
2862
|
-
}
|
|
2863
|
-
|
|
2864
|
-
if (predicateKeyword !== "visible_if") {
|
|
2865
|
-
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' must use 'visible_if'`, entry.loc);
|
|
2866
|
-
}
|
|
2867
|
-
if (!["permission", "ownership", "claim"].includes(predicateType || "")) {
|
|
2868
|
-
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' has invalid predicate '${predicateType}'`, entry.loc);
|
|
2869
|
-
}
|
|
2870
|
-
if (!predicateValue) {
|
|
2871
|
-
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' must include a predicate value`, entry.loc);
|
|
2872
|
-
}
|
|
2873
|
-
if (predicateType === "ownership" && !["owner", "owner_or_admin", "project_member", "none"].includes(predicateValue || "")) {
|
|
2874
|
-
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' has invalid ownership '${predicateValue}'`, entry.loc);
|
|
2875
|
-
}
|
|
2876
|
-
const directiveTokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2877
|
-
const directives = new Map();
|
|
2878
|
-
for (let i = 5; i < directiveTokens.length; i += 2) {
|
|
2879
|
-
const key = directiveTokens[i];
|
|
2880
|
-
const value = directiveTokens[i + 1];
|
|
2881
|
-
if (!value) {
|
|
2882
|
-
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' is missing a value for '${key}'`, entry.loc);
|
|
2883
|
-
continue;
|
|
2884
|
-
}
|
|
2885
|
-
directives.set(key, value);
|
|
2886
|
-
}
|
|
2887
|
-
for (const key of directives.keys()) {
|
|
2888
|
-
if (!["claim_value"].includes(key)) {
|
|
2889
|
-
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' has unknown directive '${key}'`, entry.loc);
|
|
2890
|
-
}
|
|
2891
|
-
}
|
|
2892
|
-
if (directives.get("claim_value") && predicateType !== "claim") {
|
|
2893
|
-
pushError(errors, `Projection ${statement.id} visibility_rules for '${capabilityId}' cannot declare claim_value without claim`, entry.loc);
|
|
2894
|
-
}
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
|
-
function validateProjectionUiLookups(errors, statement, fieldMap, registry) {
|
|
2899
|
-
if (statement.kind !== "projection") {
|
|
2900
|
-
return;
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
|
-
const lookupsField = fieldMap.get("field_lookups")?.[0];
|
|
2904
|
-
if (!lookupsField || lookupsField.value.type !== "block") {
|
|
2905
|
-
return;
|
|
2906
|
-
}
|
|
2907
|
-
|
|
2908
|
-
const screens = collectProjectionUiScreens(statement, fieldMap);
|
|
2909
|
-
|
|
2910
|
-
for (const entry of lookupsField.value.entries) {
|
|
2911
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2912
|
-
const [keyword, screenId, fieldKeyword, fieldName, entityKeyword, entityId, labelKeyword, labelField, maybeEmptyKeyword, maybeEmptyLabel] = tokens;
|
|
2913
|
-
|
|
2914
|
-
if (keyword !== "screen") {
|
|
2915
|
-
pushError(errors, `Projection ${statement.id} field_lookups entries must start with 'screen'`, entry.loc);
|
|
2916
|
-
continue;
|
|
2917
|
-
}
|
|
2918
|
-
|
|
2919
|
-
const screenEntry = screens.get(screenId);
|
|
2920
|
-
if (!screenEntry) {
|
|
2921
|
-
pushError(errors, `Projection ${statement.id} field_lookups references unknown screen '${screenId}'`, entry.loc);
|
|
2922
|
-
continue;
|
|
2923
|
-
}
|
|
2924
|
-
|
|
2925
|
-
if (fieldKeyword !== "field") {
|
|
2926
|
-
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must use 'field'`, entry.loc);
|
|
2927
|
-
}
|
|
2928
|
-
if (!fieldName) {
|
|
2929
|
-
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must include a field name`, entry.loc);
|
|
2930
|
-
}
|
|
2931
|
-
|
|
2932
|
-
if (entityKeyword !== "entity") {
|
|
2933
|
-
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must use 'entity'`, entry.loc);
|
|
2934
|
-
}
|
|
2935
|
-
const entity = entityId ? registry.get(entityId) : null;
|
|
2936
|
-
if (!entity) {
|
|
2937
|
-
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' references missing entity '${entityId}'`, entry.loc);
|
|
2938
|
-
} else if (entity.kind !== "entity") {
|
|
2939
|
-
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must reference an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
2940
|
-
}
|
|
2941
|
-
|
|
2942
|
-
if (labelKeyword !== "label_field") {
|
|
2943
|
-
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must use 'label_field'`, entry.loc);
|
|
2944
|
-
}
|
|
2945
|
-
if (!labelField) {
|
|
2946
|
-
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must include a label_field`, entry.loc);
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
if (maybeEmptyKeyword && maybeEmptyKeyword !== "empty_label") {
|
|
2950
|
-
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' has unknown directive '${maybeEmptyKeyword}'`, entry.loc);
|
|
2951
|
-
}
|
|
2952
|
-
if (maybeEmptyKeyword === "empty_label" && !maybeEmptyLabel) {
|
|
2953
|
-
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' must include a value for 'empty_label'`, entry.loc);
|
|
2954
|
-
}
|
|
2955
|
-
|
|
2956
|
-
const availableFields = resolveProjectionUiScreenFieldNames(registry, screenEntry, statement);
|
|
2957
|
-
if (fieldName && availableFields.size > 0 && !availableFields.has(fieldName)) {
|
|
2958
|
-
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' references unknown screen field '${fieldName}'`, entry.loc);
|
|
2959
|
-
}
|
|
2960
|
-
|
|
2961
|
-
if (entity?.kind === "entity") {
|
|
2962
|
-
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
2963
|
-
if (labelField && !entityFieldNames.has(labelField)) {
|
|
2964
|
-
pushError(errors, `Projection ${statement.id} field_lookups for '${screenId}' references unknown entity field '${labelField}' on '${entity.id}'`, entry.loc);
|
|
2965
|
-
}
|
|
2966
|
-
}
|
|
2967
|
-
}
|
|
2968
|
-
}
|
|
2969
|
-
|
|
2970
|
-
function validateProjectionUiRoutes(errors, statement, fieldMap, registry) {
|
|
2971
|
-
if (statement.kind !== "projection") {
|
|
2972
|
-
return;
|
|
2973
|
-
}
|
|
2974
|
-
|
|
2975
|
-
const routesField = fieldMap.get("screen_routes")?.[0];
|
|
2976
|
-
if (!routesField || routesField.value.type !== "block") {
|
|
2977
|
-
return;
|
|
2978
|
-
}
|
|
2979
|
-
|
|
2980
|
-
const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
|
|
2981
|
-
const seenPaths = new Set();
|
|
2982
|
-
const projectionType = symbolValue(getFieldValue(statement, "type"));
|
|
2983
|
-
|
|
2984
|
-
for (const entry of routesField.value.entries) {
|
|
2985
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
2986
|
-
const [keyword, screenId, pathKeyword, routePath] = tokens;
|
|
2987
|
-
|
|
2988
|
-
if (keyword !== "screen") {
|
|
2989
|
-
pushError(errors, `Projection ${statement.id} screen_routes entries must start with 'screen'`, entry.loc);
|
|
2990
|
-
continue;
|
|
2991
|
-
}
|
|
2992
|
-
if (!availableScreens.has(screenId)) {
|
|
2993
|
-
pushError(errors, `Projection ${statement.id} screen_routes references unknown screen '${screenId}'`, entry.loc);
|
|
2994
|
-
}
|
|
2995
|
-
if (pathKeyword !== "path") {
|
|
2996
|
-
pushError(errors, `Projection ${statement.id} screen_routes for '${screenId}' must use 'path'`, entry.loc);
|
|
2997
|
-
}
|
|
2998
|
-
if (!routePath) {
|
|
2999
|
-
pushError(errors, `Projection ${statement.id} screen_routes for '${screenId}' must include a path`, entry.loc);
|
|
3000
|
-
continue;
|
|
3001
|
-
}
|
|
3002
|
-
if ((projectionType === "web_surface" || projectionType === "ios_surface") && !routePath.startsWith("/")) {
|
|
3003
|
-
pushError(errors, `Projection ${statement.id} screen_routes for '${screenId}' must use an absolute path`, entry.loc);
|
|
3004
|
-
}
|
|
3005
|
-
if (seenPaths.has(routePath)) {
|
|
3006
|
-
pushError(errors, `Projection ${statement.id} screen_routes has duplicate path '${routePath}'`, entry.loc);
|
|
3007
|
-
}
|
|
3008
|
-
seenPaths.add(routePath);
|
|
3009
|
-
}
|
|
3010
|
-
}
|
|
3011
|
-
|
|
3012
|
-
function validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, surfaceBlockKey, expectedProjectionType) {
|
|
3013
|
-
if (statement.kind !== "projection") {
|
|
3014
|
-
return;
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
|
-
const surfaceField = fieldMap.get(surfaceBlockKey)?.[0];
|
|
3018
|
-
if (!surfaceField || surfaceField.value.type !== "block") {
|
|
3019
|
-
return;
|
|
3020
|
-
}
|
|
3021
|
-
|
|
3022
|
-
const projectionType = symbolValue(getFieldValue(statement, "type"));
|
|
3023
|
-
if (projectionType !== expectedProjectionType) {
|
|
3024
|
-
pushError(errors, `Projection ${statement.id} may only use '${surfaceBlockKey}' when projection type is '${expectedProjectionType}'`, surfaceField.loc);
|
|
3025
|
-
return;
|
|
3026
|
-
}
|
|
3027
|
-
|
|
3028
|
-
const availableScreens = collectAvailableUiScreenIds(statement, fieldMap, registry);
|
|
3029
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
3030
|
-
for (const entry of surfaceField.value.entries) {
|
|
3031
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
3032
|
-
const [targetKind, targetId, directive, value] = tokens;
|
|
3033
|
-
|
|
3034
|
-
if (targetKind === "screen") {
|
|
3035
|
-
if (!availableScreens.has(targetId)) {
|
|
3036
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} references unknown screen '${targetId}'`, entry.loc);
|
|
3037
|
-
}
|
|
3038
|
-
if (!["layout", "desktop_variant", "mobile_variant", "present", "shell", "collection", "breadcrumbs", "state_style"].includes(directive || "")) {
|
|
3039
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has unknown directive '${directive}'`, entry.loc);
|
|
3040
|
-
}
|
|
3041
|
-
if (directive === "desktop_variant" && !UI_COLLECTION_PRESENTATIONS.has(value || "")) {
|
|
3042
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid desktop_variant '${value}'`, entry.loc);
|
|
3043
|
-
}
|
|
3044
|
-
if (directive === "mobile_variant" && !UI_COLLECTION_PRESENTATIONS.has(value || "")) {
|
|
3045
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid mobile_variant '${value}'`, entry.loc);
|
|
3046
|
-
}
|
|
3047
|
-
if (directive === "collection" && !UI_COLLECTION_PRESENTATIONS.has(value || "")) {
|
|
3048
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid collection '${value}'`, entry.loc);
|
|
3049
|
-
}
|
|
3050
|
-
if (directive === "shell" && !["topbar", "sidebar", "dual_nav", "workspace", "wizard", "bottom_tabs", "split_view", "menu_bar"].includes(value || "")) {
|
|
3051
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid shell '${value}'`, entry.loc);
|
|
3052
|
-
}
|
|
3053
|
-
if (directive === "present" && !["page", "modal", "drawer", "sheet", "bottom_sheet", "popover"].includes(value || "")) {
|
|
3054
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid present '${value}'`, entry.loc);
|
|
3055
|
-
}
|
|
3056
|
-
if (directive === "breadcrumbs" && !["visible", "hidden"].includes(value || "")) {
|
|
3057
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid breadcrumbs '${value}'`, entry.loc);
|
|
3058
|
-
}
|
|
3059
|
-
if (directive === "state_style" && !["inline", "panel", "full_page"].includes(value || "")) {
|
|
3060
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for screen '${targetId}' has invalid state_style '${value}'`, entry.loc);
|
|
3061
|
-
}
|
|
3062
|
-
continue;
|
|
3063
|
-
}
|
|
3064
|
-
|
|
3065
|
-
if (targetKind === "action") {
|
|
3066
|
-
const capability = registry.get(targetId);
|
|
3067
|
-
if (!capability) {
|
|
3068
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} references missing capability '${targetId}'`, entry.loc);
|
|
3069
|
-
} else if (capability.kind !== "capability") {
|
|
3070
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} must reference a capability for action '${targetId}', found ${capability.kind} '${capability.id}'`, entry.loc);
|
|
3071
|
-
} else if (!realized.has(targetId)) {
|
|
3072
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} action '${targetId}' must also appear in 'realizes'`, entry.loc);
|
|
3073
|
-
}
|
|
3074
|
-
if (!["confirm", "present", "placement"].includes(directive || "")) {
|
|
3075
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for action '${targetId}' has unknown directive '${directive}'`, entry.loc);
|
|
3076
|
-
}
|
|
3077
|
-
if (directive === "confirm" && !["modal", "inline", "sheet", "bottom_sheet", "popover"].includes(value || "")) {
|
|
3078
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for action '${targetId}' has invalid confirm mode '${value}'`, entry.loc);
|
|
3079
|
-
}
|
|
3080
|
-
if (directive === "present" && !["button", "menu_item", "split_button", "bulk_action", "drawer", "sheet", "bottom_sheet", "fab", "popover"].includes(value || "")) {
|
|
3081
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for action '${targetId}' has invalid present mode '${value}'`, entry.loc);
|
|
3082
|
-
}
|
|
3083
|
-
if (directive === "placement" && !["toolbar", "menu", "bulk", "inline", "footer"].includes(value || "")) {
|
|
3084
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} for action '${targetId}' has invalid placement '${value}'`, entry.loc);
|
|
3085
|
-
}
|
|
3086
|
-
continue;
|
|
3087
|
-
}
|
|
3088
|
-
|
|
3089
|
-
pushError(errors, `Projection ${statement.id} ${surfaceBlockKey} entries must start with 'screen' or 'action'`, entry.loc);
|
|
3090
|
-
}
|
|
3091
|
-
}
|
|
3092
|
-
|
|
3093
|
-
function validateProjectionUiWeb(errors, statement, fieldMap, registry) {
|
|
3094
|
-
validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "web_hints", "web_surface");
|
|
3095
|
-
}
|
|
3096
|
-
|
|
3097
|
-
function validateProjectionUiIos(errors, statement, fieldMap, registry) {
|
|
3098
|
-
validateProjectionUiSurfaceHints(errors, statement, fieldMap, registry, "ios_hints", "ios_surface");
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
function validateProjectionGeneratorDefaults(errors, statement, fieldMap) {
|
|
3102
|
-
if (statement.kind !== "projection") {
|
|
3103
|
-
return;
|
|
3104
|
-
}
|
|
3105
|
-
|
|
3106
|
-
const generatorField = fieldMap.get("generator_defaults")?.[0];
|
|
3107
|
-
if (!generatorField || generatorField.value.type !== "block") {
|
|
3108
|
-
return;
|
|
3109
|
-
}
|
|
3110
|
-
|
|
3111
|
-
for (const entry of generatorField.value.entries) {
|
|
3112
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
3113
|
-
const [key, value] = tokens;
|
|
3114
|
-
if (!["profile", "language", "styling"].includes(key || "")) {
|
|
3115
|
-
pushError(errors, `Projection ${statement.id} generator_defaults has unknown key '${key}'`, entry.loc);
|
|
3116
|
-
continue;
|
|
3117
|
-
}
|
|
3118
|
-
if (!value) {
|
|
3119
|
-
pushError(errors, `Projection ${statement.id} generator_defaults is missing a value for '${key}'`, entry.loc);
|
|
3120
|
-
continue;
|
|
3121
|
-
}
|
|
3122
|
-
if (key === "profile" && !["vanilla", "sveltekit", "react", "swiftui", "postgres_sql", "sqlite_sql", "prisma", "drizzle"].includes(value)) {
|
|
3123
|
-
pushError(errors, `Projection ${statement.id} generator_defaults has unsupported profile '${value}'`, entry.loc);
|
|
3124
|
-
}
|
|
3125
|
-
if (key === "language" && !["typescript", "javascript", "swift", "sql"].includes(value)) {
|
|
3126
|
-
pushError(errors, `Projection ${statement.id} generator_defaults has unsupported language '${value}'`, entry.loc);
|
|
3127
|
-
}
|
|
3128
|
-
if (key === "styling" && !["tailwind", "css"].includes(value)) {
|
|
3129
|
-
pushError(errors, `Projection ${statement.id} generator_defaults has unsupported styling '${value}'`, entry.loc);
|
|
3130
|
-
}
|
|
3131
|
-
}
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
function validateProjectionDbTables(errors, statement, fieldMap, registry) {
|
|
3135
|
-
if (statement.kind !== "projection") {
|
|
3136
|
-
return;
|
|
3137
|
-
}
|
|
3138
|
-
|
|
3139
|
-
const dbTablesField = fieldMap.get("tables")?.[0];
|
|
3140
|
-
if (!dbTablesField || dbTablesField.value.type !== "block") {
|
|
3141
|
-
return;
|
|
3142
|
-
}
|
|
3143
|
-
|
|
3144
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
3145
|
-
const seenTables = new Set();
|
|
3146
|
-
for (const entry of dbTablesField.value.entries) {
|
|
3147
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
3148
|
-
const [entityId, tableKeyword, tableName] = tokens;
|
|
3149
|
-
const entity = registry.get(entityId);
|
|
3150
|
-
|
|
3151
|
-
if (!entity) {
|
|
3152
|
-
pushError(errors, `Projection ${statement.id} tables references missing entity '${entityId}'`, entry.loc);
|
|
3153
|
-
continue;
|
|
3154
|
-
}
|
|
3155
|
-
if (entity.kind !== "entity") {
|
|
3156
|
-
pushError(errors, `Projection ${statement.id} tables must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3157
|
-
}
|
|
3158
|
-
if (!realized.has(entityId)) {
|
|
3159
|
-
pushError(errors, `Projection ${statement.id} tables entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3160
|
-
}
|
|
3161
|
-
if (tableKeyword !== "table") {
|
|
3162
|
-
pushError(errors, `Projection ${statement.id} tables for '${entityId}' must use 'table'`, entry.loc);
|
|
3163
|
-
}
|
|
3164
|
-
if (!tableName) {
|
|
3165
|
-
pushError(errors, `Projection ${statement.id} tables for '${entityId}' must include a table name`, entry.loc);
|
|
3166
|
-
} else if (seenTables.has(tableName)) {
|
|
3167
|
-
pushError(errors, `Projection ${statement.id} tables has duplicate table name '${tableName}'`, entry.loc);
|
|
3168
|
-
}
|
|
3169
|
-
seenTables.add(tableName);
|
|
3170
|
-
}
|
|
3171
|
-
}
|
|
3172
|
-
|
|
3173
|
-
function validateProjectionDbColumns(errors, statement, fieldMap, registry) {
|
|
3174
|
-
if (statement.kind !== "projection") {
|
|
3175
|
-
return;
|
|
3176
|
-
}
|
|
3177
|
-
|
|
3178
|
-
const dbColumnsField = fieldMap.get("columns")?.[0];
|
|
3179
|
-
if (!dbColumnsField || dbColumnsField.value.type !== "block") {
|
|
3180
|
-
return;
|
|
3181
|
-
}
|
|
3182
|
-
|
|
3183
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
3184
|
-
for (const entry of dbColumnsField.value.entries) {
|
|
3185
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
3186
|
-
const [entityId, fieldKeyword, fieldName, columnKeyword, columnName] = tokens;
|
|
3187
|
-
const entity = registry.get(entityId);
|
|
3188
|
-
|
|
3189
|
-
if (!entity) {
|
|
3190
|
-
pushError(errors, `Projection ${statement.id} columns references missing entity '${entityId}'`, entry.loc);
|
|
3191
|
-
continue;
|
|
3192
|
-
}
|
|
3193
|
-
if (entity.kind !== "entity") {
|
|
3194
|
-
pushError(errors, `Projection ${statement.id} columns must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3195
|
-
}
|
|
3196
|
-
if (!realized.has(entityId)) {
|
|
3197
|
-
pushError(errors, `Projection ${statement.id} columns entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3198
|
-
}
|
|
3199
|
-
if (fieldKeyword !== "field") {
|
|
3200
|
-
pushError(errors, `Projection ${statement.id} columns for '${entityId}' must use 'field'`, entry.loc);
|
|
3201
|
-
}
|
|
3202
|
-
if (columnKeyword !== "column") {
|
|
3203
|
-
pushError(errors, `Projection ${statement.id} columns for '${entityId}' must use 'column'`, entry.loc);
|
|
3204
|
-
}
|
|
3205
|
-
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3206
|
-
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3207
|
-
pushError(errors, `Projection ${statement.id} columns references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3208
|
-
}
|
|
3209
|
-
if (!columnName) {
|
|
3210
|
-
pushError(errors, `Projection ${statement.id} columns for '${entityId}.${fieldName}' must include a column name`, entry.loc);
|
|
3211
|
-
}
|
|
3212
|
-
}
|
|
3213
|
-
}
|
|
3214
|
-
|
|
3215
|
-
function validateProjectionDbKeys(errors, statement, fieldMap, registry) {
|
|
3216
|
-
if (statement.kind !== "projection") {
|
|
3217
|
-
return;
|
|
3218
|
-
}
|
|
3219
|
-
|
|
3220
|
-
const dbKeysField = fieldMap.get("keys")?.[0];
|
|
3221
|
-
if (!dbKeysField || dbKeysField.value.type !== "block") {
|
|
3222
|
-
return;
|
|
3223
|
-
}
|
|
3224
|
-
|
|
3225
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
3226
|
-
for (const entry of dbKeysField.value.entries) {
|
|
3227
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
3228
|
-
const [entityId, keyType] = tokens;
|
|
3229
|
-
const entity = registry.get(entityId);
|
|
3230
|
-
|
|
3231
|
-
if (!entity) {
|
|
3232
|
-
pushError(errors, `Projection ${statement.id} keys references missing entity '${entityId}'`, entry.loc);
|
|
3233
|
-
continue;
|
|
3234
|
-
}
|
|
3235
|
-
if (entity.kind !== "entity") {
|
|
3236
|
-
pushError(errors, `Projection ${statement.id} keys must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3237
|
-
}
|
|
3238
|
-
if (!realized.has(entityId)) {
|
|
3239
|
-
pushError(errors, `Projection ${statement.id} keys entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3240
|
-
}
|
|
3241
|
-
if (!["primary", "unique"].includes(keyType || "")) {
|
|
3242
|
-
pushError(errors, `Projection ${statement.id} keys for '${entityId}' has invalid key type '${keyType}'`, entry.loc);
|
|
3243
|
-
}
|
|
3244
|
-
const fieldList = entry.items[2];
|
|
3245
|
-
if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
|
|
3246
|
-
pushError(errors, `Projection ${statement.id} keys for '${entityId}' must include a non-empty field list`, entry.loc);
|
|
3247
|
-
continue;
|
|
3248
|
-
}
|
|
3249
|
-
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3250
|
-
for (const item of fieldList.items) {
|
|
3251
|
-
if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
|
|
3252
|
-
pushError(errors, `Projection ${statement.id} keys references unknown field '${item.value}' on ${entityId}`, item.loc);
|
|
3253
|
-
}
|
|
3254
|
-
}
|
|
3255
|
-
}
|
|
3256
|
-
}
|
|
3257
|
-
|
|
3258
|
-
function validateProjectionDbIndexes(errors, statement, fieldMap, registry) {
|
|
3259
|
-
if (statement.kind !== "projection") {
|
|
3260
|
-
return;
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
|
-
const dbIndexesField = fieldMap.get("indexes")?.[0];
|
|
3264
|
-
if (!dbIndexesField || dbIndexesField.value.type !== "block") {
|
|
3265
|
-
return;
|
|
3266
|
-
}
|
|
3267
|
-
|
|
3268
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
3269
|
-
for (const entry of dbIndexesField.value.entries) {
|
|
3270
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
3271
|
-
const [entityId, indexType] = tokens;
|
|
3272
|
-
const entity = registry.get(entityId);
|
|
3273
|
-
|
|
3274
|
-
if (!entity) {
|
|
3275
|
-
pushError(errors, `Projection ${statement.id} indexes references missing entity '${entityId}'`, entry.loc);
|
|
3276
|
-
continue;
|
|
3277
|
-
}
|
|
3278
|
-
if (entity.kind !== "entity") {
|
|
3279
|
-
pushError(errors, `Projection ${statement.id} indexes must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3280
|
-
}
|
|
3281
|
-
if (!realized.has(entityId)) {
|
|
3282
|
-
pushError(errors, `Projection ${statement.id} indexes entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3283
|
-
}
|
|
3284
|
-
if (!["index", "unique"].includes(indexType || "")) {
|
|
3285
|
-
pushError(errors, `Projection ${statement.id} indexes for '${entityId}' has invalid index type '${indexType}'`, entry.loc);
|
|
3286
|
-
}
|
|
3287
|
-
const fieldList = entry.items[2];
|
|
3288
|
-
if (!fieldList || fieldList.type !== "list" || fieldList.items.length === 0) {
|
|
3289
|
-
pushError(errors, `Projection ${statement.id} indexes for '${entityId}' must include a non-empty field list`, entry.loc);
|
|
3290
|
-
continue;
|
|
3291
|
-
}
|
|
3292
|
-
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3293
|
-
for (const item of fieldList.items) {
|
|
3294
|
-
if (item.type === "symbol" && entityFieldNames.size > 0 && !entityFieldNames.has(item.value)) {
|
|
3295
|
-
pushError(errors, `Projection ${statement.id} indexes references unknown field '${item.value}' on ${entityId}`, item.loc);
|
|
3296
|
-
}
|
|
3297
|
-
}
|
|
3298
|
-
}
|
|
3299
|
-
}
|
|
3300
|
-
|
|
3301
|
-
function validateProjectionDbRelations(errors, statement, fieldMap, registry) {
|
|
3302
|
-
if (statement.kind !== "projection") {
|
|
3303
|
-
return;
|
|
3304
|
-
}
|
|
3305
|
-
|
|
3306
|
-
const dbRelationsField = fieldMap.get("relations")?.[0];
|
|
3307
|
-
if (!dbRelationsField || dbRelationsField.value.type !== "block") {
|
|
3308
|
-
return;
|
|
3309
|
-
}
|
|
3310
|
-
|
|
3311
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
3312
|
-
for (const entry of dbRelationsField.value.entries) {
|
|
3313
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
3314
|
-
const [entityId, relationType, fieldName, referencesKeyword, targetRef, onDeleteKeyword, onDeleteValue] = tokens;
|
|
3315
|
-
const entity = registry.get(entityId);
|
|
3316
|
-
|
|
3317
|
-
if (!entity) {
|
|
3318
|
-
pushError(errors, `Projection ${statement.id} relations references missing entity '${entityId}'`, entry.loc);
|
|
3319
|
-
continue;
|
|
3320
|
-
}
|
|
3321
|
-
if (entity.kind !== "entity") {
|
|
3322
|
-
pushError(errors, `Projection ${statement.id} relations must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3323
|
-
}
|
|
3324
|
-
if (!realized.has(entityId)) {
|
|
3325
|
-
pushError(errors, `Projection ${statement.id} relations entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3326
|
-
}
|
|
3327
|
-
if (relationType !== "foreign_key") {
|
|
3328
|
-
pushError(errors, `Projection ${statement.id} relations for '${entityId}' must use 'foreign_key'`, entry.loc);
|
|
3329
|
-
}
|
|
3330
|
-
if (referencesKeyword !== "references") {
|
|
3331
|
-
pushError(errors, `Projection ${statement.id} relations for '${entityId}' must use 'references'`, entry.loc);
|
|
3332
|
-
}
|
|
3333
|
-
if (onDeleteKeyword && onDeleteKeyword !== "on_delete") {
|
|
3334
|
-
pushError(errors, `Projection ${statement.id} relations for '${entityId}' has unexpected token '${onDeleteKeyword}'`, entry.loc);
|
|
3335
|
-
}
|
|
3336
|
-
if (onDeleteValue && !["cascade", "restrict", "set_null", "no_action"].includes(onDeleteValue)) {
|
|
3337
|
-
pushError(errors, `Projection ${statement.id} relations for '${entityId}' has invalid on_delete '${onDeleteValue}'`, entry.loc);
|
|
3338
|
-
}
|
|
3339
|
-
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3340
|
-
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3341
|
-
pushError(errors, `Projection ${statement.id} relations references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3342
|
-
}
|
|
3343
|
-
const [targetEntityId, targetFieldName] = (targetRef || "").split(".");
|
|
3344
|
-
const targetEntity = registry.get(targetEntityId);
|
|
3345
|
-
if (!targetEntity) {
|
|
3346
|
-
pushError(errors, `Projection ${statement.id} relations references missing target entity '${targetEntityId}'`, entry.loc);
|
|
3347
|
-
continue;
|
|
3348
|
-
}
|
|
3349
|
-
if (targetEntity.kind !== "entity") {
|
|
3350
|
-
pushError(errors, `Projection ${statement.id} relations must reference an entity target, found ${targetEntity.kind} '${targetEntity.id}'`, entry.loc);
|
|
3351
|
-
}
|
|
3352
|
-
const targetFieldNames = new Set(statementFieldNames(targetEntity));
|
|
3353
|
-
if (targetFieldName && targetFieldNames.size > 0 && !targetFieldNames.has(targetFieldName)) {
|
|
3354
|
-
pushError(errors, `Projection ${statement.id} relations references unknown target field '${targetFieldName}' on ${targetEntityId}`, entry.loc);
|
|
3355
|
-
}
|
|
3356
|
-
}
|
|
3357
|
-
}
|
|
3358
|
-
|
|
3359
|
-
function validateProjectionDbLifecycle(errors, statement, fieldMap, registry) {
|
|
3360
|
-
if (statement.kind !== "projection") {
|
|
3361
|
-
return;
|
|
3362
|
-
}
|
|
3363
|
-
|
|
3364
|
-
const dbLifecycleField = fieldMap.get("lifecycle")?.[0];
|
|
3365
|
-
if (!dbLifecycleField || dbLifecycleField.value.type !== "block") {
|
|
3366
|
-
return;
|
|
3367
|
-
}
|
|
3368
|
-
|
|
3369
|
-
const realized = new Set(symbolValues(getFieldValue(statement, "realizes")));
|
|
3370
|
-
for (const entry of dbLifecycleField.value.entries) {
|
|
3371
|
-
const tokens = blockSymbolItems(entry).map((item) => item.value);
|
|
3372
|
-
const [entityId, lifecycleType] = tokens;
|
|
3373
|
-
const entity = registry.get(entityId);
|
|
3374
|
-
|
|
3375
|
-
if (!entity) {
|
|
3376
|
-
pushError(errors, `Projection ${statement.id} lifecycle references missing entity '${entityId}'`, entry.loc);
|
|
3377
|
-
continue;
|
|
3378
|
-
}
|
|
3379
|
-
if (entity.kind !== "entity") {
|
|
3380
|
-
pushError(errors, `Projection ${statement.id} lifecycle must target an entity, found ${entity.kind} '${entity.id}'`, entry.loc);
|
|
3381
|
-
}
|
|
3382
|
-
if (!realized.has(entityId)) {
|
|
3383
|
-
pushError(errors, `Projection ${statement.id} lifecycle entity '${entityId}' must also appear in 'realizes'`, entry.loc);
|
|
3384
|
-
}
|
|
3385
|
-
|
|
3386
|
-
const directives = parseUiDirectiveMap(tokens, 2, errors, statement, entry, `lifecycle for '${entityId}'`);
|
|
3387
|
-
if (!["soft_delete", "timestamps"].includes(lifecycleType || "")) {
|
|
3388
|
-
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' has invalid lifecycle '${lifecycleType}'`, entry.loc);
|
|
3389
|
-
continue;
|
|
3390
|
-
}
|
|
3391
|
-
|
|
3392
|
-
const entityFieldNames = new Set(statementFieldNames(entity));
|
|
3393
|
-
if (lifecycleType === "soft_delete") {
|
|
3394
|
-
for (const requiredKey of ["field", "value"]) {
|
|
3395
|
-
if (!directives.has(requiredKey)) {
|
|
3396
|
-
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' must include '${requiredKey}' for soft_delete`, entry.loc);
|
|
3397
|
-
}
|
|
3398
|
-
}
|
|
3399
|
-
const fieldName = directives.get("field");
|
|
3400
|
-
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3401
|
-
pushError(errors, `Projection ${statement.id} lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3402
|
-
}
|
|
3403
|
-
}
|
|
3404
|
-
|
|
3405
|
-
if (lifecycleType === "timestamps") {
|
|
3406
|
-
for (const requiredKey of ["created_at", "updated_at"]) {
|
|
3407
|
-
if (!directives.has(requiredKey)) {
|
|
3408
|
-
pushError(errors, `Projection ${statement.id} lifecycle for '${entityId}' must include '${requiredKey}' for timestamps`, entry.loc);
|
|
3409
|
-
}
|
|
3410
|
-
const fieldName = directives.get(requiredKey);
|
|
3411
|
-
if (fieldName && entityFieldNames.size > 0 && !entityFieldNames.has(fieldName)) {
|
|
3412
|
-
pushError(errors, `Projection ${statement.id} lifecycle references unknown field '${fieldName}' on ${entityId}`, entry.loc);
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
}
|
|
3416
|
-
}
|
|
3417
|
-
}
|
|
3418
|
-
|
|
3419
|
-
export function buildRegistry(workspaceAst, errors) {
|
|
3420
|
-
const registry = new Map();
|
|
3421
|
-
|
|
3422
|
-
for (const file of workspaceAst.files) {
|
|
3423
|
-
for (const statement of file.statements) {
|
|
3424
|
-
if (!STATEMENT_KINDS.has(statement.kind)) {
|
|
3425
|
-
if (statement.kind === "component") {
|
|
3426
|
-
pushError(errors, `Statement kind ${renameDiagnostic("'component'", "'widget'", "widget widget_data_grid { ... }")}`, statement.loc);
|
|
3427
|
-
} else {
|
|
3428
|
-
pushError(errors, `Unknown statement kind '${statement.kind}'`, statement.loc);
|
|
3429
|
-
}
|
|
3430
|
-
}
|
|
3431
|
-
|
|
3432
|
-
if (!IDENTIFIER_PATTERN.test(statement.id)) {
|
|
3433
|
-
pushError(errors, `Invalid identifier '${statement.id}'`, statement.loc);
|
|
3434
|
-
}
|
|
3435
|
-
|
|
3436
|
-
if (registry.has(statement.id)) {
|
|
3437
|
-
pushError(errors, `Duplicate statement id '${statement.id}'`, statement.loc);
|
|
3438
|
-
continue;
|
|
3439
|
-
}
|
|
3440
|
-
|
|
3441
|
-
registry.set(statement.id, statement);
|
|
3442
|
-
}
|
|
3443
|
-
}
|
|
3444
|
-
|
|
3445
|
-
return registry;
|
|
3446
|
-
}
|
|
3447
|
-
|
|
3448
|
-
function validateDocs(workspaceAst, registry, errors) {
|
|
3449
|
-
const docs = workspaceAst.docs || [];
|
|
3450
|
-
const docRegistry = new Map();
|
|
3451
|
-
|
|
3452
|
-
for (const doc of docs) {
|
|
3453
|
-
if (doc.parseError) {
|
|
3454
|
-
pushError(errors, doc.parseError.message, doc.parseError.loc);
|
|
3455
|
-
continue;
|
|
3456
|
-
}
|
|
3457
|
-
|
|
3458
|
-
const { metadata } = doc;
|
|
3459
|
-
for (const required of ["id", "kind", "title", "status"]) {
|
|
3460
|
-
if (!metadata[required]) {
|
|
3461
|
-
pushError(errors, `Missing required doc metadata '${required}'`, doc.loc);
|
|
3462
|
-
}
|
|
3463
|
-
}
|
|
3464
|
-
|
|
3465
|
-
if (metadata.id && !IDENTIFIER_PATTERN.test(metadata.id)) {
|
|
3466
|
-
pushError(errors, `Invalid doc identifier '${metadata.id}'`, doc.loc);
|
|
3467
|
-
}
|
|
3468
|
-
|
|
3469
|
-
if (metadata.kind && !DOC_KINDS.has(metadata.kind)) {
|
|
3470
|
-
pushError(errors, `Unsupported doc kind '${metadata.kind}'`, doc.loc);
|
|
3471
|
-
}
|
|
3472
|
-
|
|
3473
|
-
if (metadata.status && !DOC_STATUSES.has(metadata.status)) {
|
|
3474
|
-
pushError(errors, `Unsupported doc status '${metadata.status}'`, doc.loc);
|
|
3475
|
-
}
|
|
3476
|
-
|
|
3477
|
-
if (metadata.confidence && !DOC_CONFIDENCE.has(metadata.confidence)) {
|
|
3478
|
-
pushError(errors, `Unsupported doc confidence '${metadata.confidence}'`, doc.loc);
|
|
3479
|
-
}
|
|
3480
|
-
|
|
3481
|
-
if (metadata.review_required != null && typeof metadata.review_required !== "boolean") {
|
|
3482
|
-
pushError(errors, "Doc metadata 'review_required' must be a boolean", doc.loc);
|
|
3483
|
-
}
|
|
3484
|
-
|
|
3485
|
-
for (const key of DOC_ARRAY_FIELDS) {
|
|
3486
|
-
if (metadata[key] != null && !Array.isArray(metadata[key])) {
|
|
3487
|
-
pushError(errors, `Doc metadata '${key}' must be a list`, doc.loc);
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
|
|
3491
|
-
if (metadata.id) {
|
|
3492
|
-
if (docRegistry.has(metadata.id)) {
|
|
3493
|
-
pushError(errors, `Duplicate doc id '${metadata.id}'`, doc.loc);
|
|
3494
|
-
} else {
|
|
3495
|
-
docRegistry.set(metadata.id, doc);
|
|
3496
|
-
}
|
|
3497
|
-
}
|
|
3498
|
-
}
|
|
3499
|
-
|
|
3500
|
-
for (const doc of docs) {
|
|
3501
|
-
if (doc.parseError) {
|
|
3502
|
-
continue;
|
|
3503
|
-
}
|
|
3504
|
-
const { metadata } = doc;
|
|
3505
|
-
|
|
3506
|
-
for (const entityId of metadata.related_entities || []) {
|
|
3507
|
-
const statement = registry.get(entityId);
|
|
3508
|
-
if (!statement || statement.kind !== "entity") {
|
|
3509
|
-
pushError(errors, `Doc '${metadata.id}' references missing entity '${entityId}'`, doc.loc);
|
|
3510
|
-
}
|
|
3511
|
-
}
|
|
3512
|
-
|
|
3513
|
-
for (const capabilityId of metadata.related_capabilities || []) {
|
|
3514
|
-
const statement = registry.get(capabilityId);
|
|
3515
|
-
if (!statement || statement.kind !== "capability") {
|
|
3516
|
-
pushError(errors, `Doc '${metadata.id}' references missing capability '${capabilityId}'`, doc.loc);
|
|
3517
|
-
}
|
|
3518
|
-
}
|
|
3519
|
-
|
|
3520
|
-
for (const actorId of metadata.related_actors || []) {
|
|
3521
|
-
const statement = registry.get(actorId);
|
|
3522
|
-
if (!statement || statement.kind !== "actor") {
|
|
3523
|
-
pushError(errors, `Doc '${metadata.id}' references missing actor '${actorId}'`, doc.loc);
|
|
3524
|
-
}
|
|
3525
|
-
}
|
|
3526
|
-
|
|
3527
|
-
for (const roleId of metadata.related_roles || []) {
|
|
3528
|
-
const statement = registry.get(roleId);
|
|
3529
|
-
if (!statement || statement.kind !== "role") {
|
|
3530
|
-
pushError(errors, `Doc '${metadata.id}' references missing role '${roleId}'`, doc.loc);
|
|
3531
|
-
}
|
|
3532
|
-
}
|
|
3533
|
-
|
|
3534
|
-
for (const ruleId of metadata.related_rules || []) {
|
|
3535
|
-
const statement = registry.get(ruleId);
|
|
3536
|
-
if (!statement || statement.kind !== "rule") {
|
|
3537
|
-
pushError(errors, `Doc '${metadata.id}' references missing rule '${ruleId}'`, doc.loc);
|
|
3538
|
-
}
|
|
3539
|
-
}
|
|
3540
|
-
|
|
3541
|
-
for (const workflowDocId of metadata.related_workflows || []) {
|
|
3542
|
-
const relatedDoc = docRegistry.get(workflowDocId);
|
|
3543
|
-
if (!relatedDoc || relatedDoc.metadata.kind !== "workflow") {
|
|
3544
|
-
pushError(errors, `Doc '${metadata.id}' references missing workflow doc '${workflowDocId}'`, doc.loc);
|
|
3545
|
-
}
|
|
3546
|
-
}
|
|
3547
|
-
|
|
3548
|
-
for (const decisionId of metadata.related_decisions || []) {
|
|
3549
|
-
const statement = registry.get(decisionId);
|
|
3550
|
-
if (!statement || statement.kind !== "decision") {
|
|
3551
|
-
pushError(errors, `Doc '${metadata.id}' references missing decision '${decisionId}'`, doc.loc);
|
|
3552
|
-
}
|
|
3553
|
-
}
|
|
3554
|
-
|
|
3555
|
-
for (const shapeId of metadata.related_shapes || []) {
|
|
3556
|
-
const statement = registry.get(shapeId);
|
|
3557
|
-
if (!statement || statement.kind !== "shape") {
|
|
3558
|
-
pushError(errors, `Doc '${metadata.id}' references missing shape '${shapeId}'`, doc.loc);
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
|
-
|
|
3562
|
-
for (const projectionId of metadata.related_projections || []) {
|
|
3563
|
-
const statement = registry.get(projectionId);
|
|
3564
|
-
if (!statement || statement.kind !== "projection") {
|
|
3565
|
-
pushError(errors, `Doc '${metadata.id}' references missing projection '${projectionId}'`, doc.loc);
|
|
3566
|
-
}
|
|
3567
|
-
}
|
|
3568
|
-
|
|
3569
|
-
for (const relatedDocId of metadata.related_docs || []) {
|
|
3570
|
-
if (!docRegistry.has(relatedDocId)) {
|
|
3571
|
-
pushError(errors, `Doc '${metadata.id}' references missing doc '${relatedDocId}'`, doc.loc);
|
|
3572
|
-
}
|
|
3573
|
-
}
|
|
3574
|
-
|
|
3575
|
-
for (const [fieldName, expectedKind] of Object.entries(DOC_REFERENCE_FIELDS)) {
|
|
3576
|
-
const value = metadata[fieldName];
|
|
3577
|
-
if (value == null) continue;
|
|
3578
|
-
if (typeof value !== "string") {
|
|
3579
|
-
pushError(errors, `Doc metadata '${fieldName}' must be a single id`, doc.loc);
|
|
3580
|
-
continue;
|
|
3581
|
-
}
|
|
3582
|
-
const target = registry.get(value);
|
|
3583
|
-
if (!target) {
|
|
3584
|
-
pushError(errors, `Doc '${metadata.id}' references missing ${expectedKind} '${value}'`, doc.loc);
|
|
3585
|
-
continue;
|
|
3586
|
-
}
|
|
3587
|
-
if (target.kind !== expectedKind) {
|
|
3588
|
-
pushError(
|
|
3589
|
-
errors,
|
|
3590
|
-
`Doc '${metadata.id}' ${fieldName} must reference a ${expectedKind}, found ${target.kind} '${target.id}'`,
|
|
3591
|
-
doc.loc
|
|
3592
|
-
);
|
|
3593
|
-
}
|
|
3594
|
-
}
|
|
3595
|
-
}
|
|
3596
|
-
}
|
|
66
|
+
export {
|
|
67
|
+
blockEntries,
|
|
68
|
+
collectFieldMap,
|
|
69
|
+
formatLoc,
|
|
70
|
+
getField,
|
|
71
|
+
getFieldValue,
|
|
72
|
+
pushError,
|
|
73
|
+
stringValue,
|
|
74
|
+
symbolValue,
|
|
75
|
+
symbolValues,
|
|
76
|
+
valueAsArray
|
|
77
|
+
} from "./utils.js";
|
|
78
|
+
|
|
79
|
+
export { buildRegistry } from "./registry.js";
|
|
3597
80
|
|
|
3598
81
|
export function validateWorkspace(workspaceAst) {
|
|
3599
82
|
const errors = [];
|
|
@@ -3603,49 +86,13 @@ export function validateWorkspace(workspaceAst) {
|
|
|
3603
86
|
for (const file of workspaceAst.files) {
|
|
3604
87
|
for (const statement of file.statements) {
|
|
3605
88
|
const fieldMap = collectFieldMap(statement);
|
|
3606
|
-
|
|
3607
|
-
validateProjectionTypeRenames(errors, statement, fieldMap);
|
|
3608
|
-
validateFieldShapes(errors, statement, fieldMap);
|
|
3609
|
-
validateStatus(errors, statement, fieldMap);
|
|
3610
|
-
validateRuleSeverity(errors, statement, fieldMap);
|
|
3611
|
-
validateVerification(errors, statement, fieldMap);
|
|
89
|
+
validateCoreStatement(errors, statement, fieldMap);
|
|
3612
90
|
validateShapeFrom(errors, statement, registry);
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
validateProjectionHttpFields(errors, statement, fieldMap, registry);
|
|
3619
|
-
validateProjectionHttpResponses(errors, statement, fieldMap, registry);
|
|
3620
|
-
validateProjectionHttpPreconditions(errors, statement, fieldMap, registry);
|
|
3621
|
-
validateProjectionHttpIdempotency(errors, statement, fieldMap, registry);
|
|
3622
|
-
validateProjectionHttpCache(errors, statement, fieldMap, registry);
|
|
3623
|
-
validateProjectionHttpDelete(errors, statement, fieldMap, registry);
|
|
3624
|
-
validateProjectionHttpAsync(errors, statement, fieldMap, registry);
|
|
3625
|
-
validateProjectionHttpStatus(errors, statement, fieldMap, registry);
|
|
3626
|
-
validateProjectionHttpDownload(errors, statement, fieldMap, registry);
|
|
3627
|
-
validateProjectionHttpAuthz(errors, statement, fieldMap, registry);
|
|
3628
|
-
validateProjectionHttpCallbacks(errors, statement, fieldMap, registry);
|
|
3629
|
-
validateProjectionUiOwnership(errors, statement, fieldMap);
|
|
3630
|
-
validateProjectionUiScreens(errors, statement, fieldMap, registry);
|
|
3631
|
-
validateProjectionUiCollections(errors, statement, fieldMap, registry);
|
|
3632
|
-
validateProjectionUiActions(errors, statement, fieldMap, registry);
|
|
3633
|
-
validateProjectionUiVisibility(errors, statement, fieldMap, registry);
|
|
3634
|
-
validateProjectionUiLookups(errors, statement, fieldMap, registry);
|
|
3635
|
-
validateProjectionUiRoutes(errors, statement, fieldMap, registry);
|
|
3636
|
-
validateProjectionUiAppShell(errors, statement, fieldMap);
|
|
3637
|
-
validateProjectionUiDesign(errors, statement, fieldMap);
|
|
3638
|
-
validateProjectionUiNavigation(errors, statement, fieldMap, registry);
|
|
3639
|
-
validateProjectionUiScreenRegions(errors, statement, fieldMap, registry);
|
|
3640
|
-
validateProjectionUiComponents(errors, statement, fieldMap, registry);
|
|
3641
|
-
validateProjectionUiWeb(errors, statement, fieldMap, registry);
|
|
3642
|
-
validateProjectionUiIos(errors, statement, fieldMap, registry);
|
|
3643
|
-
validateProjectionDbTables(errors, statement, fieldMap, registry);
|
|
3644
|
-
validateProjectionDbColumns(errors, statement, fieldMap, registry);
|
|
3645
|
-
validateProjectionDbKeys(errors, statement, fieldMap, registry);
|
|
3646
|
-
validateProjectionDbIndexes(errors, statement, fieldMap, registry);
|
|
3647
|
-
validateProjectionDbRelations(errors, statement, fieldMap, registry);
|
|
3648
|
-
validateProjectionDbLifecycle(errors, statement, fieldMap, registry);
|
|
91
|
+
validateReferenceRules(errors, statement, fieldMap, registry);
|
|
92
|
+
validateDataModelStatement(errors, statement, fieldMap, registry);
|
|
93
|
+
validateApiHttpProjection(errors, statement, fieldMap, registry);
|
|
94
|
+
validateUiProjection(errors, statement, fieldMap, registry);
|
|
95
|
+
validateDbProjection(errors, statement, fieldMap, registry);
|
|
3649
96
|
validateProjectionGeneratorDefaults(errors, statement, fieldMap);
|
|
3650
97
|
validateWidget(errors, statement, fieldMap, registry);
|
|
3651
98
|
validateDomain(errors, statement, fieldMap, registry);
|