@topogram/cli 0.3.34
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 +67 -0
- package/CHANGELOG.md +240 -0
- package/README.md +223 -0
- package/package.json +51 -0
- package/src/adoption/index.js +3 -0
- package/src/adoption/plan.js +702 -0
- package/src/adoption/reporting.js +464 -0
- package/src/adoption/review-groups.js +313 -0
- package/src/agent-ops/query-builders.js +5012 -0
- package/src/archive/archive.js +141 -0
- package/src/archive/compact.js +26 -0
- package/src/archive/jsonl.js +70 -0
- package/src/archive/resolver-bridge.js +82 -0
- package/src/archive/schema.js +87 -0
- package/src/archive/unarchive.js +108 -0
- package/src/catalog.js +752 -0
- package/src/cli/catalog-alias.js +166 -0
- package/src/cli.js +9738 -0
- package/src/component-behavior.js +173 -0
- package/src/example-implementation.js +91 -0
- package/src/format.js +19 -0
- package/src/generator/adapters.d.ts +4 -0
- package/src/generator/adapters.js +325 -0
- package/src/generator/api.d.ts +1 -0
- package/src/generator/api.js +1196 -0
- package/src/generator/check.js +355 -0
- package/src/generator/component-conformance.js +767 -0
- package/src/generator/components.js +39 -0
- package/src/generator/context/bundle.js +291 -0
- package/src/generator/context/diff.js +256 -0
- package/src/generator/context/digest.js +182 -0
- package/src/generator/context/domain-coverage.js +94 -0
- package/src/generator/context/domain-page.js +137 -0
- package/src/generator/context/index.js +42 -0
- package/src/generator/context/report.js +121 -0
- package/src/generator/context/shared.js +1397 -0
- package/src/generator/context/slice.js +703 -0
- package/src/generator/context/task-mode.js +466 -0
- package/src/generator/docs.js +327 -0
- package/src/generator/index.js +161 -0
- package/src/generator/native/parity-bundle.js +311 -0
- package/src/generator/output.js +300 -0
- package/src/generator/registry.js +482 -0
- package/src/generator/runtime/app-bundle.js +456 -0
- package/src/generator/runtime/bundle-shared.js +166 -0
- package/src/generator/runtime/compile-check.js +163 -0
- package/src/generator/runtime/deployment.js +287 -0
- package/src/generator/runtime/environment.js +635 -0
- package/src/generator/runtime/index.js +32 -0
- package/src/generator/runtime/runtime-check.js +554 -0
- package/src/generator/runtime/shared.js +515 -0
- package/src/generator/runtime/smoke.js +219 -0
- package/src/generator/schema.js +204 -0
- package/src/generator/sdlc/board.js +66 -0
- package/src/generator/sdlc/doc-page.js +53 -0
- package/src/generator/sdlc/index.js +23 -0
- package/src/generator/sdlc/release-notes.js +62 -0
- package/src/generator/sdlc/traceability-matrix.js +65 -0
- package/src/generator/shared.js +29 -0
- package/src/generator/surfaces/contracts.js +146 -0
- package/src/generator/surfaces/databases/contract.js +40 -0
- package/src/generator/surfaces/databases/index.js +84 -0
- package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
- package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
- package/src/generator/surfaces/databases/migration-plan.js +281 -0
- package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
- package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
- package/src/generator/surfaces/databases/postgres/index.js +9 -0
- package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
- package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
- package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
- package/src/generator/surfaces/databases/shared.d.ts +1 -0
- package/src/generator/surfaces/databases/shared.js +350 -0
- package/src/generator/surfaces/databases/snapshot.js +96 -0
- package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
- package/src/generator/surfaces/databases/sqlite/index.js +8 -0
- package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
- package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
- package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
- package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
- package/src/generator/surfaces/index.js +25 -0
- package/src/generator/surfaces/native/swiftui-app.js +38 -0
- package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
- package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
- package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
- package/src/generator/surfaces/services/express.d.ts +1 -0
- package/src/generator/surfaces/services/express.js +766 -0
- package/src/generator/surfaces/services/hono.d.ts +1 -0
- package/src/generator/surfaces/services/hono.js +204 -0
- package/src/generator/surfaces/services/index.js +42 -0
- package/src/generator/surfaces/services/persistence-wiring.js +240 -0
- package/src/generator/surfaces/services/runtime-helpers.js +631 -0
- package/src/generator/surfaces/services/server-contract.js +80 -0
- package/src/generator/surfaces/services/stateless.d.ts +1 -0
- package/src/generator/surfaces/services/stateless.js +97 -0
- package/src/generator/surfaces/shared.js +64 -0
- package/src/generator/surfaces/web/api-client.js +1 -0
- package/src/generator/surfaces/web/forms.js +1 -0
- package/src/generator/surfaces/web/index.d.ts +2 -0
- package/src/generator/surfaces/web/index.js +53 -0
- package/src/generator/surfaces/web/react-components.js +248 -0
- package/src/generator/surfaces/web/react.js +538 -0
- package/src/generator/surfaces/web/routes.js +1 -0
- package/src/generator/surfaces/web/screens.js +1 -0
- package/src/generator/surfaces/web/shared.js +369 -0
- package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
- package/src/generator/surfaces/web/sveltekit-components.js +234 -0
- package/src/generator/surfaces/web/sveltekit.js +426 -0
- package/src/generator/surfaces/web/ui-web-contract.js +65 -0
- package/src/generator/surfaces/web/vanilla.js +239 -0
- package/src/generator/verification.js +84 -0
- package/src/generator.js +1 -0
- package/src/import/core/context.js +52 -0
- package/src/import/core/contracts.js +23 -0
- package/src/import/core/registry.js +81 -0
- package/src/import/core/runner.js +646 -0
- package/src/import/core/shared.js +910 -0
- package/src/import/enrichers/auth-session.js +18 -0
- package/src/import/enrichers/django-rest.js +226 -0
- package/src/import/enrichers/doc-linking.js +20 -0
- package/src/import/enrichers/rails-controllers.js +246 -0
- package/src/import/enrichers/rails-models.js +130 -0
- package/src/import/enrichers/workflow-target-state.js +10 -0
- package/src/import/extractors/api/aspnet-core.js +304 -0
- package/src/import/extractors/api/django-routes.js +318 -0
- package/src/import/extractors/api/express.js +154 -0
- package/src/import/extractors/api/fastify.js +371 -0
- package/src/import/extractors/api/flutter-dio.js +135 -0
- package/src/import/extractors/api/generic-route-fallback.js +90 -0
- package/src/import/extractors/api/graphql-code-first.js +565 -0
- package/src/import/extractors/api/graphql-sdl.js +309 -0
- package/src/import/extractors/api/jaxrs.js +303 -0
- package/src/import/extractors/api/micronaut.js +213 -0
- package/src/import/extractors/api/next-route.js +50 -0
- package/src/import/extractors/api/next-server-action.js +51 -0
- package/src/import/extractors/api/nextauth.js +52 -0
- package/src/import/extractors/api/openapi-code.js +242 -0
- package/src/import/extractors/api/openapi.js +232 -0
- package/src/import/extractors/api/rails-routes.js +230 -0
- package/src/import/extractors/api/react-native-repository.js +128 -0
- package/src/import/extractors/api/retrofit.js +103 -0
- package/src/import/extractors/api/spring-web.js +372 -0
- package/src/import/extractors/api/swift-webapi.js +116 -0
- package/src/import/extractors/api/trpc.js +212 -0
- package/src/import/extractors/db/django-models.js +232 -0
- package/src/import/extractors/db/dotnet-models.js +93 -0
- package/src/import/extractors/db/drizzle.js +242 -0
- package/src/import/extractors/db/ef-core.js +221 -0
- package/src/import/extractors/db/flutter-entities.js +120 -0
- package/src/import/extractors/db/jpa.js +120 -0
- package/src/import/extractors/db/liquibase.js +180 -0
- package/src/import/extractors/db/mybatis-xml.js +145 -0
- package/src/import/extractors/db/prisma.js +185 -0
- package/src/import/extractors/db/rails-schema.js +175 -0
- package/src/import/extractors/db/react-native-entities.js +95 -0
- package/src/import/extractors/db/room.js +193 -0
- package/src/import/extractors/db/snapshot.js +112 -0
- package/src/import/extractors/db/sql.js +180 -0
- package/src/import/extractors/db/swiftdata.js +137 -0
- package/src/import/extractors/ui/android-compose.js +230 -0
- package/src/import/extractors/ui/backend-only.js +70 -0
- package/src/import/extractors/ui/blazor.js +227 -0
- package/src/import/extractors/ui/flutter-screens.js +152 -0
- package/src/import/extractors/ui/maui-xaml.js +135 -0
- package/src/import/extractors/ui/next-app-router.js +83 -0
- package/src/import/extractors/ui/next-pages-router.js +141 -0
- package/src/import/extractors/ui/razor-pages.js +181 -0
- package/src/import/extractors/ui/react-native-screens.js +166 -0
- package/src/import/extractors/ui/react-router.js +139 -0
- package/src/import/extractors/ui/sveltekit.js +123 -0
- package/src/import/extractors/ui/swiftui.js +193 -0
- package/src/import/extractors/ui/uikit.js +175 -0
- package/src/import/extractors/verification/generic.js +290 -0
- package/src/import/extractors/workflows/generic.js +137 -0
- package/src/import/index.js +7 -0
- package/src/import/provenance.js +158 -0
- package/src/new-project.js +2107 -0
- package/src/parser.js +439 -0
- package/src/policy/review-boundaries.js +165 -0
- package/src/project-config.js +535 -0
- package/src/proofs/backend-parity.js +19 -0
- package/src/proofs/contract-audit.js +220 -0
- package/src/proofs/ios-parity.js +7 -0
- package/src/proofs/issues-parity.js +10 -0
- package/src/proofs/web-parity.js +50 -0
- package/src/realization/api/build-api-realization.js +5 -0
- package/src/realization/api/index.js +1 -0
- package/src/realization/backend/build-backend-runtime-realization.js +82 -0
- package/src/realization/backend/index.d.ts +1 -0
- package/src/realization/backend/index.js +4 -0
- package/src/realization/db/build-db-realization.js +17 -0
- package/src/realization/db/index.js +3 -0
- package/src/realization/db/migration-plan.js +5 -0
- package/src/realization/db/snapshot.js +5 -0
- package/src/realization/ui/build-ui-shared-realization.js +305 -0
- package/src/realization/ui/build-web-realization.js +189 -0
- package/src/realization/ui/index.js +2 -0
- package/src/reconcile/docs.js +280 -0
- package/src/reconcile/index.js +3 -0
- package/src/reconcile/journeys.js +441 -0
- package/src/resolver/docs.js +1 -0
- package/src/resolver/enrich/acceptance-criterion.js +14 -0
- package/src/resolver/enrich/bug.js +12 -0
- package/src/resolver/enrich/component.js +2 -0
- package/src/resolver/enrich/index.js +1 -0
- package/src/resolver/enrich/pitch.js +18 -0
- package/src/resolver/enrich/requirement.js +20 -0
- package/src/resolver/enrich/task.js +16 -0
- package/src/resolver/expressions.js +1 -0
- package/src/resolver/index.js +2422 -0
- package/src/resolver/normalize.js +1 -0
- package/src/resolver.js +1 -0
- package/src/sdlc/adopt.js +65 -0
- package/src/sdlc/check.js +86 -0
- package/src/sdlc/dod/acceptance-criterion.js +22 -0
- package/src/sdlc/dod/bug.js +26 -0
- package/src/sdlc/dod/document.js +23 -0
- package/src/sdlc/dod/index.js +25 -0
- package/src/sdlc/dod/pitch.js +23 -0
- package/src/sdlc/dod/requirement.js +34 -0
- package/src/sdlc/dod/task.js +39 -0
- package/src/sdlc/explain.js +116 -0
- package/src/sdlc/history.js +80 -0
- package/src/sdlc/paths.js +11 -0
- package/src/sdlc/release.js +106 -0
- package/src/sdlc/scaffold.js +89 -0
- package/src/sdlc/status-filter.js +54 -0
- package/src/sdlc/transition.js +112 -0
- package/src/sdlc/transitions/acceptance-criterion.js +28 -0
- package/src/sdlc/transitions/bug.js +31 -0
- package/src/sdlc/transitions/document.js +29 -0
- package/src/sdlc/transitions/index.js +56 -0
- package/src/sdlc/transitions/pitch.js +34 -0
- package/src/sdlc/transitions/requirement.js +31 -0
- package/src/sdlc/transitions/task.js +34 -0
- package/src/template-trust.js +597 -0
- package/src/validator/expressions.js +1 -0
- package/src/validator/index.js +3424 -0
- package/src/validator/kinds.js +346 -0
- package/src/validator/per-kind/acceptance-criterion.js +91 -0
- package/src/validator/per-kind/bug.js +77 -0
- package/src/validator/per-kind/component.js +274 -0
- package/src/validator/per-kind/domain.js +205 -0
- package/src/validator/per-kind/pitch.js +101 -0
- package/src/validator/per-kind/requirement.js +75 -0
- package/src/validator/per-kind/task.js +96 -0
- package/src/validator/registry.js +1 -0
- package/src/validator/utils.js +12 -0
- package/src/validator.js +1 -0
- package/src/workflows.js +7597 -0
- package/src/workspace-docs.js +265 -0
- package/template-helpers/react.js +5 -0
- package/template-helpers/sveltekit.js +5 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
import { IMPORT_TRACKS } from "./contracts.js";
|
|
2
|
+
import { dedupeCandidateRecords, ensureTrailingNewline, idHintify, makeCandidateRecord } from "./shared.js";
|
|
3
|
+
import { createImportContext } from "./context.js";
|
|
4
|
+
import { getEnrichersForTrack, getExtractorsForTrack } from "./registry.js";
|
|
5
|
+
|
|
6
|
+
function parseImportTracks(fromValue) {
|
|
7
|
+
if (!fromValue) {
|
|
8
|
+
return ["db", "api", "ui", "workflows", "verification"];
|
|
9
|
+
}
|
|
10
|
+
const tracks = String(fromValue).split(",").map((track) => track.trim().toLowerCase()).filter(Boolean);
|
|
11
|
+
if (tracks.length === 0) {
|
|
12
|
+
throw new Error("Expected --from to include at least one import track");
|
|
13
|
+
}
|
|
14
|
+
const invalid = tracks.filter((track) => !IMPORT_TRACKS.has(track));
|
|
15
|
+
if (invalid.length > 0) {
|
|
16
|
+
throw new Error(`Unsupported import track(s): ${invalid.join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
return [...new Set(tracks)];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sortExtractors(context, extractors) {
|
|
22
|
+
return extractors
|
|
23
|
+
.map((extractor) => ({ extractor, detection: extractor.detect(context) || { score: 0, reasons: [] } }))
|
|
24
|
+
.filter((entry) => entry.detection.score > 0)
|
|
25
|
+
.sort((a, b) => b.detection.score - a.detection.score || a.extractor.id.localeCompare(b.extractor.id));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function selectDetectionsForTrack(track, detections) {
|
|
29
|
+
if (track === "db") {
|
|
30
|
+
const prisma = detections.find((entry) => entry.extractor.id === "db.prisma");
|
|
31
|
+
if (prisma) return [prisma];
|
|
32
|
+
const djangoModels = detections.find((entry) => entry.extractor.id === "db.django-models");
|
|
33
|
+
if (djangoModels) return [djangoModels];
|
|
34
|
+
const efCore = detections.find((entry) => entry.extractor.id === "db.ef-core");
|
|
35
|
+
if (efCore) return [efCore];
|
|
36
|
+
const room = detections.find((entry) => entry.extractor.id === "db.room");
|
|
37
|
+
if (room) return [room];
|
|
38
|
+
const swiftData = detections.find((entry) => entry.extractor.id === "db.swiftdata");
|
|
39
|
+
if (swiftData) return [swiftData];
|
|
40
|
+
const dotnetModels = detections.find((entry) => entry.extractor.id === "db.dotnet-models");
|
|
41
|
+
if (dotnetModels) return [dotnetModels];
|
|
42
|
+
const flutterEntities = detections.find((entry) => entry.extractor.id === "db.flutter-entities");
|
|
43
|
+
if (flutterEntities) return [flutterEntities];
|
|
44
|
+
const reactNativeEntities = detections.find((entry) => entry.extractor.id === "db.react-native-entities");
|
|
45
|
+
if (reactNativeEntities) return [reactNativeEntities];
|
|
46
|
+
const railsSchema = detections.find((entry) => entry.extractor.id === "db.rails-schema");
|
|
47
|
+
if (railsSchema) return [railsSchema];
|
|
48
|
+
const liquibase = detections.find((entry) => entry.extractor.id === "db.liquibase");
|
|
49
|
+
if (liquibase) return [liquibase];
|
|
50
|
+
const myBatisXml = detections.find((entry) => entry.extractor.id === "db.mybatis-xml");
|
|
51
|
+
if (myBatisXml) return [myBatisXml];
|
|
52
|
+
const jpa = detections.find((entry) => entry.extractor.id === "db.jpa");
|
|
53
|
+
if (jpa) return [jpa];
|
|
54
|
+
const drizzle = detections.find((entry) => entry.extractor.id === "db.drizzle");
|
|
55
|
+
if (drizzle) return [drizzle];
|
|
56
|
+
const sql = detections.find((entry) => entry.extractor.id === "db.sql");
|
|
57
|
+
if (sql) return [sql];
|
|
58
|
+
const snapshot = detections.find((entry) => entry.extractor.id === "db.snapshot");
|
|
59
|
+
return snapshot ? [snapshot] : [];
|
|
60
|
+
}
|
|
61
|
+
if (track === "api") {
|
|
62
|
+
const openApi = detections.find((entry) => entry.extractor.id === "api.openapi");
|
|
63
|
+
if (openApi) return [openApi];
|
|
64
|
+
const openApiCode = detections.find((entry) => entry.extractor.id === "api.openapi-code");
|
|
65
|
+
if (openApiCode) return [openApiCode];
|
|
66
|
+
const graphQlSdl = detections.find((entry) => entry.extractor.id === "api.graphql-sdl");
|
|
67
|
+
if (graphQlSdl) return [graphQlSdl];
|
|
68
|
+
const trpc = detections.find((entry) => entry.extractor.id === "api.trpc");
|
|
69
|
+
if (trpc) return [trpc];
|
|
70
|
+
const aspNetCore = detections.find((entry) => entry.extractor.id === "api.aspnet-core");
|
|
71
|
+
if (aspNetCore) return [aspNetCore];
|
|
72
|
+
const retrofit = detections.find((entry) => entry.extractor.id === "api.retrofit");
|
|
73
|
+
if (retrofit) return [retrofit];
|
|
74
|
+
const swiftWebApi = detections.find((entry) => entry.extractor.id === "api.swift-webapi");
|
|
75
|
+
if (swiftWebApi) return [swiftWebApi];
|
|
76
|
+
const flutterDio = detections.find((entry) => entry.extractor.id === "api.flutter-dio");
|
|
77
|
+
if (flutterDio) return [flutterDio];
|
|
78
|
+
const reactNativeRepository = detections.find((entry) => entry.extractor.id === "api.react-native-repository");
|
|
79
|
+
if (reactNativeRepository) return [reactNativeRepository];
|
|
80
|
+
const fastify = detections.find((entry) => entry.extractor.id === "api.fastify");
|
|
81
|
+
if (fastify) return [fastify];
|
|
82
|
+
const express = detections.find((entry) => entry.extractor.id === "api.express");
|
|
83
|
+
if (express) return [express];
|
|
84
|
+
const djangoRoutes = detections.find((entry) => entry.extractor.id === "api.django-routes");
|
|
85
|
+
if (djangoRoutes) return [djangoRoutes];
|
|
86
|
+
const railsRoutes = detections.find((entry) => entry.extractor.id === "api.rails-routes");
|
|
87
|
+
if (railsRoutes) return [railsRoutes];
|
|
88
|
+
const micronaut = detections.find((entry) => entry.extractor.id === "api.micronaut");
|
|
89
|
+
if (micronaut) return [micronaut];
|
|
90
|
+
const jaxrs = detections.find((entry) => entry.extractor.id === "api.jaxrs");
|
|
91
|
+
if (jaxrs) return [jaxrs];
|
|
92
|
+
const springWeb = detections.find((entry) => entry.extractor.id === "api.spring-web");
|
|
93
|
+
if (springWeb) return [springWeb];
|
|
94
|
+
}
|
|
95
|
+
return detections;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function collectionPatternFromPresentations(presentations) {
|
|
99
|
+
if (presentations.includes("data_grid")) return "data_grid_view";
|
|
100
|
+
if (presentations.includes("table")) return "resource_table";
|
|
101
|
+
if (presentations.includes("cards")) return "resource_cards";
|
|
102
|
+
if (presentations.includes("board")) return "board_view";
|
|
103
|
+
if (presentations.includes("calendar")) return "calendar_view";
|
|
104
|
+
if (presentations.includes("gallery")) return "resource_cards";
|
|
105
|
+
return "search_results";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function presentationFromPattern(pattern) {
|
|
109
|
+
if (pattern === "data_grid_view") return "data_grid";
|
|
110
|
+
if (pattern === "resource_table") return "table";
|
|
111
|
+
if (pattern === "resource_cards") return "cards";
|
|
112
|
+
if (pattern === "board_view") return "board";
|
|
113
|
+
if (pattern === "calendar_view") return "calendar";
|
|
114
|
+
return "list";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function importedApiCapabilityIds(allCandidates) {
|
|
118
|
+
return [...(allCandidates?.api?.capabilities || [])]
|
|
119
|
+
.map((capability) => capability.id_hint)
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.sort();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function loadCapabilityForScreen(screen) {
|
|
125
|
+
return capabilityHintsForScreen(screen).find((hint) => /^cap_(list|get)_/.test(hint)) || null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizeCapabilityHint(value) {
|
|
129
|
+
if (typeof value === "string") {
|
|
130
|
+
return value;
|
|
131
|
+
}
|
|
132
|
+
if (value && typeof value === "object") {
|
|
133
|
+
return value.id_hint || value.id || value.capability_hint || value.capability?.id || null;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function capabilityHintsForScreen(screen) {
|
|
139
|
+
const rawHints = Array.isArray(screen.capability_hints)
|
|
140
|
+
? screen.capability_hints
|
|
141
|
+
: [screen.capability_hints].filter(Boolean);
|
|
142
|
+
return rawHints.map(normalizeCapabilityHint).filter(Boolean);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function inferredDataSourceForComponent(component, allCandidates) {
|
|
146
|
+
if (component.data_source) {
|
|
147
|
+
return component.data_source;
|
|
148
|
+
}
|
|
149
|
+
const capabilityIds = importedApiCapabilityIds(allCandidates);
|
|
150
|
+
const screenStem = String(component.screen_id || "")
|
|
151
|
+
.replace(/_(list|index|table|grid|results)$/, "")
|
|
152
|
+
.replace(/^list_/, "");
|
|
153
|
+
return capabilityIds.find((id) => /^cap_(list|get)_/.test(id) && id.includes(screenStem)) ||
|
|
154
|
+
capabilityIds.find((id) => /^cap_(list|get)_/.test(id)) ||
|
|
155
|
+
null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function deriveUiComponentCandidates(candidates) {
|
|
159
|
+
const screens = candidates.screens || [];
|
|
160
|
+
const actions = candidates.actions || [];
|
|
161
|
+
const presentations = [...new Set(actions
|
|
162
|
+
.filter((entry) => entry.kind === "ui_presentation")
|
|
163
|
+
.map((entry) => entry.presentation)
|
|
164
|
+
.filter(Boolean))].sort();
|
|
165
|
+
const componentScreens = screens.filter((screen) => ["list", "dashboard", "analytics", "report", "feed", "inbox"].includes(screen.screen_kind));
|
|
166
|
+
|
|
167
|
+
return componentScreens.map((screen) => {
|
|
168
|
+
const pattern = collectionPatternFromPresentations(presentations);
|
|
169
|
+
const componentStem = idHintify(`${screen.id_hint}_results`);
|
|
170
|
+
const loadCapability = loadCapabilityForScreen(screen);
|
|
171
|
+
return makeCandidateRecord({
|
|
172
|
+
kind: "component",
|
|
173
|
+
idHint: `component_ui_${componentStem}`,
|
|
174
|
+
label: `${screen.label || screen.id_hint} results`,
|
|
175
|
+
confidence: presentations.length > 0 ? "medium" : "low",
|
|
176
|
+
sourceKind: "ui_projection_inference",
|
|
177
|
+
sourceOfTruth: "candidate",
|
|
178
|
+
provenance: screen.provenance || [],
|
|
179
|
+
screen_id: screen.id_hint,
|
|
180
|
+
region: "results",
|
|
181
|
+
pattern,
|
|
182
|
+
data_prop: "rows",
|
|
183
|
+
data_source: loadCapability,
|
|
184
|
+
notes: [
|
|
185
|
+
"Imported component candidates are review-only.",
|
|
186
|
+
"Confirm props, behavior, events, and reuse before adoption."
|
|
187
|
+
]
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeCandidatesForTrack(track, candidates) {
|
|
193
|
+
if (track === "db") {
|
|
194
|
+
return {
|
|
195
|
+
entities: dedupeCandidateRecords(candidates.entities || [], (record) => record.id_hint),
|
|
196
|
+
enums: dedupeCandidateRecords(candidates.enums || [], (record) => record.id_hint),
|
|
197
|
+
relations: dedupeCandidateRecords(candidates.relations || [], (record) => record.id_hint),
|
|
198
|
+
indexes: dedupeCandidateRecords(candidates.indexes || [], (record) => record.id_hint)
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (track === "api") {
|
|
202
|
+
return {
|
|
203
|
+
capabilities: dedupeCandidateRecords(candidates.capabilities || [], (record) => record.id_hint),
|
|
204
|
+
routes: dedupeCandidateRecords(
|
|
205
|
+
(candidates.routes || []).map((route) => ({ ...route, id_hint: route.id_hint || `${route.method}_${route.path}` })),
|
|
206
|
+
(record) => `${record.method}:${record.path}:${record.source_kind}`
|
|
207
|
+
).map(({ id_hint, ...route }) => route),
|
|
208
|
+
stacks: [...new Set(candidates.stacks || [])].sort()
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (track === "ui") {
|
|
212
|
+
const explicitComponents = candidates.components || [];
|
|
213
|
+
const derivedComponents = deriveUiComponentCandidates(candidates);
|
|
214
|
+
return {
|
|
215
|
+
screens: dedupeCandidateRecords(candidates.screens || [], (record) => record.id_hint),
|
|
216
|
+
routes: dedupeCandidateRecords(candidates.routes || [], (record) => record.id_hint),
|
|
217
|
+
actions: dedupeCandidateRecords(candidates.actions || [], (record) => record.id_hint),
|
|
218
|
+
components: dedupeCandidateRecords([...explicitComponents, ...derivedComponents], (record) => record.id_hint),
|
|
219
|
+
stacks: [...new Set(candidates.stacks || [])].sort()
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (track === "verification") {
|
|
223
|
+
return {
|
|
224
|
+
verifications: dedupeCandidateRecords(candidates.verifications || [], (record) => record.id_hint),
|
|
225
|
+
scenarios: dedupeCandidateRecords(candidates.scenarios || [], (record) => record.id_hint),
|
|
226
|
+
frameworks: [...new Set(candidates.frameworks || [])].sort(),
|
|
227
|
+
scripts: dedupeCandidateRecords(
|
|
228
|
+
(candidates.scripts || []).map((script) => ({
|
|
229
|
+
...script,
|
|
230
|
+
id_hint: script.id_hint || `${script.file || "package.json"}:${script.name || "test"}`
|
|
231
|
+
})),
|
|
232
|
+
(record) => record.id_hint
|
|
233
|
+
).map(({ id_hint, ...script }) => script)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
workflows: dedupeCandidateRecords(candidates.workflows || [], (record) => record.id_hint),
|
|
238
|
+
workflow_states: dedupeCandidateRecords(candidates.workflow_states || [], (record) => record.id_hint),
|
|
239
|
+
workflow_transitions: dedupeCandidateRecords(candidates.workflow_transitions || [], (record) => record.id_hint)
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function runTrack(context, track) {
|
|
244
|
+
const findings = [];
|
|
245
|
+
const rawCandidates = track === "db"
|
|
246
|
+
? { entities: [], enums: [], relations: [], indexes: [] }
|
|
247
|
+
: track === "api"
|
|
248
|
+
? { capabilities: [], routes: [], stacks: [] }
|
|
249
|
+
: track === "ui"
|
|
250
|
+
? { screens: [], routes: [], actions: [], stacks: [] }
|
|
251
|
+
: track === "verification"
|
|
252
|
+
? { verifications: [], scenarios: [], frameworks: [], scripts: [] }
|
|
253
|
+
: { workflows: [], workflow_states: [], workflow_transitions: [] };
|
|
254
|
+
|
|
255
|
+
for (const { extractor, detection } of selectDetectionsForTrack(track, sortExtractors(context, getExtractorsForTrack(track)))) {
|
|
256
|
+
const result = extractor.extract(context) || { findings: [], candidates: {} };
|
|
257
|
+
findings.push({
|
|
258
|
+
extractor: extractor.id,
|
|
259
|
+
detection,
|
|
260
|
+
findings: result.findings || []
|
|
261
|
+
});
|
|
262
|
+
for (const [key, value] of Object.entries(result.candidates || {})) {
|
|
263
|
+
if (Array.isArray(rawCandidates[key])) {
|
|
264
|
+
rawCandidates[key].push(...value);
|
|
265
|
+
} else if (Array.isArray(value)) {
|
|
266
|
+
rawCandidates[key] = [...value];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let candidates = normalizeCandidatesForTrack(track, rawCandidates);
|
|
272
|
+
for (const enricher of getEnrichersForTrack(track)) {
|
|
273
|
+
const applies = enricher.applies(context, candidates);
|
|
274
|
+
if (!applies) continue;
|
|
275
|
+
candidates = normalizeCandidatesForTrack(track, enricher.enrich(context, candidates) || candidates);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
findings: findings.flatMap((entry) => entry.findings || []),
|
|
280
|
+
candidates,
|
|
281
|
+
extractor_detections: findings.map(({ extractor, detection }) => ({ extractor, ...detection }))
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function reportMarkdown(track, candidates) {
|
|
286
|
+
if (track === "db") {
|
|
287
|
+
return ensureTrailingNewline(
|
|
288
|
+
`# DB Import Report\n\n- Entities: ${candidates.entities.length}\n- Enums: ${candidates.enums.length}\n- Relations: ${candidates.relations.length}\n- Indexes: ${candidates.indexes.length}\n`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (track === "api") {
|
|
292
|
+
return ensureTrailingNewline(
|
|
293
|
+
`# API Import Report\n\n- Capabilities: ${candidates.capabilities.length}\n- Routes: ${candidates.routes.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (track === "ui") {
|
|
297
|
+
return ensureTrailingNewline(
|
|
298
|
+
`# UI Import Report\n\n- Screens: ${candidates.screens.length}\n- Routes: ${candidates.routes.length}\n- Actions: ${candidates.actions.length}\n- Components: ${candidates.components.length}\n- Stacks: ${candidates.stacks.length ? candidates.stacks.join(", ") : "none"}\n`
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
if (track === "verification") {
|
|
302
|
+
return ensureTrailingNewline(
|
|
303
|
+
`# Verification Import Report\n\n- Verifications: ${candidates.verifications.length}\n- Scenarios: ${candidates.scenarios.length}\n- Frameworks: ${candidates.frameworks.length ? candidates.frameworks.join(", ") : "none"}\n- Scripts: ${candidates.scripts.length}\n`
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return ensureTrailingNewline(
|
|
307
|
+
`# Workflow Import Report\n\n- Workflows: ${candidates.workflows.length}\n- States: ${candidates.workflow_states.length}\n- Transitions: ${candidates.workflow_transitions.length}\n`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function uniqueSorted(values) {
|
|
312
|
+
return [...new Set((values || []).filter(Boolean))].sort();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function projectionIdStem(workspaceRoot) {
|
|
316
|
+
const base = String(workspaceRoot || "").split(/[\\/]/).filter(Boolean).pop() || "imported_app";
|
|
317
|
+
return base
|
|
318
|
+
.toLowerCase()
|
|
319
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
320
|
+
.replace(/^_+|_+$/g, "") || "imported_app";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function componentCandidateFileName(component) {
|
|
324
|
+
return `${String(component.id_hint || "component")
|
|
325
|
+
.replace(/^component_/, "")
|
|
326
|
+
.replace(/_/g, "-")}.tg`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function renderComponentCandidate(component) {
|
|
330
|
+
return `component ${component.id_hint} {
|
|
331
|
+
name "${component.label || component.id_hint}"
|
|
332
|
+
description "Candidate reusable component inferred from imported UI evidence. Review props, behavior, events, and reuse before adoption."
|
|
333
|
+
category collection
|
|
334
|
+
props {
|
|
335
|
+
${component.data_prop || "rows"} array required
|
|
336
|
+
}
|
|
337
|
+
patterns [${component.pattern || "search_results"}]
|
|
338
|
+
regions [${component.region || "results"}]
|
|
339
|
+
status proposed
|
|
340
|
+
}
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function uiComponentLinesForCandidates(componentCandidates, allCandidates) {
|
|
345
|
+
return componentCandidates
|
|
346
|
+
.filter((component) => component.screen_id && component.region && component.id_hint)
|
|
347
|
+
.map((component) => {
|
|
348
|
+
const dataSource = inferredDataSourceForComponent(component, allCandidates);
|
|
349
|
+
const dataBinding = dataSource
|
|
350
|
+
? ` data ${component.data_prop || "rows"} from ${dataSource}`
|
|
351
|
+
: "";
|
|
352
|
+
return ` screen ${component.screen_id} region ${component.region} component ${component.id_hint}${dataBinding}`;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function draftUiProjectionFiles(context, candidates, allCandidates = {}) {
|
|
357
|
+
const ui = candidates || { screens: [], routes: [], actions: [], stacks: [] };
|
|
358
|
+
const screens = [...(ui.screens || [])].sort((a, b) => String(a.route_path || "").localeCompare(String(b.route_path || "")) || a.id_hint.localeCompare(b.id_hint));
|
|
359
|
+
const routes = new Map((ui.routes || []).map((route) => [route.screen_id, route.path]));
|
|
360
|
+
const actions = ui.actions || [];
|
|
361
|
+
const componentCandidates = [...(ui.components || [])].sort((a, b) => a.id_hint.localeCompare(b.id_hint));
|
|
362
|
+
const shell = actions.find((entry) => entry.kind === "ui_shell")?.shell_kind || "topbar";
|
|
363
|
+
const navigationPatterns = uniqueSorted(actions.filter((entry) => entry.kind === "ui_navigation").map((entry) => entry.navigation_pattern));
|
|
364
|
+
const presentations = uniqueSorted(actions.filter((entry) => entry.kind === "ui_presentation").map((entry) => entry.presentation));
|
|
365
|
+
const capabilityHints = uniqueSorted([
|
|
366
|
+
...screens.flatMap((screen) => capabilityHintsForScreen(screen)),
|
|
367
|
+
...actions.map((entry) => entry.capability_hint).filter(Boolean),
|
|
368
|
+
...importedApiCapabilityIds(allCandidates)
|
|
369
|
+
]);
|
|
370
|
+
const stem = projectionIdStem(context.paths.workspaceRoot);
|
|
371
|
+
const screenKinds = new Map(screens.map((screen) => [screen.id_hint, screen.screen_kind || "flow"]));
|
|
372
|
+
const defaultScreenId = screens.find((screen) => screen.screen_kind === "list")?.id_hint || screens[0]?.id_hint || null;
|
|
373
|
+
|
|
374
|
+
const uiScreensBlock = screens.length > 0
|
|
375
|
+
? screens.map((screen) => {
|
|
376
|
+
const directives = [`kind ${screen.screen_kind || "flow"}`, `title "${screen.label || screen.id_hint}"`];
|
|
377
|
+
const screenCapabilityHints = capabilityHintsForScreen(screen);
|
|
378
|
+
if (screenCapabilityHints.length > 0) {
|
|
379
|
+
const loadHint = screenCapabilityHints.find((hint) => /^cap_(list|get)_/.test(hint));
|
|
380
|
+
const submitHint = screenCapabilityHints.find((hint) => /^cap_(create|update|sign_in|follow|delete)_/.test(hint));
|
|
381
|
+
if (loadHint && ["list", "detail", "job_status", "feed", "inbox", "dashboard", "analytics", "report"].includes(screen.screen_kind)) {
|
|
382
|
+
directives.push(`load ${loadHint}`);
|
|
383
|
+
}
|
|
384
|
+
if (submitHint && ["form", "wizard", "settings", "flow"].includes(screen.screen_kind)) {
|
|
385
|
+
directives.push(`submit ${submitHint}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return ` screen ${screen.id_hint} ${directives.join(" ")}`;
|
|
389
|
+
}).join("\n")
|
|
390
|
+
: " // No imported screens detected";
|
|
391
|
+
|
|
392
|
+
const collectionScreens = screens.filter((screen) => screen.screen_kind === "list");
|
|
393
|
+
const uiCollectionsLines = [];
|
|
394
|
+
for (const screen of collectionScreens) {
|
|
395
|
+
const screenPresentations = presentations.filter((presentation) =>
|
|
396
|
+
["table", "data_grid", "cards", "board", "calendar", "gallery", "list"].includes(presentation)
|
|
397
|
+
);
|
|
398
|
+
const preferredView =
|
|
399
|
+
screenPresentations.find((presentation) => ["data_grid", "table", "cards", "list"].includes(presentation))
|
|
400
|
+
|| "list";
|
|
401
|
+
uiCollectionsLines.push(` screen ${screen.id_hint} view ${preferredView}`);
|
|
402
|
+
if (presentations.includes("pull_to_refresh")) {
|
|
403
|
+
uiCollectionsLines.push(` screen ${screen.id_hint} refresh pull_to_refresh`);
|
|
404
|
+
}
|
|
405
|
+
if (presentations.includes("search")) {
|
|
406
|
+
uiCollectionsLines.push(` screen ${screen.id_hint} search query`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const uiActionsLines = actions
|
|
411
|
+
.filter((entry) => entry.kind === "ui_action" && entry.screen_id && entry.capability_hint)
|
|
412
|
+
.map((entry) => ` screen ${entry.screen_id} action ${entry.capability_hint} prominence ${entry.prominence || "secondary"}`);
|
|
413
|
+
|
|
414
|
+
const uiNavigationLines = [];
|
|
415
|
+
if (defaultScreenId) {
|
|
416
|
+
if (navigationPatterns.includes("command_palette")) {
|
|
417
|
+
uiNavigationLines.push(` group workspace label "Workspace" placement primary pattern command_palette`);
|
|
418
|
+
} else {
|
|
419
|
+
uiNavigationLines.push(` group workspace label "Workspace" placement primary`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
for (const screen of screens) {
|
|
423
|
+
const directives = [
|
|
424
|
+
"group workspace",
|
|
425
|
+
`label "${screen.label || screen.id_hint}"`,
|
|
426
|
+
screen.id_hint === defaultScreenId ? "default true" : null,
|
|
427
|
+
screen.id_hint === defaultScreenId || screen.screen_kind === "list" ? "visible true" : "visible false"
|
|
428
|
+
].filter(Boolean);
|
|
429
|
+
const matchedPattern =
|
|
430
|
+
navigationPatterns.find((pattern) =>
|
|
431
|
+
(pattern === "stack_navigation" && screen.screen_kind === "detail")
|
|
432
|
+
|| (pattern === "segmented_control" && screen.screen_kind === "list")
|
|
433
|
+
|| (pattern === "bottom_tabs" && screen.screen_kind === "list")
|
|
434
|
+
) || null;
|
|
435
|
+
if (matchedPattern) {
|
|
436
|
+
directives.push(`pattern ${matchedPattern}`);
|
|
437
|
+
}
|
|
438
|
+
if (screen.screen_kind === "detail" && defaultScreenId && screen.id_hint !== defaultScreenId) {
|
|
439
|
+
directives.push(`breadcrumb ${defaultScreenId}`);
|
|
440
|
+
directives.push("sitemap exclude");
|
|
441
|
+
}
|
|
442
|
+
uiNavigationLines.push(` screen ${screen.id_hint} ${directives.join(" ")}`);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const uiScreenRegionLines = [];
|
|
446
|
+
for (const screen of screens) {
|
|
447
|
+
if (screen.screen_kind === "list") {
|
|
448
|
+
uiScreenRegionLines.push(` screen ${screen.id_hint} region toolbar pattern action_bar placement primary`);
|
|
449
|
+
const preferredPattern =
|
|
450
|
+
presentations.includes("data_grid") ? "data_grid_view"
|
|
451
|
+
: presentations.includes("table") ? "resource_table"
|
|
452
|
+
: presentations.includes("cards") ? "resource_cards"
|
|
453
|
+
: "search_results";
|
|
454
|
+
uiScreenRegionLines.push(` screen ${screen.id_hint} region results pattern ${preferredPattern} placement primary`);
|
|
455
|
+
}
|
|
456
|
+
if (screen.screen_kind === "detail") {
|
|
457
|
+
uiScreenRegionLines.push(` screen ${screen.id_hint} region summary pattern detail_panel placement primary`);
|
|
458
|
+
if (presentations.includes("inspector_pane")) {
|
|
459
|
+
uiScreenRegionLines.push(` screen ${screen.id_hint} region aside pattern inspector_pane placement supporting`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
const uiComponentLines = uiComponentLinesForCandidates(componentCandidates, allCandidates);
|
|
464
|
+
|
|
465
|
+
const uiSharedDraft = `projection proj_ui_shared_imported_${stem} {
|
|
466
|
+
name "Imported Shared UI Draft"
|
|
467
|
+
description "Drafted from imported UI candidates. Review and adapt before adoption."
|
|
468
|
+
|
|
469
|
+
platform ui_shared
|
|
470
|
+
realizes [
|
|
471
|
+
${capabilityHints.length > 0 ? capabilityHints.map((hint) => ` ${hint}`).join(",\n") : " // add capability ids"}
|
|
472
|
+
]
|
|
473
|
+
outputs [ui_contract]
|
|
474
|
+
|
|
475
|
+
ui_app_shell {
|
|
476
|
+
brand "Imported ${stem.replace(/_/g, " ")}"
|
|
477
|
+
shell ${shell}
|
|
478
|
+
${presentations.includes("search") ? " global_search true\n" : ""}${presentations.includes("multi_window") ? " windowing multi_window\n" : ""} }
|
|
479
|
+
|
|
480
|
+
ui_screens {
|
|
481
|
+
${uiScreensBlock}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
${uiCollectionsLines.length > 0 ? ` ui_collections {\n${uiCollectionsLines.join("\n")}\n }\n\n` : ""}${uiActionsLines.length > 0 ? ` ui_actions {\n${uiActionsLines.join("\n")}\n }\n\n` : ""} ui_navigation {
|
|
485
|
+
${uiNavigationLines.join("\n")}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
${uiScreenRegionLines.length > 0 ? ` ui_screen_regions {\n${uiScreenRegionLines.join("\n")}\n }\n\n` : ""}${uiComponentLines.length > 0 ? ` ui_components {\n${uiComponentLines.join("\n")}\n }\n\n` : ""} status proposed
|
|
489
|
+
}
|
|
490
|
+
`;
|
|
491
|
+
|
|
492
|
+
const webCapHints = capabilityHints.length > 0 ? capabilityHints.join(",\n ") : "// add capability ids";
|
|
493
|
+
const uiRouteLines = screens
|
|
494
|
+
.filter((screen) => routes.has(screen.id_hint))
|
|
495
|
+
.map((screen) => ` screen ${screen.id_hint} path ${routes.get(screen.id_hint)}`);
|
|
496
|
+
const uiWebLines = [];
|
|
497
|
+
for (const screen of screens) {
|
|
498
|
+
if (!routes.has(screen.id_hint)) continue;
|
|
499
|
+
if (screen.id_hint === defaultScreenId) {
|
|
500
|
+
uiWebLines.push(` screen ${screen.id_hint} shell ${shell}`);
|
|
501
|
+
}
|
|
502
|
+
if (screen.screen_kind === "list") {
|
|
503
|
+
const preferredCollection =
|
|
504
|
+
presentations.includes("data_grid") ? "data_grid"
|
|
505
|
+
: presentations.includes("table") ? "table"
|
|
506
|
+
: presentations.includes("cards") ? "cards"
|
|
507
|
+
: "list";
|
|
508
|
+
uiWebLines.push(` screen ${screen.id_hint} collection ${preferredCollection}`);
|
|
509
|
+
if (presentations.includes("cards")) {
|
|
510
|
+
uiWebLines.push(` screen ${screen.id_hint} mobile_variant cards`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
if (screen.screen_kind === "detail" && presentations.includes("sheet")) {
|
|
514
|
+
uiWebLines.push(` screen ${screen.id_hint} present sheet`);
|
|
515
|
+
}
|
|
516
|
+
if (screen.screen_kind === "detail" && presentations.includes("popover")) {
|
|
517
|
+
uiWebLines.push(` screen ${screen.id_hint} present popover`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
for (const entry of actions.filter((action) => action.kind === "ui_action" && action.capability_hint)) {
|
|
521
|
+
const actionPresent =
|
|
522
|
+
presentations.includes("fab") ? "fab"
|
|
523
|
+
: presentations.includes("popover") ? "popover"
|
|
524
|
+
: "button";
|
|
525
|
+
uiWebLines.push(` action ${entry.capability_hint} present ${actionPresent}`);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const uiWebDraft = `projection proj_ui_web_imported_${stem} {
|
|
529
|
+
name "Imported Web UI Draft"
|
|
530
|
+
description "Drafted from imported UI candidates. Review and adapt before adoption."
|
|
531
|
+
|
|
532
|
+
platform ui_web
|
|
533
|
+
realizes [
|
|
534
|
+
proj_ui_shared_imported_${stem},
|
|
535
|
+
${webCapHints}
|
|
536
|
+
]
|
|
537
|
+
outputs [ui_contract, web_app]
|
|
538
|
+
|
|
539
|
+
ui_routes {
|
|
540
|
+
${uiRouteLines.length > 0 ? uiRouteLines.join("\n") : " // add routes"}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
${uiWebLines.length > 0 ? ` ui_web {\n${uiWebLines.join("\n")}\n }\n\n` : ""} generator_defaults {
|
|
544
|
+
profile react
|
|
545
|
+
language typescript
|
|
546
|
+
styling css
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
status proposed
|
|
550
|
+
}
|
|
551
|
+
`;
|
|
552
|
+
|
|
553
|
+
const coverage = `# Imported UI Projection Drafts
|
|
554
|
+
|
|
555
|
+
- Draft shared projection: \`candidates/app/ui/drafts/proj-ui-shared.tg\`
|
|
556
|
+
- Draft web projection: \`candidates/app/ui/drafts/proj-ui-web.tg\`
|
|
557
|
+
- Draft component candidates: ${componentCandidates.length}
|
|
558
|
+
- Imported screens: ${screens.length}
|
|
559
|
+
- Imported routes: ${(ui.routes || []).length}
|
|
560
|
+
- Imported UI actions/presentations: ${actions.length}
|
|
561
|
+
- Imported navigation patterns: ${navigationPatterns.length ? navigationPatterns.join(", ") : "none"}
|
|
562
|
+
- Imported presentations: ${presentations.length ? presentations.join(", ") : "none"}
|
|
563
|
+
|
|
564
|
+
## Review Notes
|
|
565
|
+
|
|
566
|
+
- These files are drafts, not adopted canonical projections.
|
|
567
|
+
- Capability ids come from imported hints and may need renaming or pruning.
|
|
568
|
+
- Component candidates are suggested reusable contracts, not canonical ownership.
|
|
569
|
+
- Review component props, events, behavior, regions, and patterns before adopting.
|
|
570
|
+
- Search and refresh directives are inferred heuristically.
|
|
571
|
+
- Navigation groups currently default to a single \`workspace\` group unless stronger grouping evidence exists.
|
|
572
|
+
`;
|
|
573
|
+
|
|
574
|
+
const files = {
|
|
575
|
+
"candidates/app/ui/drafts/proj-ui-shared.tg": ensureTrailingNewline(uiSharedDraft),
|
|
576
|
+
"candidates/app/ui/drafts/proj-ui-web.tg": ensureTrailingNewline(uiWebDraft),
|
|
577
|
+
"candidates/app/ui/drafts/README.md": ensureTrailingNewline(coverage)
|
|
578
|
+
};
|
|
579
|
+
for (const component of componentCandidates) {
|
|
580
|
+
files[`candidates/app/ui/drafts/components/${componentCandidateFileName(component)}`] = ensureTrailingNewline(renderComponentCandidate(component));
|
|
581
|
+
}
|
|
582
|
+
return files;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function runImportApp(inputPath, options = {}) {
|
|
586
|
+
const tracks = parseImportTracks(options.from);
|
|
587
|
+
const context = createImportContext(inputPath, options);
|
|
588
|
+
const resultsByTrack = {};
|
|
589
|
+
context.priorResults = resultsByTrack;
|
|
590
|
+
context.scanDocsSummary = options.scanDocsSummary || null;
|
|
591
|
+
const findings = {};
|
|
592
|
+
const candidates = {};
|
|
593
|
+
const files = {};
|
|
594
|
+
|
|
595
|
+
for (const track of tracks) {
|
|
596
|
+
if (track === "workflows") {
|
|
597
|
+
if (!resultsByTrack.db) {
|
|
598
|
+
resultsByTrack.db = runTrack(context, "db");
|
|
599
|
+
}
|
|
600
|
+
if (!resultsByTrack.api) {
|
|
601
|
+
resultsByTrack.api = runTrack(context, "api");
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (track === "verification") {
|
|
605
|
+
if (!resultsByTrack.api) {
|
|
606
|
+
resultsByTrack.api = runTrack(context, "api");
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const result = runTrack(context, track);
|
|
610
|
+
resultsByTrack[track] = result;
|
|
611
|
+
findings[track] = result.findings;
|
|
612
|
+
candidates[track] = result.candidates;
|
|
613
|
+
files[`candidates/app/${track}/findings.json`] = `${JSON.stringify(result.findings, null, 2)}\n`;
|
|
614
|
+
files[`candidates/app/${track}/candidates.json`] = `${JSON.stringify(result.candidates, null, 2)}\n`;
|
|
615
|
+
files[`candidates/app/${track}/report.md`] = reportMarkdown(track, result.candidates);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (candidates.ui) {
|
|
619
|
+
Object.assign(files, draftUiProjectionFiles(context, candidates.ui, candidates));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const summary = {
|
|
623
|
+
type: "import_app_report",
|
|
624
|
+
workspace: context.paths.workspaceRoot,
|
|
625
|
+
topogram_root: context.paths.topogramRoot,
|
|
626
|
+
bootstrapped_topogram_root: context.paths.bootstrappedTopogramRoot,
|
|
627
|
+
tracks,
|
|
628
|
+
findings_count: Object.values(findings).reduce((total, entries) => total + entries.length, 0),
|
|
629
|
+
extractor_detections: Object.fromEntries(Object.entries(resultsByTrack).map(([track, result]) => [track, result.extractor_detections])),
|
|
630
|
+
candidates
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
files["candidates/app/findings.json"] = `${JSON.stringify(findings, null, 2)}\n`;
|
|
634
|
+
files["candidates/app/candidates.json"] = `${JSON.stringify(candidates, null, 2)}\n`;
|
|
635
|
+
files["candidates/app/report.md"] = ensureTrailingNewline(
|
|
636
|
+
`# App Import Report\n\nTracks: ${tracks.join(", ")}\n\n## DB\n\n- Entities: ${candidates.db?.entities.length || 0}\n- Enums: ${candidates.db?.enums.length || 0}\n- Relations: ${candidates.db?.relations.length || 0}\n\n## API\n\n- Capabilities: ${candidates.api?.capabilities.length || 0}\n- Routes: ${candidates.api?.routes.length || 0}\n- Stacks: ${candidates.api?.stacks?.length ? candidates.api.stacks.join(", ") : "none"}\n\n## UI\n\n- Screens: ${candidates.ui?.screens.length || 0}\n- Routes: ${candidates.ui?.routes.length || 0}\n- Actions: ${candidates.ui?.actions.length || 0}\n- Components: ${candidates.ui?.components.length || 0}\n- Stacks: ${candidates.ui?.stacks?.length ? candidates.ui.stacks.join(", ") : "none"}\n\n## Workflows\n\n- Workflows: ${candidates.workflows?.workflows.length || 0}\n- States: ${candidates.workflows?.workflow_states.length || 0}\n- Transitions: ${candidates.workflows?.workflow_transitions.length || 0}\n\n## Verification\n\n- Verifications: ${candidates.verification?.verifications.length || 0}\n- Scenarios: ${candidates.verification?.scenarios.length || 0}\n- Frameworks: ${candidates.verification?.frameworks?.length ? candidates.verification.frameworks.join(", ") : "none"}\n- Scripts: ${candidates.verification?.scripts.length || 0}\n`
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
return {
|
|
640
|
+
summary,
|
|
641
|
+
files,
|
|
642
|
+
defaultOutDir: context.paths.topogramRoot
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
export { parseImportTracks };
|