@topogram/cli 0.3.51 → 0.3.52
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +4 -4
- package/CHANGELOG.md +11 -11
- package/package.json +1 -1
- package/src/adoption/plan.js +2 -2
- package/src/agent-ops/query-builders.js +42 -33
- package/src/cli.js +174 -129
- package/src/generator/adapters.d.ts +1 -0
- package/src/generator/adapters.js +64 -39
- package/src/generator/check.js +19 -12
- package/src/generator/context/diff.js +9 -9
- package/src/generator/context/domain-coverage.js +11 -10
- package/src/generator/context/domain-page.js +6 -6
- package/src/generator/context/shared.js +37 -21
- package/src/generator/context/slice.js +70 -65
- package/src/generator/index.js +12 -12
- package/src/generator/output.js +21 -20
- package/src/generator/registry.js +61 -49
- package/src/generator/runtime/app-bundle.js +15 -15
- package/src/generator/runtime/compile-check.js +7 -7
- package/src/generator/runtime/deployment.js +9 -9
- package/src/generator/runtime/environment.js +39 -39
- package/src/generator/runtime/runtime-check.js +5 -5
- package/src/generator/runtime/shared.js +40 -38
- package/src/generator/runtime/smoke.js +5 -5
- package/src/generator/surfaces/databases/contract.js +1 -1
- package/src/generator/surfaces/databases/lifecycle-shared.js +6 -5
- package/src/generator/surfaces/databases/postgres/drizzle.js +3 -2
- package/src/generator/surfaces/databases/postgres/prisma.js +3 -2
- package/src/generator/surfaces/databases/shared.js +3 -2
- package/src/generator/surfaces/databases/snapshot.js +1 -1
- package/src/generator/surfaces/databases/sqlite/prisma.js +3 -2
- package/src/generator/surfaces/native/swiftui-app.js +3 -3
- package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +1 -1
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +3 -3
- package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +3 -3
- package/src/generator/surfaces/services/persistence-wiring.js +3 -2
- package/src/generator/surfaces/services/server-contract.js +4 -4
- package/src/generator/surfaces/shared.js +2 -2
- package/src/generator/surfaces/web/design-intent.js +1 -1
- package/src/generator/surfaces/web/index.js +7 -7
- package/src/generator/surfaces/web/{react-components.js → react-widgets.js} +53 -53
- package/src/generator/surfaces/web/react.js +36 -36
- package/src/generator/surfaces/web/{sveltekit-components.js → sveltekit-widgets.js} +53 -53
- package/src/generator/surfaces/web/sveltekit.js +34 -34
- package/src/generator/surfaces/web/{ui-web-contract.js → ui-surface-contract.js} +8 -8
- package/src/generator/surfaces/web/vanilla.js +6 -6
- package/src/generator/{component-conformance.js → widget-conformance.js} +129 -128
- package/src/generator/widgets.js +40 -0
- package/src/generator-policy.js +10 -12
- package/src/import/core/runner.js +34 -34
- package/src/import/core/shared.js +1 -1
- package/src/import/extractors/ui/android-compose.js +1 -1
- package/src/import/extractors/ui/blazor.js +1 -1
- package/src/import/extractors/ui/razor-pages.js +1 -1
- package/src/import/extractors/ui/react-router.js +4 -4
- package/src/import/extractors/ui/sveltekit.js +4 -4
- package/src/import/extractors/ui/swiftui.js +1 -1
- package/src/import/extractors/ui/uikit.js +1 -1
- package/src/new-project.js +19 -18
- package/src/project-config.js +92 -42
- package/src/proofs/contract-audit.js +1 -1
- package/src/proofs/ios-parity.js +1 -1
- package/src/proofs/issues-parity.js +1 -1
- package/src/realization/backend/build-backend-runtime-realization.js +2 -2
- package/src/realization/ui/build-ui-shared-realization.js +33 -33
- package/src/realization/ui/build-web-realization.js +23 -20
- package/src/reconcile/journeys.js +1 -1
- package/src/resolver/index.js +148 -65
- package/src/validator/index.js +473 -423
- package/src/validator/kinds.js +36 -36
- package/src/validator/per-kind/{component.js → widget.js} +47 -47
- package/src/{component-behavior.js → widget-behavior.js} +3 -3
- package/src/workflows.js +39 -38
- package/template-helpers/react.js +4 -4
- package/template-helpers/sveltekit.js +4 -4
- package/src/generator/components.js +0 -39
- /package/src/resolver/enrich/{component.js → widget.js} +0 -0
package/src/project-config.js
CHANGED
|
@@ -18,14 +18,17 @@ import { validateProjectGeneratorPolicy } from "./generator-policy.js";
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* @typedef {Object}
|
|
21
|
+
* @typedef {Object} RuntimeTopologyRuntime
|
|
22
22
|
* @property {string} id
|
|
23
|
-
* @property {"
|
|
23
|
+
* @property {"api_service"|"web_surface"|"ios_surface"|"android_surface"|"database"} kind
|
|
24
24
|
* @property {string} projection
|
|
25
25
|
* @property {GeneratorBinding} generator
|
|
26
26
|
* @property {number|null} [port]
|
|
27
|
-
* @property {string} [
|
|
28
|
-
* @property {string} [
|
|
27
|
+
* @property {string} [uses_api]
|
|
28
|
+
* @property {string} [uses_database]
|
|
29
|
+
* @property {string} [type] Migration diagnostic only.
|
|
30
|
+
* @property {string} [api] Migration diagnostic only.
|
|
31
|
+
* @property {string} [database] Migration diagnostic only.
|
|
29
32
|
* @property {Record<string, string>} [env]
|
|
30
33
|
*/
|
|
31
34
|
|
|
@@ -33,7 +36,7 @@ import { validateProjectGeneratorPolicy } from "./generator-policy.js";
|
|
|
33
36
|
* @typedef {Object} ProjectConfig
|
|
34
37
|
* @property {string} version
|
|
35
38
|
* @property {Record<string, { path: string, ownership: "generated"|"maintained" }>} outputs
|
|
36
|
-
* @property {{
|
|
39
|
+
* @property {{ runtimes: RuntimeTopologyRuntime[], components?: any[] }} topology
|
|
37
40
|
* @property {{ id?: string, module?: string, export?: string, implementation_module?: string, implementation_export?: string }} [implementation]
|
|
38
41
|
*/
|
|
39
42
|
|
|
@@ -149,46 +152,46 @@ export function defaultProjectConfigForGraph(graph, implementation = null) {
|
|
|
149
152
|
const runtimeReference = implementation?.runtime?.reference || {};
|
|
150
153
|
/** @type {Array<Record<string, any>>} */
|
|
151
154
|
const projections = graph.byKind.projection || [];
|
|
152
|
-
const apiProjection = projections.find((projection) => (projection.http || []).length > 0);
|
|
155
|
+
const apiProjection = projections.find((projection) => (projection.http || []).length > 0 || projection.type === "api_contract");
|
|
153
156
|
const webProjection =
|
|
154
|
-
projections.find((projection) => projection.id === "
|
|
155
|
-
projections.find((projection) => projection.
|
|
157
|
+
projections.find((projection) => projection.id === "proj_web") ||
|
|
158
|
+
projections.find((projection) => projection.type === "web_surface");
|
|
156
159
|
const dbProjection =
|
|
157
160
|
projections.find((projection) => projection.id === runtimeReference.localDbProjectionId) ||
|
|
158
|
-
projections.find((projection) => projection.
|
|
159
|
-
projections.find((projection) => projection.platform === "db_sqlite");
|
|
161
|
+
projections.find((projection) => projection.type === "db_contract");
|
|
160
162
|
const ports = runtimeReference.ports || {};
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const dbProfile = (dbProjection?.generatorDefaults || []).find((/** @type {Record<string, any>} */ entry) => entry.key === "profile")?.value;
|
|
164
|
+
const dbGenerator = dbProfile === "sqlite_sql" ? "topogram/sqlite" : "topogram/postgres";
|
|
165
|
+
const dbRuntimeId = dbProfile === "sqlite_sql" ? "app_sqlite" : "app_postgres";
|
|
166
|
+
/** @type {RuntimeTopologyRuntime[]} */
|
|
167
|
+
const runtimes = [
|
|
165
168
|
...(apiProjection
|
|
166
169
|
? [{
|
|
167
170
|
id: "app_api",
|
|
168
|
-
|
|
171
|
+
kind: /** @type {"api_service"} */ ("api_service"),
|
|
169
172
|
projection: apiProjection.id,
|
|
170
173
|
generator: { id: "topogram/hono", version: "1" },
|
|
171
174
|
port: ports.server || 3000,
|
|
172
|
-
...(dbProjection ? {
|
|
175
|
+
...(dbProjection ? { uses_database: dbRuntimeId } : {})
|
|
173
176
|
}]
|
|
174
177
|
: []),
|
|
175
178
|
...(webProjection
|
|
176
179
|
? [{
|
|
177
180
|
id: "app_sveltekit",
|
|
178
|
-
|
|
181
|
+
kind: /** @type {"web_surface"} */ ("web_surface"),
|
|
179
182
|
projection: webProjection.id,
|
|
180
183
|
generator: { id: "topogram/sveltekit", version: "1" },
|
|
181
184
|
port: ports.web || 5173,
|
|
182
|
-
...(apiProjection ? {
|
|
185
|
+
...(apiProjection ? { uses_api: "app_api" } : {})
|
|
183
186
|
}]
|
|
184
187
|
: []),
|
|
185
188
|
...(dbProjection
|
|
186
189
|
? [{
|
|
187
|
-
id:
|
|
188
|
-
|
|
190
|
+
id: dbRuntimeId,
|
|
191
|
+
kind: /** @type {"database"} */ ("database"),
|
|
189
192
|
projection: dbProjection.id,
|
|
190
193
|
generator: { id: dbGenerator, version: "1" },
|
|
191
|
-
port:
|
|
194
|
+
port: dbProfile === "sqlite_sql" ? null : 5432
|
|
192
195
|
}]
|
|
193
196
|
: [])
|
|
194
197
|
];
|
|
@@ -207,7 +210,41 @@ export function defaultProjectConfigForGraph(graph, implementation = null) {
|
|
|
207
210
|
}
|
|
208
211
|
},
|
|
209
212
|
topology: {
|
|
210
|
-
|
|
213
|
+
runtimes
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* @param {string} kind
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
function legacyRuntimeType(kind) {
|
|
223
|
+
if (kind === "api_service") return "api";
|
|
224
|
+
if (kind === "web_surface") return "web";
|
|
225
|
+
if (kind === "ios_surface" || kind === "android_surface") return "native";
|
|
226
|
+
return kind;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* @param {any} config
|
|
231
|
+
* @returns {any}
|
|
232
|
+
*/
|
|
233
|
+
function normalizeProjectConfigRuntimeAliases(config) {
|
|
234
|
+
if (!config?.topology || !Array.isArray(config.topology.runtimes)) {
|
|
235
|
+
return config;
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
...config,
|
|
239
|
+
topology: {
|
|
240
|
+
...config.topology,
|
|
241
|
+
__normalizedRuntimeAliases: true,
|
|
242
|
+
components: config.topology.runtimes.map((/** @type {RuntimeTopologyRuntime} */ runtime) => ({
|
|
243
|
+
...runtime,
|
|
244
|
+
type: legacyRuntimeType(runtime.kind),
|
|
245
|
+
api: runtime.uses_api,
|
|
246
|
+
database: runtime.uses_database
|
|
247
|
+
}))
|
|
211
248
|
}
|
|
212
249
|
};
|
|
213
250
|
}
|
|
@@ -223,6 +260,7 @@ export function loadProjectConfig(root) {
|
|
|
223
260
|
}
|
|
224
261
|
return {
|
|
225
262
|
...found,
|
|
263
|
+
config: normalizeProjectConfigRuntimeAliases(found.config),
|
|
226
264
|
compatibility: false
|
|
227
265
|
};
|
|
228
266
|
}
|
|
@@ -242,7 +280,7 @@ export function projectConfigOrDefault(root, graph = null, implementation = null
|
|
|
242
280
|
return null;
|
|
243
281
|
}
|
|
244
282
|
return {
|
|
245
|
-
config: defaultProjectConfigForGraph(graph, implementation),
|
|
283
|
+
config: normalizeProjectConfigRuntimeAliases(defaultProjectConfigForGraph(graph, implementation)),
|
|
246
284
|
configPath: null,
|
|
247
285
|
configDir: path.dirname(path.resolve(root)),
|
|
248
286
|
compatibility: true
|
|
@@ -298,7 +336,7 @@ function validateOutputConfig(errors, config) {
|
|
|
298
336
|
* @returns {string}
|
|
299
337
|
*/
|
|
300
338
|
function componentLabel(component) {
|
|
301
|
-
return component?.id ? `
|
|
339
|
+
return component?.id ? `Runtime '${component.id}'` : "Topology runtime";
|
|
302
340
|
}
|
|
303
341
|
|
|
304
342
|
/**
|
|
@@ -319,8 +357,11 @@ function validateComponentShape(errors, component, seenIds) {
|
|
|
319
357
|
} else {
|
|
320
358
|
seenIds.add(component.id);
|
|
321
359
|
}
|
|
322
|
-
if (
|
|
323
|
-
pushError(errors, `${componentLabel(component)} type
|
|
360
|
+
if (component.type != null) {
|
|
361
|
+
pushError(errors, `${componentLabel(component)} type was renamed to kind`);
|
|
362
|
+
}
|
|
363
|
+
if (!["api_service", "web_surface", "ios_surface", "android_surface", "database"].includes(component.kind)) {
|
|
364
|
+
pushError(errors, `${componentLabel(component)} kind must be api_service, web_surface, ios_surface, android_surface, or database`);
|
|
324
365
|
}
|
|
325
366
|
if (typeof component.projection !== "string" || component.projection.length === 0) {
|
|
326
367
|
pushError(errors, `${componentLabel(component)} projection must be a non-empty string`);
|
|
@@ -346,7 +387,7 @@ function validateComponentShape(errors, component, seenIds) {
|
|
|
346
387
|
|
|
347
388
|
/**
|
|
348
389
|
* @param {ValidationError[]} errors
|
|
349
|
-
* @param {
|
|
390
|
+
* @param {RuntimeTopologyRuntime} component
|
|
350
391
|
* @param {Map<string, Record<string, any>>} projections
|
|
351
392
|
* @param {{ configDir?: string|null, rootDir?: string|null }} [options]
|
|
352
393
|
* @returns {void}
|
|
@@ -377,14 +418,14 @@ function validateComponentCompatibility(errors, component, projections, options
|
|
|
377
418
|
if (manifest.version !== component.generator.version) {
|
|
378
419
|
pushError(errors, `${componentLabel(component)} for projection '${projection.id}' generator '${manifest.id}' version '${component.generator.version}' is unsupported; expected '${manifest.version}'`);
|
|
379
420
|
}
|
|
380
|
-
if (!isGeneratorCompatible(manifest, component.
|
|
381
|
-
pushError(errors, `${componentLabel(component)} for projection '${projection.id}' generator '${manifest.id}@${manifest.version}' is incompatible with
|
|
421
|
+
if (!isGeneratorCompatible(manifest, component.kind, projection)) {
|
|
422
|
+
pushError(errors, `${componentLabel(component)} for projection '${projection.id}' generator '${manifest.id}@${manifest.version}' is incompatible with runtime kind '${component.kind}' and projection type '${projection.type || "api_contract"}'`);
|
|
382
423
|
}
|
|
383
424
|
}
|
|
384
425
|
|
|
385
426
|
/**
|
|
386
427
|
* @param {ValidationError[]} errors
|
|
387
|
-
* @param {
|
|
428
|
+
* @param {RuntimeTopologyRuntime[]} components
|
|
388
429
|
* @returns {void}
|
|
389
430
|
*/
|
|
390
431
|
function validateTopologyReferences(errors, components) {
|
|
@@ -399,14 +440,20 @@ function validateTopologyReferences(errors, components) {
|
|
|
399
440
|
usedPorts.set(component.port, component.id);
|
|
400
441
|
}
|
|
401
442
|
}
|
|
402
|
-
if (component.
|
|
403
|
-
|
|
404
|
-
|
|
443
|
+
if (component.database != null) {
|
|
444
|
+
pushError(errors, `${componentLabel(component)} database was renamed to uses_database`);
|
|
445
|
+
}
|
|
446
|
+
if (component.api != null) {
|
|
447
|
+
pushError(errors, `${componentLabel(component)} api was renamed to uses_api`);
|
|
448
|
+
}
|
|
449
|
+
if (component.kind === "api_service") {
|
|
450
|
+
if (component.uses_database && byId.get(component.uses_database)?.kind !== "database") {
|
|
451
|
+
pushError(errors, `${componentLabel(component)} references missing database runtime '${component.uses_database}'`);
|
|
405
452
|
}
|
|
406
453
|
}
|
|
407
|
-
if (
|
|
408
|
-
if (component.
|
|
409
|
-
pushError(errors, `${componentLabel(component)} references missing api
|
|
454
|
+
if (["web_surface", "ios_surface", "android_surface"].includes(component.kind)) {
|
|
455
|
+
if (component.uses_api && byId.get(component.uses_api)?.kind !== "api_service") {
|
|
456
|
+
pushError(errors, `${componentLabel(component)} references missing api runtime '${component.uses_api}'`);
|
|
410
457
|
}
|
|
411
458
|
}
|
|
412
459
|
}
|
|
@@ -428,23 +475,26 @@ export function validateProjectConfig(config, graph = null, options = {}) {
|
|
|
428
475
|
pushError(errors, "topogram.project.json version must be a non-empty string");
|
|
429
476
|
}
|
|
430
477
|
validateOutputConfig(errors, config);
|
|
431
|
-
if (
|
|
432
|
-
pushError(errors, "topogram.project.json topology.components
|
|
478
|
+
if (config.topology?.components != null && config.topology.__normalizedRuntimeAliases !== true) {
|
|
479
|
+
pushError(errors, "topogram.project.json topology.components was renamed to topology.runtimes");
|
|
480
|
+
}
|
|
481
|
+
if (!config.topology || typeof config.topology !== "object" || !Array.isArray(config.topology.runtimes)) {
|
|
482
|
+
pushError(errors, "topogram.project.json topology.runtimes must be an array");
|
|
433
483
|
} else {
|
|
434
484
|
const seenIds = new Set();
|
|
435
|
-
for (const component of config.topology.
|
|
485
|
+
for (const component of config.topology.runtimes) {
|
|
436
486
|
validateComponentShape(errors, component, seenIds);
|
|
437
487
|
}
|
|
438
|
-
const generatorPolicy = validateProjectGeneratorPolicy(config, options);
|
|
488
|
+
const generatorPolicy = validateProjectGeneratorPolicy(normalizeProjectConfigRuntimeAliases(config), options);
|
|
439
489
|
for (const error of generatorPolicy.errors) {
|
|
440
490
|
pushError(errors, error.message, error.loc);
|
|
441
491
|
}
|
|
442
492
|
if (graph) {
|
|
443
493
|
const projections = projectionById(graph);
|
|
444
|
-
for (const component of config.topology.
|
|
494
|
+
for (const component of config.topology.runtimes) {
|
|
445
495
|
validateComponentCompatibility(errors, component, projections, options);
|
|
446
496
|
}
|
|
447
|
-
validateTopologyReferences(errors, config.topology.
|
|
497
|
+
validateTopologyReferences(errors, config.topology.runtimes);
|
|
448
498
|
}
|
|
449
499
|
}
|
|
450
500
|
return {
|
|
@@ -187,7 +187,7 @@ export function auditUiContractPair(leftContract, rightContract) {
|
|
|
187
187
|
const appShellParity = stableStringify(left.appShell) === stableStringify(right.appShell);
|
|
188
188
|
|
|
189
189
|
return {
|
|
190
|
-
seam: "
|
|
190
|
+
seam: "ui_surface_contract",
|
|
191
191
|
semanticParity: screenDiffs.length === 0 && navigationParity && appShellParity,
|
|
192
192
|
summary: {
|
|
193
193
|
screenCount: left.screens.length,
|
package/src/proofs/ios-parity.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stableStringify } from "../format.js";
|
|
2
2
|
import { normalizeScreens } from "./web-parity.js";
|
|
3
3
|
|
|
4
|
-
/** Semantic fingerprint of screens slice embedded in Swift ui-
|
|
4
|
+
/** Semantic fingerprint of screens slice embedded in Swift ui-surface-contract JSON (matches web normalization). */
|
|
5
5
|
export function fingerprintIosEmbeddedUiContract(contract) {
|
|
6
6
|
return stableStringify(normalizeScreens(contract));
|
|
7
7
|
}
|
|
@@ -2,7 +2,7 @@ import { buildBackendParityEvidence } from "./backend-parity.js";
|
|
|
2
2
|
import { buildWebParityEvidence } from "./web-parity.js";
|
|
3
3
|
|
|
4
4
|
export function buildIssuesParityEvidence(graph) {
|
|
5
|
-
const web = buildWebParityEvidence(graph, "
|
|
5
|
+
const web = buildWebParityEvidence(graph, "proj_web_surface__react", "proj_web_surface__sveltekit");
|
|
6
6
|
return {
|
|
7
7
|
web,
|
|
8
8
|
runtime: buildBackendParityEvidence(graph, "proj_api")
|
|
@@ -28,8 +28,8 @@ export function getDefaultBackendDbProjection(graph, options = {}) {
|
|
|
28
28
|
return (
|
|
29
29
|
explicit ||
|
|
30
30
|
preferred ||
|
|
31
|
-
candidates.find((projection) => projection.platform === "
|
|
32
|
-
candidates.find((projection) => projection.platform === "
|
|
31
|
+
candidates.find((projection) => projection.platform === "db_contract") ||
|
|
32
|
+
candidates.find((projection) => projection.platform === "db_contract") ||
|
|
33
33
|
candidates[0] ||
|
|
34
34
|
null
|
|
35
35
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getProjection, uiProjectionCandidates } from "../../generator/surfaces/shared.js";
|
|
2
|
-
import {
|
|
2
|
+
import { buildWidgetBehaviorRealizations } from "../../widget-behavior.js";
|
|
3
3
|
import { defaultPatternForScreen } from "../../ui/taxonomy.js";
|
|
4
4
|
|
|
5
5
|
function toBooleanFlag(value, fallback = false) {
|
|
@@ -48,25 +48,25 @@ function ownershipFieldByCapability(graph) {
|
|
|
48
48
|
return output;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function
|
|
52
|
-
return (graph.byKind.component || []).find((
|
|
51
|
+
function widgetById(graph, widgetId) {
|
|
52
|
+
return (graph.byKind.widget || graph.byKind.component || []).find((widget) => widget.id === widgetId) || null;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function
|
|
56
|
-
const
|
|
57
|
-
return
|
|
55
|
+
function widgetContractFor(graph, widgetId) {
|
|
56
|
+
const widget = widgetById(graph, widgetId);
|
|
57
|
+
return widget?.widgetContract || widget?.componentContract || null;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function
|
|
61
|
-
const
|
|
62
|
-
if (!
|
|
63
|
-
return { id:
|
|
60
|
+
function summarizeWidgetRef(graph, widgetId) {
|
|
61
|
+
const widget = widgetById(graph, widgetId);
|
|
62
|
+
if (!widget) {
|
|
63
|
+
return { id: widgetId, name: widgetId, category: null, version: null };
|
|
64
64
|
}
|
|
65
65
|
return {
|
|
66
|
-
id:
|
|
67
|
-
name:
|
|
68
|
-
category:
|
|
69
|
-
version:
|
|
66
|
+
id: widget.id,
|
|
67
|
+
name: widget.name || widget.id,
|
|
68
|
+
category: widget.category || null,
|
|
69
|
+
version: widget.version || null
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -118,16 +118,16 @@ function buildDesignIntentContract(projection) {
|
|
|
118
118
|
return design;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
export function
|
|
122
|
-
const
|
|
123
|
-
const contract =
|
|
121
|
+
export function buildWidgetUsageContract(graph, entry, options = {}) {
|
|
122
|
+
const widgetId = entry.widget?.id || entry.component?.id || null;
|
|
123
|
+
const contract = widgetId ? widgetContractFor(graph, widgetId) : null;
|
|
124
124
|
const region = options.region || null;
|
|
125
125
|
return {
|
|
126
|
-
type: "
|
|
126
|
+
type: "ui_widget_usage",
|
|
127
127
|
region: entry.region || null,
|
|
128
128
|
pattern: region?.pattern || null,
|
|
129
129
|
placement: region?.placement || null,
|
|
130
|
-
|
|
130
|
+
widget: widgetId ? summarizeWidgetRef(graph, widgetId) : null,
|
|
131
131
|
dataBindings: (entry.dataBindings || []).map((binding) => ({
|
|
132
132
|
prop: binding.prop || null,
|
|
133
133
|
source: binding.source || null
|
|
@@ -137,15 +137,15 @@ export function buildComponentUsageContract(graph, entry, options = {}) {
|
|
|
137
137
|
action: binding.action || null,
|
|
138
138
|
target: binding.target || null
|
|
139
139
|
})),
|
|
140
|
-
behaviorRealizations:
|
|
140
|
+
behaviorRealizations: buildWidgetBehaviorRealizations(contract, entry)
|
|
141
141
|
};
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
export function
|
|
144
|
+
export function buildWidgetContractMap(graph, widgetUsages) {
|
|
145
145
|
return Object.fromEntries(
|
|
146
|
-
[...new Set(
|
|
146
|
+
[...new Set(widgetUsages.map((entry) => entry.widget?.id || entry.component?.id).filter(Boolean))]
|
|
147
147
|
.sort()
|
|
148
|
-
.map((
|
|
148
|
+
.map((widgetId) => [widgetId, widgetContractFor(graph, widgetId)])
|
|
149
149
|
.filter(([, contract]) => contract)
|
|
150
150
|
);
|
|
151
151
|
}
|
|
@@ -202,7 +202,7 @@ function buildUiScreenContract(graph, projection, screen, ownershipFields) {
|
|
|
202
202
|
const actionEntries = (projection.uiActions || []).filter((entry) => entry.screenId === screen.id);
|
|
203
203
|
const lookupEntries = (projection.uiLookups || []).filter((entry) => entry.screenId === screen.id);
|
|
204
204
|
const regionEntries = (projection.uiScreenRegions || []).filter((entry) => entry.screenId === screen.id);
|
|
205
|
-
const
|
|
205
|
+
const widgetEntries = (projection.uiComponents || []).filter((entry) => entry.screenId === screen.id);
|
|
206
206
|
const screenActionIds = new Set(
|
|
207
207
|
[
|
|
208
208
|
screen.primaryAction?.id,
|
|
@@ -292,7 +292,7 @@ function buildUiScreenContract(graph, projection, screen, ownershipFields) {
|
|
|
292
292
|
state: entry.state || null,
|
|
293
293
|
variant: entry.variant || null
|
|
294
294
|
})),
|
|
295
|
-
|
|
295
|
+
widgets: widgetEntries.map((entry) => buildWidgetUsageContract(graph, entry, {
|
|
296
296
|
region: regionContractFor(regionEntries, entry.region)
|
|
297
297
|
})),
|
|
298
298
|
patterns: [...patterns]
|
|
@@ -305,18 +305,18 @@ export function buildUiSharedRealization(graph, options = {}) {
|
|
|
305
305
|
|
|
306
306
|
if (options.projectionId) {
|
|
307
307
|
const projection = projections[0];
|
|
308
|
-
const
|
|
308
|
+
const widgetUsages = projection.uiComponents || [];
|
|
309
309
|
const screens = (projection.uiScreens || []).map((screen) => buildUiScreenContract(graph, projection, screen, ownershipFields));
|
|
310
310
|
return {
|
|
311
311
|
projection: {
|
|
312
312
|
id: projection.id,
|
|
313
313
|
name: projection.name || projection.id,
|
|
314
|
-
|
|
314
|
+
type: projection.type || projection.platform
|
|
315
315
|
},
|
|
316
316
|
realizes: projection.realizes,
|
|
317
317
|
outputs: projection.outputs,
|
|
318
|
-
|
|
319
|
-
|
|
318
|
+
widgets: buildWidgetContractMap(graph, widgetUsages),
|
|
319
|
+
designTokens: buildDesignIntentContract(projection),
|
|
320
320
|
appShell: buildAppShellContract(projection),
|
|
321
321
|
navigation: buildNavigationContract(projection, screens),
|
|
322
322
|
screens
|
|
@@ -325,18 +325,18 @@ export function buildUiSharedRealization(graph, options = {}) {
|
|
|
325
325
|
|
|
326
326
|
const output = {};
|
|
327
327
|
for (const projection of projections) {
|
|
328
|
-
const
|
|
328
|
+
const widgetUsages = projection.uiComponents || [];
|
|
329
329
|
const screens = (projection.uiScreens || []).map((screen) => buildUiScreenContract(graph, projection, screen, ownershipFields));
|
|
330
330
|
output[projection.id] = {
|
|
331
331
|
projection: {
|
|
332
332
|
id: projection.id,
|
|
333
333
|
name: projection.name || projection.id,
|
|
334
|
-
|
|
334
|
+
type: projection.type || projection.platform
|
|
335
335
|
},
|
|
336
336
|
realizes: projection.realizes,
|
|
337
337
|
outputs: projection.outputs,
|
|
338
|
-
|
|
339
|
-
|
|
338
|
+
widgets: buildWidgetContractMap(graph, widgetUsages),
|
|
339
|
+
designTokens: buildDesignIntentContract(projection),
|
|
340
340
|
appShell: buildAppShellContract(projection),
|
|
341
341
|
navigation: buildNavigationContract(projection, screens),
|
|
342
342
|
screens
|
|
@@ -26,19 +26,20 @@ export function buildWebRealization(graph, options = {}) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const projection = getProjection(graph, options.projectionId);
|
|
29
|
+
const projectionType = projection.type || projection.platform;
|
|
29
30
|
const surfaceHints =
|
|
30
|
-
|
|
31
|
+
projectionType === "ios_surface" ? projection.uiIos || [] : projection.uiWeb || [];
|
|
31
32
|
const sharedProjection = sharedUiProjectionForWeb(graph, projection);
|
|
32
33
|
const sharedContract = sharedProjection
|
|
33
34
|
? buildUiSharedRealization(graph, { projectionId: sharedProjection.id })
|
|
34
35
|
: {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
projection: null,
|
|
37
|
+
realizes: [],
|
|
38
|
+
outputs: [],
|
|
39
|
+
widgets: {},
|
|
40
|
+
designTokens: null,
|
|
41
|
+
screens: []
|
|
42
|
+
};
|
|
42
43
|
const concreteContract = buildUiSharedRealization(graph, { projectionId: projection.id });
|
|
43
44
|
|
|
44
45
|
const routeMap = new Map((projection.uiRoutes || []).map((entry) => [entry.screenId, entry]));
|
|
@@ -59,17 +60,17 @@ export function buildWebRealization(graph, options = {}) {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
const screenMap = new Map((sharedContract.screens || []).map((screen) => [screen.id, { ...screen,
|
|
63
|
+
const screenMap = new Map((sharedContract.screens || []).map((screen) => [screen.id, { ...screen, widgets: [...(screen.widgets || [])] }]));
|
|
63
64
|
for (const screen of concreteContract.screens || []) {
|
|
64
65
|
if (!screenMap.has(screen.id)) {
|
|
65
|
-
screenMap.set(screen.id, { ...screen,
|
|
66
|
+
screenMap.set(screen.id, { ...screen, widgets: [...(screen.widgets || [])] });
|
|
66
67
|
continue;
|
|
67
68
|
}
|
|
68
69
|
const existing = screenMap.get(screen.id);
|
|
69
70
|
screenMap.set(screen.id, {
|
|
70
71
|
...existing,
|
|
71
72
|
...screen,
|
|
72
|
-
|
|
73
|
+
widgets: [...(existing.widgets || [])],
|
|
73
74
|
regions: mergeByKey(existing.regions || [], screen.regions || [], (entry) => entry.region),
|
|
74
75
|
patterns: [...new Set([...(existing.patterns || []), ...(screen.patterns || [])])]
|
|
75
76
|
});
|
|
@@ -77,24 +78,26 @@ export function buildWebRealization(graph, options = {}) {
|
|
|
77
78
|
|
|
78
79
|
const appShell = projection.uiAppShell?.length || !sharedProjection ? concreteContract.appShell : sharedContract.appShell;
|
|
79
80
|
const navigation = projection.uiNavigation?.length || !sharedProjection ? concreteContract.navigation : sharedContract.navigation;
|
|
80
|
-
const
|
|
81
|
+
const designTokens = projection.uiDesign?.length || !sharedProjection ? concreteContract.designTokens : sharedContract.designTokens;
|
|
81
82
|
|
|
82
83
|
const contract = {
|
|
84
|
+
type: "ui_surface_contract",
|
|
83
85
|
projection: {
|
|
84
86
|
id: projection.id,
|
|
85
87
|
name: projection.name || projection.id,
|
|
86
|
-
|
|
88
|
+
type: projection.type || projection.platform
|
|
87
89
|
},
|
|
88
|
-
|
|
90
|
+
uiContract: sharedProjection
|
|
89
91
|
? {
|
|
90
92
|
id: sharedProjection.id,
|
|
91
|
-
name: sharedProjection.name || sharedProjection.id
|
|
93
|
+
name: sharedProjection.name || sharedProjection.id,
|
|
94
|
+
type: sharedProjection.type || sharedProjection.platform
|
|
92
95
|
}
|
|
93
96
|
: null,
|
|
94
97
|
generatorDefaults: generatorDefaultsMap(projection),
|
|
95
98
|
outputs: projection.outputs,
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
widgets: sharedProjection ? (sharedContract.widgets || {}) : (concreteContract.widgets || {}),
|
|
100
|
+
designTokens: designTokens || null,
|
|
98
101
|
appShell: appShell || null,
|
|
99
102
|
navigation: {
|
|
100
103
|
groups: navigation?.groups || [],
|
|
@@ -108,8 +111,8 @@ export function buildWebRealization(graph, options = {}) {
|
|
|
108
111
|
screens: [...screenMap.values()].map((screen) => ({
|
|
109
112
|
...screen,
|
|
110
113
|
route: routeMap.get(screen.id)?.path || null,
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
surfaceHints: Object.fromEntries((uiWebByScreen.get(screen.id) || []).map((entry) => [entry.directive, entry.value])),
|
|
115
|
+
actionSurfaceHints: Object.fromEntries(
|
|
113
116
|
[...screen.actions.screen, screen.actions.primary, screen.actions.secondary, screen.actions.destructive, screen.actions.terminal]
|
|
114
117
|
.filter(Boolean)
|
|
115
118
|
.map((action) => {
|
|
@@ -134,7 +137,7 @@ export function buildWebRealization(graph, options = {}) {
|
|
|
134
137
|
apiContracts[capabilityId] = buildApiRealization(graph, { capabilityId });
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
const isNativeUi =
|
|
140
|
+
const isNativeUi = projectionType === "ios_surface";
|
|
138
141
|
|
|
139
142
|
return {
|
|
140
143
|
type: isNativeUi ? "native_ui_realization" : "web_app_realization",
|
|
@@ -40,7 +40,7 @@ function collectJourneyGenerationContext(graph) {
|
|
|
40
40
|
const rules = graph.byKind.rule || [];
|
|
41
41
|
const projections = graph.byKind.projection || [];
|
|
42
42
|
const uiSharedScreens = projections
|
|
43
|
-
.filter((projection) => projection.platform === "
|
|
43
|
+
.filter((projection) => projection.platform === "ui_contract")
|
|
44
44
|
.flatMap((projection) => (projection.uiScreens || []).map((screen) => ({ ...screen, projectionId: projection.id })));
|
|
45
45
|
const canonicalJourneys = (graph.docs || []).filter((doc) => doc.kind === "journey");
|
|
46
46
|
const coveredEntityIds = new Set(canonicalJourneys.flatMap((doc) => doc.relatedEntities || []));
|