@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,703 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { stableStringify } from "../format.js";
|
|
6
|
+
import { relativeTo } from "../path-helpers.js";
|
|
7
|
+
import { canonicalCandidateTerm, ensureTrailingNewline, extractRankedTerms, idHintify, slugify, titleCase } from "../text-helpers.js";
|
|
8
|
+
import { listFilesRecursive, markdownTitle, normalizeWorkspacePaths, readTextIfExists, renderMarkdownDoc } from "./shared.js";
|
|
9
|
+
import { tryLoadResolvedGraph } from "./docs-generate.js";
|
|
10
|
+
import { renderCandidateMetadataComments } from "./reconcile/renderers.js";
|
|
11
|
+
|
|
12
|
+
/** @param {WorkspacePaths} paths @returns {any} */
|
|
13
|
+
function discoverDocSources(paths) {
|
|
14
|
+
const candidates = new Set();
|
|
15
|
+
const pushIfExists = (/** @type {any} */ filePath) => {
|
|
16
|
+
if (filePath && fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
|
|
17
|
+
candidates.add(filePath);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
pushIfExists(path.join(paths.exampleRoot, "README.md"));
|
|
22
|
+
pushIfExists(path.join(paths.exampleRoot, "apps", "README.md"));
|
|
23
|
+
pushIfExists(path.join(paths.exampleRoot, "artifacts", "README.md"));
|
|
24
|
+
pushIfExists(path.join(paths.exampleRoot, "implementation", "README.md"));
|
|
25
|
+
|
|
26
|
+
for (const filePath of listFilesRecursive(path.join(paths.exampleRoot, "artifacts", "docs"), (/** @type {any} */ child) => child.endsWith(".md"))) {
|
|
27
|
+
candidates.add(filePath);
|
|
28
|
+
}
|
|
29
|
+
for (const filePath of listFilesRecursive(path.join(paths.exampleRoot, "apps"), (/** @type {any} */ child) => path.basename(child).toLowerCase().startsWith("readme"))) {
|
|
30
|
+
candidates.add(filePath);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return [...candidates].sort();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** @param {string} markdown @returns {any} */
|
|
37
|
+
function extractTerms(markdown) {
|
|
38
|
+
return extractRankedTerms(markdown, { technicalStopwords: true });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {string} markdown @returns {any} */
|
|
42
|
+
function extractWorkflowSignals(markdown) {
|
|
43
|
+
const text = markdown.toLowerCase();
|
|
44
|
+
/** @type {any[]} */
|
|
45
|
+
const signals = [];
|
|
46
|
+
if (/(workflow|review|approve|reject|revision|resubmit)/.test(text)) {
|
|
47
|
+
signals.push("review_workflow");
|
|
48
|
+
}
|
|
49
|
+
if (/(create|edit|update|close|resolve|archive|export)/.test(text)) {
|
|
50
|
+
signals.push("lifecycle_flow");
|
|
51
|
+
}
|
|
52
|
+
return [...new Set(signals)];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {string} capabilityId @returns {any} */
|
|
56
|
+
function tokenizeCapabilityId(capabilityId) {
|
|
57
|
+
return capabilityId
|
|
58
|
+
.replace(/^cap_/, "")
|
|
59
|
+
.split(/[_-]+/)
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @param {string} token @returns {any} */
|
|
64
|
+
function expandTokenVariants(token) {
|
|
65
|
+
const variants = new Set([token]);
|
|
66
|
+
if (token.endsWith("y")) {
|
|
67
|
+
variants.add(`${token.slice(0, -1)}ies`);
|
|
68
|
+
} else {
|
|
69
|
+
variants.add(`${token}s`);
|
|
70
|
+
}
|
|
71
|
+
if (token === "close") {
|
|
72
|
+
variants.add("closed");
|
|
73
|
+
variants.add("closing");
|
|
74
|
+
}
|
|
75
|
+
if (token === "complete") {
|
|
76
|
+
variants.add("completed");
|
|
77
|
+
variants.add("completion");
|
|
78
|
+
}
|
|
79
|
+
if (token === "create") {
|
|
80
|
+
variants.add("created");
|
|
81
|
+
variants.add("creating");
|
|
82
|
+
variants.add("new");
|
|
83
|
+
}
|
|
84
|
+
if (token === "update") {
|
|
85
|
+
variants.add("updated");
|
|
86
|
+
variants.add("updating");
|
|
87
|
+
variants.add("edit");
|
|
88
|
+
variants.add("edited");
|
|
89
|
+
variants.add("editing");
|
|
90
|
+
}
|
|
91
|
+
if (token === "export") {
|
|
92
|
+
variants.add("exports");
|
|
93
|
+
variants.add("download");
|
|
94
|
+
variants.add("downloaded");
|
|
95
|
+
}
|
|
96
|
+
if (token === "request") {
|
|
97
|
+
variants.add("requested");
|
|
98
|
+
variants.add("requesting");
|
|
99
|
+
}
|
|
100
|
+
if (token === "reject") {
|
|
101
|
+
variants.add("rejected");
|
|
102
|
+
variants.add("rejecting");
|
|
103
|
+
}
|
|
104
|
+
if (token === "approve") {
|
|
105
|
+
variants.add("approved");
|
|
106
|
+
variants.add("approving");
|
|
107
|
+
}
|
|
108
|
+
if (token === "revision") {
|
|
109
|
+
variants.add("revise");
|
|
110
|
+
variants.add("revisions");
|
|
111
|
+
}
|
|
112
|
+
return [...variants];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** @param {string} markdown @param {string} token @returns {any} */
|
|
116
|
+
function includesAnyVariant(markdown, token) {
|
|
117
|
+
const text = markdown.toLowerCase();
|
|
118
|
+
return expandTokenVariants(token).some((/** @type {any} */ variant) => includesTerm(text, variant));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** @param {ResolvedGraph} graph @returns {any} */
|
|
122
|
+
function buildCapabilityWorkflowHints(graph) {
|
|
123
|
+
const capabilities = graph?.byKind.capability || [];
|
|
124
|
+
return capabilities
|
|
125
|
+
.filter((/** @type {any} */ capability) => {
|
|
126
|
+
const id = capability.id.replace(/^cap_/, "");
|
|
127
|
+
const tokens = tokenizeCapabilityId(capability.id);
|
|
128
|
+
if (tokens[0] === "get" || tokens[0] === "list") {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return (
|
|
132
|
+
capability.creates.length > 0 ||
|
|
133
|
+
capability.updates.length > 0 ||
|
|
134
|
+
capability.deletes.length > 0 ||
|
|
135
|
+
/(close|complete|export|download|approve|reject|request|revision)/.test(id)
|
|
136
|
+
);
|
|
137
|
+
})
|
|
138
|
+
.map((/** @type {any} */ capability) => {
|
|
139
|
+
const id = capability.id.replace(/^cap_/, "");
|
|
140
|
+
const tokens = tokenizeCapabilityId(capability.id);
|
|
141
|
+
const actionTokens = tokens.filter((/** @type {any} */ token) => !["task", "tasks", "issue", "issues", "user", "users", "project", "projects", "article", "articles", "board", "boards"].includes(token));
|
|
142
|
+
const nounTokens = tokens.filter((/** @type {any} */ token) => !actionTokens.includes(token));
|
|
143
|
+
return {
|
|
144
|
+
id,
|
|
145
|
+
capabilityId: capability.id,
|
|
146
|
+
title: capability.name || titleCase(id),
|
|
147
|
+
actionTokens,
|
|
148
|
+
nounTokens
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** @param {string} markdown @param {any[]} workflowHints @returns {any} */
|
|
154
|
+
function inferCapabilityWorkflowSignals(markdown, workflowHints) {
|
|
155
|
+
/** @type {any[]} */
|
|
156
|
+
const matches = [];
|
|
157
|
+
for (const hint of workflowHints) {
|
|
158
|
+
const actionMatched = hint.actionTokens.length === 0 || hint.actionTokens.some((/** @type {any} */ token) => includesAnyVariant(markdown, token));
|
|
159
|
+
const nounMatched = hint.nounTokens.length === 0 || hint.nounTokens.some((/** @type {any} */ token) => includesAnyVariant(markdown, token));
|
|
160
|
+
if (actionMatched && nounMatched) {
|
|
161
|
+
matches.push(hint);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return matches;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @param {any} signal @returns {any} */
|
|
168
|
+
function workflowPriority(signal) {
|
|
169
|
+
if (/export|download/.test(signal)) {
|
|
170
|
+
return 7;
|
|
171
|
+
}
|
|
172
|
+
if (/request|revision|approve|reject/.test(signal)) {
|
|
173
|
+
return 6;
|
|
174
|
+
}
|
|
175
|
+
if (/close|complete/.test(signal)) {
|
|
176
|
+
return 5;
|
|
177
|
+
}
|
|
178
|
+
if (/create/.test(signal)) {
|
|
179
|
+
return 4;
|
|
180
|
+
}
|
|
181
|
+
if (/update/.test(signal)) {
|
|
182
|
+
return 3;
|
|
183
|
+
}
|
|
184
|
+
if (/delete/.test(signal)) {
|
|
185
|
+
return 2;
|
|
186
|
+
}
|
|
187
|
+
if (signal === "review_workflow") {
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
if (signal === "lifecycle_flow") {
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @param {string} markdown @param {string} term @returns {any} */
|
|
197
|
+
function includesTerm(markdown, term) {
|
|
198
|
+
if (!markdown || !term) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
const pattern = new RegExp(`\\b${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}s?\\b`, "i");
|
|
202
|
+
return pattern.test(markdown);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const DOC_ACTOR_HINTS = [
|
|
206
|
+
{
|
|
207
|
+
id: "actor_user",
|
|
208
|
+
title: "User",
|
|
209
|
+
phrases: ["user", "users", "workspace member", "workspace members"],
|
|
210
|
+
participantPatterns: [/\busers?\s+(?:(?:can|still)\s+)?(?:browse|view|open|return|sign\s+in|complete|create|update|see)\b/i, /\bworkspace members?\s+(?:browse|view|open|return|complete|see)\b/i]
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "actor_author",
|
|
214
|
+
title: "Author",
|
|
215
|
+
phrases: ["author", "authors"],
|
|
216
|
+
participantPatterns: [/\bauthors?\s+(?:returns?|edits?|opens?|submits?|resubmits?|drafts?|updates?|sees?|receives?|understands?)\b/i]
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "actor_reviewer",
|
|
220
|
+
title: "Reviewer",
|
|
221
|
+
phrases: ["reviewer", "reviewers"],
|
|
222
|
+
participantPatterns: [/\breviewers?\s+(?:reviews?|requests?|opens?|sees?|receives?|assigns?|returns?)\b/i]
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: "actor_manager",
|
|
226
|
+
title: "Manager",
|
|
227
|
+
phrases: ["manager", "managers"],
|
|
228
|
+
participantPatterns: [/\bmanagers?\s+(?:reviews?|assigns?|sees?|opens?|returns?)\b/i]
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "actor_admin",
|
|
232
|
+
title: "Admin",
|
|
233
|
+
phrases: ["admin", "admins", "administrator", "administrators"],
|
|
234
|
+
participantPatterns: [/\badmins?\s+(?:reviews?|opens?|sees?|assigns?|returns?)\b/i, /\badministrators?\s+(?:reviews?|opens?|sees?|assigns?|returns?)\b/i]
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "actor_system_job",
|
|
238
|
+
title: "System Job",
|
|
239
|
+
phrases: ["system job", "background job", "worker process"],
|
|
240
|
+
participantPatterns: [/\b(system job|background job|worker process)\s+(?:runs|retries|processes|updates)\b/i]
|
|
241
|
+
}
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
const DOC_ROLE_HINTS = [
|
|
245
|
+
{
|
|
246
|
+
id: "role_author",
|
|
247
|
+
title: "Author",
|
|
248
|
+
exactPatterns: [/\brole[_\s-]?author\b/i, /\bauthors?\s+may\b/i, /\bauthors?\s+can\b/i]
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "role_reviewer",
|
|
252
|
+
title: "Reviewer",
|
|
253
|
+
exactPatterns: [/\brole[_\s-]?reviewer\b/i, /\breviewers?\s+may\b/i, /\breviewers?\s+can\b/i]
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: "role_manager",
|
|
257
|
+
title: "Manager",
|
|
258
|
+
exactPatterns: [/\brole[_\s-]?manager\b/i, /\bonly\s+managers?\s+may\b/i, /\bmanagers?\s+may\b/i, /\bmanagers?\s+can\b/i]
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: "role_admin",
|
|
262
|
+
title: "Admin",
|
|
263
|
+
exactPatterns: [/\brole[_\s-]?admin\b/i, /\bonly\s+admins?\s+may\b/i, /\badmins?\s+may\b/i, /\badmins?\s+can\b/i]
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: "role_owner",
|
|
267
|
+
title: "Owner",
|
|
268
|
+
exactPatterns: [/\brole[_\s-]?owner\b/i, /\bowners?\s+may\b/i, /\bowners?\s+can\b/i]
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "role_assignee",
|
|
272
|
+
title: "Assignee",
|
|
273
|
+
exactPatterns: [/\brole[_\s-]?assignee\b/i, /\bassignees?\s+may\b/i, /\bassignees?\s+can\b/i]
|
|
274
|
+
}
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
/** @param {string} markdown @param {any} phrase @returns {any} */
|
|
278
|
+
function countPhrase(markdown, phrase) {
|
|
279
|
+
if (!markdown || !phrase) {
|
|
280
|
+
return 0;
|
|
281
|
+
}
|
|
282
|
+
const pattern = new RegExp(`\\b${phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi");
|
|
283
|
+
return markdown.match(pattern)?.length || 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** @param {string} markdown @param {any[]} patterns @returns {any} */
|
|
287
|
+
function countPatternMatches(markdown, patterns = []) {
|
|
288
|
+
return patterns.reduce((/** @type {any} */ total, /** @type {any} */ pattern) => total + (markdown.match(new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`))?.length || 0), 0);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** @param {string} value @returns {any} */
|
|
292
|
+
export function confidenceRank(value) {
|
|
293
|
+
if (value === "high") {
|
|
294
|
+
return 3;
|
|
295
|
+
}
|
|
296
|
+
if (value === "medium") {
|
|
297
|
+
return 2;
|
|
298
|
+
}
|
|
299
|
+
return 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** @param {any} a @param {any} b @returns {any} */
|
|
303
|
+
function maxConfidence(a, b) {
|
|
304
|
+
return confidenceRank(a) >= confidenceRank(b) ? a : b;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** @param {string} markdown @returns {any} */
|
|
308
|
+
function inferRoleSignals(markdown) {
|
|
309
|
+
/** @type {any[]} */
|
|
310
|
+
const hits = [];
|
|
311
|
+
for (const hint of DOC_ROLE_HINTS) {
|
|
312
|
+
const explicitRolePatternCount = countPatternMatches(markdown, hint.exactPatterns.filter((/** @type {any} */ pattern) => pattern.source.includes("role[")));
|
|
313
|
+
const restrictivePatternCount = countPatternMatches(markdown, hint.exactPatterns.filter((/** @type {any} */ pattern) => pattern.source.includes("only\\s+")));
|
|
314
|
+
const permissionPatternCount = countPatternMatches(markdown, hint.exactPatterns);
|
|
315
|
+
if (permissionPatternCount === 0) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
hits.push({
|
|
319
|
+
...hint,
|
|
320
|
+
confidence: explicitRolePatternCount > 0 || restrictivePatternCount > 0 ? "high" : "medium",
|
|
321
|
+
evidence: {
|
|
322
|
+
permission_pattern_count: permissionPatternCount,
|
|
323
|
+
restrictive_pattern_count: restrictivePatternCount,
|
|
324
|
+
explicit_role_pattern_count: explicitRolePatternCount
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
return hits;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** @param {string} markdown @param {any[]} roleSignals @returns {any} */
|
|
332
|
+
function inferActorSignals(markdown, roleSignals = []) {
|
|
333
|
+
/** @type {any[]} */
|
|
334
|
+
const hits = [];
|
|
335
|
+
const roleSignalsByTitle = new Map(roleSignals.map((/** @type {any} */ signal) => [signal.title.toLowerCase(), signal]));
|
|
336
|
+
for (const hint of DOC_ACTOR_HINTS) {
|
|
337
|
+
const genericPhraseCount = hint.phrases.reduce((/** @type {any} */ total, /** @type {any} */ phrase) => total + countPhrase(markdown, phrase), 0);
|
|
338
|
+
const participantPatternCount = countPatternMatches(markdown, hint.participantPatterns || []);
|
|
339
|
+
if (genericPhraseCount === 0 && participantPatternCount === 0) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
const relatedRoleSignal = roleSignalsByTitle.get(hint.title.toLowerCase());
|
|
343
|
+
const permissionOverlapCount = relatedRoleSignal?.evidence?.permission_pattern_count || 0;
|
|
344
|
+
if (participantPatternCount === 0 && permissionOverlapCount > 0 && genericPhraseCount <= permissionOverlapCount) {
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
hits.push({
|
|
348
|
+
...hint,
|
|
349
|
+
confidence: participantPatternCount > 0 ? "medium" : genericPhraseCount >= 2 ? "medium" : "low",
|
|
350
|
+
evidence: {
|
|
351
|
+
phrase_count: genericPhraseCount,
|
|
352
|
+
participant_pattern_count: participantPatternCount,
|
|
353
|
+
permission_overlap_count: permissionOverlapCount
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return hits;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** @param {WorkflowRecord} record @returns {any} */
|
|
361
|
+
export function renderCandidateActor(record) {
|
|
362
|
+
const metadataLines = [renderCandidateMetadataComments(record)];
|
|
363
|
+
for (const docId of (record.related_docs || []).slice(0, 3)) {
|
|
364
|
+
metadataLines.push(`# imported related_doc: ${docId}`);
|
|
365
|
+
}
|
|
366
|
+
for (const capabilityId of (record.related_capabilities || []).slice(0, 3)) {
|
|
367
|
+
metadataLines.push(`# imported related_capability: ${capabilityId}`);
|
|
368
|
+
}
|
|
369
|
+
return ensureTrailingNewline(
|
|
370
|
+
`${metadataLines.join("\n")}\n` +
|
|
371
|
+
`actor ${record.id_hint} {\n` +
|
|
372
|
+
` name "${record.label || titleCase(record.id_hint.replace(/^actor_/, ""))}"\n` +
|
|
373
|
+
` description "Candidate actor inferred from existing documentation"\n\n` +
|
|
374
|
+
` status proposed\n` +
|
|
375
|
+
`}\n`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** @param {WorkflowRecord} record @returns {any} */
|
|
380
|
+
export function renderCandidateRole(record) {
|
|
381
|
+
const metadataLines = [renderCandidateMetadataComments(record)];
|
|
382
|
+
for (const docId of (record.related_docs || []).slice(0, 3)) {
|
|
383
|
+
metadataLines.push(`# imported related_doc: ${docId}`);
|
|
384
|
+
}
|
|
385
|
+
for (const capabilityId of (record.related_capabilities || []).slice(0, 3)) {
|
|
386
|
+
metadataLines.push(`# imported related_capability: ${capabilityId}`);
|
|
387
|
+
}
|
|
388
|
+
return ensureTrailingNewline(
|
|
389
|
+
`${metadataLines.join("\n")}\n` +
|
|
390
|
+
`role ${record.id_hint} {\n` +
|
|
391
|
+
` name "${record.label || titleCase(record.id_hint.replace(/^role_/, ""))}"\n` +
|
|
392
|
+
` description "Candidate role inferred from existing documentation"\n\n` +
|
|
393
|
+
` status proposed\n` +
|
|
394
|
+
`}\n`
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** @param {string} inputPath @returns {any} */
|
|
399
|
+
export function scanDocsWorkflow(inputPath) {
|
|
400
|
+
const paths = normalizeWorkspacePaths(inputPath);
|
|
401
|
+
const graph = tryLoadResolvedGraph(paths.topogramRoot);
|
|
402
|
+
const sources = discoverDocSources(paths);
|
|
403
|
+
/** @type {any[]} */
|
|
404
|
+
const findings = [];
|
|
405
|
+
const glossaryCandidates = new Map();
|
|
406
|
+
const workflowCandidates = new Map();
|
|
407
|
+
const actorCandidates = new Map();
|
|
408
|
+
const roleCandidates = new Map();
|
|
409
|
+
const exampleName = path.basename(paths.exampleRoot).toLowerCase();
|
|
410
|
+
const exampleTerms = new Set(
|
|
411
|
+
exampleName
|
|
412
|
+
.split(/[^a-z0-9]+/)
|
|
413
|
+
.filter(Boolean)
|
|
414
|
+
.flatMap((/** @type {any} */ term) => [term, canonicalCandidateTerm(term)])
|
|
415
|
+
);
|
|
416
|
+
const preferredTerms = new Set([
|
|
417
|
+
...((graph?.byKind.entity || []).map((/** @type {any} */ entity) => entity.id.replace(/^entity_/, ""))),
|
|
418
|
+
...((graph?.byKind.term || []).map((/** @type {any} */ term) => term.id))
|
|
419
|
+
]);
|
|
420
|
+
const workflowHints = buildCapabilityWorkflowHints(graph);
|
|
421
|
+
|
|
422
|
+
for (const filePath of sources) {
|
|
423
|
+
const markdown = readTextIfExists(filePath) || "";
|
|
424
|
+
const title = markdownTitle(filePath, markdown);
|
|
425
|
+
const terms = extractTerms(markdown).slice(0, 8);
|
|
426
|
+
const workflowSignals = [
|
|
427
|
+
...extractWorkflowSignals(markdown),
|
|
428
|
+
...inferCapabilityWorkflowSignals(markdown, workflowHints).map((/** @type {any} */ hint) => hint.id)
|
|
429
|
+
];
|
|
430
|
+
const workflowHintsForFile = inferCapabilityWorkflowSignals(markdown, workflowHints);
|
|
431
|
+
const relatedDocIdsForFile = [...new Set(workflowSignals.map((/** @type {any} */ signal) => slugify(signal)))];
|
|
432
|
+
const relatedCapabilityIdsForFile = [...new Set(workflowHintsForFile.map((/** @type {any} */ hint) => hint.capabilityId).filter(Boolean))];
|
|
433
|
+
const roleSignals = inferRoleSignals(markdown);
|
|
434
|
+
const actorSignals = inferActorSignals(markdown, roleSignals);
|
|
435
|
+
findings.push({
|
|
436
|
+
file: filePath,
|
|
437
|
+
relative_path: relativeTo(paths.repoRoot, filePath),
|
|
438
|
+
title,
|
|
439
|
+
term_candidates: terms,
|
|
440
|
+
workflow_signals: workflowSignals,
|
|
441
|
+
actor_signals: actorSignals.map((/** @type {any} */ signal) => signal.id),
|
|
442
|
+
role_signals: roleSignals.map((/** @type {any} */ signal) => signal.id)
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
for (const term of terms) {
|
|
446
|
+
if (!preferredTerms.has(term)) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
if (!glossaryCandidates.has(term)) {
|
|
450
|
+
glossaryCandidates.set(term, []);
|
|
451
|
+
}
|
|
452
|
+
glossaryCandidates.get(term).push(relativeTo(paths.repoRoot, filePath));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
for (const term of preferredTerms) {
|
|
456
|
+
if (!includesTerm(markdown, term)) {
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (!glossaryCandidates.has(term)) {
|
|
460
|
+
glossaryCandidates.set(term, []);
|
|
461
|
+
}
|
|
462
|
+
glossaryCandidates.get(term).push(relativeTo(paths.repoRoot, filePath));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
for (const term of terms.slice(0, 4)) {
|
|
466
|
+
if (!preferredTerms.has(term) && (exampleTerms.has(term) || exampleTerms.has(canonicalCandidateTerm(term)))) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
if (!glossaryCandidates.has(term)) {
|
|
470
|
+
glossaryCandidates.set(term, []);
|
|
471
|
+
}
|
|
472
|
+
glossaryCandidates.get(term).push(relativeTo(paths.repoRoot, filePath));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
for (const signal of workflowSignals) {
|
|
476
|
+
if (!workflowCandidates.has(signal)) {
|
|
477
|
+
workflowCandidates.set(signal, []);
|
|
478
|
+
}
|
|
479
|
+
workflowCandidates.get(signal).push(relativeTo(paths.repoRoot, filePath));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (const signal of actorSignals) {
|
|
483
|
+
if (!actorCandidates.has(signal.id)) {
|
|
484
|
+
actorCandidates.set(signal.id, {
|
|
485
|
+
id_hint: signal.id,
|
|
486
|
+
label: signal.title,
|
|
487
|
+
confidence: signal.confidence,
|
|
488
|
+
source_kind: "docs",
|
|
489
|
+
provenance: [],
|
|
490
|
+
related_docs: [],
|
|
491
|
+
related_capabilities: [],
|
|
492
|
+
evidence: []
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
const candidate = actorCandidates.get(signal.id);
|
|
496
|
+
candidate.confidence = maxConfidence(candidate.confidence, signal.confidence);
|
|
497
|
+
candidate.provenance.push(relativeTo(paths.repoRoot, filePath));
|
|
498
|
+
candidate.related_docs.push(...relatedDocIdsForFile);
|
|
499
|
+
candidate.related_capabilities.push(...relatedCapabilityIdsForFile);
|
|
500
|
+
candidate.evidence.push(signal.evidence);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
for (const signal of roleSignals) {
|
|
504
|
+
if (!roleCandidates.has(signal.id)) {
|
|
505
|
+
roleCandidates.set(signal.id, {
|
|
506
|
+
id_hint: signal.id,
|
|
507
|
+
label: signal.title,
|
|
508
|
+
confidence: signal.confidence,
|
|
509
|
+
source_kind: "docs",
|
|
510
|
+
provenance: [],
|
|
511
|
+
related_docs: [],
|
|
512
|
+
related_capabilities: [],
|
|
513
|
+
evidence: []
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const candidate = roleCandidates.get(signal.id);
|
|
517
|
+
candidate.confidence = maxConfidence(candidate.confidence, signal.confidence);
|
|
518
|
+
candidate.provenance.push(relativeTo(paths.repoRoot, filePath));
|
|
519
|
+
candidate.related_docs.push(...relatedDocIdsForFile);
|
|
520
|
+
candidate.related_capabilities.push(...relatedCapabilityIdsForFile);
|
|
521
|
+
candidate.evidence.push(signal.evidence);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** @type {WorkflowFiles} */
|
|
526
|
+
|
|
527
|
+
/** @type {WorkflowFiles} */
|
|
528
|
+
|
|
529
|
+
const files = {};
|
|
530
|
+
/** @type {any[]} */
|
|
531
|
+
const candidateDocs = [];
|
|
532
|
+
const orderedGlossaryCandidates = [...glossaryCandidates.entries()]
|
|
533
|
+
.filter((/** @type {any} */ [term]) => preferredTerms.has(term) || (!exampleTerms.has(term) && !exampleTerms.has(canonicalCandidateTerm(term))))
|
|
534
|
+
.sort((/** @type {any} */ a, /** @type {any} */ b) => {
|
|
535
|
+
const aPreferred = preferredTerms.has(a[0]) ? 1 : 0;
|
|
536
|
+
const bPreferred = preferredTerms.has(b[0]) ? 1 : 0;
|
|
537
|
+
if (aPreferred !== bPreferred) {
|
|
538
|
+
return bPreferred - aPreferred;
|
|
539
|
+
}
|
|
540
|
+
if (a[1].length !== b[1].length) {
|
|
541
|
+
return b[1].length - a[1].length;
|
|
542
|
+
}
|
|
543
|
+
return a[0].localeCompare(b[0]);
|
|
544
|
+
});
|
|
545
|
+
const preferredGlossaryCandidates = orderedGlossaryCandidates.filter((/** @type {any} */ [term]) => preferredTerms.has(term));
|
|
546
|
+
const fallbackGlossaryCandidates = orderedGlossaryCandidates.filter((/** @type {any} */ [term]) => !preferredTerms.has(term));
|
|
547
|
+
const seenCanonicalTerms = new Set();
|
|
548
|
+
for (const [term, provenance] of [...preferredGlossaryCandidates, ...fallbackGlossaryCandidates]) {
|
|
549
|
+
const canonicalTerm = canonicalCandidateTerm(term);
|
|
550
|
+
if (seenCanonicalTerms.has(canonicalTerm)) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
seenCanonicalTerms.add(canonicalTerm);
|
|
554
|
+
/** @type {WorkflowRecord} */
|
|
555
|
+
const metadata = {
|
|
556
|
+
id: slugify(term),
|
|
557
|
+
kind: "glossary",
|
|
558
|
+
title: titleCase(term),
|
|
559
|
+
status: "inferred",
|
|
560
|
+
source_of_truth: "imported",
|
|
561
|
+
confidence: "low",
|
|
562
|
+
review_required: true,
|
|
563
|
+
provenance,
|
|
564
|
+
tags: ["import", "glossary"]
|
|
565
|
+
};
|
|
566
|
+
const body = `Candidate glossary term inferred from existing documentation.\n\nObserved term: \`${term}\`\n\nThis entry should be reviewed and either promoted, renamed, merged, or discarded.`;
|
|
567
|
+
const relativePath = `candidates/docs/glossary/${slugify(term)}.md`;
|
|
568
|
+
files[relativePath] = renderMarkdownDoc(metadata, body);
|
|
569
|
+
candidateDocs.push({
|
|
570
|
+
id: metadata.id,
|
|
571
|
+
kind: metadata.kind,
|
|
572
|
+
title: metadata.title,
|
|
573
|
+
path: relativePath,
|
|
574
|
+
confidence: metadata.confidence,
|
|
575
|
+
provenance: metadata.provenance,
|
|
576
|
+
related_entities: metadata.related_entities || [],
|
|
577
|
+
related_capabilities: metadata.related_capabilities || [],
|
|
578
|
+
source_of_truth: metadata.source_of_truth
|
|
579
|
+
});
|
|
580
|
+
if (candidateDocs.filter((/** @type {any} */ doc) => doc.kind === "glossary").length >= 6) {
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const genericWorkflowSignals = new Set(["review_workflow", "lifecycle_flow"]);
|
|
586
|
+
const orderedWorkflowCandidates = [...workflowCandidates.entries()].sort((/** @type {any} */ a, /** @type {any} */ b) => {
|
|
587
|
+
const aGeneric = genericWorkflowSignals.has(a[0]) ? 1 : 0;
|
|
588
|
+
const bGeneric = genericWorkflowSignals.has(b[0]) ? 1 : 0;
|
|
589
|
+
if (aGeneric !== bGeneric) {
|
|
590
|
+
return aGeneric - bGeneric;
|
|
591
|
+
}
|
|
592
|
+
const aPriority = workflowPriority(a[0]);
|
|
593
|
+
const bPriority = workflowPriority(b[0]);
|
|
594
|
+
if (aPriority !== bPriority) {
|
|
595
|
+
return bPriority - aPriority;
|
|
596
|
+
}
|
|
597
|
+
if (a[1].length !== b[1].length) {
|
|
598
|
+
return b[1].length - a[1].length;
|
|
599
|
+
}
|
|
600
|
+
return a[0].localeCompare(b[0]);
|
|
601
|
+
});
|
|
602
|
+
let specificWorkflowCount = 0;
|
|
603
|
+
let genericWorkflowCount = 0;
|
|
604
|
+
for (const [signal, provenance] of orderedWorkflowCandidates) {
|
|
605
|
+
const workflowHint = workflowHints.find((/** @type {any} */ hint) => hint.id === signal);
|
|
606
|
+
const isGeneric = genericWorkflowSignals.has(signal);
|
|
607
|
+
if (isGeneric && genericWorkflowCount >= 2) {
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (!isGeneric && specificWorkflowCount >= 4) {
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
const title = workflowHint?.title || (signal === "review_workflow" ? "Review Workflow" : "Lifecycle Flow");
|
|
614
|
+
/** @type {WorkflowRecord} */
|
|
615
|
+
const metadata = {
|
|
616
|
+
id: slugify(signal),
|
|
617
|
+
kind: "workflow",
|
|
618
|
+
title,
|
|
619
|
+
status: "inferred",
|
|
620
|
+
source_of_truth: "imported",
|
|
621
|
+
confidence: genericWorkflowSignals.has(signal) ? "low" : "medium",
|
|
622
|
+
review_required: true,
|
|
623
|
+
related_capabilities: workflowHint ? [workflowHint.capabilityId] : [],
|
|
624
|
+
provenance,
|
|
625
|
+
tags: ["import", "workflow"]
|
|
626
|
+
};
|
|
627
|
+
const body = workflowHint
|
|
628
|
+
? `Candidate workflow inferred from existing documentation.\n\nMatched capability: \`${workflowHint.capabilityId}\`\n\nThis entry should be reconciled with the eventual capability and UI model before promotion.`
|
|
629
|
+
: `Candidate workflow inferred from existing documentation.\n\nWorkflow signal: \`${signal}\`\n\nThis entry should be reconciled with the eventual capability and UI model before promotion.`;
|
|
630
|
+
const relativePath = `candidates/docs/workflows/${slugify(signal)}.md`;
|
|
631
|
+
files[relativePath] = renderMarkdownDoc(metadata, body);
|
|
632
|
+
candidateDocs.push({
|
|
633
|
+
id: metadata.id,
|
|
634
|
+
kind: metadata.kind,
|
|
635
|
+
title: metadata.title,
|
|
636
|
+
path: relativePath,
|
|
637
|
+
confidence: metadata.confidence,
|
|
638
|
+
provenance: metadata.provenance,
|
|
639
|
+
related_entities: metadata.related_entities || [],
|
|
640
|
+
related_capabilities: metadata.related_capabilities || [],
|
|
641
|
+
source_of_truth: metadata.source_of_truth
|
|
642
|
+
});
|
|
643
|
+
if (isGeneric) {
|
|
644
|
+
genericWorkflowCount += 1;
|
|
645
|
+
} else {
|
|
646
|
+
specificWorkflowCount += 1;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
const candidateActors = [...actorCandidates.values()]
|
|
651
|
+
.map((/** @type {any} */ record) => ({
|
|
652
|
+
...record,
|
|
653
|
+
provenance: [...new Set(record.provenance)].sort(),
|
|
654
|
+
related_docs: [...new Set(record.related_docs || [])].sort(),
|
|
655
|
+
related_capabilities: [...new Set(record.related_capabilities || [])].sort(),
|
|
656
|
+
inference_summary: `phrases=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.phrase_count || 0), 0)}, participant_hits=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.participant_pattern_count || 0), 0)}, permission_overlap=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.permission_overlap_count || 0), 0)}`
|
|
657
|
+
}))
|
|
658
|
+
.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint))
|
|
659
|
+
.slice(0, 6);
|
|
660
|
+
for (const record of candidateActors) {
|
|
661
|
+
const relativePath = `candidates/docs/actors/${record.id_hint.replace(/^actor_/, "").replaceAll("_", "-")}.tg`;
|
|
662
|
+
files[relativePath] = renderCandidateActor(record);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const candidateRoles = [...roleCandidates.values()]
|
|
666
|
+
.map((/** @type {any} */ record) => ({
|
|
667
|
+
...record,
|
|
668
|
+
provenance: [...new Set(record.provenance)].sort(),
|
|
669
|
+
related_docs: [...new Set(record.related_docs || [])].sort(),
|
|
670
|
+
related_capabilities: [...new Set(record.related_capabilities || [])].sort(),
|
|
671
|
+
inference_summary: `permission_hits=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.permission_pattern_count || 0), 0)}, restrictive_hits=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.restrictive_pattern_count || 0), 0)}, explicit_role_hits=${(record.evidence || []).reduce((/** @type {any} */ total, /** @type {any} */ item) => total + (item?.explicit_role_pattern_count || 0), 0)}`
|
|
672
|
+
}))
|
|
673
|
+
.sort((/** @type {any} */ a, /** @type {any} */ b) => a.id_hint.localeCompare(b.id_hint))
|
|
674
|
+
.slice(0, 6);
|
|
675
|
+
for (const record of candidateRoles) {
|
|
676
|
+
const relativePath = `candidates/docs/roles/${record.id_hint.replace(/^role_/, "").replaceAll("_", "-")}.tg`;
|
|
677
|
+
files[relativePath] = renderCandidateRole(record);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** @type {WorkflowRecord} */
|
|
681
|
+
const report = {
|
|
682
|
+
type: "scan_docs_report",
|
|
683
|
+
workspace: paths.topogramRoot,
|
|
684
|
+
bootstrapped_topogram_root: paths.bootstrappedTopogramRoot,
|
|
685
|
+
source_count: sources.length,
|
|
686
|
+
sources: sources.map((/** @type {any} */ filePath) => relativeTo(paths.repoRoot, filePath)),
|
|
687
|
+
findings,
|
|
688
|
+
candidate_docs: candidateDocs,
|
|
689
|
+
candidate_actors: candidateActors,
|
|
690
|
+
candidate_roles: candidateRoles
|
|
691
|
+
};
|
|
692
|
+
files["candidates/docs/findings.json"] = `${stableStringify(findings)}\n`;
|
|
693
|
+
files["candidates/docs/import-report.json"] = `${stableStringify(report)}\n`;
|
|
694
|
+
files["candidates/docs/import-report.md"] = ensureTrailingNewline(
|
|
695
|
+
`# Docs Import Report\n\nScanned ${sources.length} source document(s).\n\n## Candidate Docs\n\n${candidateDocs.length === 0 ? "- None" : candidateDocs.map((/** @type {any} */ doc) => `- \`${doc.kind}\` ${doc.title}`).join("\n")}\n\n## Candidate Actors\n\n${candidateActors.length === 0 ? "- None" : candidateActors.map((/** @type {any} */ actor) => `- \`${actor.id_hint}\` (${actor.confidence})`).join("\n")}\n\n## Candidate Roles\n\n${candidateRoles.length === 0 ? "- None" : candidateRoles.map((/** @type {any} */ role) => `- \`${role.id_hint}\` (${role.confidence})`).join("\n")}\n`
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
return {
|
|
699
|
+
summary: report,
|
|
700
|
+
files,
|
|
701
|
+
defaultOutDir: paths.topogramRoot
|
|
702
|
+
};
|
|
703
|
+
}
|