@topogram/cli 0.3.51 → 0.3.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +4 -4
- package/CHANGELOG.md +11 -11
- package/package.json +1 -1
- package/src/adoption/plan.js +2 -2
- package/src/agent-ops/query-builders.js +42 -33
- package/src/cli.js +174 -129
- package/src/generator/adapters.d.ts +1 -0
- package/src/generator/adapters.js +64 -39
- package/src/generator/check.js +19 -12
- package/src/generator/context/diff.js +9 -9
- package/src/generator/context/domain-coverage.js +11 -10
- package/src/generator/context/domain-page.js +6 -6
- package/src/generator/context/shared.js +37 -21
- package/src/generator/context/slice.js +70 -65
- package/src/generator/index.js +12 -12
- package/src/generator/output.js +21 -20
- package/src/generator/registry.js +71 -49
- package/src/generator/runtime/app-bundle.js +15 -15
- package/src/generator/runtime/compile-check.js +7 -7
- package/src/generator/runtime/deployment.js +9 -9
- package/src/generator/runtime/environment.js +39 -39
- package/src/generator/runtime/runtime-check.js +5 -5
- package/src/generator/runtime/shared.js +40 -38
- package/src/generator/runtime/smoke.js +5 -5
- package/src/generator/surfaces/databases/contract.js +1 -1
- package/src/generator/surfaces/databases/lifecycle-shared.js +6 -5
- package/src/generator/surfaces/databases/postgres/drizzle.js +3 -2
- package/src/generator/surfaces/databases/postgres/prisma.js +3 -2
- package/src/generator/surfaces/databases/shared.js +3 -2
- package/src/generator/surfaces/databases/snapshot.js +1 -1
- package/src/generator/surfaces/databases/sqlite/prisma.js +3 -2
- package/src/generator/surfaces/native/swiftui-app.js +3 -3
- package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +1 -1
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +3 -3
- package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +3 -3
- package/src/generator/surfaces/services/persistence-wiring.js +3 -2
- package/src/generator/surfaces/services/server-contract.js +4 -4
- package/src/generator/surfaces/shared.js +2 -2
- package/src/generator/surfaces/web/design-intent.js +1 -1
- package/src/generator/surfaces/web/index.js +7 -7
- package/src/generator/surfaces/web/{react-components.js → react-widgets.js} +53 -53
- package/src/generator/surfaces/web/react.js +36 -36
- package/src/generator/surfaces/web/{sveltekit-components.js → sveltekit-widgets.js} +53 -53
- package/src/generator/surfaces/web/sveltekit.js +34 -34
- package/src/generator/surfaces/web/{ui-web-contract.js → ui-surface-contract.js} +8 -8
- package/src/generator/surfaces/web/vanilla.js +6 -6
- package/src/generator/{component-conformance.js → widget-conformance.js} +129 -128
- package/src/generator/widgets.js +40 -0
- package/src/generator-policy.js +10 -12
- package/src/import/core/runner.js +34 -34
- package/src/import/core/shared.js +1 -1
- package/src/import/extractors/ui/android-compose.js +1 -1
- package/src/import/extractors/ui/blazor.js +1 -1
- package/src/import/extractors/ui/razor-pages.js +1 -1
- package/src/import/extractors/ui/react-router.js +4 -4
- package/src/import/extractors/ui/sveltekit.js +4 -4
- package/src/import/extractors/ui/swiftui.js +1 -1
- package/src/import/extractors/ui/uikit.js +1 -1
- package/src/new-project.js +19 -18
- package/src/project-config.js +104 -44
- package/src/proofs/contract-audit.js +1 -1
- package/src/proofs/ios-parity.js +1 -1
- package/src/proofs/issues-parity.js +1 -1
- package/src/realization/backend/build-backend-runtime-realization.js +2 -2
- package/src/realization/ui/build-ui-shared-realization.js +33 -33
- package/src/realization/ui/build-web-realization.js +23 -20
- package/src/reconcile/journeys.js +1 -1
- package/src/resolver/index.js +148 -65
- package/src/validator/index.js +509 -423
- package/src/validator/kinds.js +36 -36
- package/src/validator/per-kind/{component.js → widget.js} +47 -47
- package/src/{component-behavior.js → widget-behavior.js} +3 -3
- package/src/workflows.js +39 -38
- package/template-helpers/react.js +4 -4
- package/template-helpers/sveltekit.js +4 -4
- package/src/generator/components.js +0 -39
- /package/src/resolver/enrich/{component.js → widget.js} +0 -0
package/src/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
|
|
|
@@ -103,6 +106,16 @@ function readJson(filePath) {
|
|
|
103
106
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
/**
|
|
110
|
+
* @param {string} oldName
|
|
111
|
+
* @param {string} newName
|
|
112
|
+
* @param {string} example
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function renameDiagnostic(oldName, newName, example) {
|
|
116
|
+
return `${oldName} was renamed to ${newName}. Example fix: ${example}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
106
119
|
/**
|
|
107
120
|
* @param {string} root
|
|
108
121
|
* @param {string} fileName
|
|
@@ -149,46 +162,46 @@ export function defaultProjectConfigForGraph(graph, implementation = null) {
|
|
|
149
162
|
const runtimeReference = implementation?.runtime?.reference || {};
|
|
150
163
|
/** @type {Array<Record<string, any>>} */
|
|
151
164
|
const projections = graph.byKind.projection || [];
|
|
152
|
-
const apiProjection = projections.find((projection) => (projection.http || []).length > 0);
|
|
165
|
+
const apiProjection = projections.find((projection) => (projection.http || []).length > 0 || projection.type === "api_contract");
|
|
153
166
|
const webProjection =
|
|
154
|
-
projections.find((projection) => projection.id === "
|
|
155
|
-
projections.find((projection) => projection.
|
|
167
|
+
projections.find((projection) => projection.id === "proj_web") ||
|
|
168
|
+
projections.find((projection) => projection.type === "web_surface");
|
|
156
169
|
const dbProjection =
|
|
157
170
|
projections.find((projection) => projection.id === runtimeReference.localDbProjectionId) ||
|
|
158
|
-
projections.find((projection) => projection.
|
|
159
|
-
projections.find((projection) => projection.platform === "db_sqlite");
|
|
171
|
+
projections.find((projection) => projection.type === "db_contract");
|
|
160
172
|
const ports = runtimeReference.ports || {};
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
const dbProfile = (dbProjection?.generatorDefaults || []).find((/** @type {Record<string, any>} */ entry) => entry.key === "profile")?.value;
|
|
174
|
+
const dbGenerator = dbProfile === "sqlite_sql" ? "topogram/sqlite" : "topogram/postgres";
|
|
175
|
+
const dbRuntimeId = dbProfile === "sqlite_sql" ? "app_sqlite" : "app_postgres";
|
|
176
|
+
/** @type {RuntimeTopologyRuntime[]} */
|
|
177
|
+
const runtimes = [
|
|
165
178
|
...(apiProjection
|
|
166
179
|
? [{
|
|
167
180
|
id: "app_api",
|
|
168
|
-
|
|
181
|
+
kind: /** @type {"api_service"} */ ("api_service"),
|
|
169
182
|
projection: apiProjection.id,
|
|
170
183
|
generator: { id: "topogram/hono", version: "1" },
|
|
171
184
|
port: ports.server || 3000,
|
|
172
|
-
...(dbProjection ? {
|
|
185
|
+
...(dbProjection ? { uses_database: dbRuntimeId } : {})
|
|
173
186
|
}]
|
|
174
187
|
: []),
|
|
175
188
|
...(webProjection
|
|
176
189
|
? [{
|
|
177
190
|
id: "app_sveltekit",
|
|
178
|
-
|
|
191
|
+
kind: /** @type {"web_surface"} */ ("web_surface"),
|
|
179
192
|
projection: webProjection.id,
|
|
180
193
|
generator: { id: "topogram/sveltekit", version: "1" },
|
|
181
194
|
port: ports.web || 5173,
|
|
182
|
-
...(apiProjection ? {
|
|
195
|
+
...(apiProjection ? { uses_api: "app_api" } : {})
|
|
183
196
|
}]
|
|
184
197
|
: []),
|
|
185
198
|
...(dbProjection
|
|
186
199
|
? [{
|
|
187
|
-
id:
|
|
188
|
-
|
|
200
|
+
id: dbRuntimeId,
|
|
201
|
+
kind: /** @type {"database"} */ ("database"),
|
|
189
202
|
projection: dbProjection.id,
|
|
190
203
|
generator: { id: dbGenerator, version: "1" },
|
|
191
|
-
port:
|
|
204
|
+
port: dbProfile === "sqlite_sql" ? null : 5432
|
|
192
205
|
}]
|
|
193
206
|
: [])
|
|
194
207
|
];
|
|
@@ -207,7 +220,41 @@ export function defaultProjectConfigForGraph(graph, implementation = null) {
|
|
|
207
220
|
}
|
|
208
221
|
},
|
|
209
222
|
topology: {
|
|
210
|
-
|
|
223
|
+
runtimes
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* @param {string} kind
|
|
230
|
+
* @returns {string}
|
|
231
|
+
*/
|
|
232
|
+
function legacyRuntimeType(kind) {
|
|
233
|
+
if (kind === "api_service") return "api";
|
|
234
|
+
if (kind === "web_surface") return "web";
|
|
235
|
+
if (kind === "ios_surface" || kind === "android_surface") return "native";
|
|
236
|
+
return kind;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @param {any} config
|
|
241
|
+
* @returns {any}
|
|
242
|
+
*/
|
|
243
|
+
function normalizeProjectConfigRuntimeAliases(config) {
|
|
244
|
+
if (!config?.topology || !Array.isArray(config.topology.runtimes)) {
|
|
245
|
+
return config;
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
...config,
|
|
249
|
+
topology: {
|
|
250
|
+
...config.topology,
|
|
251
|
+
__normalizedRuntimeAliases: true,
|
|
252
|
+
components: config.topology.runtimes.map((/** @type {RuntimeTopologyRuntime} */ runtime) => ({
|
|
253
|
+
...runtime,
|
|
254
|
+
type: legacyRuntimeType(runtime.kind),
|
|
255
|
+
api: runtime.uses_api,
|
|
256
|
+
database: runtime.uses_database
|
|
257
|
+
}))
|
|
211
258
|
}
|
|
212
259
|
};
|
|
213
260
|
}
|
|
@@ -223,6 +270,7 @@ export function loadProjectConfig(root) {
|
|
|
223
270
|
}
|
|
224
271
|
return {
|
|
225
272
|
...found,
|
|
273
|
+
config: normalizeProjectConfigRuntimeAliases(found.config),
|
|
226
274
|
compatibility: false
|
|
227
275
|
};
|
|
228
276
|
}
|
|
@@ -242,7 +290,7 @@ export function projectConfigOrDefault(root, graph = null, implementation = null
|
|
|
242
290
|
return null;
|
|
243
291
|
}
|
|
244
292
|
return {
|
|
245
|
-
config: defaultProjectConfigForGraph(graph, implementation),
|
|
293
|
+
config: normalizeProjectConfigRuntimeAliases(defaultProjectConfigForGraph(graph, implementation)),
|
|
246
294
|
configPath: null,
|
|
247
295
|
configDir: path.dirname(path.resolve(root)),
|
|
248
296
|
compatibility: true
|
|
@@ -298,7 +346,7 @@ function validateOutputConfig(errors, config) {
|
|
|
298
346
|
* @returns {string}
|
|
299
347
|
*/
|
|
300
348
|
function componentLabel(component) {
|
|
301
|
-
return component?.id ? `
|
|
349
|
+
return component?.id ? `Runtime '${component.id}'` : "Topology runtime";
|
|
302
350
|
}
|
|
303
351
|
|
|
304
352
|
/**
|
|
@@ -309,18 +357,27 @@ function componentLabel(component) {
|
|
|
309
357
|
*/
|
|
310
358
|
function validateComponentShape(errors, component, seenIds) {
|
|
311
359
|
if (!component || typeof component !== "object" || Array.isArray(component)) {
|
|
312
|
-
pushError(errors, "Topology
|
|
360
|
+
pushError(errors, "Topology runtime must be an object");
|
|
313
361
|
return false;
|
|
314
362
|
}
|
|
315
363
|
if (typeof component.id !== "string" || !IDENTIFIER_PATTERN.test(component.id)) {
|
|
316
364
|
pushError(errors, `${componentLabel(component)} id must match ${IDENTIFIER_PATTERN}`);
|
|
317
365
|
} else if (seenIds.has(component.id)) {
|
|
318
|
-
pushError(errors, `Duplicate topology
|
|
366
|
+
pushError(errors, `Duplicate topology runtime id '${component.id}'`);
|
|
319
367
|
} else {
|
|
320
368
|
seenIds.add(component.id);
|
|
321
369
|
}
|
|
322
|
-
if (
|
|
323
|
-
pushError(errors, `${componentLabel(component)} type
|
|
370
|
+
if (component.type != null) {
|
|
371
|
+
pushError(errors, `${componentLabel(component)} ${renameDiagnostic("'type'", "'kind'", `"kind": "api_service"`)}`);
|
|
372
|
+
}
|
|
373
|
+
if (component.database != null) {
|
|
374
|
+
pushError(errors, `${componentLabel(component)} ${renameDiagnostic("'database'", "'uses_database'", `"uses_database": "app_db"`)}`);
|
|
375
|
+
}
|
|
376
|
+
if (component.api != null) {
|
|
377
|
+
pushError(errors, `${componentLabel(component)} ${renameDiagnostic("'api'", "'uses_api'", `"uses_api": "app_api"`)}`);
|
|
378
|
+
}
|
|
379
|
+
if (!["api_service", "web_surface", "ios_surface", "android_surface", "database"].includes(component.kind)) {
|
|
380
|
+
pushError(errors, `${componentLabel(component)} kind must be api_service, web_surface, ios_surface, android_surface, or database`);
|
|
324
381
|
}
|
|
325
382
|
if (typeof component.projection !== "string" || component.projection.length === 0) {
|
|
326
383
|
pushError(errors, `${componentLabel(component)} projection must be a non-empty string`);
|
|
@@ -346,7 +403,7 @@ function validateComponentShape(errors, component, seenIds) {
|
|
|
346
403
|
|
|
347
404
|
/**
|
|
348
405
|
* @param {ValidationError[]} errors
|
|
349
|
-
* @param {
|
|
406
|
+
* @param {RuntimeTopologyRuntime} component
|
|
350
407
|
* @param {Map<string, Record<string, any>>} projections
|
|
351
408
|
* @param {{ configDir?: string|null, rootDir?: string|null }} [options]
|
|
352
409
|
* @returns {void}
|
|
@@ -377,14 +434,14 @@ function validateComponentCompatibility(errors, component, projections, options
|
|
|
377
434
|
if (manifest.version !== component.generator.version) {
|
|
378
435
|
pushError(errors, `${componentLabel(component)} for projection '${projection.id}' generator '${manifest.id}' version '${component.generator.version}' is unsupported; expected '${manifest.version}'`);
|
|
379
436
|
}
|
|
380
|
-
if (!isGeneratorCompatible(manifest, component.
|
|
381
|
-
pushError(errors, `${componentLabel(component)} for projection '${projection.id}' generator '${manifest.id}@${manifest.version}' is incompatible with
|
|
437
|
+
if (!isGeneratorCompatible(manifest, component.kind, projection)) {
|
|
438
|
+
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
439
|
}
|
|
383
440
|
}
|
|
384
441
|
|
|
385
442
|
/**
|
|
386
443
|
* @param {ValidationError[]} errors
|
|
387
|
-
* @param {
|
|
444
|
+
* @param {RuntimeTopologyRuntime[]} components
|
|
388
445
|
* @returns {void}
|
|
389
446
|
*/
|
|
390
447
|
function validateTopologyReferences(errors, components) {
|
|
@@ -399,14 +456,14 @@ function validateTopologyReferences(errors, components) {
|
|
|
399
456
|
usedPorts.set(component.port, component.id);
|
|
400
457
|
}
|
|
401
458
|
}
|
|
402
|
-
if (component.
|
|
403
|
-
if (component.
|
|
404
|
-
pushError(errors, `${componentLabel(component)} references missing database
|
|
459
|
+
if (component.kind === "api_service") {
|
|
460
|
+
if (component.uses_database && byId.get(component.uses_database)?.kind !== "database") {
|
|
461
|
+
pushError(errors, `${componentLabel(component)} references missing database runtime '${component.uses_database}'`);
|
|
405
462
|
}
|
|
406
463
|
}
|
|
407
|
-
if (
|
|
408
|
-
if (component.
|
|
409
|
-
pushError(errors, `${componentLabel(component)} references missing api
|
|
464
|
+
if (["web_surface", "ios_surface", "android_surface"].includes(component.kind)) {
|
|
465
|
+
if (component.uses_api && byId.get(component.uses_api)?.kind !== "api_service") {
|
|
466
|
+
pushError(errors, `${componentLabel(component)} references missing api runtime '${component.uses_api}'`);
|
|
410
467
|
}
|
|
411
468
|
}
|
|
412
469
|
}
|
|
@@ -428,23 +485,26 @@ export function validateProjectConfig(config, graph = null, options = {}) {
|
|
|
428
485
|
pushError(errors, "topogram.project.json version must be a non-empty string");
|
|
429
486
|
}
|
|
430
487
|
validateOutputConfig(errors, config);
|
|
431
|
-
if (
|
|
432
|
-
pushError(errors,
|
|
488
|
+
if (config.topology?.components != null && config.topology.__normalizedRuntimeAliases !== true) {
|
|
489
|
+
pushError(errors, `topogram.project.json ${renameDiagnostic("'topology.components'", "'topology.runtimes'", `"topology": { "runtimes": [] }`)}`);
|
|
490
|
+
}
|
|
491
|
+
if (!config.topology || typeof config.topology !== "object" || !Array.isArray(config.topology.runtimes)) {
|
|
492
|
+
pushError(errors, "topogram.project.json topology.runtimes must be an array");
|
|
433
493
|
} else {
|
|
434
494
|
const seenIds = new Set();
|
|
435
|
-
for (const component of config.topology.
|
|
495
|
+
for (const component of config.topology.runtimes) {
|
|
436
496
|
validateComponentShape(errors, component, seenIds);
|
|
437
497
|
}
|
|
438
|
-
const generatorPolicy = validateProjectGeneratorPolicy(config, options);
|
|
498
|
+
const generatorPolicy = validateProjectGeneratorPolicy(normalizeProjectConfigRuntimeAliases(config), options);
|
|
439
499
|
for (const error of generatorPolicy.errors) {
|
|
440
500
|
pushError(errors, error.message, error.loc);
|
|
441
501
|
}
|
|
442
502
|
if (graph) {
|
|
443
503
|
const projections = projectionById(graph);
|
|
444
|
-
for (const component of config.topology.
|
|
504
|
+
for (const component of config.topology.runtimes) {
|
|
445
505
|
validateComponentCompatibility(errors, component, projections, options);
|
|
446
506
|
}
|
|
447
|
-
validateTopologyReferences(errors, config.topology.
|
|
507
|
+
validateTopologyReferences(errors, config.topology.runtimes);
|
|
448
508
|
}
|
|
449
509
|
}
|
|
450
510
|
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 || []));
|