@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,306 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
|
|
6
|
+
export const TOPOGRAM_CONFIG_FILE = "topogram.config.json";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_FIRST_PARTY_GENERATOR_REPOS = [
|
|
9
|
+
"topogram-generator-express-api",
|
|
10
|
+
"topogram-generator-hono-api",
|
|
11
|
+
"topogram-generator-postgres-db",
|
|
12
|
+
"topogram-generator-react-web",
|
|
13
|
+
"topogram-generator-sqlite-db",
|
|
14
|
+
"topogram-generator-sveltekit-web",
|
|
15
|
+
"topogram-generator-swiftui-native",
|
|
16
|
+
"topogram-generator-vanilla-web"
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const DEFAULT_RELEASE_CONSUMER_REPOS = [
|
|
20
|
+
...DEFAULT_FIRST_PARTY_GENERATOR_REPOS,
|
|
21
|
+
"topogram-starters",
|
|
22
|
+
"topogram-template-todo",
|
|
23
|
+
"topogram-demo-todo",
|
|
24
|
+
"topogram-hello",
|
|
25
|
+
"topograms"
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const DEFAULT_RELEASE_CONSUMER_WORKFLOWS = {
|
|
29
|
+
"topogram-generator-express-api": "Generator Verification",
|
|
30
|
+
"topogram-generator-hono-api": "Generator Verification",
|
|
31
|
+
"topogram-generator-postgres-db": "Generator Verification",
|
|
32
|
+
"topogram-generator-react-web": "Generator Verification",
|
|
33
|
+
"topogram-generator-sqlite-db": "Generator Verification",
|
|
34
|
+
"topogram-generator-sveltekit-web": "Generator Verification",
|
|
35
|
+
"topogram-generator-swiftui-native": "Generator Verification",
|
|
36
|
+
"topogram-generator-vanilla-web": "Generator Verification",
|
|
37
|
+
"topogram-starters": "Starter Verification",
|
|
38
|
+
"topogram-template-todo": "Template Verification",
|
|
39
|
+
"topogram-demo-todo": "Demo Verification",
|
|
40
|
+
"topogram-hello": "Topogram Package Verification",
|
|
41
|
+
"topograms": "Catalog Verification"
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_RELEASE_CONSUMER_WORKFLOW_JOBS = {
|
|
45
|
+
topograms: [
|
|
46
|
+
"Validate catalog",
|
|
47
|
+
"Smoke native starter",
|
|
48
|
+
"Smoke starter alias (hello-web)",
|
|
49
|
+
"Smoke starter alias (hello-api)",
|
|
50
|
+
"Smoke starter alias (hello-db)",
|
|
51
|
+
"Smoke starter alias (web-api)",
|
|
52
|
+
"Smoke starter alias (web-api-db)"
|
|
53
|
+
]
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const DEFAULT_TOPOGRAM_CONFIG = {
|
|
57
|
+
github: {
|
|
58
|
+
owner: "attebury",
|
|
59
|
+
repo: "topogram"
|
|
60
|
+
},
|
|
61
|
+
catalog: {
|
|
62
|
+
owner: "attebury",
|
|
63
|
+
repo: "topograms",
|
|
64
|
+
ref: "main",
|
|
65
|
+
path: "topograms.catalog.json",
|
|
66
|
+
source: null
|
|
67
|
+
},
|
|
68
|
+
release: {
|
|
69
|
+
consumers: DEFAULT_RELEASE_CONSUMER_REPOS,
|
|
70
|
+
workflows: DEFAULT_RELEASE_CONSUMER_WORKFLOWS,
|
|
71
|
+
workflowJobs: DEFAULT_RELEASE_CONSUMER_WORKFLOW_JOBS
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const DEFAULT_CATALOG_SOURCE = `https://raw.githubusercontent.com/${DEFAULT_TOPOGRAM_CONFIG.catalog.owner}/${DEFAULT_TOPOGRAM_CONFIG.catalog.repo}/${DEFAULT_TOPOGRAM_CONFIG.catalog.ref}/${DEFAULT_TOPOGRAM_CONFIG.catalog.path}`;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @typedef {Object} TopogramRuntimeConfig
|
|
79
|
+
* @property {{ owner: string, repo: string }} github
|
|
80
|
+
* @property {{ owner: string, repo: string, ref: string, path: string, source: string|null }} catalog
|
|
81
|
+
* @property {{ consumers: string[], workflows: Record<string, string>, workflowJobs: Record<string, string[]> }} release
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @param {string} cwd
|
|
86
|
+
* @returns {string|null}
|
|
87
|
+
*/
|
|
88
|
+
export function findTopogramConfigFile(cwd = process.cwd()) {
|
|
89
|
+
if (process.env.TOPOGRAM_CONFIG_PATH) {
|
|
90
|
+
return path.resolve(process.env.TOPOGRAM_CONFIG_PATH);
|
|
91
|
+
}
|
|
92
|
+
let current = path.resolve(cwd);
|
|
93
|
+
while (true) {
|
|
94
|
+
const candidate = path.join(current, TOPOGRAM_CONFIG_FILE);
|
|
95
|
+
if (fs.existsSync(candidate)) {
|
|
96
|
+
return candidate;
|
|
97
|
+
}
|
|
98
|
+
const parent = path.dirname(current);
|
|
99
|
+
if (parent === current) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
current = parent;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* @param {string} cwd
|
|
108
|
+
* @returns {Record<string, any>}
|
|
109
|
+
*/
|
|
110
|
+
export function readTopogramConfigFile(cwd = process.cwd()) {
|
|
111
|
+
const filePath = findTopogramConfigFile(cwd);
|
|
112
|
+
if (!filePath) {
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* @param {string|null|undefined} value
|
|
120
|
+
* @returns {string[]|null}
|
|
121
|
+
*/
|
|
122
|
+
function parseListEnv(value) {
|
|
123
|
+
if (!value) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return String(value)
|
|
127
|
+
.split(",")
|
|
128
|
+
.map((item) => item.trim())
|
|
129
|
+
.filter(Boolean);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {string|null|undefined} value
|
|
134
|
+
* @returns {Record<string, any>|null}
|
|
135
|
+
*/
|
|
136
|
+
function parseJsonEnv(value) {
|
|
137
|
+
if (!value) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return JSON.parse(value);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* @param {Record<string, any>} fileConfig
|
|
145
|
+
* @returns {Record<string, any>}
|
|
146
|
+
*/
|
|
147
|
+
function envConfig(fileConfig = {}) {
|
|
148
|
+
const consumers = parseListEnv(process.env.TOPOGRAM_RELEASE_CONSUMERS);
|
|
149
|
+
const workflows = parseJsonEnv(process.env.TOPOGRAM_RELEASE_WORKFLOWS || process.env.TOPOGRAM_RELEASE_CONSUMER_WORKFLOWS_JSON);
|
|
150
|
+
const workflowJobs = parseJsonEnv(process.env.TOPOGRAM_RELEASE_WORKFLOW_JOBS || process.env.TOPOGRAM_RELEASE_CONSUMER_WORKFLOW_JOBS_JSON);
|
|
151
|
+
return {
|
|
152
|
+
github: {
|
|
153
|
+
owner: process.env.TOPOGRAM_GITHUB_OWNER || fileConfig.github?.owner,
|
|
154
|
+
repo: process.env.TOPOGRAM_GITHUB_REPO || process.env.TOPOGRAM_REPO_NAME || fileConfig.github?.repo
|
|
155
|
+
},
|
|
156
|
+
catalog: {
|
|
157
|
+
owner: process.env.TOPOGRAM_CATALOG_OWNER || fileConfig.catalog?.owner,
|
|
158
|
+
repo: process.env.TOPOGRAM_CATALOG_REPO || fileConfig.catalog?.repo,
|
|
159
|
+
ref: process.env.TOPOGRAM_CATALOG_REF || fileConfig.catalog?.ref,
|
|
160
|
+
path: process.env.TOPOGRAM_CATALOG_PATH || fileConfig.catalog?.path,
|
|
161
|
+
source: fileConfig.catalog?.source
|
|
162
|
+
},
|
|
163
|
+
release: {
|
|
164
|
+
consumers: consumers || fileConfig.release?.consumers,
|
|
165
|
+
workflows: workflows || fileConfig.release?.workflows,
|
|
166
|
+
workflowJobs: workflowJobs || fileConfig.release?.workflowJobs
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {unknown} value
|
|
173
|
+
* @param {string[]} fallback
|
|
174
|
+
* @returns {string[]}
|
|
175
|
+
*/
|
|
176
|
+
function normalizeStringList(value, fallback) {
|
|
177
|
+
if (!Array.isArray(value)) {
|
|
178
|
+
return [...fallback];
|
|
179
|
+
}
|
|
180
|
+
const items = value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
181
|
+
return [...new Set(items)];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {unknown} value
|
|
186
|
+
* @param {Record<string, string>} fallback
|
|
187
|
+
* @returns {Record<string, string>}
|
|
188
|
+
*/
|
|
189
|
+
function normalizeStringMap(value, fallback) {
|
|
190
|
+
const output = { ...fallback };
|
|
191
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
192
|
+
return output;
|
|
193
|
+
}
|
|
194
|
+
for (const [key, item] of Object.entries(value)) {
|
|
195
|
+
const name = String(key || "").trim();
|
|
196
|
+
const text = String(item || "").trim();
|
|
197
|
+
if (name && text) {
|
|
198
|
+
output[name] = text;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return output;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {unknown} value
|
|
206
|
+
* @param {Record<string, string[]>} fallback
|
|
207
|
+
* @returns {Record<string, string[]>}
|
|
208
|
+
*/
|
|
209
|
+
function normalizeStringListMap(value, fallback) {
|
|
210
|
+
const output = Object.fromEntries(Object.entries(fallback).map(([key, items]) => [key, [...items]]));
|
|
211
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
212
|
+
return output;
|
|
213
|
+
}
|
|
214
|
+
for (const [key, item] of Object.entries(value)) {
|
|
215
|
+
const name = String(key || "").trim();
|
|
216
|
+
if (!name) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
output[name] = normalizeStringList(item, []);
|
|
220
|
+
}
|
|
221
|
+
return output;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* @param {string} cwd
|
|
226
|
+
* @returns {TopogramRuntimeConfig}
|
|
227
|
+
*/
|
|
228
|
+
export function topogramRuntimeConfig(cwd = process.cwd()) {
|
|
229
|
+
const fileConfig = readTopogramConfigFile(cwd);
|
|
230
|
+
const overrides = envConfig(fileConfig);
|
|
231
|
+
return {
|
|
232
|
+
github: {
|
|
233
|
+
owner: overrides.github.owner || DEFAULT_TOPOGRAM_CONFIG.github.owner,
|
|
234
|
+
repo: overrides.github.repo || DEFAULT_TOPOGRAM_CONFIG.github.repo
|
|
235
|
+
},
|
|
236
|
+
catalog: {
|
|
237
|
+
owner: overrides.catalog.owner || DEFAULT_TOPOGRAM_CONFIG.catalog.owner,
|
|
238
|
+
repo: overrides.catalog.repo || DEFAULT_TOPOGRAM_CONFIG.catalog.repo,
|
|
239
|
+
ref: overrides.catalog.ref || DEFAULT_TOPOGRAM_CONFIG.catalog.ref,
|
|
240
|
+
path: overrides.catalog.path || DEFAULT_TOPOGRAM_CONFIG.catalog.path,
|
|
241
|
+
source: overrides.catalog.source || DEFAULT_TOPOGRAM_CONFIG.catalog.source
|
|
242
|
+
},
|
|
243
|
+
release: {
|
|
244
|
+
consumers: normalizeStringList(overrides.release.consumers, DEFAULT_TOPOGRAM_CONFIG.release.consumers),
|
|
245
|
+
workflows: normalizeStringMap(overrides.release.workflows, DEFAULT_TOPOGRAM_CONFIG.release.workflows),
|
|
246
|
+
workflowJobs: normalizeStringListMap(overrides.release.workflowJobs, DEFAULT_TOPOGRAM_CONFIG.release.workflowJobs)
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {TopogramRuntimeConfig} [config]
|
|
253
|
+
* @returns {string}
|
|
254
|
+
*/
|
|
255
|
+
export function defaultCatalogSource(config = topogramRuntimeConfig()) {
|
|
256
|
+
if (config.catalog.source) {
|
|
257
|
+
return config.catalog.source;
|
|
258
|
+
}
|
|
259
|
+
return `https://raw.githubusercontent.com/${config.catalog.owner}/${config.catalog.repo}/${config.catalog.ref}/${config.catalog.path}`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @param {string|null|undefined} repo
|
|
264
|
+
* @param {string} [cwd]
|
|
265
|
+
* @param {string|null|undefined} owner
|
|
266
|
+
* @returns {string}
|
|
267
|
+
*/
|
|
268
|
+
export function githubRepoSlug(repo, cwd = process.cwd(), owner = null) {
|
|
269
|
+
const config = topogramRuntimeConfig(cwd);
|
|
270
|
+
return `${owner || config.github.owner}/${repo || config.github.repo}`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @param {string} [cwd]
|
|
275
|
+
* @returns {string}
|
|
276
|
+
*/
|
|
277
|
+
export function catalogRepoSlug(cwd = process.cwd()) {
|
|
278
|
+
const config = topogramRuntimeConfig(cwd);
|
|
279
|
+
return githubRepoSlug(config.catalog.repo, cwd, config.catalog.owner);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* @param {string} [cwd]
|
|
284
|
+
* @returns {string[]}
|
|
285
|
+
*/
|
|
286
|
+
export function releaseConsumerRepos(cwd = process.cwd()) {
|
|
287
|
+
return [...topogramRuntimeConfig(cwd).release.consumers];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* @param {string} name
|
|
292
|
+
* @param {string} [cwd]
|
|
293
|
+
* @returns {string|null}
|
|
294
|
+
*/
|
|
295
|
+
export function releaseConsumerWorkflowName(name, cwd = process.cwd()) {
|
|
296
|
+
return topogramRuntimeConfig(cwd).release.workflows[name] || null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {string} name
|
|
301
|
+
* @param {string} [cwd]
|
|
302
|
+
* @returns {string[]}
|
|
303
|
+
*/
|
|
304
|
+
export function releaseConsumerWorkflowJobs(name, cwd = process.cwd()) {
|
|
305
|
+
return [...(topogramRuntimeConfig(cwd).release.workflowJobs[name] || [])];
|
|
306
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import {
|
|
3
|
+
buildAdoptionStatusFiles as buildAdoptionStatusFilesReport,
|
|
4
|
+
buildAdoptionStatusSummary as buildAdoptionStatusSummaryReport
|
|
5
|
+
} from "../../adoption/reporting.js";
|
|
6
|
+
import { selectNextBundle } from "../../adoption/review-groups.js";
|
|
7
|
+
import {
|
|
8
|
+
formatDocDriftSummaryInline,
|
|
9
|
+
formatDocLinkSuggestionInline,
|
|
10
|
+
formatDocMetadataPatchInline,
|
|
11
|
+
reconcileWorkflow
|
|
12
|
+
} from "../reconcile/index.js";
|
|
13
|
+
import { normalizeWorkspacePaths } from "../shared.js";
|
|
14
|
+
|
|
15
|
+
/** @param {string} inputPath @returns {any} */
|
|
16
|
+
export function adoptionStatusWorkflow(inputPath) {
|
|
17
|
+
const reconcile = reconcileWorkflow(inputPath);
|
|
18
|
+
const report = reconcile.summary;
|
|
19
|
+
const summary = buildAdoptionStatusSummaryReport(report, selectNextBundle);
|
|
20
|
+
const files = buildAdoptionStatusFilesReport(summary, formatDocLinkSuggestionInline, formatDocDriftSummaryInline, formatDocMetadataPatchInline);
|
|
21
|
+
return {
|
|
22
|
+
summary,
|
|
23
|
+
files,
|
|
24
|
+
defaultOutDir: normalizeWorkspacePaths(inputPath).topogramRoot
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { stableStringify } from "../format.js";
|
|
6
|
+
import { parsePath } from "../parser.js";
|
|
7
|
+
import { resolveWorkspace } from "../resolver.js";
|
|
8
|
+
import { buildJourneyDrafts as buildJourneyDraftsReconcile } from "../reconcile/journeys.js";
|
|
9
|
+
import { relativeTo } from "../path-helpers.js";
|
|
10
|
+
import { ensureTrailingNewline, titleCase } from "../text-helpers.js";
|
|
11
|
+
import { listFilesRecursive, markdownTitle, normalizeWorkspacePaths, renderMarkdownDoc } from "./shared.js";
|
|
12
|
+
|
|
13
|
+
/** @param {string} kind @returns {any} */
|
|
14
|
+
export function docDirForKind(kind) {
|
|
15
|
+
if (kind === "glossary") {
|
|
16
|
+
return "glossary";
|
|
17
|
+
}
|
|
18
|
+
if (kind === "workflow") {
|
|
19
|
+
return "workflows";
|
|
20
|
+
}
|
|
21
|
+
if (kind === "journey") {
|
|
22
|
+
return "journeys";
|
|
23
|
+
}
|
|
24
|
+
return "reports";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @param {ResolvedGraph} graph @returns {any} */
|
|
28
|
+
export function generateDocsBundleFromGraph(graph) {
|
|
29
|
+
/** @type {WorkflowFiles} */
|
|
30
|
+
/** @type {WorkflowFiles} */
|
|
31
|
+
const files = {};
|
|
32
|
+
for (const entity of graph.byKind.entity || []) {
|
|
33
|
+
const id = entity.id.replace(/^entity_/, "");
|
|
34
|
+
/** @type {WorkflowRecord} */
|
|
35
|
+
const metadata = {
|
|
36
|
+
id,
|
|
37
|
+
kind: "glossary",
|
|
38
|
+
title: entity.name || titleCase(id),
|
|
39
|
+
status: "canonical",
|
|
40
|
+
summary: entity.description || `Generated glossary entry for ${entity.id}.`,
|
|
41
|
+
related_entities: [entity.id],
|
|
42
|
+
source_of_truth: "canonical",
|
|
43
|
+
confidence: "high",
|
|
44
|
+
review_required: false,
|
|
45
|
+
tags: ["generated", "glossary"]
|
|
46
|
+
};
|
|
47
|
+
const body = [
|
|
48
|
+
entity.description || `Canonical entity \`${entity.id}\`.`,
|
|
49
|
+
"",
|
|
50
|
+
"Fields:",
|
|
51
|
+
...(entity.fields || []).map((/** @type {any} */ field) => `- \`${field.name}\` (${field.fieldType}) ${field.required ? "required" : "optional"}`)
|
|
52
|
+
].join("\n");
|
|
53
|
+
files[`glossary/${id}.md`] = renderMarkdownDoc(metadata, body);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const capability of graph.byKind.capability || []) {
|
|
57
|
+
const writes = [...capability.creates, ...capability.updates, ...capability.deletes];
|
|
58
|
+
if (writes.length === 0) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const id = capability.id.replace(/^cap_/, "");
|
|
62
|
+
const relatedEntities = [...new Set(writes.map((/** @type {any} */ entry) => entry.id).filter(Boolean))];
|
|
63
|
+
const metadata = {
|
|
64
|
+
id,
|
|
65
|
+
kind: "workflow",
|
|
66
|
+
title: capability.name || titleCase(id),
|
|
67
|
+
status: "canonical",
|
|
68
|
+
summary: capability.description || `Generated workflow entry for ${capability.id}.`,
|
|
69
|
+
related_capabilities: [capability.id],
|
|
70
|
+
related_entities: relatedEntities,
|
|
71
|
+
source_of_truth: "canonical",
|
|
72
|
+
confidence: "high",
|
|
73
|
+
review_required: false,
|
|
74
|
+
tags: ["generated", "workflow"]
|
|
75
|
+
};
|
|
76
|
+
const body = [
|
|
77
|
+
capability.description || `Canonical workflow surface for \`${capability.id}\`.`,
|
|
78
|
+
"",
|
|
79
|
+
`Actors: ${(capability.actors || []).map((/** @type {any} */ actor) => `\`${actor.id}\``).join(", ") || "_none_"}`,
|
|
80
|
+
`Creates: ${(capability.creates || []).map((/** @type {any} */ ref) => `\`${ref.id}\``).join(", ") || "_none_"}`,
|
|
81
|
+
`Updates: ${(capability.updates || []).map((/** @type {any} */ ref) => `\`${ref.id}\``).join(", ") || "_none_"}`,
|
|
82
|
+
`Deletes: ${(capability.deletes || []).map((/** @type {any} */ ref) => `\`${ref.id}\``).join(", ") || "_none_"}`,
|
|
83
|
+
`Input: ${capability.input?.id ? `\`${capability.input.id}\`` : "_none_"}`,
|
|
84
|
+
`Output: ${capability.output?.id ? `\`${capability.output.id}\`` : "_none_"}`
|
|
85
|
+
].join("\n\n");
|
|
86
|
+
files[`workflows/${id}.md`] = renderMarkdownDoc(metadata, body);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const reportMetadata = {
|
|
90
|
+
id: "model_overview",
|
|
91
|
+
kind: "report",
|
|
92
|
+
title: "Model Overview",
|
|
93
|
+
status: "canonical",
|
|
94
|
+
summary: "Generated overview of the current Topogram model surface.",
|
|
95
|
+
source_of_truth: "canonical",
|
|
96
|
+
confidence: "high",
|
|
97
|
+
review_required: false,
|
|
98
|
+
tags: ["generated", "report"]
|
|
99
|
+
};
|
|
100
|
+
files["reports/model-overview.md"] = renderMarkdownDoc(
|
|
101
|
+
reportMetadata,
|
|
102
|
+
[
|
|
103
|
+
`Entities: ${(graph.byKind.entity || []).length}`,
|
|
104
|
+
`Capabilities: ${(graph.byKind.capability || []).length}`,
|
|
105
|
+
`Shapes: ${(graph.byKind.shape || []).length}`,
|
|
106
|
+
`Projections: ${(graph.byKind.projection || []).length}`,
|
|
107
|
+
`Companion docs: ${(graph.docs || []).length}`
|
|
108
|
+
].join("\n\n")
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const index = Object.entries(files).map(([filePath, contents]) => ({
|
|
112
|
+
path: filePath,
|
|
113
|
+
title: markdownTitle(filePath, contents)
|
|
114
|
+
}));
|
|
115
|
+
files["docs-index.json"] = `${stableStringify(index)}\n`;
|
|
116
|
+
return files;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** @param {string} inputPath @returns {any} */
|
|
120
|
+
export function loadResolvedGraph(inputPath) {
|
|
121
|
+
const ast = parsePath(inputPath);
|
|
122
|
+
const resolved = resolveWorkspace(ast);
|
|
123
|
+
if (!resolved.ok) {
|
|
124
|
+
const error = /** @type {Error & { validation?: any }} */ (new Error("Workspace validation failed"));
|
|
125
|
+
error.validation = resolved.validation;
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
return resolved.graph;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @param {string} inputPath @returns {any} */
|
|
132
|
+
export function tryLoadResolvedGraph(inputPath) {
|
|
133
|
+
try {
|
|
134
|
+
return loadResolvedGraph(inputPath);
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** @param {string} inputPath @returns {any} */
|
|
141
|
+
export function refreshDocsWorkflow(inputPath) {
|
|
142
|
+
const paths = normalizeWorkspacePaths(inputPath);
|
|
143
|
+
const graph = loadResolvedGraph(paths.topogramRoot);
|
|
144
|
+
const generated = generateDocsBundleFromGraph(graph);
|
|
145
|
+
const canonicalRoot = path.join(paths.topogramRoot, "docs");
|
|
146
|
+
const generatedRoot = path.join(paths.topogramRoot, "candidates", "docs", "refreshed");
|
|
147
|
+
/** @type {WorkflowRecord} */
|
|
148
|
+
const report = {
|
|
149
|
+
type: "refresh_docs",
|
|
150
|
+
workspace: paths.topogramRoot,
|
|
151
|
+
missing: [],
|
|
152
|
+
stale: [],
|
|
153
|
+
orphaned: []
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
for (const [relativePath, contents] of Object.entries(generated)) {
|
|
157
|
+
if (relativePath === "docs-index.json") {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
const canonicalPath = path.join(canonicalRoot, relativePath);
|
|
161
|
+
if (!fs.existsSync(canonicalPath)) {
|
|
162
|
+
report.missing.push(relativePath);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if ((fs.readFileSync(canonicalPath, "utf8")) !== contents) {
|
|
166
|
+
report.stale.push(relativePath);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const filePath of listFilesRecursive(canonicalRoot, (/** @type {any} */ child) => child.endsWith(".md"))) {
|
|
171
|
+
const relativePath = relativeTo(canonicalRoot, filePath);
|
|
172
|
+
if (!generated[relativePath]) {
|
|
173
|
+
report.orphaned.push(relativePath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** @type {WorkflowFiles} */
|
|
178
|
+
|
|
179
|
+
const files = {
|
|
180
|
+
"candidates/docs/refreshed/report.json": `${stableStringify(report)}\n`,
|
|
181
|
+
"candidates/docs/refreshed/report.md": ensureTrailingNewline(
|
|
182
|
+
`# Docs Refresh Report\n\n## Missing\n\n${report.missing.length ? report.missing.map((/** @type {any} */ item) => `- \`${item}\``).join("\n") : "- None"}\n\n## Stale\n\n${report.stale.length ? report.stale.map((/** @type {any} */ item) => `- \`${item}\``).join("\n") : "- None"}\n\n## Orphaned\n\n${report.orphaned.length ? report.orphaned.map((/** @type {any} */ item) => `- \`${item}\``).join("\n") : "- None"}\n`
|
|
183
|
+
)
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
for (const [relativePath, contents] of Object.entries(generated)) {
|
|
187
|
+
files[path.join("candidates/docs/refreshed/generated", relativePath).replaceAll(path.sep, "/")] = contents;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
summary: report,
|
|
192
|
+
files,
|
|
193
|
+
defaultOutDir: paths.topogramRoot
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** @param {string} inputPath @returns {any} */
|
|
198
|
+
export function generateDocsWorkflow(inputPath) {
|
|
199
|
+
const paths = normalizeWorkspacePaths(inputPath);
|
|
200
|
+
const graph = loadResolvedGraph(paths.topogramRoot);
|
|
201
|
+
const files = generateDocsBundleFromGraph(graph);
|
|
202
|
+
return {
|
|
203
|
+
summary: {
|
|
204
|
+
type: "generate_docs",
|
|
205
|
+
workspace: paths.topogramRoot,
|
|
206
|
+
bootstrapped_topogram_root: paths.bootstrappedTopogramRoot,
|
|
207
|
+
file_count: Object.keys(files).length
|
|
208
|
+
},
|
|
209
|
+
files,
|
|
210
|
+
defaultOutDir: path.join(paths.topogramRoot, "docs-generated")
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** @param {string} inputPath @returns {any} */
|
|
215
|
+
export function generateJourneyDraftsWorkflow(inputPath) {
|
|
216
|
+
const paths = normalizeWorkspacePaths(inputPath);
|
|
217
|
+
const graph = loadResolvedGraph(paths.topogramRoot);
|
|
218
|
+
const canonicalJourneys = (graph.docs || []).filter((/** @type {any} */ doc) => doc.kind === "journey");
|
|
219
|
+
const { drafts, skippedEntities } = buildJourneyDraftsReconcile(graph);
|
|
220
|
+
/** @type {WorkflowFiles} */
|
|
221
|
+
/** @type {WorkflowFiles} */
|
|
222
|
+
const files = {};
|
|
223
|
+
|
|
224
|
+
for (const draft of drafts) {
|
|
225
|
+
files[draft.path] = renderMarkdownDoc(draft.metadata, draft.body);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** @type {WorkflowRecord} */
|
|
229
|
+
const summary = {
|
|
230
|
+
type: "generate_journeys",
|
|
231
|
+
workspace: paths.topogramRoot,
|
|
232
|
+
bootstrapped_topogram_root: paths.bootstrappedTopogramRoot,
|
|
233
|
+
canonical_journey_count: canonicalJourneys.length,
|
|
234
|
+
generated_draft_count: drafts.length,
|
|
235
|
+
draft_journeys: drafts.map((/** @type {any} */ draft) => ({
|
|
236
|
+
id: draft.id,
|
|
237
|
+
title: draft.title,
|
|
238
|
+
entity_id: draft.entity_id,
|
|
239
|
+
path: draft.path,
|
|
240
|
+
type: draft.type,
|
|
241
|
+
related_capabilities: draft.related_capabilities
|
|
242
|
+
})),
|
|
243
|
+
skipped_entities: skippedEntities
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
files["candidates/docs/journeys/import-report.json"] = `${stableStringify(summary)}\n`;
|
|
247
|
+
files["candidates/docs/journeys/import-report.md"] = ensureTrailingNewline(
|
|
248
|
+
`# Journey Draft Report\n\n` +
|
|
249
|
+
`Canonical journeys: ${canonicalJourneys.length}\n\n` +
|
|
250
|
+
`Generated drafts: ${drafts.length}\n\n` +
|
|
251
|
+
`## Draft Journeys\n\n` +
|
|
252
|
+
`${drafts.length === 0 ? "- None" : drafts.map((/** @type {any} */ draft) => `- \`${draft.id}\` -> \`${draft.path}\``).join("\n")}\n\n` +
|
|
253
|
+
`## Skipped Entities\n\n` +
|
|
254
|
+
`${skippedEntities.length === 0 ? "- None" : skippedEntities.map((/** @type {any} */ entry) => `- \`${entry.entity_id}\` (${entry.reason})`).join("\n")}\n`
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
summary,
|
|
259
|
+
files,
|
|
260
|
+
defaultOutDir: paths.topogramRoot
|
|
261
|
+
};
|
|
262
|
+
}
|