@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,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
|
+
}
|