@topogram/cli 0.3.37 → 0.3.39
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.js +2 -1
- package/src/cli/catalog-alias.js +3 -2
- package/src/cli.js +69 -5
- package/src/generator/component-conformance.js +66 -9
- package/src/generator/surfaces/web/react-components.js +22 -7
- package/src/generator/surfaces/web/react.js +32 -2
- package/src/generator/surfaces/web/sveltekit-components.js +22 -7
- package/src/generator/surfaces/web/sveltekit.js +32 -2
- package/src/new-project.js +18 -0
- package/src/realization/ui/build-ui-shared-realization.js +11 -2
- package/src/realization/ui/build-web-realization.js +3 -1
- package/src/workflows.js +70 -2
package/package.json
CHANGED
package/src/adoption/plan.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
reviewBoundaryForImportProposal
|
|
7
7
|
} from "../policy/review-boundaries.js";
|
|
8
8
|
|
|
9
|
-
const ADOPT_SELECTORS = new Set(["from-plan", "actors", "roles", "enums", "shapes", "entities", "capabilities", "docs", "journeys", "workflows", "verification", "ui"]);
|
|
9
|
+
const ADOPT_SELECTORS = new Set(["from-plan", "actors", "roles", "enums", "shapes", "entities", "capabilities", "components", "docs", "journeys", "workflows", "verification", "ui"]);
|
|
10
10
|
|
|
11
11
|
function stableSortedStrings(values) {
|
|
12
12
|
return [...new Set((values || []).filter(Boolean))].sort();
|
|
@@ -355,6 +355,7 @@ export function selectorMatchesItem(selector, item) {
|
|
|
355
355
|
if (selector === "shapes") return item.kind === "shape";
|
|
356
356
|
if (selector === "entities") return item.kind === "entity";
|
|
357
357
|
if (selector === "capabilities") return item.kind === "capability";
|
|
358
|
+
if (selector === "components") return item.kind === "component";
|
|
358
359
|
if (selector === "docs") return item.track === "docs";
|
|
359
360
|
if (selector === "journeys") return item.track === "docs" && String(item.canonical_rel_path || "").startsWith("docs/journeys/");
|
|
360
361
|
if (selector === "workflows") return item.track === "workflows" || item.kind === "decision";
|
package/src/cli/catalog-alias.js
CHANGED
|
@@ -21,7 +21,7 @@ function messageFromError(error) {
|
|
|
21
21
|
/**
|
|
22
22
|
* @param {string} templateName
|
|
23
23
|
* @param {string|null} [source]
|
|
24
|
-
* @returns {{ templateName: string, provenance: { id: string, source: string, package: string, version: string, packageSpec: string }|null }}
|
|
24
|
+
* @returns {{ templateName: string, provenance: { id: string, source: string, package: string, version: string, packageSpec: string, includesExecutableImplementation: boolean }|null }}
|
|
25
25
|
*/
|
|
26
26
|
export function resolveCatalogTemplateAlias(templateName, source = null) {
|
|
27
27
|
if (!isCatalogAliasCandidate(templateName)) {
|
|
@@ -47,7 +47,8 @@ export function resolveCatalogTemplateAlias(templateName, source = null) {
|
|
|
47
47
|
source: loaded.source,
|
|
48
48
|
package: entry.package,
|
|
49
49
|
version: entry.defaultVersion,
|
|
50
|
-
packageSpec
|
|
50
|
+
packageSpec,
|
|
51
|
+
includesExecutableImplementation: Boolean(entry.trust?.includesExecutableImplementation)
|
|
51
52
|
}
|
|
52
53
|
};
|
|
53
54
|
} catch (error) {
|
package/src/cli.js
CHANGED
|
@@ -134,6 +134,8 @@ const PACKAGE_UPDATE_CLI_CHECK_SCRIPTS = [
|
|
|
134
134
|
"pack:check",
|
|
135
135
|
"verify"
|
|
136
136
|
];
|
|
137
|
+
const PACKAGE_UPDATE_CLI_INFO_SCRIPTS = ["cli:surface", "doctor", "catalog:show", "catalog:template-show"];
|
|
138
|
+
const PACKAGE_UPDATE_CLI_VERIFICATION_SCRIPTS = ["verify", "pack:check", "check"];
|
|
137
139
|
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
138
140
|
const ENGINE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
139
141
|
const TEMPLATES_ROOT = path.join(ENGINE_ROOT, "templates");
|
|
@@ -236,6 +238,14 @@ function printUsage(options = {}) {
|
|
|
236
238
|
console.log(" topogram import status ./imported-topogram");
|
|
237
239
|
console.log(" topogram import history ./imported-topogram --verify");
|
|
238
240
|
console.log("");
|
|
241
|
+
console.log("Fresh install:");
|
|
242
|
+
console.log(" npm install --save-dev @topogram/cli");
|
|
243
|
+
console.log(" npx topogram doctor");
|
|
244
|
+
console.log(" npx topogram template list");
|
|
245
|
+
console.log(" npx topogram new ./my-app --template hello-web");
|
|
246
|
+
console.log(" cd ./my-app && npm install && npm run check && npm run generate");
|
|
247
|
+
console.log(" npm --prefix app run compile");
|
|
248
|
+
console.log("");
|
|
239
249
|
console.log("Template and catalog discovery:");
|
|
240
250
|
console.log(" topogram template list");
|
|
241
251
|
console.log(" topogram catalog list");
|
|
@@ -325,7 +335,7 @@ function printUsage(options = {}) {
|
|
|
325
335
|
console.log("Targets: json-schema, docs, docs-index, verification-plan, verification-checklist, shape-transform-graph, shape-transform-debug, api-contract-graph, api-contract-debug, ui-contract-graph, ui-contract-debug, ui-component-contract, component-conformance-report, component-behavior-report, ui-web-contract, ui-web-debug, sveltekit-app, swiftui-app, db-contract-graph, db-contract-debug, db-schema-snapshot, db-migration-plan, db-lifecycle-plan, db-lifecycle-bundle, environment-plan, environment-bundle, deployment-plan, deployment-bundle, runtime-smoke-plan, runtime-smoke-bundle, runtime-check-plan, runtime-check-bundle, compile-check-plan, compile-check-bundle, app-bundle-plan, app-bundle, native-parity-plan, native-parity-bundle, sql-migration, sql-schema, prisma-schema, drizzle-schema, persistence-scaffold, server-contract, hono-server, openapi, context-digest, context-diff, context-slice, context-bundle, context-report, context-task-mode");
|
|
326
336
|
console.log("Workflows: import-app, scan-docs, reconcile, adoption-status, generate-docs, generate-journeys, refresh-docs, report-gaps");
|
|
327
337
|
console.log("Import tracks: db, api, ui, workflows, verification");
|
|
328
|
-
console.log("Reconcile adopt selectors: from-plan, actors, roles, enums, shapes, entities, capabilities, docs, journeys, workflows, ui, bundle:<slug>, projection-review:<id>, ui-review:<id>, workflow-review:<id>, bundle-review:<slug>");
|
|
338
|
+
console.log("Reconcile adopt selectors: from-plan, actors, roles, enums, shapes, entities, capabilities, components, docs, journeys, workflows, ui, bundle:<slug>, projection-review:<id>, ui-review:<id>, workflow-review:<id>, bundle-review:<slug>");
|
|
329
339
|
}
|
|
330
340
|
|
|
331
341
|
function printNewHelp() {
|
|
@@ -334,6 +344,13 @@ function printNewHelp() {
|
|
|
334
344
|
console.log("");
|
|
335
345
|
console.log("Creates a new editable Topogram workspace from a template package or local template path.");
|
|
336
346
|
console.log("");
|
|
347
|
+
console.log("Fresh install flow:");
|
|
348
|
+
console.log(" npm install --save-dev @topogram/cli");
|
|
349
|
+
console.log(" npx topogram template list");
|
|
350
|
+
console.log(" npx topogram new ./my-app --template hello-web");
|
|
351
|
+
console.log(" cd ./my-app && npm install && npm run check && npm run generate");
|
|
352
|
+
console.log(" npm --prefix app run compile");
|
|
353
|
+
console.log("");
|
|
337
354
|
console.log("Examples:");
|
|
338
355
|
console.log(" topogram new ./my-app");
|
|
339
356
|
console.log(" topogram new --list-templates");
|
|
@@ -615,6 +632,12 @@ function printDoctorHelp() {
|
|
|
615
632
|
console.log("");
|
|
616
633
|
console.log("Checks local runtime, npm, public package access, CLI lockfile metadata, and catalog access.");
|
|
617
634
|
console.log("");
|
|
635
|
+
console.log("Fresh install check:");
|
|
636
|
+
console.log(" npm install --save-dev @topogram/cli");
|
|
637
|
+
console.log(" npx topogram doctor");
|
|
638
|
+
console.log(" npx topogram template list");
|
|
639
|
+
console.log(" npx topogram new ./my-app --template hello-web");
|
|
640
|
+
console.log("");
|
|
618
641
|
console.log("Related setup commands:");
|
|
619
642
|
console.log(" topogram setup package-auth");
|
|
620
643
|
console.log(" topogram setup catalog-auth");
|
|
@@ -680,6 +703,7 @@ function printPackageHelp() {
|
|
|
680
703
|
console.log(" - npm install updates package.json and package-lock.json.");
|
|
681
704
|
console.log(" - Available consumer verification scripts run after install.");
|
|
682
705
|
console.log(` - Recognized scripts: ${PACKAGE_UPDATE_CLI_CHECK_SCRIPTS.join(", ")}.`);
|
|
706
|
+
console.log(" - Verification scripts are selected by strength: verify, then pack:check, then check.");
|
|
683
707
|
console.log("");
|
|
684
708
|
console.log("Examples:");
|
|
685
709
|
console.log(" topogram package update-cli 0.3.5");
|
|
@@ -1651,11 +1675,10 @@ function buildPackageUpdateCliPayload(requested, options = {}) {
|
|
|
1651
1675
|
const versionConvention = writeTopogramCliVersionConventionIfPresent(cwd, version);
|
|
1652
1676
|
const packageJson = readPackageJsonForUpdate(cwd);
|
|
1653
1677
|
const scripts = packageJson.scripts && typeof packageJson.scripts === "object" ? packageJson.scripts : {};
|
|
1654
|
-
const candidateScripts = PACKAGE_UPDATE_CLI_CHECK_SCRIPTS;
|
|
1655
1678
|
const scriptsRun = [];
|
|
1656
1679
|
const skippedScripts = [];
|
|
1657
1680
|
if (dependencyUpdatedBy !== "npm-install") {
|
|
1658
|
-
skippedScripts.push(...
|
|
1681
|
+
skippedScripts.push(...PACKAGE_UPDATE_CLI_CHECK_SCRIPTS);
|
|
1659
1682
|
diagnostics.push({
|
|
1660
1683
|
code: "package_update_cli_checks_skipped_after_file_update",
|
|
1661
1684
|
severity: "warning",
|
|
@@ -1664,11 +1687,23 @@ function buildPackageUpdateCliPayload(requested, options = {}) {
|
|
|
1664
1687
|
suggestedFix: "Run npm install or npm ci, then rerun consumer verification."
|
|
1665
1688
|
});
|
|
1666
1689
|
} else {
|
|
1667
|
-
|
|
1690
|
+
const scriptsToRun = packageUpdateCliScriptsToRun(scripts);
|
|
1691
|
+
for (const scriptName of PACKAGE_UPDATE_CLI_INFO_SCRIPTS) {
|
|
1668
1692
|
if (!Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
1669
1693
|
skippedScripts.push(scriptName);
|
|
1670
|
-
continue;
|
|
1671
1694
|
}
|
|
1695
|
+
}
|
|
1696
|
+
for (const scriptName of PACKAGE_UPDATE_CLI_VERIFICATION_SCRIPTS) {
|
|
1697
|
+
if (!Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
1698
|
+
skippedScripts.push(scriptName);
|
|
1699
|
+
} else if (!scriptsToRun.includes(scriptName)) {
|
|
1700
|
+
const coveringScript = scriptsToRun.find((candidate) =>
|
|
1701
|
+
PACKAGE_UPDATE_CLI_VERIFICATION_SCRIPTS.includes(candidate)
|
|
1702
|
+
);
|
|
1703
|
+
skippedScripts.push(`${scriptName} (covered by ${coveringScript})`);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
for (const scriptName of scriptsToRun) {
|
|
1672
1707
|
const result = runNpmForPackageUpdate(["run", scriptName], cwd);
|
|
1673
1708
|
if (result.status !== 0) {
|
|
1674
1709
|
throw new Error(formatPackageUpdateNpmError(`npm run ${scriptName}`, "check", result));
|
|
@@ -1695,6 +1730,26 @@ function buildPackageUpdateCliPayload(requested, options = {}) {
|
|
|
1695
1730
|
};
|
|
1696
1731
|
}
|
|
1697
1732
|
|
|
1733
|
+
/**
|
|
1734
|
+
* @param {Record<string, any>} scripts
|
|
1735
|
+
* @returns {string[]}
|
|
1736
|
+
*/
|
|
1737
|
+
function packageUpdateCliScriptsToRun(scripts) {
|
|
1738
|
+
const selected = [];
|
|
1739
|
+
for (const scriptName of PACKAGE_UPDATE_CLI_INFO_SCRIPTS) {
|
|
1740
|
+
if (Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
1741
|
+
selected.push(scriptName);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
const verificationScript = PACKAGE_UPDATE_CLI_VERIFICATION_SCRIPTS.find((scriptName) =>
|
|
1745
|
+
Object.prototype.hasOwnProperty.call(scripts, scriptName)
|
|
1746
|
+
);
|
|
1747
|
+
if (verificationScript) {
|
|
1748
|
+
selected.push(verificationScript);
|
|
1749
|
+
}
|
|
1750
|
+
return selected;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1698
1753
|
/**
|
|
1699
1754
|
* @param {ReturnType<typeof buildPackageUpdateCliPayload>} payload
|
|
1700
1755
|
* @returns {void}
|
|
@@ -5851,6 +5906,15 @@ function diagnosticForTemplateCreateFailure(message, templateSpec, step) {
|
|
|
5851
5906
|
step
|
|
5852
5907
|
});
|
|
5853
5908
|
}
|
|
5909
|
+
if (message.includes("contains implementation/") && message.includes("includesExecutableImplementation: true")) {
|
|
5910
|
+
return templateCheckDiagnostic({
|
|
5911
|
+
code: "template_implementation_undeclared",
|
|
5912
|
+
message,
|
|
5913
|
+
path: localTemplatePath(templateSpec, "topogram-template.json"),
|
|
5914
|
+
suggestedFix: "Set includesExecutableImplementation to true after reviewing implementation/, or remove implementation/.",
|
|
5915
|
+
step
|
|
5916
|
+
});
|
|
5917
|
+
}
|
|
5854
5918
|
if (message.includes("is missing required string field") || message.includes("topogram-template.json")) {
|
|
5855
5919
|
return templateCheckDiagnostic({
|
|
5856
5920
|
code: "template_manifest_invalid",
|
|
@@ -65,14 +65,71 @@ function projectionRealizesIds(projection) {
|
|
|
65
65
|
return new Set((projection?.realizes || []).map((ref) => ref.id).filter(Boolean));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function
|
|
68
|
+
function ownProjectionScreenMap(projection) {
|
|
69
69
|
return new Map((projection?.uiScreens || []).map((screen) => [screen.id, screen]));
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
function
|
|
72
|
+
function ownProjectionRegionKeys(projection) {
|
|
73
73
|
return new Set((projection?.uiScreenRegions || []).map((entry) => `${entry.screenId}:${entry.region}`));
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
function projectionById(graph) {
|
|
77
|
+
return byId(graph.byKind.projection || []);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function projectionContext(graph, projection) {
|
|
81
|
+
const projections = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
const projectionsById = projectionById(graph);
|
|
84
|
+
|
|
85
|
+
function visit(current) {
|
|
86
|
+
if (!current || seen.has(current.id)) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
seen.add(current.id);
|
|
90
|
+
projections.push(current);
|
|
91
|
+
for (const ref of current.realizes || []) {
|
|
92
|
+
const target = projectionsById.get(ref.id);
|
|
93
|
+
if (target) {
|
|
94
|
+
visit(target);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
visit(projection);
|
|
100
|
+
return projections;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function projectionScreenMap(graph, projection) {
|
|
104
|
+
const screens = new Map();
|
|
105
|
+
for (const contextProjection of projectionContext(graph, projection).reverse()) {
|
|
106
|
+
for (const [id, screen] of ownProjectionScreenMap(contextProjection)) {
|
|
107
|
+
screens.set(id, screen);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return screens;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function projectionRegionKeys(graph, projection) {
|
|
114
|
+
const regions = new Set();
|
|
115
|
+
for (const contextProjection of projectionContext(graph, projection)) {
|
|
116
|
+
for (const key of ownProjectionRegionKeys(contextProjection)) {
|
|
117
|
+
regions.add(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return regions;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function projectionContextRealizesIds(graph, projection) {
|
|
124
|
+
const ids = new Set();
|
|
125
|
+
for (const contextProjection of projectionContext(graph, projection)) {
|
|
126
|
+
for (const id of projectionRealizesIds(contextProjection)) {
|
|
127
|
+
ids.add(id);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return ids;
|
|
131
|
+
}
|
|
132
|
+
|
|
76
133
|
function checkRecord({
|
|
77
134
|
code,
|
|
78
135
|
severity,
|
|
@@ -122,9 +179,9 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
122
179
|
const eventNames = new Set(events.map((event) => event.id));
|
|
123
180
|
const boundProps = new Set((usage.dataBindings || []).map((binding) => binding.prop).filter(Boolean));
|
|
124
181
|
const statements = byId(graph.statements || []);
|
|
125
|
-
const screens = projectionScreenMap(sourceProjection);
|
|
126
|
-
const regionKeys = projectionRegionKeys(sourceProjection);
|
|
127
|
-
const realizedIds =
|
|
182
|
+
const screens = projectionScreenMap(graph, sourceProjection);
|
|
183
|
+
const regionKeys = projectionRegionKeys(graph, sourceProjection);
|
|
184
|
+
const realizedIds = projectionContextRealizesIds(graph, sourceProjection);
|
|
128
185
|
|
|
129
186
|
if (!component) {
|
|
130
187
|
checks.push(checkRecord({
|
|
@@ -270,13 +327,13 @@ function collectUsageChecks({ graph, projection, sourceProjection, usage, compon
|
|
|
270
327
|
checks.push(checkRecord({
|
|
271
328
|
code: "component_event_action_not_in_projection",
|
|
272
329
|
severity: "error",
|
|
273
|
-
message: `Event '${binding.event}' targets capability '${target.id}', but projection '${sourceProjection.id}' does not realize it.`,
|
|
330
|
+
message: `Event '${binding.event}' targets capability '${target.id}', but projection '${sourceProjection.id}' does not realize it through its UI context.`,
|
|
274
331
|
projection,
|
|
275
332
|
sourceProjection,
|
|
276
333
|
component,
|
|
277
334
|
usage,
|
|
278
335
|
event: binding.event || null,
|
|
279
|
-
suggestedFix: `Add '${target.id}' to projection '${sourceProjection.id}' realizes or choose a capability already in this projection context.`
|
|
336
|
+
suggestedFix: `Add '${target.id}' to projection '${sourceProjection.id}' or an inherited shared projection realizes list, or choose a capability already in this projection context.`
|
|
280
337
|
}));
|
|
281
338
|
}
|
|
282
339
|
} else {
|
|
@@ -491,8 +548,8 @@ export function generateComponentConformanceReport(graph, options = {}) {
|
|
|
491
548
|
source_projection: entry.sourceProjection.id === entry.projection.id ? null : summarizeProjection(entry.sourceProjection),
|
|
492
549
|
screen: {
|
|
493
550
|
id: entry.usage.screenId || null,
|
|
494
|
-
kind: projectionScreenMap(entry.sourceProjection).get(entry.usage.screenId)?.kind || null,
|
|
495
|
-
title: projectionScreenMap(entry.sourceProjection).get(entry.usage.screenId)?.title || null
|
|
551
|
+
kind: projectionScreenMap(graph, entry.sourceProjection).get(entry.usage.screenId)?.kind || null,
|
|
552
|
+
title: projectionScreenMap(graph, entry.sourceProjection).get(entry.usage.screenId)?.title || null
|
|
496
553
|
},
|
|
497
554
|
region: entry.usage.region || null,
|
|
498
555
|
component: summarizeComponent(component) || { id: componentId, name: componentId, category: null, version: null, status: null, source_path: null },
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {{ id?: string, name?: string }} ComponentReference
|
|
5
|
-
* @typedef {{ component?: ComponentReference, region?: string }} ComponentUsage
|
|
5
|
+
* @typedef {{ component?: ComponentReference, region?: string, pattern?: string }} ComponentUsage
|
|
6
6
|
* @typedef {{ patterns?: string[] }} ComponentContract
|
|
7
7
|
* @typedef {Record<string, ComponentContract>} ComponentContractMap
|
|
8
8
|
* @typedef {{ itemsExpression?: string, componentContracts?: ComponentContractMap, useTypescript?: boolean }} RenderOptions
|
|
@@ -63,8 +63,22 @@ function componentPatterns(usage, componentContracts) {
|
|
|
63
63
|
* @param {string} pattern
|
|
64
64
|
* @returns {boolean}
|
|
65
65
|
*/
|
|
66
|
-
function
|
|
67
|
-
return componentPatterns(usage, componentContracts)
|
|
66
|
+
function usagePattern(usage, componentContracts) {
|
|
67
|
+
return usage?.pattern || componentPatterns(usage, componentContracts)[0] || null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function reactComponentUsageSupport(usage, componentContracts) {
|
|
71
|
+
const pattern = usagePattern(usage, componentContracts);
|
|
72
|
+
return {
|
|
73
|
+
pattern,
|
|
74
|
+
supported: [
|
|
75
|
+
"summary_stats",
|
|
76
|
+
"board_view",
|
|
77
|
+
"calendar_view",
|
|
78
|
+
"resource_table",
|
|
79
|
+
"data_grid_view"
|
|
80
|
+
].includes(pattern || "")
|
|
81
|
+
};
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
/**
|
|
@@ -198,16 +212,17 @@ function renderCalendar(usage, options) {
|
|
|
198
212
|
*/
|
|
199
213
|
function renderUsage(usage, options) {
|
|
200
214
|
const componentContracts = options.componentContracts || {};
|
|
201
|
-
|
|
215
|
+
const { pattern } = reactComponentUsageSupport(usage, componentContracts);
|
|
216
|
+
if (pattern === "summary_stats") {
|
|
202
217
|
return renderSummaryStats(usage, options);
|
|
203
218
|
}
|
|
204
|
-
if (
|
|
219
|
+
if (pattern === "board_view") {
|
|
205
220
|
return renderBoard(usage, options);
|
|
206
221
|
}
|
|
207
|
-
if (
|
|
222
|
+
if (pattern === "calendar_view") {
|
|
208
223
|
return renderCalendar(usage, options);
|
|
209
224
|
}
|
|
210
|
-
if (
|
|
225
|
+
if (pattern === "resource_table" || pattern === "data_grid_view") {
|
|
211
226
|
return renderCollectionTable(usage, options);
|
|
212
227
|
}
|
|
213
228
|
return "";
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { buildWebRealization } from "../../../realization/ui/index.js";
|
|
2
2
|
import { getExampleImplementation } from "../../../example-implementation.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
reactComponentUsageSupport,
|
|
5
|
+
renderReactComponentRegion
|
|
6
|
+
} from "./react-components.js";
|
|
4
7
|
import { renderApiClientModule, renderLookupModule, renderVisibilityModule } from "./shared.js";
|
|
5
8
|
|
|
6
9
|
function componentNameForScreen(screenId) {
|
|
@@ -214,7 +217,21 @@ function buildReactGenerationCoverage(contract, files, routeScreens) {
|
|
|
214
217
|
const componentUsages = screenComponentUsages(screen).map((usage) => {
|
|
215
218
|
const componentId = usage.component?.id || null;
|
|
216
219
|
const marker = componentId ? `data-topogram-component="${componentId}"` : null;
|
|
220
|
+
const support = reactComponentUsageSupport(usage, contract.components);
|
|
217
221
|
const usageRendered = Boolean(marker && contents.includes(marker));
|
|
222
|
+
if (componentId && rendered && !support.supported) {
|
|
223
|
+
diagnostics.push({
|
|
224
|
+
code: "component_pattern_not_supported",
|
|
225
|
+
severity: "error",
|
|
226
|
+
screen: screen.id,
|
|
227
|
+
route: screen.route,
|
|
228
|
+
region: usage.region || null,
|
|
229
|
+
pattern: support.pattern || null,
|
|
230
|
+
component: componentId,
|
|
231
|
+
message: `Screen '${screen.id}' uses component '${componentId}' with unsupported React component pattern '${support.pattern || "(missing)"}'.`,
|
|
232
|
+
suggested_fix: "Use a supported component pattern for this generator or provide an implementation override."
|
|
233
|
+
});
|
|
234
|
+
}
|
|
218
235
|
if (componentId && rendered && !usageRendered) {
|
|
219
236
|
diagnostics.push({
|
|
220
237
|
code: "component_usage_not_rendered",
|
|
@@ -230,6 +247,8 @@ function buildReactGenerationCoverage(contract, files, routeScreens) {
|
|
|
230
247
|
return {
|
|
231
248
|
component: componentId,
|
|
232
249
|
region: usage.region || null,
|
|
250
|
+
pattern: support.pattern || null,
|
|
251
|
+
supported: support.supported,
|
|
233
252
|
rendered: usageRendered,
|
|
234
253
|
marker
|
|
235
254
|
};
|
|
@@ -272,6 +291,15 @@ function buildReactGenerationCoverage(contract, files, routeScreens) {
|
|
|
272
291
|
};
|
|
273
292
|
}
|
|
274
293
|
|
|
294
|
+
function assertGenerationCoverage(coverage) {
|
|
295
|
+
const errors = (coverage.diagnostics || []).filter((diagnostic) => diagnostic.severity === "error");
|
|
296
|
+
if (errors.length === 0) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const details = errors.map((diagnostic) => diagnostic.message).join("; ");
|
|
300
|
+
throw new Error(`React generation coverage failed: ${details}`);
|
|
301
|
+
}
|
|
302
|
+
|
|
275
303
|
function buildAppTsx(contract, webReference) {
|
|
276
304
|
const navLinks = resolveNavLinks(contract, webReference);
|
|
277
305
|
const brandName = contract.appShell?.brand || webReference.brandName;
|
|
@@ -528,7 +556,9 @@ button, .button-link { display: inline-flex; align-items: center; justify-conten
|
|
|
528
556
|
files[screenPagePath(screen)] = buildReactScreenPage(screen, contract);
|
|
529
557
|
}
|
|
530
558
|
|
|
531
|
-
|
|
559
|
+
const coverage = buildReactGenerationCoverage(contract, files, routeScreens);
|
|
560
|
+
assertGenerationCoverage(coverage);
|
|
561
|
+
files["src/lib/topogram/generation-coverage.json"] = `${JSON.stringify(coverage, null, 2)}\n`;
|
|
532
562
|
return files;
|
|
533
563
|
}
|
|
534
564
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @typedef {{ id?: string, name?: string }} ComponentReference
|
|
5
|
-
* @typedef {{ component?: ComponentReference, region?: string }} ComponentUsage
|
|
5
|
+
* @typedef {{ component?: ComponentReference, region?: string, pattern?: string }} ComponentUsage
|
|
6
6
|
* @typedef {{ patterns?: string[] }} ComponentContract
|
|
7
7
|
* @typedef {Record<string, ComponentContract>} ComponentContractMap
|
|
8
8
|
* @typedef {{ itemsExpression?: string, componentContracts?: ComponentContractMap, useTypescript?: boolean }} RenderOptions
|
|
@@ -54,8 +54,22 @@ function componentPatterns(usage, componentContracts) {
|
|
|
54
54
|
* @param {string} pattern
|
|
55
55
|
* @returns {boolean}
|
|
56
56
|
*/
|
|
57
|
-
function
|
|
58
|
-
return componentPatterns(usage, componentContracts)
|
|
57
|
+
function usagePattern(usage, componentContracts) {
|
|
58
|
+
return usage?.pattern || componentPatterns(usage, componentContracts)[0] || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function svelteKitComponentUsageSupport(usage, componentContracts) {
|
|
62
|
+
const pattern = usagePattern(usage, componentContracts);
|
|
63
|
+
return {
|
|
64
|
+
pattern,
|
|
65
|
+
supported: [
|
|
66
|
+
"summary_stats",
|
|
67
|
+
"board_view",
|
|
68
|
+
"calendar_view",
|
|
69
|
+
"resource_table",
|
|
70
|
+
"data_grid_view"
|
|
71
|
+
].includes(pattern || "")
|
|
72
|
+
};
|
|
59
73
|
}
|
|
60
74
|
|
|
61
75
|
/**
|
|
@@ -184,16 +198,17 @@ function renderCalendar(usage, options) {
|
|
|
184
198
|
*/
|
|
185
199
|
function renderUsage(usage, options) {
|
|
186
200
|
const componentContracts = options.componentContracts || {};
|
|
187
|
-
|
|
201
|
+
const { pattern } = svelteKitComponentUsageSupport(usage, componentContracts);
|
|
202
|
+
if (pattern === "summary_stats") {
|
|
188
203
|
return renderSummaryStats(usage, options);
|
|
189
204
|
}
|
|
190
|
-
if (
|
|
205
|
+
if (pattern === "board_view") {
|
|
191
206
|
return renderBoard(usage, options);
|
|
192
207
|
}
|
|
193
|
-
if (
|
|
208
|
+
if (pattern === "calendar_view") {
|
|
194
209
|
return renderCalendar(usage, options);
|
|
195
210
|
}
|
|
196
|
-
if (
|
|
211
|
+
if (pattern === "resource_table" || pattern === "data_grid_view") {
|
|
197
212
|
return renderCollectionTable(usage, options);
|
|
198
213
|
}
|
|
199
214
|
return "";
|
|
@@ -2,7 +2,10 @@ import { buildWebRealization } from "../../../realization/ui/index.js";
|
|
|
2
2
|
import { lookupRouteSegment } from "../services/runtime-helpers.js";
|
|
3
3
|
import { getExampleImplementation } from "../../../example-implementation.js";
|
|
4
4
|
import { renderApiClientModule, renderLookupModule, renderVisibilityModule } from "./shared.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
renderSvelteKitComponentRegion,
|
|
7
|
+
svelteKitComponentUsageSupport
|
|
8
|
+
} from "./sveltekit-components.js";
|
|
6
9
|
|
|
7
10
|
function routePathToSvelteKitDirectory(routePath) {
|
|
8
11
|
if (!routePath || routePath === "/") {
|
|
@@ -186,7 +189,21 @@ function buildSvelteKitGenerationCoverage(contract, files, implementationScreenI
|
|
|
186
189
|
const componentUsages = screenComponentUsages(screen).map((usage) => {
|
|
187
190
|
const componentId = usage.component?.id || null;
|
|
188
191
|
const marker = componentId ? `data-topogram-component="${componentId}"` : null;
|
|
192
|
+
const support = svelteKitComponentUsageSupport(usage, contract.components);
|
|
189
193
|
const usageRendered = Boolean(marker && contents.includes(marker));
|
|
194
|
+
if (componentId && rendered && renderer !== "implementation" && !support.supported) {
|
|
195
|
+
diagnostics.push({
|
|
196
|
+
code: "component_pattern_not_supported",
|
|
197
|
+
severity: "error",
|
|
198
|
+
screen: screen.id,
|
|
199
|
+
route: screen.route,
|
|
200
|
+
region: usage.region || null,
|
|
201
|
+
pattern: support.pattern || null,
|
|
202
|
+
component: componentId,
|
|
203
|
+
message: `Screen '${screen.id}' uses component '${componentId}' with unsupported SvelteKit component pattern '${support.pattern || "(missing)"}'.`,
|
|
204
|
+
suggested_fix: "Use a supported component pattern for this generator or provide an implementation override."
|
|
205
|
+
});
|
|
206
|
+
}
|
|
190
207
|
if (componentId && rendered && !usageRendered) {
|
|
191
208
|
diagnostics.push({
|
|
192
209
|
code: "component_usage_not_rendered",
|
|
@@ -202,6 +219,8 @@ function buildSvelteKitGenerationCoverage(contract, files, implementationScreenI
|
|
|
202
219
|
return {
|
|
203
220
|
component: componentId,
|
|
204
221
|
region: usage.region || null,
|
|
222
|
+
pattern: support.pattern || null,
|
|
223
|
+
supported: support.supported,
|
|
205
224
|
rendered: usageRendered,
|
|
206
225
|
marker
|
|
207
226
|
};
|
|
@@ -244,6 +263,15 @@ function buildSvelteKitGenerationCoverage(contract, files, implementationScreenI
|
|
|
244
263
|
};
|
|
245
264
|
}
|
|
246
265
|
|
|
266
|
+
function assertGenerationCoverage(coverage) {
|
|
267
|
+
const errors = (coverage.diagnostics || []).filter((diagnostic) => diagnostic.severity === "error");
|
|
268
|
+
if (errors.length === 0) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const details = errors.map((diagnostic) => diagnostic.message).join("; ");
|
|
272
|
+
throw new Error(`SvelteKit generation coverage failed: ${details}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
247
275
|
function resolveNavLinks(contract, webReference) {
|
|
248
276
|
const contractLinks = (contract.navigation?.items || [])
|
|
249
277
|
.filter((item) => item.visible && item.route && item.placement === "primary")
|
|
@@ -415,7 +443,9 @@ function buildSvelteKitScaffold(contract, apiContracts, options = {}) {
|
|
|
415
443
|
}
|
|
416
444
|
}
|
|
417
445
|
|
|
418
|
-
|
|
446
|
+
const coverage = buildSvelteKitGenerationCoverage(contract, files, implementationScreenIds);
|
|
447
|
+
assertGenerationCoverage(coverage);
|
|
448
|
+
files["src/lib/topogram/generation-coverage.json"] = `${JSON.stringify(coverage, null, 2)}\n`;
|
|
419
449
|
files["src/lib/topogram/ui-web-contract.json"] = `${JSON.stringify(contract, null, 2)}\n`;
|
|
420
450
|
return files;
|
|
421
451
|
}
|
package/src/new-project.js
CHANGED
|
@@ -122,6 +122,7 @@ const SURFACE_ORDER = new Map([
|
|
|
122
122
|
* @property {string} package
|
|
123
123
|
* @property {string} version
|
|
124
124
|
* @property {string} packageSpec
|
|
125
|
+
* @property {boolean} [includesExecutableImplementation]
|
|
125
126
|
*/
|
|
126
127
|
|
|
127
128
|
/**
|
|
@@ -351,6 +352,13 @@ function validateTemplateRoot(templateRoot) {
|
|
|
351
352
|
`Template '${manifest.id}' declares executable implementation code but is missing implementation/.`
|
|
352
353
|
);
|
|
353
354
|
}
|
|
355
|
+
} else {
|
|
356
|
+
const implementationRoot = path.join(templateRoot, "implementation");
|
|
357
|
+
if (fs.existsSync(implementationRoot) && fs.statSync(implementationRoot).isDirectory()) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Template '${manifest.id}' contains implementation/ but ${TEMPLATE_MANIFEST} does not declare includesExecutableImplementation: true.`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
354
362
|
}
|
|
355
363
|
return manifest;
|
|
356
364
|
}
|
|
@@ -2076,6 +2084,16 @@ export function createNewProject({
|
|
|
2076
2084
|
const projectRoot = path.resolve(targetPath);
|
|
2077
2085
|
assertProjectOutsideEngine(projectRoot, engineRoot);
|
|
2078
2086
|
const template = resolveTemplate(templateName, templatesRoot);
|
|
2087
|
+
if (
|
|
2088
|
+
templateProvenance &&
|
|
2089
|
+
typeof templateProvenance.includesExecutableImplementation === "boolean" &&
|
|
2090
|
+
templateProvenance.includesExecutableImplementation !== Boolean(template.manifest.includesExecutableImplementation)
|
|
2091
|
+
) {
|
|
2092
|
+
throw new Error(
|
|
2093
|
+
`Catalog entry '${templateProvenance.id}' declares includesExecutableImplementation: ${templateProvenance.includesExecutableImplementation}, ` +
|
|
2094
|
+
`but template package '${template.packageSpec || template.requested}' declares includesExecutableImplementation: ${Boolean(template.manifest.includesExecutableImplementation)}.`
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2079
2097
|
|
|
2080
2098
|
ensureCreatableProjectRoot(projectRoot);
|
|
2081
2099
|
copyTopogramWorkspace(template.root, projectRoot);
|
|
@@ -84,12 +84,19 @@ function summarizeComponentRef(graph, componentId) {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
function regionContractFor(regionEntries, regionName) {
|
|
88
|
+
return (regionEntries || []).find((entry) => entry.region === regionName) || null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function buildComponentUsageContract(graph, entry, options = {}) {
|
|
88
92
|
const componentId = entry.component?.id || null;
|
|
89
93
|
const contract = componentId ? componentContractFor(graph, componentId) : null;
|
|
94
|
+
const region = options.region || null;
|
|
90
95
|
return {
|
|
91
96
|
type: "ui_component_usage",
|
|
92
97
|
region: entry.region || null,
|
|
98
|
+
pattern: region?.pattern || null,
|
|
99
|
+
placement: region?.placement || null,
|
|
93
100
|
component: componentId ? summarizeComponentRef(graph, componentId) : null,
|
|
94
101
|
dataBindings: (entry.dataBindings || []).map((binding) => ({
|
|
95
102
|
prop: binding.prop || null,
|
|
@@ -255,7 +262,9 @@ function buildUiScreenContract(graph, projection, screen, ownershipFields) {
|
|
|
255
262
|
state: entry.state || null,
|
|
256
263
|
variant: entry.variant || null
|
|
257
264
|
})),
|
|
258
|
-
components: componentEntries.map((entry) => buildComponentUsageContract(graph, entry
|
|
265
|
+
components: componentEntries.map((entry) => buildComponentUsageContract(graph, entry, {
|
|
266
|
+
region: regionContractFor(regionEntries, entry.region)
|
|
267
|
+
})),
|
|
259
268
|
patterns: [...patterns]
|
|
260
269
|
};
|
|
261
270
|
}
|
|
@@ -83,7 +83,9 @@ export function buildWebRealization(graph, options = {}) {
|
|
|
83
83
|
if ((screen.components || []).some((usage) => componentUsageFingerprint(usage) === componentUsageFingerprintFromEntry(entry))) {
|
|
84
84
|
continue;
|
|
85
85
|
}
|
|
86
|
-
screen.components = [...(screen.components || []), buildComponentUsageContract(graph, entry
|
|
86
|
+
screen.components = [...(screen.components || []), buildComponentUsageContract(graph, entry, {
|
|
87
|
+
region: (screen.regions || []).find((region) => region.region === entry.region) || null
|
|
88
|
+
})];
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
const appShell = projection.uiAppShell?.length || !sharedProjection ? concreteContract.appShell : sharedContract.appShell;
|
package/src/workflows.js
CHANGED
|
@@ -1464,6 +1464,27 @@ function renderCandidateUiReportDoc(screen, routes, actions) {
|
|
|
1464
1464
|
return renderMarkdownDoc(metadata, body);
|
|
1465
1465
|
}
|
|
1466
1466
|
|
|
1467
|
+
function renderCandidateComponent(component) {
|
|
1468
|
+
const propName = component.data_prop || "rows";
|
|
1469
|
+
const pattern = component.pattern || "search_results";
|
|
1470
|
+
const region = component.region || "results";
|
|
1471
|
+
return ensureTrailingNewline(
|
|
1472
|
+
[
|
|
1473
|
+
`component ${component.id_hint} {`,
|
|
1474
|
+
` name "${component.label || component.id_hint}"`,
|
|
1475
|
+
' description "Candidate reusable component inferred from imported UI evidence. Review props, behavior, events, and reuse before adoption."',
|
|
1476
|
+
" category collection",
|
|
1477
|
+
" props {",
|
|
1478
|
+
` ${propName} array required`,
|
|
1479
|
+
" }",
|
|
1480
|
+
` patterns [${pattern}]`,
|
|
1481
|
+
` regions [${region}]`,
|
|
1482
|
+
" status proposed",
|
|
1483
|
+
"}"
|
|
1484
|
+
].join("\n")
|
|
1485
|
+
);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1467
1488
|
function renderProjectionPatchDoc(patch) {
|
|
1468
1489
|
const lines = [
|
|
1469
1490
|
`# ${patch.projection_id} Patch Candidate`,
|
|
@@ -2393,6 +2414,7 @@ function getOrCreateCandidateBundle(bundles, conceptId, label) {
|
|
|
2393
2414
|
enums: [],
|
|
2394
2415
|
capabilities: [],
|
|
2395
2416
|
shapes: [],
|
|
2417
|
+
components: [],
|
|
2396
2418
|
screens: [],
|
|
2397
2419
|
uiRoutes: [],
|
|
2398
2420
|
uiActions: [],
|
|
@@ -3160,6 +3182,18 @@ function buildBundleAdoptionPlan(bundle, canonicalShapeIndex) {
|
|
|
3160
3182
|
canonical_rel_path: `verifications/${dashedTopogramId(entry.id_hint)}.tg`
|
|
3161
3183
|
});
|
|
3162
3184
|
}
|
|
3185
|
+
for (const entry of bundle.components || []) {
|
|
3186
|
+
steps.push({
|
|
3187
|
+
action: "promote_component",
|
|
3188
|
+
item: entry.id_hint,
|
|
3189
|
+
target: null,
|
|
3190
|
+
confidence: entry.confidence || "low",
|
|
3191
|
+
inference_summary: entry.inference_summary || null,
|
|
3192
|
+
related_capabilities: [entry.data_source].filter(Boolean),
|
|
3193
|
+
source_path: `candidates/reconcile/model/bundles/${bundle.slug}/components/${entry.id_hint}.tg`,
|
|
3194
|
+
canonical_rel_path: `components/${dashedTopogramId(entry.id_hint)}.tg`
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3163
3197
|
for (const screen of bundle.screens) {
|
|
3164
3198
|
steps.push({
|
|
3165
3199
|
action: "promote_ui_report",
|
|
@@ -3640,7 +3674,7 @@ function bestContextBundleForCandidate(bundles, candidate) {
|
|
|
3640
3674
|
function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
3641
3675
|
const dbCandidates = appImport.candidates.db || { entities: [], enums: [] };
|
|
3642
3676
|
const apiCandidates = appImport.candidates.api || { capabilities: [] };
|
|
3643
|
-
const uiCandidates = appImport.candidates.ui || { screens: [], routes: [], actions: [] };
|
|
3677
|
+
const uiCandidates = appImport.candidates.ui || { screens: [], routes: [], actions: [], components: [] };
|
|
3644
3678
|
const workflowCandidates = appImport.candidates.workflows || { workflows: [], workflow_states: [], workflow_transitions: [] };
|
|
3645
3679
|
const verificationCandidates = appImport.candidates.verification || { verifications: [], scenarios: [], frameworks: [], scripts: [] };
|
|
3646
3680
|
const docCandidates = appImport.candidates.docs || [];
|
|
@@ -3651,6 +3685,7 @@ function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
3651
3685
|
const canonicalRoleIds = new Set((graph?.byKind.role || []).map((entry) => entry.id));
|
|
3652
3686
|
const canonicalEntityIds = new Set((graph?.byKind.entity || []).map((entry) => entry.id));
|
|
3653
3687
|
const canonicalEnumIds = new Set((graph?.byKind.enum || []).map((entry) => entry.id));
|
|
3688
|
+
const canonicalComponentIds = new Set((graph?.byKind.component || []).map((entry) => entry.id));
|
|
3654
3689
|
const canonicalUi = collectCanonicalUiSurface(graph || { byKind: { projection: [] } });
|
|
3655
3690
|
const canonicalWorkflow = collectCanonicalWorkflowSurface(graph || { byKind: { decision: [] }, docs: [] });
|
|
3656
3691
|
const canonicalDocsByKind = new Map();
|
|
@@ -3749,6 +3784,24 @@ function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
3749
3784
|
const bundle = getOrCreateCandidateBundle(bundles, conceptId, bundleLabelFromConceptId(conceptId || entry.screen_id || entry.id_hint));
|
|
3750
3785
|
bundle.uiActions.push(entry);
|
|
3751
3786
|
}
|
|
3787
|
+
function componentConceptId(entry) {
|
|
3788
|
+
if (entry.entity_id || entry.concept_id) {
|
|
3789
|
+
return entry.entity_id || entry.concept_id;
|
|
3790
|
+
}
|
|
3791
|
+
const screenStem = String(entry.screen_id || entry.id_hint || "")
|
|
3792
|
+
.replace(/_(list|index|table|grid|results)$/, "")
|
|
3793
|
+
.replace(/^list_/, "");
|
|
3794
|
+
return `entity_${canonicalCandidateTerm(screenStem || entry.id_hint)}`;
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
for (const entry of uiCandidates.components || []) {
|
|
3798
|
+
if (canonicalComponentIds.has(entry.id_hint)) {
|
|
3799
|
+
continue;
|
|
3800
|
+
}
|
|
3801
|
+
const conceptId = componentConceptId(entry);
|
|
3802
|
+
const bundle = getOrCreateCandidateBundle(bundles, conceptId, bundleLabelFromConceptId(conceptId || entry.screen_id || entry.id_hint));
|
|
3803
|
+
bundle.components.push(entry);
|
|
3804
|
+
}
|
|
3752
3805
|
for (const entry of workflowCandidates.workflows || []) {
|
|
3753
3806
|
if (canonicalWorkflow.workflow_docs.includes(entry.id_hint)) {
|
|
3754
3807
|
continue;
|
|
@@ -3834,6 +3887,7 @@ function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
3834
3887
|
bundle.enums.length > 0 ||
|
|
3835
3888
|
bundle.capabilities.length > 0 ||
|
|
3836
3889
|
bundle.shapes.length > 0 ||
|
|
3890
|
+
bundle.components.length > 0 ||
|
|
3837
3891
|
bundle.screens.length > 0 ||
|
|
3838
3892
|
bundle.uiRoutes.length > 0 ||
|
|
3839
3893
|
bundle.uiActions.length > 0 ||
|
|
@@ -3851,6 +3905,7 @@ function buildCandidateModelBundles(graph, appImport, topogramRoot) {
|
|
|
3851
3905
|
enums: bundle.enums.sort((a, b) => a.id_hint.localeCompare(b.id_hint)),
|
|
3852
3906
|
capabilities: bundle.capabilities.sort((a, b) => a.id_hint.localeCompare(b.id_hint)),
|
|
3853
3907
|
shapes: bundle.shapes.sort((a, b) => a.id.localeCompare(b.id)),
|
|
3908
|
+
components: bundle.components.sort((a, b) => a.id_hint.localeCompare(b.id_hint)),
|
|
3854
3909
|
screens: bundle.screens.sort((a, b) => a.id_hint.localeCompare(b.id_hint)),
|
|
3855
3910
|
uiRoutes: bundle.uiRoutes.sort((a, b) => a.id_hint.localeCompare(b.id_hint)),
|
|
3856
3911
|
uiActions: bundle.uiActions.sort((a, b) => a.id_hint.localeCompare(b.id_hint)),
|
|
@@ -3952,6 +4007,9 @@ function buildCandidateModelFiles(graph, appImport, topogramRoot) {
|
|
|
3952
4007
|
for (const entry of bundle.verifications || []) {
|
|
3953
4008
|
files[`${bundleRoot}/verifications/${entry.id_hint}.tg`] = renderCandidateVerification(entry, entry.scenarios || []);
|
|
3954
4009
|
}
|
|
4010
|
+
for (const entry of bundle.components || []) {
|
|
4011
|
+
files[`${bundleRoot}/components/${entry.id_hint}.tg`] = renderCandidateComponent(entry);
|
|
4012
|
+
}
|
|
3955
4013
|
for (const entry of bundle.docs) {
|
|
3956
4014
|
if (entry.existing_canonical) {
|
|
3957
4015
|
continue;
|
|
@@ -4021,6 +4079,8 @@ function canonicalRelativePathForItem(kind, item) {
|
|
|
4021
4079
|
return `shapes/${dashedTopogramId(item)}.tg`;
|
|
4022
4080
|
case "capability":
|
|
4023
4081
|
return `capabilities/${dashedTopogramId(item)}.tg`;
|
|
4082
|
+
case "component":
|
|
4083
|
+
return `components/${dashedTopogramId(item)}.tg`;
|
|
4024
4084
|
case "verification":
|
|
4025
4085
|
return `verifications/${dashedTopogramId(item)}.tg`;
|
|
4026
4086
|
default:
|
|
@@ -4048,6 +4108,8 @@ function candidateSourcePathForItem(bundle, kind, item) {
|
|
|
4048
4108
|
return `${base}/shapes/${item}.tg`;
|
|
4049
4109
|
case "capability":
|
|
4050
4110
|
return `${base}/capabilities/${item}.tg`;
|
|
4111
|
+
case "component":
|
|
4112
|
+
return `${base}/components/${item}.tg`;
|
|
4051
4113
|
case "verification":
|
|
4052
4114
|
return `${base}/verifications/${item}.tg`;
|
|
4053
4115
|
default:
|
|
@@ -4069,6 +4131,8 @@ function reasonForAdoptionItem(step) {
|
|
|
4069
4131
|
return step.target ? `Promote this shape to support concept ${step.target}.` : "Promote this imported shape into canonical Topogram.";
|
|
4070
4132
|
case "promote_capability":
|
|
4071
4133
|
return "Promote this imported capability into canonical Topogram.";
|
|
4134
|
+
case "promote_component":
|
|
4135
|
+
return "Promote this imported reusable UI component into canonical Topogram.";
|
|
4072
4136
|
case "merge_capability_into_existing_entity":
|
|
4073
4137
|
return `Adopt this capability while preserving the existing canonical entity ${step.target}.`;
|
|
4074
4138
|
case "promote_doc":
|
|
@@ -4114,6 +4178,9 @@ function recommendationForAdoptionItem(step) {
|
|
|
4114
4178
|
if (step.action === "apply_projection_ownership_patch") {
|
|
4115
4179
|
return `Update \`${step.target}\` with inferred ownership auth rules for ${(step.related_capabilities || []).map((item) => `\`${item}\``).join(", ") || "the related capabilities"}.`;
|
|
4116
4180
|
}
|
|
4181
|
+
if (step.action === "promote_component") {
|
|
4182
|
+
return "Promote this reviewed component candidate before binding or reusing it from canonical projections.";
|
|
4183
|
+
}
|
|
4117
4184
|
if (!["promote_actor", "promote_role"].includes(step.action)) {
|
|
4118
4185
|
return null;
|
|
4119
4186
|
}
|
|
@@ -4232,6 +4299,7 @@ function buildAdoptionPlan(bundles) {
|
|
|
4232
4299
|
step.action.includes("doc") ? "doc" :
|
|
4233
4300
|
step.action.includes("decision") ? "decision" :
|
|
4234
4301
|
step.action.includes("verification") ? "verification" :
|
|
4302
|
+
step.action.includes("component") ? "component" :
|
|
4235
4303
|
step.action.includes("ui_") ? "ui" :
|
|
4236
4304
|
step.action.includes("actor") ? "actor" :
|
|
4237
4305
|
step.action.includes("role") ? "role" :
|
|
@@ -4403,7 +4471,7 @@ function buildAdoptionPlan(bundles) {
|
|
|
4403
4471
|
);
|
|
4404
4472
|
}
|
|
4405
4473
|
|
|
4406
|
-
const ADOPT_SELECTORS = new Set(["from-plan", "actors", "roles", "enums", "shapes", "entities", "capabilities", "docs", "journeys", "workflows", "verification", "ui"]);
|
|
4474
|
+
const ADOPT_SELECTORS = new Set(["from-plan", "actors", "roles", "enums", "shapes", "entities", "capabilities", "components", "docs", "journeys", "workflows", "verification", "ui"]);
|
|
4407
4475
|
|
|
4408
4476
|
function readAdoptionPlan(paths) {
|
|
4409
4477
|
return readJsonIfExists(path.join(paths.topogramRoot, "candidates", "reconcile", "adoption-plan.json"));
|