@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.
Files changed (121) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan.d.ts +6 -0
  3. package/src/adoption/reporting.d.ts +10 -0
  4. package/src/adoption/review-groups.d.ts +6 -0
  5. package/src/agent-brief.d.ts +3 -0
  6. package/src/agent-brief.js +495 -0
  7. package/src/agent-ops/query-builders.d.ts +26 -0
  8. package/src/archive/archive.d.ts +2 -0
  9. package/src/archive/compact.d.ts +1 -0
  10. package/src/archive/unarchive.d.ts +1 -0
  11. package/src/catalog.d.ts +10 -0
  12. package/src/catalog.js +62 -66
  13. package/src/cli/catalog-alias.d.ts +1 -0
  14. package/src/cli/command-parser.js +38 -0
  15. package/src/cli/command-parsers/core.js +102 -0
  16. package/src/cli/command-parsers/generator.js +39 -0
  17. package/src/cli/command-parsers/import.js +44 -0
  18. package/src/cli/command-parsers/legacy-workflow.js +21 -0
  19. package/src/cli/command-parsers/project.js +47 -0
  20. package/src/cli/command-parsers/sdlc.js +47 -0
  21. package/src/cli/command-parsers/shared.js +51 -0
  22. package/src/cli/command-parsers/template.js +48 -0
  23. package/src/cli/commands/agent.js +47 -0
  24. package/src/cli/commands/catalog.js +617 -0
  25. package/src/cli/commands/check.js +268 -0
  26. package/src/cli/commands/doctor.js +268 -0
  27. package/src/cli/commands/emit.js +149 -0
  28. package/src/cli/commands/generate.js +96 -0
  29. package/src/cli/commands/generator-policy.js +785 -0
  30. package/src/cli/commands/generator.js +443 -0
  31. package/src/cli/commands/import-runner.js +157 -0
  32. package/src/cli/commands/import.js +1734 -0
  33. package/src/cli/commands/inspect.js +55 -0
  34. package/src/cli/commands/new.js +94 -0
  35. package/src/cli/commands/package.js +815 -0
  36. package/src/cli/commands/query.js +1302 -0
  37. package/src/cli/commands/release-rollout.js +257 -0
  38. package/src/cli/commands/release-shared.js +528 -0
  39. package/src/cli/commands/release-status.js +429 -0
  40. package/src/cli/commands/release.js +107 -0
  41. package/src/cli/commands/sdlc.js +168 -0
  42. package/src/cli/commands/setup.js +76 -0
  43. package/src/cli/commands/source.js +291 -0
  44. package/src/cli/commands/template-runner.js +198 -0
  45. package/src/cli/commands/template.js +2145 -0
  46. package/src/cli/commands/trust.js +219 -0
  47. package/src/cli/commands/version.js +40 -0
  48. package/src/cli/commands/widget.js +168 -0
  49. package/src/cli/commands/workflow.js +63 -0
  50. package/src/cli/dispatcher.js +392 -0
  51. package/src/cli/help-dispatch.js +188 -0
  52. package/src/cli/help.js +296 -0
  53. package/src/cli/migration-guidance.js +59 -0
  54. package/src/cli/options.js +96 -0
  55. package/src/cli/output-safety.js +107 -0
  56. package/src/cli/path-normalization.js +29 -0
  57. package/src/cli.js +47 -11711
  58. package/src/example-implementation.d.ts +2 -0
  59. package/src/format.d.ts +1 -0
  60. package/src/generator/check.d.ts +1 -0
  61. package/src/generator/context/bundle.d.ts +1 -0
  62. package/src/generator/context/shared.d.ts +2 -0
  63. package/src/generator/native/parity-bundle.js +2 -1
  64. package/src/generator/surfaces/web/html-escape.js +22 -0
  65. package/src/generator/surfaces/web/react.js +10 -8
  66. package/src/generator/surfaces/web/sveltekit.js +7 -5
  67. package/src/generator/surfaces/web/vanilla.js +8 -4
  68. package/src/generator.d.ts +2 -0
  69. package/src/github-client.js +520 -0
  70. package/src/import/core/shared.js +20 -62
  71. package/src/import/extractors/api/flutter-dio.js +4 -8
  72. package/src/import/extractors/api/react-native-repository.js +4 -8
  73. package/src/import/index.d.ts +4 -0
  74. package/src/import/provenance.d.ts +4 -0
  75. package/src/new-project.js +100 -11
  76. package/src/npm-safety.js +79 -0
  77. package/src/parser.d.ts +1 -0
  78. package/src/path-helpers.d.ts +1 -0
  79. package/src/path-helpers.js +20 -0
  80. package/src/project-config.js +1 -0
  81. package/src/reconcile/docs.d.ts +8 -0
  82. package/src/reconcile/journeys.d.ts +1 -0
  83. package/src/resolver.d.ts +1 -0
  84. package/src/runtime-support.js +29 -0
  85. package/src/sdlc/adopt.d.ts +1 -0
  86. package/src/sdlc/check.d.ts +1 -0
  87. package/src/sdlc/explain.d.ts +1 -0
  88. package/src/sdlc/release.d.ts +1 -0
  89. package/src/sdlc/scaffold.d.ts +1 -0
  90. package/src/sdlc/transition.d.ts +1 -0
  91. package/src/text-helpers.d.ts +6 -0
  92. package/src/text-helpers.js +245 -0
  93. package/src/topogram-config.js +306 -0
  94. package/src/validator.d.ts +2 -0
  95. package/src/workflows/adoption/index.js +26 -0
  96. package/src/workflows/docs-generate.js +262 -0
  97. package/src/workflows/docs-scan.js +703 -0
  98. package/src/workflows/docs.js +15 -0
  99. package/src/workflows/import-app/api.js +799 -0
  100. package/src/workflows/import-app/db.js +538 -0
  101. package/src/workflows/import-app/index.js +30 -0
  102. package/src/workflows/import-app/shared.js +218 -0
  103. package/src/workflows/import-app/ui.js +443 -0
  104. package/src/workflows/import-app/workflow.js +159 -0
  105. package/src/workflows/reconcile/adoption-plan.js +742 -0
  106. package/src/workflows/reconcile/auth.js +692 -0
  107. package/src/workflows/reconcile/bundle-core.js +600 -0
  108. package/src/workflows/reconcile/bundle-shared.js +75 -0
  109. package/src/workflows/reconcile/candidate-model.js +477 -0
  110. package/src/workflows/reconcile/canonical-surface.js +264 -0
  111. package/src/workflows/reconcile/gap-report.js +333 -0
  112. package/src/workflows/reconcile/ids.js +6 -0
  113. package/src/workflows/reconcile/impacts.js +625 -0
  114. package/src/workflows/reconcile/index.js +7 -0
  115. package/src/workflows/reconcile/renderers.js +461 -0
  116. package/src/workflows/reconcile/summary.js +90 -0
  117. package/src/workflows/reconcile/workflow.js +309 -0
  118. package/src/workflows/shared.js +189 -0
  119. package/src/workflows/types.d.ts +93 -0
  120. package/src/workflows.d.ts +1 -0
  121. package/src/workflows.js +10 -7652
@@ -0,0 +1,538 @@
1
+ // @ts-check
2
+ import path from "node:path";
3
+
4
+ import { relativeTo } from "../../path-helpers.js";
5
+ import { idHintify, slugify, titleCase } from "../../text-helpers.js";
6
+ import { readJsonIfExists, readTextIfExists } from "../shared.js";
7
+ import { dedupeCandidateRecords, findImportFiles, makeCandidateRecord, selectPreferredImportFiles } from "./shared.js";
8
+
9
+ /** @param {any} typeName @returns {any} */
10
+ function normalizePrismaType(typeName) {
11
+ const normalized = String(typeName || "").toLowerCase();
12
+ switch (normalized) {
13
+ case "string":
14
+ return "string";
15
+ case "int":
16
+ return "int";
17
+ case "bigint":
18
+ return "bigint";
19
+ case "float":
20
+ return "float";
21
+ case "decimal":
22
+ return "decimal";
23
+ case "boolean":
24
+ case "bool":
25
+ return "boolean";
26
+ case "datetime":
27
+ return "datetime";
28
+ case "bytes":
29
+ return "bytes";
30
+ case "json":
31
+ return "json";
32
+ default:
33
+ return typeName;
34
+ }
35
+ }
36
+
37
+ /** @param {any} schemaText @returns {any} */
38
+ function parsePrismaSchema(schemaText) {
39
+ /** @type {any[]} */
40
+ const enums = [];
41
+ /** @type {any[]} */
42
+ const entities = [];
43
+ /** @type {any[]} */
44
+ const relations = [];
45
+ /** @type {any[]} */
46
+ const indexes = [];
47
+ const enumNames = new Set();
48
+ /** @type {any[]} */
49
+ const modelNames = [];
50
+
51
+ for (const match of schemaText.matchAll(/^enum\s+([A-Za-z0-9_]+)\s*\{([\s\S]*?)^\}/gm)) {
52
+ const [, enumName, body] = match;
53
+ const values = body
54
+ .split(/\r?\n/)
55
+ .map((/** @type {any} */ line) => line.replace(/\/\/.*$/, "").trim())
56
+ .filter((/** @type {any} */ line) => line && !line.startsWith("@@"))
57
+ .map((/** @type {any} */ line) => line.split(/\s+/)[0]);
58
+ enumNames.add(enumName);
59
+ enums.push({ name: enumName, values });
60
+ }
61
+
62
+ for (const match of schemaText.matchAll(/^model\s+([A-Za-z0-9_]+)\s*\{/gm)) {
63
+ modelNames.push(match[1]);
64
+ }
65
+ const modelNameSet = new Set(modelNames);
66
+
67
+ for (const match of schemaText.matchAll(/^model\s+([A-Za-z0-9_]+)\s*\{([\s\S]*?)^\}/gm)) {
68
+ const [, modelName, body] = match;
69
+ /** @type {any[]} */
70
+ const fields = [];
71
+ /** @type {any[]} */
72
+ const localIndexes = [];
73
+ const lines = body
74
+ .split(/\r?\n/)
75
+ .map((/** @type {any} */ line) => line.replace(/\/\/.*$/, "").trim())
76
+ .filter(Boolean);
77
+
78
+ for (const line of lines) {
79
+ if (line.startsWith("@@")) {
80
+ const indexMatch = line.match(/^@@(unique|index)\(\[([^\]]+)\]/);
81
+ if (indexMatch) {
82
+ const [, type, rawFields] = indexMatch;
83
+ localIndexes.push({
84
+ id_hint: `index_${slugify(`${modelName}_${rawFields}`)}`,
85
+ fields: rawFields.split(",").map((/** @type {any} */ field) => field.trim()),
86
+ unique: type === "unique"
87
+ });
88
+ }
89
+ continue;
90
+ }
91
+
92
+ const fieldMatch = line.match(/^([A-Za-z0-9_]+)\s+([^\s]+)(.*)$/);
93
+ if (!fieldMatch) {
94
+ continue;
95
+ }
96
+ const [, fieldName, rawTypeToken, remainder] = fieldMatch;
97
+ const list = rawTypeToken.endsWith("[]");
98
+ const optional = rawTypeToken.endsWith("?");
99
+ const baseType = rawTypeToken.replace(/\?|\[\]/g, "");
100
+ const referencesModel = modelNameSet.has(baseType) && !enumNames.has(baseType);
101
+ const hasRelationDirective = remainder.includes("@relation(");
102
+
103
+ if (referencesModel && hasRelationDirective) {
104
+ const relationMatch = remainder.match(/@relation\(([^)]*)\)/);
105
+ const relationArgs = relationMatch?.[1] || "";
106
+ const fieldsMatch = relationArgs.match(/fields:\s*\[([^\]]+)\]/);
107
+ const refsMatch = relationArgs.match(/references:\s*\[([^\]]+)\]/);
108
+ relations.push({
109
+ from_entity: `entity_${slugify(modelName)}`,
110
+ to_entity: `entity_${slugify(baseType)}`,
111
+ relation_field: fieldName,
112
+ fields: fieldsMatch ? fieldsMatch[1].split(",").map((/** @type {any} */ field) => field.trim()) : [],
113
+ references: refsMatch ? refsMatch[1].split(",").map((/** @type {any} */ field) => field.trim()) : []
114
+ });
115
+ continue;
116
+ }
117
+
118
+ if (referencesModel) {
119
+ continue;
120
+ }
121
+
122
+ const fieldType = enumNames.has(baseType) ? baseType : normalizePrismaType(baseType);
123
+ fields.push({
124
+ name: fieldName,
125
+ field_type: fieldType,
126
+ required: !optional && !list,
127
+ list,
128
+ unique: /@unique\b/.test(remainder),
129
+ primary_key: /@id\b/.test(remainder)
130
+ });
131
+
132
+ if (/@unique\b/.test(remainder)) {
133
+ localIndexes.push({
134
+ id_hint: `index_${slugify(`${modelName}_${fieldName}_unique`)}`,
135
+ fields: [fieldName],
136
+ unique: true
137
+ });
138
+ }
139
+ }
140
+
141
+ entities.push({ name: modelName, fields });
142
+ indexes.push(...localIndexes.map((/** @type {any} */ index) => ({ ...index, entity: `entity_${slugify(modelName)}` })));
143
+ }
144
+
145
+ return { entities, enums, relations, indexes };
146
+ }
147
+
148
+ /** @param {string} body @returns {any} */
149
+ function splitSqlSegments(body) {
150
+ return body
151
+ .split(/,\s*\n/)
152
+ .map((/** @type {any} */ segment) => segment.trim())
153
+ .filter(Boolean);
154
+ }
155
+
156
+ /** @param {any} sqlText @returns {any} */
157
+ function parseSqlSchema(sqlText) {
158
+ /** @type {any[]} */
159
+ const entities = [];
160
+ /** @type {any[]} */
161
+ const enums = [];
162
+ /** @type {any[]} */
163
+ const relations = [];
164
+ /** @type {any[]} */
165
+ const indexes = [];
166
+
167
+ for (const match of sqlText.matchAll(/CREATE\s+TYPE\s+([A-Za-z0-9_"]+)\s+AS\s+ENUM\s*\(([\s\S]*?)\);/gi)) {
168
+ const enumName = match[1].replace(/"/g, "");
169
+ const values = [...match[2].matchAll(/'([^']+)'/g)].map((/** @type {any} */ valueMatch) => valueMatch[1]);
170
+ enums.push({ name: enumName, values });
171
+ }
172
+
173
+ for (const match of sqlText.matchAll(/CREATE\s+TABLE\s+([A-Za-z0-9_"]+)\s*\(([\s\S]*?)\);/gi)) {
174
+ const tableName = match[1].replace(/"/g, "");
175
+ const entityId = `entity_${slugify(tableName.replace(/s$/, ""))}`;
176
+ /** @type {any[]} */
177
+ const fields = [];
178
+ for (const segment of splitSqlSegments(match[2])) {
179
+ if (/^(PRIMARY\s+KEY|UNIQUE|CONSTRAINT|FOREIGN\s+KEY)/i.test(segment)) {
180
+ const foreignKeyMatch = segment.match(/FOREIGN\s+KEY\s*\(([^)]+)\)\s+REFERENCES\s+([A-Za-z0-9_"]+)\s*\(([^)]+)\)/i);
181
+ if (foreignKeyMatch) {
182
+ relations.push({
183
+ from_entity: entityId,
184
+ to_entity: `entity_${slugify(foreignKeyMatch[2].replace(/"/g, "").replace(/s$/, ""))}`,
185
+ relation_field: foreignKeyMatch[1].replace(/"/g, "").trim(),
186
+ fields: foreignKeyMatch[1].split(",").map((/** @type {any} */ field) => field.replace(/"/g, "").trim()),
187
+ references: foreignKeyMatch[3].split(",").map((/** @type {any} */ field) => field.replace(/"/g, "").trim())
188
+ });
189
+ }
190
+ const uniqueMatch = segment.match(/UNIQUE\s*\(([^)]+)\)/i);
191
+ if (uniqueMatch) {
192
+ indexes.push({
193
+ entity: entityId,
194
+ id_hint: `index_${slugify(`${tableName}_${uniqueMatch[1]}`)}`,
195
+ fields: uniqueMatch[1].split(",").map((/** @type {any} */ field) => field.replace(/"/g, "").trim()),
196
+ unique: true
197
+ });
198
+ }
199
+ continue;
200
+ }
201
+ const fieldMatch = segment.match(/^"?([A-Za-z0-9_]+)"?\s+([A-Za-z0-9_()[\]]+)(.*)$/i);
202
+ if (!fieldMatch) {
203
+ continue;
204
+ }
205
+ const [, fieldName, rawType, remainder] = fieldMatch;
206
+ fields.push({
207
+ name: fieldName,
208
+ field_type: normalizePrismaType(rawType.replace(/\(.+$/, "")),
209
+ required: /NOT\s+NULL/i.test(remainder),
210
+ list: false,
211
+ unique: /\bUNIQUE\b/i.test(remainder),
212
+ primary_key: /\bPRIMARY\s+KEY\b/i.test(remainder)
213
+ });
214
+ const inlineReferenceMatch = remainder.match(/REFERENCES\s+([A-Za-z0-9_"]+)\s*\(([^)]+)\)/i);
215
+ if (inlineReferenceMatch) {
216
+ relations.push({
217
+ from_entity: entityId,
218
+ to_entity: `entity_${slugify(inlineReferenceMatch[1].replace(/"/g, "").replace(/s$/, ""))}`,
219
+ relation_field: fieldName,
220
+ fields: [fieldName],
221
+ references: inlineReferenceMatch[2].split(",").map((/** @type {any} */ field) => field.replace(/"/g, "").trim())
222
+ });
223
+ }
224
+ if (/\bUNIQUE\b/i.test(remainder)) {
225
+ indexes.push({
226
+ entity: entityId,
227
+ id_hint: `index_${slugify(`${tableName}_${fieldName}_unique`)}`,
228
+ fields: [fieldName],
229
+ unique: true
230
+ });
231
+ }
232
+ }
233
+ entities.push({ name: tableName.replace(/s$/, ""), table_name: tableName, fields });
234
+ }
235
+
236
+ for (const match of sqlText.matchAll(/CREATE\s+(UNIQUE\s+)?INDEX\s+([A-Za-z0-9_"]+)\s+ON\s+([A-Za-z0-9_"]+)\s*\(([^)]+)\)/gi)) {
237
+ indexes.push({
238
+ entity: `entity_${slugify(match[3].replace(/"/g, "").replace(/s$/, ""))}`,
239
+ id_hint: `index_${slugify(match[2].replace(/"/g, ""))}`,
240
+ fields: match[4].split(",").map((/** @type {any} */ field) => field.replace(/"/g, "").trim()),
241
+ unique: Boolean(match[1])
242
+ });
243
+ }
244
+
245
+ return { entities, enums, relations, indexes };
246
+ }
247
+
248
+ /** @param {any} snapshot @returns {any} */
249
+ function parseDbSchemaSnapshot(snapshot) {
250
+ return {
251
+ entities: (snapshot.tables || []).map((/** @type {any} */ table) => ({
252
+ name: table.entity?.name || table.table.replace(/s$/, ""),
253
+ table_name: table.table,
254
+ fields: (table.columns || []).map((/** @type {any} */ column) => ({
255
+ name: column.name,
256
+ field_type: column.type,
257
+ required: !column.nullable,
258
+ list: false,
259
+ unique: false,
260
+ primary_key: false
261
+ }))
262
+ })),
263
+ enums: (snapshot.enums || []).map((/** @type {any} */ entry) => ({
264
+ name: entry.name || entry.id,
265
+ values: entry.values || []
266
+ })),
267
+ relations: (snapshot.tables || []).flatMap((/** @type {any} */ table) =>
268
+ (table.foreignKeys || []).map((/** @type {any} */ foreignKey) => ({
269
+ from_entity: table.entity?.id || `entity_${slugify(table.table.replace(/s$/, ""))}`,
270
+ to_entity: foreignKey.references?.id || foreignKey.reference?.id || `entity_${slugify((foreignKey.references?.table || "").replace(/s$/, ""))}`,
271
+ relation_field: foreignKey.columns?.[0] || "",
272
+ fields: foreignKey.columns || [],
273
+ references: foreignKey.references?.columns || []
274
+ }))
275
+ ),
276
+ indexes: (snapshot.tables || []).flatMap((/** @type {any} */ table) =>
277
+ (table.indexes || []).map((/** @type {any} */ index) => ({
278
+ entity: table.entity?.id || `entity_${slugify(table.table.replace(/s$/, ""))}`,
279
+ id_hint: `index_${slugify(index.name || `${table.table}_${(index.columns || []).join("_")}`)}`,
280
+ fields: index.columns || [],
281
+ unique: Boolean(index.unique)
282
+ }))
283
+ )
284
+ };
285
+ }
286
+
287
+ /** @param {WorkspacePaths} paths @returns {any} */
288
+ export function discoverDbSources(paths) {
289
+ const allPrismaFiles = findImportFiles(paths, (/** @type {any} */ filePath) => filePath.endsWith(path.join("prisma", "schema.prisma")) || filePath.endsWith("/prisma/schema.prisma"));
290
+ const allSqlFiles = findImportFiles(paths, (/** @type {any} */ filePath) => filePath.endsWith(".sql") && /(schema|migration|migrations|db)/i.test(filePath));
291
+ const snapshotFiles = findImportFiles(paths, (/** @type {any} */ filePath) => filePath.endsWith(".db-schema-snapshot.json"));
292
+ const prismaFiles = selectPreferredImportFiles(paths, allPrismaFiles, "prisma");
293
+ const schemaSqlFiles = allSqlFiles.filter((/** @type {any} */ filePath) => !/migration/i.test(path.basename(filePath)));
294
+ const migrationSqlFiles = allSqlFiles.filter((/** @type {any} */ filePath) => /migration/i.test(path.basename(filePath)));
295
+ const sqlFiles =
296
+ prismaFiles.length > 0
297
+ ? []
298
+ : schemaSqlFiles.length > 0
299
+ ? selectPreferredImportFiles(paths, schemaSqlFiles, "sql")
300
+ : selectPreferredImportFiles(paths, migrationSqlFiles, "sql");
301
+ return { prismaFiles, sqlFiles, snapshotFiles };
302
+ }
303
+
304
+ /** @param {WorkspacePaths} paths @returns {any} */
305
+ export function collectDbImport(paths) {
306
+ /** @type {any[]} */
307
+ const findings = [];
308
+ /** @type {WorkflowRecord} */
309
+ const candidates = {
310
+ entities: [],
311
+ enums: [],
312
+ relations: [],
313
+ indexes: []
314
+ };
315
+ const { prismaFiles, sqlFiles, snapshotFiles } = discoverDbSources(paths);
316
+ let hasPrimarySchemaSource = false;
317
+
318
+ for (const filePath of prismaFiles) {
319
+ const parsed = parsePrismaSchema(readTextIfExists(filePath) || "");
320
+ const provenance = relativeTo(paths.repoRoot, filePath);
321
+ hasPrimarySchemaSource = true;
322
+ findings.push({
323
+ kind: "prisma_schema",
324
+ file: provenance,
325
+ entity_count: parsed.entities.length,
326
+ enum_count: parsed.enums.length
327
+ });
328
+ candidates.entities.push(
329
+ ...parsed.entities.map((/** @type {any} */ entity) =>
330
+ makeCandidateRecord({
331
+ kind: "entity",
332
+ idHint: `entity_${slugify(entity.name)}`,
333
+ label: titleCase(entity.name),
334
+ confidence: "high",
335
+ sourceKind: "schema",
336
+ provenance,
337
+ table_name: slugify(entity.table_name || entity.name),
338
+ fields: entity.fields
339
+ })
340
+ )
341
+ );
342
+ candidates.enums.push(
343
+ ...parsed.enums.map((/** @type {any} */ entry) =>
344
+ makeCandidateRecord({
345
+ kind: "enum",
346
+ idHint: idHintify(entry.name),
347
+ label: titleCase(entry.name),
348
+ confidence: "high",
349
+ sourceKind: "schema",
350
+ provenance,
351
+ values: entry.values
352
+ })
353
+ )
354
+ );
355
+ candidates.relations.push(
356
+ ...parsed.relations.map((/** @type {any} */ relation) =>
357
+ makeCandidateRecord({
358
+ kind: "relation",
359
+ idHint: slugify(`${relation.from_entity}_${relation.relation_field}_${relation.to_entity}`),
360
+ label: `${relation.from_entity} -> ${relation.to_entity}`,
361
+ confidence: "high",
362
+ sourceKind: "schema",
363
+ provenance,
364
+ ...relation
365
+ })
366
+ )
367
+ );
368
+ candidates.indexes.push(
369
+ ...parsed.indexes.map((/** @type {any} */ index) =>
370
+ makeCandidateRecord({
371
+ kind: "index",
372
+ idHint: index.id_hint,
373
+ label: titleCase(index.id_hint.replace(/^index_/, "")),
374
+ confidence: "medium",
375
+ sourceKind: "schema",
376
+ provenance,
377
+ entity: index.entity,
378
+ fields: index.fields,
379
+ unique: index.unique
380
+ })
381
+ )
382
+ );
383
+ }
384
+
385
+ for (const filePath of sqlFiles) {
386
+ const parsed = parseSqlSchema(readTextIfExists(filePath) || "");
387
+ const provenance = relativeTo(paths.repoRoot, filePath);
388
+ hasPrimarySchemaSource = true;
389
+ findings.push({
390
+ kind: "sql_schema",
391
+ file: provenance,
392
+ entity_count: parsed.entities.length,
393
+ enum_count: parsed.enums.length
394
+ });
395
+ candidates.entities.push(
396
+ ...parsed.entities.map((/** @type {any} */ entity) =>
397
+ makeCandidateRecord({
398
+ kind: "entity",
399
+ idHint: `entity_${slugify(entity.name)}`,
400
+ label: titleCase(entity.name),
401
+ confidence: /migration/i.test(filePath) ? "medium" : "high",
402
+ sourceKind: /migration/i.test(filePath) ? "migration" : "schema",
403
+ provenance,
404
+ table_name: entity.table_name || slugify(entity.name),
405
+ fields: entity.fields
406
+ })
407
+ )
408
+ );
409
+ candidates.enums.push(
410
+ ...parsed.enums.map((/** @type {any} */ entry) =>
411
+ makeCandidateRecord({
412
+ kind: "enum",
413
+ idHint: idHintify(entry.name),
414
+ label: titleCase(entry.name),
415
+ confidence: /migration/i.test(filePath) ? "medium" : "high",
416
+ sourceKind: /migration/i.test(filePath) ? "migration" : "schema",
417
+ provenance,
418
+ values: entry.values
419
+ })
420
+ )
421
+ );
422
+ candidates.relations.push(
423
+ ...parsed.relations.map((/** @type {any} */ relation) =>
424
+ makeCandidateRecord({
425
+ kind: "relation",
426
+ idHint: slugify(`${relation.from_entity}_${relation.relation_field}_${relation.to_entity}`),
427
+ label: `${relation.from_entity} -> ${relation.to_entity}`,
428
+ confidence: "medium",
429
+ sourceKind: /migration/i.test(filePath) ? "migration" : "schema",
430
+ provenance,
431
+ ...relation
432
+ })
433
+ )
434
+ );
435
+ candidates.indexes.push(
436
+ ...parsed.indexes.map((/** @type {any} */ index) =>
437
+ makeCandidateRecord({
438
+ kind: "index",
439
+ idHint: index.id_hint,
440
+ label: titleCase(index.id_hint.replace(/^index_/, "")),
441
+ confidence: "medium",
442
+ sourceKind: /migration/i.test(filePath) ? "migration" : "schema",
443
+ provenance,
444
+ entity: index.entity,
445
+ fields: index.fields,
446
+ unique: index.unique
447
+ })
448
+ )
449
+ );
450
+ }
451
+
452
+ if (!hasPrimarySchemaSource) {
453
+ for (const filePath of snapshotFiles) {
454
+ const snapshot = readJsonIfExists(filePath);
455
+ if (!snapshot) {
456
+ continue;
457
+ }
458
+ const parsed = parseDbSchemaSnapshot(snapshot);
459
+ const provenance = relativeTo(paths.repoRoot, filePath);
460
+ findings.push({
461
+ kind: "db_schema_snapshot",
462
+ file: provenance,
463
+ entity_count: parsed.entities.length,
464
+ enum_count: parsed.enums.length
465
+ });
466
+ candidates.entities.push(
467
+ ...parsed.entities.map((/** @type {any} */ entity) =>
468
+ makeCandidateRecord({
469
+ kind: "entity",
470
+ idHint: `entity_${slugify(entity.name)}`,
471
+ label: titleCase(entity.name),
472
+ confidence: "medium",
473
+ sourceKind: "generated_artifact",
474
+ provenance,
475
+ table_name: entity.table_name || slugify(entity.name),
476
+ fields: entity.fields
477
+ })
478
+ )
479
+ );
480
+ candidates.enums.push(
481
+ ...parsed.enums.map((/** @type {any} */ entry) =>
482
+ makeCandidateRecord({
483
+ kind: "enum",
484
+ idHint: idHintify(entry.name),
485
+ label: titleCase(entry.name),
486
+ confidence: "medium",
487
+ sourceKind: "generated_artifact",
488
+ provenance,
489
+ values: entry.values
490
+ })
491
+ )
492
+ );
493
+ candidates.relations.push(
494
+ ...parsed.relations.map((/** @type {any} */ relation) =>
495
+ makeCandidateRecord({
496
+ kind: "relation",
497
+ idHint: slugify(`${relation.from_entity}_${relation.relation_field}_${relation.to_entity}`),
498
+ label: `${relation.from_entity} -> ${relation.to_entity}`,
499
+ confidence: "medium",
500
+ sourceKind: "generated_artifact",
501
+ provenance,
502
+ ...relation
503
+ })
504
+ )
505
+ );
506
+ candidates.indexes.push(
507
+ ...parsed.indexes.map((/** @type {any} */ index) =>
508
+ makeCandidateRecord({
509
+ kind: "index",
510
+ idHint: index.id_hint,
511
+ label: titleCase(index.id_hint.replace(/^index_/, "")),
512
+ confidence: "medium",
513
+ sourceKind: "generated_artifact",
514
+ provenance,
515
+ entity: index.entity,
516
+ fields: index.fields,
517
+ unique: index.unique
518
+ })
519
+ )
520
+ );
521
+ }
522
+ } else {
523
+ for (const filePath of snapshotFiles) {
524
+ findings.push({
525
+ kind: "db_schema_snapshot",
526
+ file: relativeTo(paths.repoRoot, filePath),
527
+ used_as_primary: false
528
+ });
529
+ }
530
+ }
531
+
532
+ candidates.entities = dedupeCandidateRecords(candidates.entities, (/** @type {any} */ record) => record.id_hint);
533
+ candidates.enums = dedupeCandidateRecords(candidates.enums, (/** @type {any} */ record) => record.id_hint);
534
+ candidates.relations = dedupeCandidateRecords(candidates.relations, (/** @type {any} */ record) => record.id_hint);
535
+ candidates.indexes = dedupeCandidateRecords(candidates.indexes, (/** @type {any} */ record) => record.id_hint);
536
+
537
+ return { findings, candidates };
538
+ }
@@ -0,0 +1,30 @@
1
+ // @ts-check
2
+ import { runImportAppWorkflow } from "../../import/index.js";
3
+ import { scanDocsWorkflow } from "../docs.js";
4
+
5
+ export { collectApiImport, discoverApiSources } from "./api.js";
6
+ export { collectDbImport, discoverDbSources } from "./db.js";
7
+ export {
8
+ IMPORT_TRACKS,
9
+ SCALAR_FIELD_TYPES,
10
+ dedupeCandidateRecords,
11
+ findImportFiles,
12
+ importSearchRoots,
13
+ inferCapabilityEntityId,
14
+ makeCandidateRecord,
15
+ normalizeEndpointPathForMatch,
16
+ normalizeImportRelativePath,
17
+ normalizeOpenApiPath,
18
+ parseImportTracks,
19
+ selectPreferredImportFiles
20
+ } from "./shared.js";
21
+ export { collectUiImport } from "./ui.js";
22
+ export { collectWorkflowImport } from "./workflow.js";
23
+
24
+ /** @param {string} inputPath @param {WorkflowOptions} options @returns {any} */
25
+ export function importAppWorkflow(inputPath, options = {}) {
26
+ return runImportAppWorkflow(inputPath, {
27
+ ...options,
28
+ scanDocsSummary: () => scanDocsWorkflow(inputPath).summary
29
+ });
30
+ }