@topogram/cli 0.3.63 → 0.3.64
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.d.ts +6 -0
- package/src/adoption/reporting.d.ts +10 -0
- package/src/adoption/review-groups.d.ts +6 -0
- package/src/agent-brief.d.ts +3 -0
- package/src/agent-brief.js +495 -0
- package/src/agent-ops/query-builders.d.ts +26 -0
- package/src/archive/archive.d.ts +2 -0
- package/src/archive/compact.d.ts +1 -0
- package/src/archive/unarchive.d.ts +1 -0
- package/src/catalog.d.ts +10 -0
- package/src/catalog.js +62 -66
- package/src/cli/catalog-alias.d.ts +1 -0
- package/src/cli/command-parser.js +38 -0
- package/src/cli/command-parsers/core.js +102 -0
- package/src/cli/command-parsers/generator.js +39 -0
- package/src/cli/command-parsers/import.js +44 -0
- package/src/cli/command-parsers/legacy-workflow.js +21 -0
- package/src/cli/command-parsers/project.js +47 -0
- package/src/cli/command-parsers/sdlc.js +47 -0
- package/src/cli/command-parsers/shared.js +51 -0
- package/src/cli/command-parsers/template.js +48 -0
- package/src/cli/commands/agent.js +47 -0
- package/src/cli/commands/catalog.js +617 -0
- package/src/cli/commands/check.js +268 -0
- package/src/cli/commands/doctor.js +268 -0
- package/src/cli/commands/emit.js +149 -0
- package/src/cli/commands/generate.js +96 -0
- package/src/cli/commands/generator-policy.js +785 -0
- package/src/cli/commands/generator.js +443 -0
- package/src/cli/commands/import-runner.js +157 -0
- package/src/cli/commands/import.js +1734 -0
- package/src/cli/commands/inspect.js +55 -0
- package/src/cli/commands/new.js +94 -0
- package/src/cli/commands/package.js +815 -0
- package/src/cli/commands/query.js +1302 -0
- package/src/cli/commands/release-rollout.js +257 -0
- package/src/cli/commands/release-shared.js +528 -0
- package/src/cli/commands/release-status.js +429 -0
- package/src/cli/commands/release.js +107 -0
- package/src/cli/commands/sdlc.js +168 -0
- package/src/cli/commands/setup.js +76 -0
- package/src/cli/commands/source.js +291 -0
- package/src/cli/commands/template-runner.js +198 -0
- package/src/cli/commands/template.js +2145 -0
- package/src/cli/commands/trust.js +219 -0
- package/src/cli/commands/version.js +40 -0
- package/src/cli/commands/widget.js +168 -0
- package/src/cli/commands/workflow.js +63 -0
- package/src/cli/dispatcher.js +392 -0
- package/src/cli/help-dispatch.js +188 -0
- package/src/cli/help.js +296 -0
- package/src/cli/migration-guidance.js +59 -0
- package/src/cli/options.js +96 -0
- package/src/cli/output-safety.js +107 -0
- package/src/cli/path-normalization.js +29 -0
- package/src/cli.js +47 -11711
- package/src/example-implementation.d.ts +2 -0
- package/src/format.d.ts +1 -0
- package/src/generator/check.d.ts +1 -0
- package/src/generator/context/bundle.d.ts +1 -0
- package/src/generator/context/shared.d.ts +2 -0
- package/src/generator/native/parity-bundle.js +2 -1
- package/src/generator/surfaces/web/html-escape.js +22 -0
- package/src/generator/surfaces/web/react.js +10 -8
- package/src/generator/surfaces/web/sveltekit.js +7 -5
- package/src/generator/surfaces/web/vanilla.js +8 -4
- package/src/generator.d.ts +2 -0
- package/src/github-client.js +520 -0
- package/src/import/core/shared.js +20 -62
- package/src/import/extractors/api/flutter-dio.js +4 -8
- package/src/import/extractors/api/react-native-repository.js +4 -8
- package/src/import/index.d.ts +4 -0
- package/src/import/provenance.d.ts +4 -0
- package/src/new-project.js +100 -11
- package/src/npm-safety.js +79 -0
- package/src/parser.d.ts +1 -0
- package/src/path-helpers.d.ts +1 -0
- package/src/path-helpers.js +20 -0
- package/src/project-config.js +1 -0
- package/src/reconcile/docs.d.ts +8 -0
- package/src/reconcile/journeys.d.ts +1 -0
- package/src/resolver.d.ts +1 -0
- package/src/runtime-support.js +29 -0
- package/src/sdlc/adopt.d.ts +1 -0
- package/src/sdlc/check.d.ts +1 -0
- package/src/sdlc/explain.d.ts +1 -0
- package/src/sdlc/release.d.ts +1 -0
- package/src/sdlc/scaffold.d.ts +1 -0
- package/src/sdlc/transition.d.ts +1 -0
- package/src/text-helpers.d.ts +6 -0
- package/src/text-helpers.js +245 -0
- package/src/topogram-config.js +306 -0
- package/src/validator.d.ts +2 -0
- package/src/workflows/adoption/index.js +26 -0
- package/src/workflows/docs-generate.js +262 -0
- package/src/workflows/docs-scan.js +703 -0
- package/src/workflows/docs.js +15 -0
- package/src/workflows/import-app/api.js +799 -0
- package/src/workflows/import-app/db.js +538 -0
- package/src/workflows/import-app/index.js +30 -0
- package/src/workflows/import-app/shared.js +218 -0
- package/src/workflows/import-app/ui.js +443 -0
- package/src/workflows/import-app/workflow.js +159 -0
- package/src/workflows/reconcile/adoption-plan.js +742 -0
- package/src/workflows/reconcile/auth.js +692 -0
- package/src/workflows/reconcile/bundle-core.js +600 -0
- package/src/workflows/reconcile/bundle-shared.js +75 -0
- package/src/workflows/reconcile/candidate-model.js +477 -0
- package/src/workflows/reconcile/canonical-surface.js +264 -0
- package/src/workflows/reconcile/gap-report.js +333 -0
- package/src/workflows/reconcile/ids.js +6 -0
- package/src/workflows/reconcile/impacts.js +625 -0
- package/src/workflows/reconcile/index.js +7 -0
- package/src/workflows/reconcile/renderers.js +461 -0
- package/src/workflows/reconcile/summary.js +90 -0
- package/src/workflows/reconcile/workflow.js +309 -0
- package/src/workflows/shared.js +189 -0
- package/src/workflows/types.d.ts +93 -0
- package/src/workflows.d.ts +1 -0
- package/src/workflows.js +10 -7652
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { relativeTo } from "../../path-helpers.js";
|
|
6
|
+
import { canonicalCandidateTerm, idHintify } from "../../text-helpers.js";
|
|
7
|
+
import { listFilesRecursive } from "../shared.js";
|
|
8
|
+
|
|
9
|
+
export const IMPORT_TRACKS = new Set(["db", "api", "ui", "workflows", "verification"]);
|
|
10
|
+
export const SCALAR_FIELD_TYPES = new Set([
|
|
11
|
+
"bigint",
|
|
12
|
+
"boolean",
|
|
13
|
+
"bytes",
|
|
14
|
+
"datetime",
|
|
15
|
+
"decimal",
|
|
16
|
+
"float",
|
|
17
|
+
"int",
|
|
18
|
+
"json",
|
|
19
|
+
"string",
|
|
20
|
+
"text",
|
|
21
|
+
"uuid"
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/** @param {any} fromValue @returns {any} */
|
|
25
|
+
export function parseImportTracks(fromValue) {
|
|
26
|
+
if (!fromValue) {
|
|
27
|
+
return ["db", "api"];
|
|
28
|
+
}
|
|
29
|
+
const tracks = String(fromValue)
|
|
30
|
+
.split(",")
|
|
31
|
+
.map((/** @type {any} */ track) => track.trim().toLowerCase())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
if (tracks.length === 0) {
|
|
34
|
+
throw new Error("Expected --from to include at least one import track");
|
|
35
|
+
}
|
|
36
|
+
const invalid = tracks.filter((/** @type {any} */ track) => !IMPORT_TRACKS.has(track));
|
|
37
|
+
if (invalid.length > 0) {
|
|
38
|
+
throw new Error(`Unsupported import track(s): ${invalid.join(", ")}`);
|
|
39
|
+
}
|
|
40
|
+
return [...new Set(tracks)];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** @param {WorkspacePaths} paths @returns {any} */
|
|
44
|
+
export function importSearchRoots(paths) {
|
|
45
|
+
return [...new Set([paths.workspaceRoot, paths.topogramRoot].filter(Boolean))];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {WorkspacePaths} paths @param {string} filePath @returns {any} */
|
|
49
|
+
export function normalizeImportRelativePath(paths, filePath) {
|
|
50
|
+
return relativeTo(paths.repoRoot, filePath);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @param {WorkspacePaths} paths @param {string} filePath @param {string} kind @returns {any} */
|
|
54
|
+
export function canonicalSourceRank(paths, filePath, kind) {
|
|
55
|
+
const relativePath = normalizeImportRelativePath(paths, filePath);
|
|
56
|
+
const normalizedPath = relativePath.replaceAll(path.sep, "/");
|
|
57
|
+
const penalties = [
|
|
58
|
+
{ pattern: /\/apps\/local-stack\//, weight: 80 },
|
|
59
|
+
{ pattern: /\/artifacts\/environment\//, weight: 60 },
|
|
60
|
+
{ pattern: /\/artifacts\/deploy\//, weight: 60 },
|
|
61
|
+
{ pattern: /\/artifacts\/compile-check\//, weight: 50 },
|
|
62
|
+
{ pattern: /\/artifacts\/db-lifecycle\//, weight: 50 },
|
|
63
|
+
{ pattern: /\/artifacts\/migrations\//, weight: 40 }
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
let rank = 100;
|
|
67
|
+
if (kind === "prisma") {
|
|
68
|
+
if (/\/prisma\/schema\.prisma$/i.test(normalizedPath) && !normalizedPath.includes("/artifacts/")) {
|
|
69
|
+
rank = 0;
|
|
70
|
+
} else if (/\/apps\/backend\/prisma\/schema\.prisma$/i.test(normalizedPath)) {
|
|
71
|
+
rank = 0;
|
|
72
|
+
} else if (/\/artifacts\/prisma\/schema\.prisma$/i.test(normalizedPath)) {
|
|
73
|
+
rank = 10;
|
|
74
|
+
}
|
|
75
|
+
} else if (kind === "sql") {
|
|
76
|
+
if (/\/db\/schema\.sql$/i.test(normalizedPath) || /\/schema\.sql$/i.test(normalizedPath)) {
|
|
77
|
+
rank = 0;
|
|
78
|
+
} else if (/\/artifacts\/db\/.+\.sql$/i.test(normalizedPath)) {
|
|
79
|
+
rank = 10;
|
|
80
|
+
} else if (/migration/i.test(path.basename(normalizedPath))) {
|
|
81
|
+
rank = 30;
|
|
82
|
+
}
|
|
83
|
+
} else if (kind === "openapi") {
|
|
84
|
+
if (/\/artifacts\/openapi\/openapi\.(json|ya?ml)$/i.test(normalizedPath)) {
|
|
85
|
+
rank = 0;
|
|
86
|
+
} else if (/\/openapi\.(json|ya?ml)$/i.test(normalizedPath) || /\/swagger\.(json|ya?ml)$/i.test(normalizedPath)) {
|
|
87
|
+
rank = 10;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const penalty of penalties) {
|
|
92
|
+
if (penalty.pattern.test(normalizedPath)) {
|
|
93
|
+
rank += penalty.weight;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return rank;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @param {WorkspacePaths} paths @param {any[]} files @param {string} kind @returns {any} */
|
|
100
|
+
export function selectPreferredImportFiles(paths, files, kind) {
|
|
101
|
+
if (files.length === 0) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const rankedFiles = files.map((/** @type {any} */ filePath) => ({
|
|
105
|
+
filePath,
|
|
106
|
+
rank: canonicalSourceRank(paths, filePath, kind)
|
|
107
|
+
}));
|
|
108
|
+
const bestRank = Math.min(...rankedFiles.map((/** @type {any} */ entry) => entry.rank));
|
|
109
|
+
return rankedFiles
|
|
110
|
+
.filter((/** @type {any} */ entry) => entry.rank === bestRank)
|
|
111
|
+
.map((/** @type {any} */ entry) => entry.filePath)
|
|
112
|
+
.sort();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @param {WorkspacePaths} paths @param {any} predicate @returns {any} */
|
|
116
|
+
export function findImportFiles(paths, predicate) {
|
|
117
|
+
const files = new Set();
|
|
118
|
+
for (const rootDir of importSearchRoots(paths)) {
|
|
119
|
+
for (const filePath of listFilesRecursive(rootDir, predicate)) {
|
|
120
|
+
if (
|
|
121
|
+
filePath.includes(`${path.sep}candidates${path.sep}`) ||
|
|
122
|
+
filePath.includes(`${path.sep}docs-generated${path.sep}`) ||
|
|
123
|
+
filePath.includes(`${path.sep}topogram${path.sep}tests${path.sep}fixtures${path.sep}expected${path.sep}`)
|
|
124
|
+
) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
files.add(filePath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [...files].sort();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** @param {WorkflowRecord} input @returns {CandidateRecord} */
|
|
134
|
+
export function makeCandidateRecord({
|
|
135
|
+
kind,
|
|
136
|
+
idHint,
|
|
137
|
+
label,
|
|
138
|
+
confidence = "medium",
|
|
139
|
+
sourceKind,
|
|
140
|
+
sourceOfTruth = "imported",
|
|
141
|
+
provenance,
|
|
142
|
+
track = null,
|
|
143
|
+
...payload
|
|
144
|
+
}) {
|
|
145
|
+
const inferredTrack =
|
|
146
|
+
track ||
|
|
147
|
+
(["entity", "enum", "relation", "index"].includes(kind)
|
|
148
|
+
? "db"
|
|
149
|
+
: kind === "capability"
|
|
150
|
+
? "api"
|
|
151
|
+
: null);
|
|
152
|
+
return {
|
|
153
|
+
kind,
|
|
154
|
+
id_hint: idHint,
|
|
155
|
+
label,
|
|
156
|
+
confidence,
|
|
157
|
+
source_kind: sourceKind,
|
|
158
|
+
source_of_truth: sourceOfTruth,
|
|
159
|
+
provenance: Array.isArray(provenance) ? provenance : [provenance].filter(Boolean),
|
|
160
|
+
track: inferredTrack,
|
|
161
|
+
...payload
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** @param {any[]} records @param {any} keyFn @returns {any} */
|
|
166
|
+
export function dedupeCandidateRecords(records, keyFn) {
|
|
167
|
+
const seen = new Map();
|
|
168
|
+
for (const record of records) {
|
|
169
|
+
const key = keyFn(record);
|
|
170
|
+
const recordProvenance = Array.isArray(record.provenance) ? record.provenance : [record.provenance].filter(Boolean);
|
|
171
|
+
if (!seen.has(key)) {
|
|
172
|
+
seen.set(key, { ...record, provenance: recordProvenance });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const current = seen.get(key);
|
|
176
|
+
const currentProvenance = Array.isArray(current.provenance) ? current.provenance : [current.provenance].filter(Boolean);
|
|
177
|
+
current.provenance = [...new Set([...currentProvenance, ...recordProvenance])];
|
|
178
|
+
}
|
|
179
|
+
return [...seen.values()];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** @param {string} pathValue @returns {any} */
|
|
183
|
+
export function normalizeOpenApiPath(pathValue) {
|
|
184
|
+
return String(pathValue || "")
|
|
185
|
+
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "{$1}")
|
|
186
|
+
.replace(/\/+$/, "") || "/";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** @param {string} pathValue @returns {any} */
|
|
190
|
+
export function normalizeEndpointPathForMatch(pathValue) {
|
|
191
|
+
const normalizedPath = normalizeOpenApiPath(pathValue);
|
|
192
|
+
const segments = normalizedPath
|
|
193
|
+
.split("/")
|
|
194
|
+
.filter(Boolean)
|
|
195
|
+
.map((/** @type {any} */ segment) => {
|
|
196
|
+
if (/^\{[^}]+\}$/.test(segment)) {
|
|
197
|
+
return "{}";
|
|
198
|
+
}
|
|
199
|
+
return segment
|
|
200
|
+
.split("-")
|
|
201
|
+
.map((/** @type {any} */ part) => canonicalCandidateTerm(part))
|
|
202
|
+
.join("-");
|
|
203
|
+
});
|
|
204
|
+
return `/${segments.join("/")}`.replace(/\/+$/, "") || "/";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** @param {WorkflowRecord} record @returns {any} */
|
|
208
|
+
export function inferCapabilityEntityId(record) {
|
|
209
|
+
if (record.entity_id) {
|
|
210
|
+
return record.entity_id;
|
|
211
|
+
}
|
|
212
|
+
const pathSegments = normalizeEndpointPathForMatch(record.endpoint?.path || "")
|
|
213
|
+
.split("/")
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.filter((/** @type {any} */ segment) => segment !== "{}");
|
|
216
|
+
const resourceSegment = pathSegments[0] || record.id_hint.replace(/^cap_(create|update|delete|get|list)_/, "");
|
|
217
|
+
return `entity_${idHintify(canonicalCandidateTerm(resourceSegment))}`;
|
|
218
|
+
}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { relativeTo } from "../../path-helpers.js";
|
|
6
|
+
import { canonicalCandidateTerm, idHintify, titleCase } from "../../text-helpers.js";
|
|
7
|
+
import { listFilesRecursive, readTextIfExists } from "../shared.js";
|
|
8
|
+
import { dedupeCandidateRecords, makeCandidateRecord } from "./shared.js";
|
|
9
|
+
|
|
10
|
+
/** @param {string} rootDir @returns {any} */
|
|
11
|
+
export function inferSvelteRoutes(rootDir) {
|
|
12
|
+
const routesRoot = path.join(rootDir, "src", "routes");
|
|
13
|
+
if (!fs.existsSync(routesRoot)) {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
const files = listFilesRecursive(routesRoot, (/** @type {any} */ child) => child.endsWith("+page.svelte") || child.endsWith("+page.ts") || child.endsWith("+page.server.ts"));
|
|
17
|
+
const routes = new Set();
|
|
18
|
+
for (const filePath of files) {
|
|
19
|
+
const relative = relativeTo(routesRoot, filePath)
|
|
20
|
+
.replace(/(^|\/)\+page(\.server|)\.(svelte|ts)$/, "")
|
|
21
|
+
.replace(/\[(.+?)\]/g, ":$1")
|
|
22
|
+
.replace(/^$/, "/");
|
|
23
|
+
routes.add(relative.startsWith("/") ? relative : `/${relative}`);
|
|
24
|
+
}
|
|
25
|
+
return [...routes].sort();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** @param {string} rootDir @returns {any} */
|
|
29
|
+
export function inferReactRoutes(rootDir) {
|
|
30
|
+
const appPath = path.join(rootDir, "src", "App.tsx");
|
|
31
|
+
const text = readTextIfExists(appPath);
|
|
32
|
+
if (!text) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const routes = new Set();
|
|
36
|
+
for (const match of text.matchAll(/path:\s*"([^"]+)"/g)) {
|
|
37
|
+
routes.add(match[1]);
|
|
38
|
+
}
|
|
39
|
+
for (const match of text.matchAll(/path="([^"]+)"/g)) {
|
|
40
|
+
routes.add(match[1]);
|
|
41
|
+
}
|
|
42
|
+
return [...routes].sort();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @param {string} rootDir @returns {any} */
|
|
46
|
+
export function inferNextAppRoutes(rootDir) {
|
|
47
|
+
const appDir = path.join(rootDir, "app");
|
|
48
|
+
if (!fs.existsSync(appDir)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
const routeFiles = listFilesRecursive(
|
|
52
|
+
appDir,
|
|
53
|
+
(/** @type {any} */ child) =>
|
|
54
|
+
/\/page\.(tsx|ts|jsx|js|mdx)$/.test(child) ||
|
|
55
|
+
/\/route\.(tsx|ts|jsx|js)$/.test(child)
|
|
56
|
+
);
|
|
57
|
+
/** @type {any[]} */
|
|
58
|
+
const routes = [];
|
|
59
|
+
for (const filePath of routeFiles) {
|
|
60
|
+
const relative = relativeTo(appDir, filePath);
|
|
61
|
+
const isPage = /\/page\.(tsx|ts|jsx|js|mdx)$/.test(`/${relative}`) || /^page\.(tsx|ts|jsx|js|mdx)$/.test(relative);
|
|
62
|
+
const normalizedPath = `/${relative}`
|
|
63
|
+
.replace(/\/page\.(tsx|ts|jsx|js|mdx)$/, "")
|
|
64
|
+
.replace(/\/route\.(tsx|ts|jsx|js)$/, "")
|
|
65
|
+
.replace(/\[(\.\.\.)?([^\]]+)\]/g, (/** @type {any} */ _match, /** @type {any} */ catchAll, /** @type {any} */ name) => catchAll ? `:${name}*` : `:${name}`)
|
|
66
|
+
.replace(/\/index$/, "")
|
|
67
|
+
.replace(/^\/$/, "/");
|
|
68
|
+
routes.push({
|
|
69
|
+
path: normalizedPath === "" ? "/" : normalizedPath,
|
|
70
|
+
kind: isPage ? "page" : "route",
|
|
71
|
+
file: filePath
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return routes.sort((/** @type {any} */ a, /** @type {any} */ b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @param {string} routePath @returns {any} */
|
|
78
|
+
export function nextScreenKindForRoute(routePath) {
|
|
79
|
+
const normalized = String(routePath || "");
|
|
80
|
+
if (/\/(login|register|setup)$/.test(normalized)) {
|
|
81
|
+
return "flow";
|
|
82
|
+
}
|
|
83
|
+
return screenKindForRoute(routePath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** @param {string} routePath @returns {any} */
|
|
87
|
+
export function nextScreenIdForRoute(routePath) {
|
|
88
|
+
const normalized = String(routePath || "");
|
|
89
|
+
if (normalized === "/") {
|
|
90
|
+
return "home";
|
|
91
|
+
}
|
|
92
|
+
if (/\/login$/.test(normalized)) {
|
|
93
|
+
return "login";
|
|
94
|
+
}
|
|
95
|
+
if (/\/register$/.test(normalized)) {
|
|
96
|
+
return "register";
|
|
97
|
+
}
|
|
98
|
+
if (/\/setup$/.test(normalized)) {
|
|
99
|
+
return "setup";
|
|
100
|
+
}
|
|
101
|
+
return screenIdForRoute(routePath);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** @param {string} routePath @returns {any} */
|
|
105
|
+
export function entityIdForNextRoute(routePath) {
|
|
106
|
+
const normalized = String(routePath || "");
|
|
107
|
+
if (/^\/posts(\/|$)/.test(normalized)) {
|
|
108
|
+
return "entity_post";
|
|
109
|
+
}
|
|
110
|
+
if (/^\/users(\/|$)/.test(normalized)) {
|
|
111
|
+
return "entity_user";
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** @param {string} routePath @returns {any} */
|
|
117
|
+
export function conceptIdForNextRoute(routePath) {
|
|
118
|
+
const normalized = String(routePath || "");
|
|
119
|
+
if (normalized === "/") {
|
|
120
|
+
return "surface_home";
|
|
121
|
+
}
|
|
122
|
+
if (/\/login$/.test(normalized)) {
|
|
123
|
+
return "flow_login";
|
|
124
|
+
}
|
|
125
|
+
if (/\/register$/.test(normalized)) {
|
|
126
|
+
return "flow_register";
|
|
127
|
+
}
|
|
128
|
+
if (/\/setup$/.test(normalized)) {
|
|
129
|
+
return "flow_setup";
|
|
130
|
+
}
|
|
131
|
+
return entityIdForNextRoute(routePath) || entityIdForRoute(routePath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** @param {string} routePath @returns {any} */
|
|
135
|
+
export function uiCapabilityHintsForNextRoute(routePath) {
|
|
136
|
+
const normalized = String(routePath || "");
|
|
137
|
+
if (normalized === "/") {
|
|
138
|
+
return { load: null, submit: null, primary_action: null };
|
|
139
|
+
}
|
|
140
|
+
if (/\/login$/.test(normalized)) {
|
|
141
|
+
return { load: null, submit: "cap_sign_in_user", primary_action: "cap_sign_in_user" };
|
|
142
|
+
}
|
|
143
|
+
if (/\/register$/.test(normalized)) {
|
|
144
|
+
return { load: null, submit: "cap_register_user", primary_action: "cap_register_user" };
|
|
145
|
+
}
|
|
146
|
+
if (/\/setup$/.test(normalized)) {
|
|
147
|
+
return { load: null, submit: null, primary_action: null };
|
|
148
|
+
}
|
|
149
|
+
if (/^\/posts\/new$/.test(normalized)) {
|
|
150
|
+
return { load: null, submit: "cap_create_post", primary_action: "cap_create_post" };
|
|
151
|
+
}
|
|
152
|
+
if (/^\/posts\/:id$/.test(normalized) || /^\/posts\/:[^/]+$/.test(normalized)) {
|
|
153
|
+
return { load: "cap_get_post", submit: null, primary_action: "cap_update_post" };
|
|
154
|
+
}
|
|
155
|
+
if (/^\/posts$/.test(normalized)) {
|
|
156
|
+
return { load: "cap_list_posts", submit: null, primary_action: "cap_create_post" };
|
|
157
|
+
}
|
|
158
|
+
if (/^\/users\/new$/.test(normalized)) {
|
|
159
|
+
return { load: null, submit: "cap_create_user", primary_action: "cap_create_user" };
|
|
160
|
+
}
|
|
161
|
+
return uiCapabilityHintsForRoute(routePath);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** @param {string} routePath @returns {any} */
|
|
165
|
+
export function routeSegments(routePath) {
|
|
166
|
+
return String(routePath || "")
|
|
167
|
+
.split("/")
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.map((/** @type {any} */ segment) => segment.replace(/^:/, ""));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @param {string} routePath @returns {any} */
|
|
173
|
+
export function screenKindForRoute(routePath) {
|
|
174
|
+
const normalized = String(routePath || "");
|
|
175
|
+
const segments = routeSegments(normalized);
|
|
176
|
+
if (/\/new$/.test(normalized)) {
|
|
177
|
+
return "form";
|
|
178
|
+
}
|
|
179
|
+
if (/\/:?[A-Za-z0-9_]+\/edit$/.test(normalized)) {
|
|
180
|
+
return "form";
|
|
181
|
+
}
|
|
182
|
+
if (segments.length >= 2 && !/\/new$/.test(normalized) && !/\/edit$/.test(normalized)) {
|
|
183
|
+
return "detail";
|
|
184
|
+
}
|
|
185
|
+
return "list";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** @param {string} routePath @returns {any} */
|
|
189
|
+
export function screenIdForRoute(routePath) {
|
|
190
|
+
const segments = routeSegments(routePath);
|
|
191
|
+
const resource = canonicalCandidateTerm(segments[0] || "home");
|
|
192
|
+
const kind = screenKindForRoute(routePath);
|
|
193
|
+
if (kind === "form" && /\/new$/.test(routePath)) {
|
|
194
|
+
return `${resource}_create`;
|
|
195
|
+
}
|
|
196
|
+
if (kind === "form" && /\/edit$/.test(routePath)) {
|
|
197
|
+
return `${resource}_edit`;
|
|
198
|
+
}
|
|
199
|
+
if (kind === "detail") {
|
|
200
|
+
return `${resource}_detail`;
|
|
201
|
+
}
|
|
202
|
+
return `${resource}_list`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** @param {string} routePath @returns {any} */
|
|
206
|
+
export function uiCapabilityHintsForRoute(routePath) {
|
|
207
|
+
const segments = routeSegments(routePath);
|
|
208
|
+
const resource = canonicalCandidateTerm(segments[0] || "item");
|
|
209
|
+
const idSegment = segments[1] || null;
|
|
210
|
+
if (/\/new$/.test(routePath)) {
|
|
211
|
+
return { load: null, submit: `cap_create_${resource}`, primary_action: `cap_create_${resource}` };
|
|
212
|
+
}
|
|
213
|
+
if (/\/edit$/.test(routePath)) {
|
|
214
|
+
return { load: `cap_get_${resource}`, submit: `cap_update_${resource}`, primary_action: `cap_update_${resource}` };
|
|
215
|
+
}
|
|
216
|
+
if (idSegment && !/new|edit/.test(idSegment)) {
|
|
217
|
+
return { load: `cap_get_${resource}`, submit: null, primary_action: `cap_update_${resource}` };
|
|
218
|
+
}
|
|
219
|
+
return { load: `cap_list_${resource}s`, submit: null, primary_action: `cap_create_${resource}` };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** @param {string} routePath @returns {any} */
|
|
223
|
+
export function entityIdForRoute(routePath) {
|
|
224
|
+
const segments = routeSegments(routePath);
|
|
225
|
+
return `entity_${canonicalCandidateTerm(segments[0] || "item")}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** @param {WorkspacePaths} paths @returns {any} */
|
|
229
|
+
export function collectUiImport(paths) {
|
|
230
|
+
/** @type {any[]} */
|
|
231
|
+
const findings = [];
|
|
232
|
+
/** @type {WorkflowRecord} */
|
|
233
|
+
const candidates = {
|
|
234
|
+
screens: [],
|
|
235
|
+
routes: [],
|
|
236
|
+
actions: [],
|
|
237
|
+
stacks: []
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const reactRoots = [
|
|
241
|
+
path.join(paths.workspaceRoot, "apps", "web"),
|
|
242
|
+
path.join(paths.workspaceRoot, "examples", "maintained", "proof-app")
|
|
243
|
+
];
|
|
244
|
+
const svelteRoots = [
|
|
245
|
+
path.join(paths.workspaceRoot, "apps", "web-sveltekit"),
|
|
246
|
+
path.join(paths.workspaceRoot, "apps", "local-stack", "web")
|
|
247
|
+
];
|
|
248
|
+
const nextRoots = [paths.workspaceRoot];
|
|
249
|
+
|
|
250
|
+
for (const rootDir of reactRoots) {
|
|
251
|
+
const routes = inferReactRoutes(rootDir);
|
|
252
|
+
if (routes.length === 0) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const provenance = relativeTo(paths.repoRoot, path.join(rootDir, "src", "App.tsx"));
|
|
256
|
+
findings.push({
|
|
257
|
+
kind: "react_screen_routes",
|
|
258
|
+
file: provenance,
|
|
259
|
+
routes
|
|
260
|
+
});
|
|
261
|
+
candidates.stacks.push("react_web");
|
|
262
|
+
for (const routePath of routes) {
|
|
263
|
+
const screenId = screenIdForRoute(routePath);
|
|
264
|
+
const screenKind = screenKindForRoute(routePath);
|
|
265
|
+
const capabilityHints = uiCapabilityHintsForRoute(routePath);
|
|
266
|
+
candidates.screens.push(
|
|
267
|
+
makeCandidateRecord({
|
|
268
|
+
kind: "screen",
|
|
269
|
+
idHint: screenId,
|
|
270
|
+
label: titleCase(screenId),
|
|
271
|
+
confidence: "medium",
|
|
272
|
+
sourceKind: "route_code",
|
|
273
|
+
provenance: `${provenance}#${routePath}`,
|
|
274
|
+
track: "ui",
|
|
275
|
+
entity_id: entityIdForRoute(routePath),
|
|
276
|
+
screen_kind: screenKind,
|
|
277
|
+
route_path: routePath,
|
|
278
|
+
capability_hints: capabilityHints
|
|
279
|
+
})
|
|
280
|
+
);
|
|
281
|
+
candidates.routes.push(
|
|
282
|
+
makeCandidateRecord({
|
|
283
|
+
kind: "ui_route",
|
|
284
|
+
idHint: `${screenId}_route`,
|
|
285
|
+
label: routePath,
|
|
286
|
+
confidence: "medium",
|
|
287
|
+
sourceKind: "route_code",
|
|
288
|
+
provenance: `${provenance}#${routePath}`,
|
|
289
|
+
track: "ui",
|
|
290
|
+
screen_id: screenId,
|
|
291
|
+
entity_id: entityIdForRoute(routePath),
|
|
292
|
+
path: routePath
|
|
293
|
+
})
|
|
294
|
+
);
|
|
295
|
+
if (capabilityHints.primary_action) {
|
|
296
|
+
candidates.actions.push(
|
|
297
|
+
makeCandidateRecord({
|
|
298
|
+
kind: "ui_action",
|
|
299
|
+
idHint: `${screenId}_${idHintify(capabilityHints.primary_action)}`,
|
|
300
|
+
label: capabilityHints.primary_action,
|
|
301
|
+
confidence: "low",
|
|
302
|
+
sourceKind: "route_code",
|
|
303
|
+
provenance: `${provenance}#${routePath}`,
|
|
304
|
+
track: "ui",
|
|
305
|
+
screen_id: screenId,
|
|
306
|
+
entity_id: entityIdForRoute(routePath),
|
|
307
|
+
capability_hint: capabilityHints.primary_action,
|
|
308
|
+
prominence: screenKind === "list" ? "primary" : "secondary"
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const rootDir of svelteRoots) {
|
|
316
|
+
const routes = inferSvelteRoutes(rootDir);
|
|
317
|
+
if (routes.length === 0) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const provenance = relativeTo(paths.repoRoot, path.join(rootDir, "src", "routes"));
|
|
321
|
+
findings.push({
|
|
322
|
+
kind: "sveltekit_screen_routes",
|
|
323
|
+
file: provenance,
|
|
324
|
+
routes
|
|
325
|
+
});
|
|
326
|
+
candidates.stacks.push("sveltekit_web");
|
|
327
|
+
for (const routePath of routes) {
|
|
328
|
+
const screenId = screenIdForRoute(routePath);
|
|
329
|
+
const screenKind = screenKindForRoute(routePath);
|
|
330
|
+
const capabilityHints = uiCapabilityHintsForRoute(routePath);
|
|
331
|
+
candidates.screens.push(
|
|
332
|
+
makeCandidateRecord({
|
|
333
|
+
kind: "screen",
|
|
334
|
+
idHint: screenId,
|
|
335
|
+
label: titleCase(screenId),
|
|
336
|
+
confidence: "medium",
|
|
337
|
+
sourceKind: "route_code",
|
|
338
|
+
provenance: `${provenance}#${routePath}`,
|
|
339
|
+
track: "ui",
|
|
340
|
+
entity_id: entityIdForRoute(routePath),
|
|
341
|
+
screen_kind: screenKind,
|
|
342
|
+
route_path: routePath,
|
|
343
|
+
capability_hints: capabilityHints
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
candidates.routes.push(
|
|
347
|
+
makeCandidateRecord({
|
|
348
|
+
kind: "ui_route",
|
|
349
|
+
idHint: `${screenId}_route`,
|
|
350
|
+
label: routePath,
|
|
351
|
+
confidence: "medium",
|
|
352
|
+
sourceKind: "route_code",
|
|
353
|
+
provenance: `${provenance}#${routePath}`,
|
|
354
|
+
track: "ui",
|
|
355
|
+
screen_id: screenId,
|
|
356
|
+
entity_id: entityIdForRoute(routePath),
|
|
357
|
+
path: routePath
|
|
358
|
+
})
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
for (const rootDir of nextRoots) {
|
|
364
|
+
const routes = inferNextAppRoutes(rootDir);
|
|
365
|
+
if (routes.length === 0) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const provenanceRoot = relativeTo(paths.repoRoot, path.join(rootDir, "app"));
|
|
369
|
+
findings.push({
|
|
370
|
+
kind: "next_app_routes",
|
|
371
|
+
file: provenanceRoot,
|
|
372
|
+
routes: routes.map((/** @type {any} */ route) => route.path)
|
|
373
|
+
});
|
|
374
|
+
candidates.stacks.push("next_app_router");
|
|
375
|
+
for (const route of routes) {
|
|
376
|
+
if (route.kind !== "page") {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const routeProvenance = `${relativeTo(paths.repoRoot, route.file)}#${route.path}`;
|
|
380
|
+
const screenId = nextScreenIdForRoute(route.path);
|
|
381
|
+
const screenKind = nextScreenKindForRoute(route.path);
|
|
382
|
+
const capabilityHints = uiCapabilityHintsForNextRoute(route.path);
|
|
383
|
+
const entityId = entityIdForNextRoute(route.path);
|
|
384
|
+
const conceptId = conceptIdForNextRoute(route.path);
|
|
385
|
+
candidates.screens.push(
|
|
386
|
+
makeCandidateRecord({
|
|
387
|
+
kind: "screen",
|
|
388
|
+
idHint: screenId,
|
|
389
|
+
label: titleCase(screenId),
|
|
390
|
+
confidence: "medium",
|
|
391
|
+
sourceKind: "route_code",
|
|
392
|
+
provenance: routeProvenance,
|
|
393
|
+
track: "ui",
|
|
394
|
+
entity_id: entityId,
|
|
395
|
+
concept_id: conceptId,
|
|
396
|
+
screen_kind: screenKind,
|
|
397
|
+
route_path: route.path,
|
|
398
|
+
capability_hints: capabilityHints
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
candidates.routes.push(
|
|
402
|
+
makeCandidateRecord({
|
|
403
|
+
kind: "ui_route",
|
|
404
|
+
idHint: `${screenId}_route`,
|
|
405
|
+
label: route.path,
|
|
406
|
+
confidence: "medium",
|
|
407
|
+
sourceKind: "route_code",
|
|
408
|
+
provenance: routeProvenance,
|
|
409
|
+
track: "ui",
|
|
410
|
+
screen_id: screenId,
|
|
411
|
+
entity_id: entityId,
|
|
412
|
+
concept_id: conceptId,
|
|
413
|
+
path: route.path
|
|
414
|
+
})
|
|
415
|
+
);
|
|
416
|
+
if (capabilityHints.primary_action) {
|
|
417
|
+
candidates.actions.push(
|
|
418
|
+
makeCandidateRecord({
|
|
419
|
+
kind: "ui_action",
|
|
420
|
+
idHint: `${screenId}_${idHintify(capabilityHints.primary_action)}`,
|
|
421
|
+
label: capabilityHints.primary_action,
|
|
422
|
+
confidence: "low",
|
|
423
|
+
sourceKind: "route_code",
|
|
424
|
+
provenance: routeProvenance,
|
|
425
|
+
track: "ui",
|
|
426
|
+
screen_id: screenId,
|
|
427
|
+
entity_id: entityId,
|
|
428
|
+
concept_id: conceptId,
|
|
429
|
+
capability_hint: capabilityHints.primary_action,
|
|
430
|
+
prominence: screenKind === "list" ? "primary" : "secondary"
|
|
431
|
+
})
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
candidates.screens = dedupeCandidateRecords(candidates.screens, (/** @type {any} */ record) => record.id_hint);
|
|
438
|
+
candidates.routes = dedupeCandidateRecords(candidates.routes, (/** @type {any} */ record) => record.id_hint);
|
|
439
|
+
candidates.actions = dedupeCandidateRecords(candidates.actions, (/** @type {any} */ record) => record.id_hint);
|
|
440
|
+
candidates.stacks = [...new Set(candidates.stacks)].sort();
|
|
441
|
+
|
|
442
|
+
return { findings, candidates };
|
|
443
|
+
}
|