@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,264 @@
1
+ // @ts-check
2
+ import { generateApiContractGraph } from "../../generator/api.js";
3
+ import { confidenceRank } from "../docs.js";
4
+ import { normalizeEndpointPathForMatch, normalizeOpenApiPath } from "../import-app/index.js";
5
+
6
+ /** @param {any} importedEntity @param {any} graphEntity @returns {any} */
7
+ export function compareEntityFields(importedEntity, graphEntity) {
8
+ const graphFields = new Map((graphEntity.fields || []).map((/** @type {any} */ field) => [field.name, field]));
9
+ /** @type {any[]} */
10
+ const missing = [];
11
+ /** @type {any[]} */
12
+ const typeMismatches = [];
13
+ /** @type {any[]} */
14
+ const requiredMismatches = [];
15
+ for (const field of importedEntity.fields || []) {
16
+ const graphField = graphFields.get(field.name);
17
+ if (!graphField) {
18
+ missing.push(field.name);
19
+ continue;
20
+ }
21
+ if (String(graphField.fieldType) !== String(field.field_type)) {
22
+ typeMismatches.push({
23
+ field: field.name,
24
+ imported: field.field_type,
25
+ topogram: graphField.fieldType
26
+ });
27
+ }
28
+ if (Boolean(graphField.required) !== Boolean(field.required)) {
29
+ requiredMismatches.push({
30
+ field: field.name,
31
+ imported: Boolean(field.required),
32
+ topogram: Boolean(graphField.required)
33
+ });
34
+ }
35
+ }
36
+ return { missing, typeMismatches, requiredMismatches };
37
+ }
38
+
39
+ /** @param {string} value @returns {any} */
40
+ export function normalizeApiCandidateId(value) {
41
+ return String(value || "").trim().toLowerCase();
42
+ }
43
+
44
+ /** @param {any[]} fields @param {any} jsonSchema @returns {any} */
45
+ export function collectContractFieldNames(fields, jsonSchema) {
46
+ const names = new Set((fields || []).map((/** @type {any} */ field) => field.name).filter(Boolean));
47
+ for (const propertyName of Object.keys(jsonSchema?.properties || {})) {
48
+ names.add(propertyName);
49
+ }
50
+ return [...names].sort();
51
+ }
52
+
53
+ /** @param {ResolvedGraph} graph @returns {any} */
54
+ export function buildTopogramApiCapabilityIndex(graph) {
55
+ const contracts = generateApiContractGraph(graph);
56
+ /** @type {any[]} */
57
+ const records = [];
58
+ for (const capability of graph.byKind.capability || []) {
59
+ const contract = contracts[capability.id];
60
+ if (!contract) {
61
+ continue;
62
+ }
63
+ records.push({
64
+ id: capability.id,
65
+ endpoint: {
66
+ method: contract.endpoint.method,
67
+ path: normalizeOpenApiPath(contract.endpoint.path)
68
+ },
69
+ input_fields: collectContractFieldNames(contract.requestContract?.fields, contract.requestContract?.jsonSchema),
70
+ output_fields: collectContractFieldNames(contract.responseContract?.fields, contract.responseContract?.jsonSchema),
71
+ path_params: (contract.requestContract?.transport?.path || []).map((/** @type {any} */ field) => field.name).filter(Boolean).sort(),
72
+ query_params: (contract.requestContract?.transport?.query || []).map((/** @type {any} */ field) => field.name).filter(Boolean).sort()
73
+ });
74
+ }
75
+ return records;
76
+ }
77
+
78
+ /** @param {any} importedCapability @param {any[]} topogramCapabilities @returns {any} */
79
+ export function matchImportedApiCapability(importedCapability, topogramCapabilities) {
80
+ const importedId = normalizeApiCandidateId(importedCapability.id_hint);
81
+ const importedMethod = String(importedCapability.endpoint?.method || "").toUpperCase();
82
+ const importedPath = normalizeEndpointPathForMatch(importedCapability.endpoint?.path || "");
83
+ return topogramCapabilities.find((/** @type {any} */ capability) =>
84
+ normalizeApiCandidateId(capability.id) === importedId ||
85
+ (capability.endpoint.method === importedMethod && normalizeEndpointPathForMatch(capability.endpoint.path) === importedPath)
86
+ ) || null;
87
+ }
88
+
89
+ /** @param {any} importedCapability @param {any} topogramCapability @returns {any} */
90
+ export function compareApiCapabilityFields(importedCapability, topogramCapability) {
91
+ const missingInputFields = (importedCapability.input_fields || []).filter((/** @type {any} */ field) => !topogramCapability.input_fields.includes(field));
92
+ const missingOutputFields = (importedCapability.output_fields || []).filter((/** @type {any} */ field) => !topogramCapability.output_fields.includes(field));
93
+ const missingPathParams = (importedCapability.path_params || []).map((/** @type {any} */ entry) => entry.name).filter((/** @type {any} */ name) => !topogramCapability.path_params.includes(name));
94
+ const missingQueryParams = (importedCapability.query_params || []).map((/** @type {any} */ entry) => entry.name).filter((/** @type {any} */ name) => !topogramCapability.query_params.includes(name));
95
+ return {
96
+ missing_input_fields_in_topogram: missingInputFields,
97
+ missing_output_fields_in_topogram: missingOutputFields,
98
+ missing_path_params_in_topogram: missingPathParams,
99
+ missing_query_params_in_topogram: missingQueryParams
100
+ };
101
+ }
102
+
103
+ /** @param {ResolvedGraph} graph @returns {any} */
104
+ export function collectCanonicalUiSurface(graph) {
105
+ const screens = new Set();
106
+ const routes = new Set();
107
+ for (const projection of graph.byKind.projection || []) {
108
+ if (!["ui_contract", "web_surface"].includes(projection.type)) {
109
+ continue;
110
+ }
111
+ for (const screen of projection.uiScreens || []) {
112
+ screens.add(screen.id);
113
+ }
114
+ for (const route of projection.uiRoutes || []) {
115
+ routes.add(route.path);
116
+ }
117
+ }
118
+ return {
119
+ screens: [...screens].sort(),
120
+ routes: [...routes].sort()
121
+ };
122
+ }
123
+
124
+ /** @param {ResolvedGraph} graph @returns {any} */
125
+ export function collectCanonicalWorkflowSurface(graph) {
126
+ const docs = (graph.docs || []).filter((/** @type {any} */ doc) => doc.kind === "workflow");
127
+ const decisions = (graph.byKind.decision || []).map((/** @type {any} */ decision) => decision.id);
128
+ return {
129
+ workflow_docs: docs.map((/** @type {any} */ doc) => doc.id).sort(),
130
+ decisions: decisions.sort()
131
+ };
132
+ }
133
+
134
+ /** @param {ResolvedGraph} graph @returns {any} */
135
+ export function collectCanonicalActorRoleSurface(graph) {
136
+ const journeyDocs = (graph.docs || []).filter((/** @type {any} */ doc) => doc.kind === "journey");
137
+ const workflowDocs = (graph.docs || []).filter((/** @type {any} */ doc) => doc.kind === "workflow");
138
+ return {
139
+ actor_ids: ((graph.byKind.actor || []).map((/** @type {any} */ entry) => entry.id)).sort(),
140
+ role_ids: ((graph.byKind.role || []).map((/** @type {any} */ entry) => entry.id)).sort(),
141
+ journey_docs: journeyDocs,
142
+ workflow_docs: workflowDocs
143
+ };
144
+ }
145
+
146
+ /** @param {CandidateBundle} bundle @param {ResolvedGraph} graph @returns {any} */
147
+ export function buildBundleDocLinkSuggestions(bundle, graph) {
148
+ if (!graph) {
149
+ return [];
150
+ }
151
+ const canonicalDocs = new Map(
152
+ (graph.docs || [])
153
+ .filter((/** @type {any} */ doc) => ["journey", "workflow"].includes(doc.kind))
154
+ .map((/** @type {any} */ doc) => [doc.id, doc])
155
+ );
156
+ const suggestions = new Map();
157
+ const getOrCreateSuggestion = (/** @type {any} */ doc) => {
158
+ if (!suggestions.has(doc.id)) {
159
+ suggestions.set(doc.id, {
160
+ doc_id: doc.id,
161
+ doc_kind: doc.kind,
162
+ canonical_rel_path: doc.relativePath,
163
+ add_related_actors: [],
164
+ add_related_roles: [],
165
+ add_related_capabilities: [],
166
+ add_related_rules: [],
167
+ add_related_workflows: []
168
+ });
169
+ }
170
+ return suggestions.get(doc.id);
171
+ };
172
+ for (const entry of [...(bundle.actors || []), ...(bundle.roles || [])]) {
173
+ const kind = entry.id_hint.startsWith("actor_") ? "actor" : "role";
174
+ for (const docId of entry.related_docs || []) {
175
+ const doc = canonicalDocs.get(docId);
176
+ if (!doc) {
177
+ continue;
178
+ }
179
+ const target = getOrCreateSuggestion(doc);
180
+ if (kind === "actor" && !(doc.relatedActors || []).includes(entry.id_hint)) {
181
+ target.add_related_actors.push(entry.id_hint);
182
+ }
183
+ if (kind === "role" && !(doc.relatedRoles || []).includes(entry.id_hint)) {
184
+ target.add_related_roles.push(entry.id_hint);
185
+ }
186
+ }
187
+ }
188
+ for (const entry of bundle.docs || []) {
189
+ const doc = canonicalDocs.get(entry.id);
190
+ if (!doc) {
191
+ continue;
192
+ }
193
+ const target = getOrCreateSuggestion(doc);
194
+ for (const capabilityId of entry.related_capabilities || []) {
195
+ if (!(doc.relatedCapabilities || []).includes(capabilityId)) {
196
+ target.add_related_capabilities.push(capabilityId);
197
+ }
198
+ }
199
+ for (const ruleId of entry.related_rules || []) {
200
+ if (!(doc.relatedRules || []).includes(ruleId)) {
201
+ target.add_related_rules.push(ruleId);
202
+ }
203
+ }
204
+ for (const workflowId of entry.related_workflows || []) {
205
+ if (!(doc.relatedWorkflows || []).includes(workflowId)) {
206
+ target.add_related_workflows.push(workflowId);
207
+ }
208
+ }
209
+ }
210
+ return [...suggestions.values()]
211
+ .map((/** @type {any} */ entry) => ({
212
+ ...entry,
213
+ add_related_actors: [...new Set(entry.add_related_actors)].sort(),
214
+ add_related_roles: [...new Set(entry.add_related_roles)].sort(),
215
+ add_related_capabilities: [...new Set(entry.add_related_capabilities)].sort(),
216
+ add_related_rules: [...new Set(entry.add_related_rules)].sort(),
217
+ add_related_workflows: [...new Set(entry.add_related_workflows)].sort()
218
+ }))
219
+ .filter((/** @type {any} */ entry) =>
220
+ entry.add_related_actors.length > 0 ||
221
+ entry.add_related_roles.length > 0 ||
222
+ entry.add_related_capabilities.length > 0 ||
223
+ entry.add_related_rules.length > 0 ||
224
+ entry.add_related_workflows.length > 0
225
+ )
226
+ .map((/** @type {any} */ entry) => ({
227
+ ...entry,
228
+ patch_rel_path: `doc-link-patches/${entry.doc_id}.md`,
229
+ recommendation:
230
+ `Update \`${entry.doc_id}\` to add` +
231
+ `${entry.add_related_actors.length ? ` related_actors ${entry.add_related_actors.map((/** @type {any} */ item) => `\`${item}\``).join(", ")}` : ""}` +
232
+ `${entry.add_related_actors.length && (entry.add_related_roles.length || entry.add_related_capabilities.length || entry.add_related_rules.length || entry.add_related_workflows.length) ? " and" : ""}` +
233
+ `${entry.add_related_roles.length ? ` related_roles ${entry.add_related_roles.map((/** @type {any} */ item) => `\`${item}\``).join(", ")}` : ""}` +
234
+ `${entry.add_related_roles.length && (entry.add_related_capabilities.length || entry.add_related_rules.length || entry.add_related_workflows.length) ? "," : ""}` +
235
+ `${entry.add_related_capabilities.length ? ` related_capabilities ${entry.add_related_capabilities.map((/** @type {any} */ item) => `\`${item}\``).join(", ")}` : ""}` +
236
+ `${entry.add_related_capabilities.length && (entry.add_related_rules.length || entry.add_related_workflows.length) ? "," : ""}` +
237
+ `${entry.add_related_rules.length ? ` related_rules ${entry.add_related_rules.map((/** @type {any} */ item) => `\`${item}\``).join(", ")}` : ""}` +
238
+ `${entry.add_related_rules.length && entry.add_related_workflows.length ? "," : ""}` +
239
+ `${entry.add_related_workflows.length ? ` related_workflows ${entry.add_related_workflows.map((/** @type {any} */ item) => `\`${item}\``).join(", ")}` : ""}.`
240
+ }))
241
+ .sort((/** @type {any} */ a, /** @type {any} */ b) =>
242
+ (b.add_related_actors.length + b.add_related_roles.length) - (a.add_related_actors.length + a.add_related_roles.length) ||
243
+ a.doc_id.localeCompare(b.doc_id)
244
+ );
245
+ }
246
+
247
+ /** @param {any[]} records @returns {any} */
248
+ export function summarizeGapCandidates(records = []) {
249
+ return records
250
+ .map((/** @type {any} */ record) => ({
251
+ id: record.id_hint,
252
+ confidence: record.confidence || "low",
253
+ inference: record.inference_summary || null,
254
+ related_docs: record.related_docs || [],
255
+ related_capabilities: record.related_capabilities || []
256
+ }))
257
+ .sort((/** @type {any} */ a, /** @type {any} */ b) => {
258
+ const confidenceDelta = confidenceRank(b.confidence) - confidenceRank(a.confidence);
259
+ if (confidenceDelta !== 0) {
260
+ return confidenceDelta;
261
+ }
262
+ return a.id.localeCompare(b.id);
263
+ });
264
+ }
@@ -0,0 +1,333 @@
1
+ // @ts-check
2
+ import path from "node:path";
3
+
4
+ import { stableStringify } from "../../format.js";
5
+ import { ensureTrailingNewline } from "../../text-helpers.js";
6
+ import { tryLoadResolvedGraph, scanDocsWorkflow } from "../docs.js";
7
+ import { importAppWorkflow } from "../import-app/index.js";
8
+ import { normalizeWorkspacePaths, readJsonIfExists } from "../shared.js";
9
+ import {
10
+ buildTopogramApiCapabilityIndex,
11
+ collectCanonicalActorRoleSurface,
12
+ collectCanonicalUiSurface,
13
+ collectCanonicalWorkflowSurface,
14
+ compareApiCapabilityFields,
15
+ compareEntityFields,
16
+ matchImportedApiCapability,
17
+ summarizeGapCandidates
18
+ } from "./canonical-surface.js";
19
+
20
+ /** @param {WorkspacePaths} paths @param {string} inputPath @returns {any} */
21
+ export function loadImportArtifacts(paths, inputPath) {
22
+ const dbCandidates = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "app", "db", "candidates.json"));
23
+ const apiCandidates = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "app", "api", "candidates.json"));
24
+ const uiCandidates = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "app", "ui", "candidates.json"));
25
+ const workflowCandidates = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "app", "workflows", "candidates.json"));
26
+ const verificationCandidates = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "app", "verification", "candidates.json"));
27
+ const docsReport = readJsonIfExists(path.join(paths.topogramRoot, "candidates", "docs", "import-report.json"));
28
+ if (dbCandidates || apiCandidates || uiCandidates || workflowCandidates || verificationCandidates || docsReport) {
29
+ return {
30
+ type: "import_app_report",
31
+ workspace: paths.workspaceRoot,
32
+ candidates: {
33
+ db: dbCandidates || { entities: [], enums: [], relations: [], indexes: [] },
34
+ api: apiCandidates || { capabilities: [], routes: [], stacks: [] },
35
+ ui: uiCandidates || { screens: [], routes: [], actions: [], stacks: [] },
36
+ workflows: workflowCandidates || { workflows: [], workflow_states: [], workflow_transitions: [] },
37
+ verification: verificationCandidates || { verifications: [], scenarios: [], frameworks: [], scripts: [] },
38
+ docs: docsReport?.candidate_docs || [],
39
+ actors: docsReport?.candidate_actors || [],
40
+ roles: docsReport?.candidate_roles || []
41
+ }
42
+ };
43
+ }
44
+ const imported = importAppWorkflow(inputPath, { from: "db,api,ui,workflows,verification" }).summary;
45
+ const docsSummary = scanDocsWorkflow(inputPath).summary;
46
+ imported.candidates.docs = docsSummary.candidate_docs || [];
47
+ imported.candidates.actors = docsSummary.candidate_actors || [];
48
+ imported.candidates.roles = docsSummary.candidate_roles || [];
49
+ return imported;
50
+ }
51
+
52
+ /** @param {string} inputPath @returns {any} */
53
+ export function reportGapsWorkflow(inputPath) {
54
+ const paths = normalizeWorkspacePaths(inputPath);
55
+ const graph = tryLoadResolvedGraph(paths.topogramRoot);
56
+ const scan = graph ? scanDocsWorkflow(paths.topogramRoot).summary : { candidate_docs: [] };
57
+ const appImport = loadImportArtifacts(paths, inputPath);
58
+
59
+ const importedDb = appImport.candidates.db || { entities: [], enums: [], relations: [], indexes: [] };
60
+ const importedApi = appImport.candidates.api || { capabilities: [], routes: [], stacks: [] };
61
+ const importedUi = appImport.candidates.ui || { screens: [], routes: [], actions: [], stacks: [] };
62
+ const importedWorkflows = appImport.candidates.workflows || { workflows: [], workflow_states: [], workflow_transitions: [] };
63
+ const importedActors = appImport.candidates.actors || [];
64
+ const importedRoles = appImport.candidates.roles || [];
65
+
66
+ if (!graph) {
67
+ /** @type {WorkflowRecord} */
68
+ const report = {
69
+ type: "gap_report",
70
+ workspace: paths.workspaceRoot,
71
+ bootstrapped_topogram_root: paths.bootstrappedTopogramRoot,
72
+ topogram_available: false,
73
+ imported: {
74
+ db: {
75
+ entity_count: importedDb.entities.length,
76
+ enum_count: importedDb.enums.length,
77
+ relation_count: importedDb.relations.length
78
+ },
79
+ api: {
80
+ capability_count: importedApi.capabilities.length,
81
+ route_count: importedApi.routes.length
82
+ },
83
+ ui: {
84
+ screen_count: importedUi.screens.length,
85
+ route_count: importedUi.routes.length
86
+ },
87
+ workflows: {
88
+ workflow_count: importedWorkflows.workflows.length,
89
+ transition_count: importedWorkflows.workflow_transitions.length
90
+ },
91
+ actors_roles: {
92
+ actor_count: importedActors.length,
93
+ role_count: importedRoles.length
94
+ }
95
+ }
96
+ };
97
+ /** @type {WorkflowFiles} */
98
+ const files = {
99
+ "candidates/reports/gap-report.json": `${stableStringify(report)}\n`,
100
+ "candidates/reports/gap-report.md": ensureTrailingNewline(
101
+ `# Gap Report\n\nNo canonical Topogram was found.\n\n- Imported DB entities: ${importedDb.entities.length}\n- Imported DB enums: ${importedDb.enums.length}\n- Imported API capabilities: ${importedApi.capabilities.length}\n- Imported API routes: ${importedApi.routes.length}\n- Imported UI screens: ${importedUi.screens.length}\n- Imported workflows: ${importedWorkflows.workflows.length}\n- Imported actors: ${importedActors.length}\n- Imported roles: ${importedRoles.length}\n`
102
+ )
103
+ };
104
+ return {
105
+ summary: report,
106
+ files,
107
+ defaultOutDir: paths.topogramRoot
108
+ };
109
+ }
110
+
111
+ const glossaryDocs = new Set((graph.docs || []).filter((/** @type {any} */ doc) => doc.kind === "glossary").map((/** @type {any} */ doc) => doc.id));
112
+ const workflowDocs = (graph.docs || []).filter((/** @type {any} */ doc) => doc.kind === "workflow");
113
+ const canonicalUi = collectCanonicalUiSurface(graph);
114
+ const canonicalWorkflow = collectCanonicalWorkflowSurface(graph);
115
+ const canonicalActorRole = collectCanonicalActorRoleSurface(graph);
116
+ const entityMap = new Map((graph.byKind.entity || []).map((/** @type {any} */ entity) => [entity.id.replace(/^entity_/, ""), entity]));
117
+ const enumMap = new Map((graph.byKind.enum || []).map((/** @type {any} */ entry) => [entry.id, entry]));
118
+ const topogramApiCapabilities = buildTopogramApiCapabilityIndex(graph);
119
+ const capabilityIds = new Set(topogramApiCapabilities.map((/** @type {any} */ capability) => capability.id));
120
+ const canonicalActorIds = new Set(canonicalActorRole.actor_ids);
121
+ const canonicalRoleIds = new Set(canonicalActorRole.role_ids);
122
+ const capabilityById = new Map((graph.byKind.capability || []).map((/** @type {any} */ entry) => [entry.id, entry]));
123
+
124
+ const missingGlossary = [...entityMap.keys()].filter((/** @type {any} */ id) => !glossaryDocs.has(id));
125
+ const missingWorkflowDocs = (graph.byKind.capability || [])
126
+ .filter((/** @type {any} */ capability) => [...capability.creates, ...capability.updates, ...capability.deletes].length > 0)
127
+ .filter((/** @type {any} */ capability) => !workflowDocs.some((/** @type {any} */ doc) => doc.relatedCapabilities.includes(capability.id)))
128
+ .map((/** @type {any} */ capability) => capability.id);
129
+
130
+ /** @type {any[]} */
131
+
132
+ const dbEntitiesMissing = [];
133
+ /** @type {any[]} */
134
+ const dbFieldMismatches = [];
135
+ for (const candidate of importedDb.entities || []) {
136
+ const canonicalId = candidate.id_hint.replace(/^entity_/, "");
137
+ const graphEntity = entityMap.get(canonicalId);
138
+ if (!graphEntity) {
139
+ dbEntitiesMissing.push(canonicalId);
140
+ continue;
141
+ }
142
+ const mismatch = compareEntityFields(candidate, graphEntity);
143
+ if (mismatch.missing.length || mismatch.typeMismatches.length || mismatch.requiredMismatches.length) {
144
+ dbFieldMismatches.push({
145
+ entity: canonicalId,
146
+ missing_fields_in_topogram: mismatch.missing,
147
+ type_mismatches: mismatch.typeMismatches,
148
+ required_mismatches: mismatch.requiredMismatches
149
+ });
150
+ }
151
+ }
152
+
153
+ /** @type {any[]} */
154
+
155
+ const dbEnumsMissing = [];
156
+ /** @type {any[]} */
157
+ const dbEnumValueMismatches = [];
158
+ for (const candidate of importedDb.enums || []) {
159
+ const graphEnum = enumMap.get(candidate.id_hint);
160
+ if (!graphEnum) {
161
+ dbEnumsMissing.push(candidate.id_hint);
162
+ continue;
163
+ }
164
+ const graphValues = new Set((graphEnum.values || []).map((/** @type {any} */ value) => value.id || value));
165
+ const missingValues = (candidate.values || []).filter((/** @type {any} */ value) => !graphValues.has(value));
166
+ if (missingValues.length > 0) {
167
+ dbEnumValueMismatches.push({
168
+ enum: candidate.id_hint,
169
+ missing_values_in_topogram: missingValues
170
+ });
171
+ }
172
+ }
173
+
174
+ /** @type {any[]} */
175
+
176
+ const importedCapabilitiesMissing = [];
177
+ /** @type {any[]} */
178
+ const importedEndpointsWithoutMatchingCapabilities = [];
179
+ /** @type {any[]} */
180
+ const apiFieldMismatches = [];
181
+ const matchedTopogramCapabilities = new Set();
182
+ for (const entry of importedApi.capabilities || []) {
183
+ const match = matchImportedApiCapability(entry, topogramApiCapabilities);
184
+ if (!match) {
185
+ importedCapabilitiesMissing.push(entry.id_hint);
186
+ importedEndpointsWithoutMatchingCapabilities.push({
187
+ capability: entry.id_hint,
188
+ method: entry.endpoint?.method || null,
189
+ path: entry.endpoint?.path || null
190
+ });
191
+ continue;
192
+ }
193
+ matchedTopogramCapabilities.add(match.id);
194
+ const fieldMismatch = compareApiCapabilityFields(entry, match);
195
+ if (
196
+ fieldMismatch.missing_input_fields_in_topogram.length > 0 ||
197
+ fieldMismatch.missing_output_fields_in_topogram.length > 0 ||
198
+ fieldMismatch.missing_path_params_in_topogram.length > 0 ||
199
+ fieldMismatch.missing_query_params_in_topogram.length > 0
200
+ ) {
201
+ apiFieldMismatches.push({
202
+ capability: match.id,
203
+ imported_capability: entry.id_hint,
204
+ method: entry.endpoint?.method || null,
205
+ path: entry.endpoint?.path || null,
206
+ ...fieldMismatch
207
+ });
208
+ }
209
+ }
210
+ const topogramCapabilitiesWithoutImportedEndpointEvidence = topogramApiCapabilities
211
+ .filter((/** @type {any} */ capability) => !matchedTopogramCapabilities.has(capability.id))
212
+ .map((/** @type {any} */ capability) => capability.id);
213
+
214
+ const scannedTermsMissingInGlossary = (scan.candidate_docs || [])
215
+ .filter((/** @type {any} */ doc) => doc.kind === "glossary")
216
+ .map((/** @type {any} */ doc) => doc.id)
217
+ .filter((/** @type {any} */ id) => entityMap.has(id) && !glossaryDocs.has(id));
218
+
219
+ const importedScreensMissing = (importedUi.screens || [])
220
+ .map((/** @type {any} */ screen) => screen.id_hint)
221
+ .filter((/** @type {any} */ id) => !canonicalUi.screens.includes(id));
222
+ const importedUiRoutesMissing = (importedUi.routes || [])
223
+ .map((/** @type {any} */ route) => route.path)
224
+ .filter((/** @type {any} */ route) => !canonicalUi.routes.includes(route));
225
+
226
+ const importedWorkflowsMissing = (importedWorkflows.workflows || [])
227
+ .map((/** @type {any} */ workflow) => workflow.id_hint)
228
+ .filter((/** @type {any} */ id) => !canonicalWorkflow.workflow_docs.includes(id));
229
+ const importedWorkflowTransitionsMissing = (importedWorkflows.workflow_transitions || []).map((/** @type {any} */ transition) => ({
230
+ workflow: transition.workflow_id,
231
+ capability: transition.capability_id,
232
+ to_state: transition.to_state
233
+ }));
234
+
235
+ const actorGapCandidates = summarizeGapCandidates(importedActors.filter((/** @type {any} */ entry) => !canonicalActorIds.has(entry.id_hint)));
236
+ const roleGapCandidates = summarizeGapCandidates(importedRoles.filter((/** @type {any} */ entry) => !canonicalRoleIds.has(entry.id_hint)));
237
+ const importedActorsMissing = importedActors
238
+ .map((/** @type {any} */ entry) => entry.id_hint)
239
+ .filter((/** @type {any} */ id) => !canonicalActorIds.has(id));
240
+ const importedRolesMissing = importedRoles
241
+ .map((/** @type {any} */ entry) => entry.id_hint)
242
+ .filter((/** @type {any} */ id) => !canonicalRoleIds.has(id));
243
+ /** @type {any[]} */
244
+ const securedCapabilitiesWithoutCanonicalRoles = [];
245
+ for (const entry of importedApi.capabilities || []) {
246
+ if (entry.auth_hint !== "secured") {
247
+ continue;
248
+ }
249
+ const match = matchImportedApiCapability(entry, topogramApiCapabilities);
250
+ if (!match) {
251
+ continue;
252
+ }
253
+ const canonicalCapability = capabilityById.get(match.id);
254
+ if (!canonicalCapability || (canonicalCapability.roles || []).length > 0) {
255
+ continue;
256
+ }
257
+ securedCapabilitiesWithoutCanonicalRoles.push(match.id);
258
+ }
259
+ const journeyDocsMissingActorLinks = canonicalActorRole.journey_docs
260
+ .filter((/** @type {any} */ doc) => (doc.relatedActors || []).length === 0)
261
+ .filter((/** @type {any} */ doc) => importedActors.some((/** @type {any} */ entry) => (entry.related_docs || []).includes(doc.id)))
262
+ .map((/** @type {any} */ doc) => doc.id);
263
+ const journeyDocsMissingRoleLinks = canonicalActorRole.journey_docs
264
+ .filter((/** @type {any} */ doc) => (doc.relatedRoles || []).length === 0)
265
+ .filter((/** @type {any} */ doc) => importedRoles.some((/** @type {any} */ entry) => (entry.related_docs || []).includes(doc.id)))
266
+ .map((/** @type {any} */ doc) => doc.id);
267
+ const workflowDocsMissingActorLinks = canonicalActorRole.workflow_docs
268
+ .filter((/** @type {any} */ doc) => (doc.relatedActors || []).length === 0)
269
+ .filter((/** @type {any} */ doc) => importedActors.some((/** @type {any} */ entry) => (entry.related_docs || []).includes(doc.id)))
270
+ .map((/** @type {any} */ doc) => doc.id);
271
+ const workflowDocsMissingRoleLinks = canonicalActorRole.workflow_docs
272
+ .filter((/** @type {any} */ doc) => (doc.relatedRoles || []).length === 0)
273
+ .filter((/** @type {any} */ doc) => importedRoles.some((/** @type {any} */ entry) => (entry.related_docs || []).includes(doc.id)))
274
+ .map((/** @type {any} */ doc) => doc.id);
275
+
276
+ const report = {
277
+ type: "gap_report",
278
+ workspace: paths.workspaceRoot,
279
+ bootstrapped_topogram_root: paths.bootstrappedTopogramRoot,
280
+ topogram_available: true,
281
+ docs_vs_topogram: {
282
+ missing_glossary_docs: missingGlossary,
283
+ missing_workflow_docs: missingWorkflowDocs,
284
+ scanned_terms_missing_in_glossary: scannedTermsMissingInGlossary
285
+ },
286
+ db_vs_topogram: {
287
+ entities_missing_in_topogram: dbEntitiesMissing,
288
+ field_mismatches: dbFieldMismatches,
289
+ enums_missing_in_topogram: dbEnumsMissing,
290
+ enum_value_mismatches: dbEnumValueMismatches
291
+ },
292
+ api_vs_topogram: {
293
+ capabilities_missing_in_topogram: importedCapabilitiesMissing,
294
+ endpoints_without_matching_capabilities: importedEndpointsWithoutMatchingCapabilities,
295
+ field_mismatches: apiFieldMismatches,
296
+ topogram_capabilities_without_imported_endpoint_evidence: topogramCapabilitiesWithoutImportedEndpointEvidence
297
+ },
298
+ ui_vs_topogram: {
299
+ screens_missing_in_topogram: importedScreensMissing,
300
+ routes_missing_in_topogram: importedUiRoutesMissing
301
+ },
302
+ workflows_vs_topogram: {
303
+ workflows_missing_in_topogram: importedWorkflowsMissing,
304
+ transitions_without_canonical_workflow_representation: importedWorkflowTransitionsMissing
305
+ },
306
+ actors_roles_vs_topogram: {
307
+ actors_missing_in_topogram: importedActorsMissing,
308
+ actor_gap_candidates: actorGapCandidates,
309
+ roles_missing_in_topogram: importedRolesMissing,
310
+ role_gap_candidates: roleGapCandidates,
311
+ secured_capabilities_without_canonical_roles: [...new Set(securedCapabilitiesWithoutCanonicalRoles)].sort(),
312
+ journey_docs_missing_actor_links: journeyDocsMissingActorLinks,
313
+ journey_docs_missing_role_links: journeyDocsMissingRoleLinks,
314
+ workflow_docs_missing_actor_links: workflowDocsMissingActorLinks,
315
+ workflow_docs_missing_role_links: workflowDocsMissingRoleLinks
316
+ }
317
+ };
318
+
319
+ /** @type {WorkflowFiles} */
320
+
321
+ const files = {
322
+ "candidates/reports/gap-report.json": `${stableStringify(report)}\n`,
323
+ "candidates/reports/gap-report.md": ensureTrailingNewline(
324
+ `# Gap Report\n\n## Docs vs Topogram\n\n- Missing glossary docs: ${missingGlossary.length}\n- Missing workflow docs: ${missingWorkflowDocs.length}\n- Scanned terms not in glossary: ${scannedTermsMissingInGlossary.length}\n\n## DB vs Topogram\n\n- Imported entities missing in Topogram: ${dbEntitiesMissing.length}\n- Imported field mismatches: ${dbFieldMismatches.length}\n- Imported enums missing in Topogram: ${dbEnumsMissing.length}\n- Imported enum value mismatches: ${dbEnumValueMismatches.length}\n\n## API vs Topogram\n\n- Imported capabilities missing in Topogram: ${importedCapabilitiesMissing.length}\n- Imported endpoints without matching capabilities: ${importedEndpointsWithoutMatchingCapabilities.length}\n- Topogram capabilities without imported endpoint evidence: ${topogramCapabilitiesWithoutImportedEndpointEvidence.length}\n\n## UI vs Topogram\n\n- Imported screens missing in Topogram: ${importedScreensMissing.length}\n- Imported routes missing in Topogram: ${importedUiRoutesMissing.length}\n\n## Workflows vs Topogram\n\n- Imported workflows missing in Topogram: ${importedWorkflowsMissing.length}\n- Imported transitions without canonical workflow representation: ${importedWorkflowTransitionsMissing.length}\n\n## Actors/Roles vs Topogram\n\n- Imported actors missing in Topogram: ${importedActorsMissing.length}\n- Imported roles missing in Topogram: ${importedRolesMissing.length}\n- Secured capabilities without canonical roles: ${securedCapabilitiesWithoutCanonicalRoles.length}\n- Journey docs missing actor links: ${journeyDocsMissingActorLinks.length}\n- Journey docs missing role links: ${journeyDocsMissingRoleLinks.length}\n- Workflow docs missing actor links: ${workflowDocsMissingActorLinks.length}\n- Workflow docs missing role links: ${workflowDocsMissingRoleLinks.length}\n\n### Ranked Missing Actors\n\n${actorGapCandidates.length ? actorGapCandidates.map((/** @type {any} */ entry) => `- \`${entry.id}\` (${entry.confidence})${entry.inference ? ` ${entry.inference}` : ""}`).join("\n") : "- None"}\n\n### Ranked Missing Roles\n\n${roleGapCandidates.length ? roleGapCandidates.map((/** @type {any} */ entry) => `- \`${entry.id}\` (${entry.confidence})${entry.inference ? ` ${entry.inference}` : ""}`).join("\n") : "- None"}\n`
325
+ )
326
+ };
327
+
328
+ return {
329
+ summary: report,
330
+ files,
331
+ defaultOutDir: paths.topogramRoot
332
+ };
333
+ }
@@ -0,0 +1,6 @@
1
+ // @ts-check
2
+
3
+ /** @param {string} id @returns {any} */
4
+ export function dashedTopogramId(id) {
5
+ return String(id || "").replaceAll("_", "-");
6
+ }