sdx-cli 0.3.1 → 0.3.2
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/README.md +12 -6
- package/dist/commands/docs/readme.js +1 -1
- package/dist/lib/github.js +83 -0
- package/dist/lib/readme.js +519 -133
- package/dist/lib/repoRegistry.js +8 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -180,6 +180,16 @@ Use overrides to:
|
|
|
180
180
|
### Canonical Root README Generation
|
|
181
181
|
Generate a complete root `README.md` as the canonical onboarding and architecture overview for your org workspace.
|
|
182
182
|
|
|
183
|
+
What `docs readme` now does:
|
|
184
|
+
- traverses Markdown docs across repos in map scope (`README*`, `docs/**`, ADRs, runbooks),
|
|
185
|
+
- infers service purpose, interfaces, async behavior, deployment cues, and operating notes,
|
|
186
|
+
- combines that with map/contracts/architecture artifacts,
|
|
187
|
+
- writes a clean narrative README (no SDX section marker blocks in output).
|
|
188
|
+
|
|
189
|
+
For best results:
|
|
190
|
+
- register local clones for repos you care about (`repo add`) so SDX can deeply scan docs,
|
|
191
|
+
- set `GITHUB_TOKEN` to let SDX fetch Markdown docs for repos without local clones.
|
|
192
|
+
|
|
183
193
|
```bash
|
|
184
194
|
# generate/update root README.md
|
|
185
195
|
./scripts/sdx docs readme --map platform-core
|
|
@@ -187,10 +197,10 @@ Generate a complete root `README.md` as the canonical onboarding and architectur
|
|
|
187
197
|
# write to a different output file
|
|
188
198
|
./scripts/sdx docs readme --map platform-core --output ARCHITECTURE.md
|
|
189
199
|
|
|
190
|
-
# check mode for CI (non-zero on stale
|
|
200
|
+
# check mode for CI (non-zero on stale/missing required artifacts or README drift)
|
|
191
201
|
./scripts/sdx docs readme --map platform-core --check
|
|
192
202
|
|
|
193
|
-
# dry-run preview with unified diff
|
|
203
|
+
# dry-run preview with unified diff + readiness summary
|
|
194
204
|
./scripts/sdx docs readme --map platform-core --dry-run
|
|
195
205
|
|
|
196
206
|
# selective sections
|
|
@@ -230,10 +240,6 @@ Config capabilities:
|
|
|
230
240
|
- custom intro text (`customIntro`)
|
|
231
241
|
- stale threshold override in hours (`staleThresholdHours`, default `72`)
|
|
232
242
|
|
|
233
|
-
Manual content preservation:
|
|
234
|
-
- generated wrappers: `<!-- SDX:SECTION:<id>:START --> ... <!-- SDX:SECTION:<id>:END -->`
|
|
235
|
-
- preserved manual blocks: `<!-- SDX:SECTION:<id>:MANUAL:START --> ... <!-- SDX:SECTION:<id>:MANUAL:END -->`
|
|
236
|
-
|
|
237
243
|
CI automation example:
|
|
238
244
|
- copy [`docs/examples/readme-refresh.yml`](./docs/examples/readme-refresh.yml) into your consumer workspace repo under `.github/workflows/`.
|
|
239
245
|
- set repo/org variables:
|
|
@@ -24,7 +24,7 @@ class DocsReadmeCommand extends core_1.Command {
|
|
|
24
24
|
const context = (0, project_1.loadProject)(process.cwd());
|
|
25
25
|
const includeSections = (0, readme_1.parseReadmeSectionList)(flags.include);
|
|
26
26
|
const excludeSections = (0, readme_1.parseReadmeSectionList)(flags.exclude);
|
|
27
|
-
const result = (0, readme_1.generateReadme)({
|
|
27
|
+
const result = await (0, readme_1.generateReadme)({
|
|
28
28
|
mapId: flags.map,
|
|
29
29
|
db: context.db,
|
|
30
30
|
cwd: context.cwd,
|
package/dist/lib/github.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fetchOrgRepos = fetchOrgRepos;
|
|
4
4
|
exports.ensureOrgRepo = ensureOrgRepo;
|
|
5
|
+
exports.fetchRepositoryMarkdownDocs = fetchRepositoryMarkdownDocs;
|
|
5
6
|
async function fetchOrgRepos(org, token) {
|
|
6
7
|
const { Octokit } = await import('@octokit/rest');
|
|
7
8
|
const octokit = new Octokit({ auth: token });
|
|
@@ -50,3 +51,85 @@ async function ensureOrgRepo(org, repoName, token) {
|
|
|
50
51
|
htmlUrl: created.data.html_url ?? undefined,
|
|
51
52
|
};
|
|
52
53
|
}
|
|
54
|
+
function markdownPathPriority(filePath) {
|
|
55
|
+
const lower = filePath.toLowerCase();
|
|
56
|
+
if (lower === 'readme.md' || lower.endsWith('/readme.md') || lower.endsWith('/readme.mdx')) {
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
if (lower.includes('/docs/architecture/') || lower.includes('/architecture/')) {
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
if (lower.includes('/docs/api/') || lower.includes('/api/')) {
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
if (lower.includes('/docs/')) {
|
|
66
|
+
return 3;
|
|
67
|
+
}
|
|
68
|
+
return 4;
|
|
69
|
+
}
|
|
70
|
+
function isIgnoredPath(filePath) {
|
|
71
|
+
const lower = filePath.toLowerCase();
|
|
72
|
+
return (lower.includes('/node_modules/') ||
|
|
73
|
+
lower.includes('/dist/') ||
|
|
74
|
+
lower.includes('/build/') ||
|
|
75
|
+
lower.includes('/.next/') ||
|
|
76
|
+
lower.includes('/coverage/') ||
|
|
77
|
+
lower.includes('/vendor/'));
|
|
78
|
+
}
|
|
79
|
+
function toBlobUrl(owner, repo, branch, filePath) {
|
|
80
|
+
const encodedPath = filePath
|
|
81
|
+
.split('/')
|
|
82
|
+
.map((part) => encodeURIComponent(part))
|
|
83
|
+
.join('/');
|
|
84
|
+
return `https://github.com/${owner}/${repo}/blob/${encodeURIComponent(branch)}/${encodedPath}`;
|
|
85
|
+
}
|
|
86
|
+
async function fetchRepositoryMarkdownDocs(options) {
|
|
87
|
+
const { Octokit } = await import('@octokit/rest');
|
|
88
|
+
const octokit = new Octokit({ auth: options.token });
|
|
89
|
+
const maxFiles = options.maxFiles ?? 30;
|
|
90
|
+
const maxBytesPerFile = options.maxBytesPerFile ?? 120_000;
|
|
91
|
+
const tree = await octokit.rest.git.getTree({
|
|
92
|
+
owner: options.owner,
|
|
93
|
+
repo: options.repo,
|
|
94
|
+
tree_sha: options.defaultBranch,
|
|
95
|
+
recursive: 'true',
|
|
96
|
+
});
|
|
97
|
+
const markdownFiles = (tree.data.tree ?? [])
|
|
98
|
+
.filter((entry) => Boolean(entry.path && entry.type))
|
|
99
|
+
.map((entry) => ({ path: entry.path, type: entry.type }))
|
|
100
|
+
.filter((entry) => entry.type === 'blob')
|
|
101
|
+
.map((entry) => entry.path)
|
|
102
|
+
.filter((filePath) => /\.(md|mdx)$/i.test(filePath))
|
|
103
|
+
.filter((filePath) => !isIgnoredPath(filePath))
|
|
104
|
+
.sort((a, b) => {
|
|
105
|
+
const priorityDelta = markdownPathPriority(a) - markdownPathPriority(b);
|
|
106
|
+
if (priorityDelta !== 0) {
|
|
107
|
+
return priorityDelta;
|
|
108
|
+
}
|
|
109
|
+
return a.localeCompare(b);
|
|
110
|
+
})
|
|
111
|
+
.slice(0, maxFiles);
|
|
112
|
+
const docs = [];
|
|
113
|
+
for (const filePath of markdownFiles) {
|
|
114
|
+
const response = await octokit.rest.repos.getContent({
|
|
115
|
+
owner: options.owner,
|
|
116
|
+
repo: options.repo,
|
|
117
|
+
path: filePath,
|
|
118
|
+
ref: options.defaultBranch,
|
|
119
|
+
});
|
|
120
|
+
if (Array.isArray(response.data) || response.data.type !== 'file' || !response.data.content) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const raw = Buffer.from(response.data.content, 'base64').toString('utf8');
|
|
124
|
+
const body = raw.slice(0, maxBytesPerFile);
|
|
125
|
+
if (body.trim().length === 0) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
docs.push({
|
|
129
|
+
path: filePath,
|
|
130
|
+
body,
|
|
131
|
+
referenceUrl: toBlobUrl(options.owner, options.repo, options.defaultBranch, filePath),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return docs;
|
|
135
|
+
}
|
package/dist/lib/readme.js
CHANGED
|
@@ -11,9 +11,12 @@ const node_path_1 = __importDefault(require("node:path"));
|
|
|
11
11
|
const yaml_1 = __importDefault(require("yaml"));
|
|
12
12
|
const zod_1 = require("zod");
|
|
13
13
|
const architecture_1 = require("./architecture");
|
|
14
|
+
const config_1 = require("./config");
|
|
14
15
|
const constants_1 = require("./constants");
|
|
15
16
|
const contracts_1 = require("./contracts");
|
|
17
|
+
const fileScan_1 = require("./fileScan");
|
|
16
18
|
const fs_1 = require("./fs");
|
|
19
|
+
const github_1 = require("./github");
|
|
17
20
|
const mapBuilder_1 = require("./mapBuilder");
|
|
18
21
|
const repoRegistry_1 = require("./repoRegistry");
|
|
19
22
|
const scope_1 = require("./scope");
|
|
@@ -84,6 +87,20 @@ const README_CONFIG_SCHEMA = zod_1.z.object({
|
|
|
84
87
|
customIntro: zod_1.z.string().optional(),
|
|
85
88
|
staleThresholdHours: zod_1.z.number().positive().optional(),
|
|
86
89
|
});
|
|
90
|
+
const EMPTY_REPO_INSIGHTS = {
|
|
91
|
+
summary: 'Unknown',
|
|
92
|
+
responsibilities: [],
|
|
93
|
+
interfaces: [],
|
|
94
|
+
asyncPatterns: [],
|
|
95
|
+
deployment: [],
|
|
96
|
+
runbooks: [],
|
|
97
|
+
localDevelopment: [],
|
|
98
|
+
security: [],
|
|
99
|
+
adrs: [],
|
|
100
|
+
dataStores: [],
|
|
101
|
+
glossary: [],
|
|
102
|
+
docReferences: [],
|
|
103
|
+
};
|
|
87
104
|
function normalizeRepoName(value) {
|
|
88
105
|
return value.trim().replace(/^https?:\/\/github\.com\//i, '').replace(/\.git$/i, '').split('/').pop() ?? value.trim();
|
|
89
106
|
}
|
|
@@ -256,6 +273,265 @@ function computeSnapshotTimestamp(sources, fallback) {
|
|
|
256
273
|
}
|
|
257
274
|
return sourceCandidates.reduce((latest, candidate) => (candidate.getTime() > latest.getTime() ? candidate : latest), sourceCandidates[0]).toISOString();
|
|
258
275
|
}
|
|
276
|
+
function markdownPriority(filePath) {
|
|
277
|
+
const lower = filePath.toLowerCase();
|
|
278
|
+
if (lower === 'readme.md' || lower.endsWith('/readme.md')) {
|
|
279
|
+
return 0;
|
|
280
|
+
}
|
|
281
|
+
if (lower.includes('/docs/architecture/') || lower.includes('/architecture/')) {
|
|
282
|
+
return 1;
|
|
283
|
+
}
|
|
284
|
+
if (lower.includes('/docs/api/') || lower.includes('/api/')) {
|
|
285
|
+
return 2;
|
|
286
|
+
}
|
|
287
|
+
if (lower.includes('/docs/')) {
|
|
288
|
+
return 3;
|
|
289
|
+
}
|
|
290
|
+
return 4;
|
|
291
|
+
}
|
|
292
|
+
function parseOwnerRepo(fullName) {
|
|
293
|
+
if (!fullName) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const parts = fullName.split('/').map((part) => part.trim()).filter((part) => part.length > 0);
|
|
297
|
+
if (parts.length !== 2) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return { owner: parts[0], repo: parts[1] };
|
|
301
|
+
}
|
|
302
|
+
function normalizeMarkdown(input) {
|
|
303
|
+
return input
|
|
304
|
+
.replace(/\r\n/g, '\n')
|
|
305
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
306
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
307
|
+
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1')
|
|
308
|
+
.replace(/[#>*_~\-]{1,}/g, ' ')
|
|
309
|
+
.replace(/\s+/g, ' ')
|
|
310
|
+
.trim();
|
|
311
|
+
}
|
|
312
|
+
function splitSentences(input) {
|
|
313
|
+
const cleaned = normalizeMarkdown(input);
|
|
314
|
+
if (cleaned.length === 0) {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
return cleaned
|
|
318
|
+
.split(/(?<=[.!?])\s+/)
|
|
319
|
+
.map((sentence) => sentence.trim())
|
|
320
|
+
.filter((sentence) => sentence.length >= 20);
|
|
321
|
+
}
|
|
322
|
+
function markdownSections(content) {
|
|
323
|
+
const lines = content.replace(/\r\n/g, '\n').split('\n');
|
|
324
|
+
const out = [];
|
|
325
|
+
let currentHeading = 'root';
|
|
326
|
+
let buffer = [];
|
|
327
|
+
function flush() {
|
|
328
|
+
const body = buffer.join('\n').trim();
|
|
329
|
+
if (body.length > 0) {
|
|
330
|
+
out.push({ heading: currentHeading, body });
|
|
331
|
+
}
|
|
332
|
+
buffer = [];
|
|
333
|
+
}
|
|
334
|
+
for (const line of lines) {
|
|
335
|
+
const match = line.match(/^#{1,6}\s+(.+)$/);
|
|
336
|
+
if (match) {
|
|
337
|
+
flush();
|
|
338
|
+
currentHeading = match[1].trim().toLowerCase();
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
buffer.push(line);
|
|
342
|
+
}
|
|
343
|
+
flush();
|
|
344
|
+
return out;
|
|
345
|
+
}
|
|
346
|
+
function firstParagraph(content) {
|
|
347
|
+
const paragraphs = content
|
|
348
|
+
.replace(/\r\n/g, '\n')
|
|
349
|
+
.split(/\n\s*\n/)
|
|
350
|
+
.map((chunk) => normalizeMarkdown(chunk))
|
|
351
|
+
.filter((chunk) => chunk.length >= 20);
|
|
352
|
+
return paragraphs[0];
|
|
353
|
+
}
|
|
354
|
+
function topUnique(values, limit) {
|
|
355
|
+
const out = [];
|
|
356
|
+
for (const value of values) {
|
|
357
|
+
if (out.includes(value)) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
out.push(value);
|
|
361
|
+
if (out.length >= limit) {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
function collectLocalMarkdownDocs(repo, maxFiles = 180, maxChars = 180_000) {
|
|
368
|
+
if (!repo.localPath || !(0, fs_1.fileExists)(repo.localPath)) {
|
|
369
|
+
return [];
|
|
370
|
+
}
|
|
371
|
+
const candidates = (0, fileScan_1.listFilesRecursive)(repo.localPath)
|
|
372
|
+
.filter((candidate) => /\.(md|mdx)$/i.test(candidate))
|
|
373
|
+
.map((candidate) => node_path_1.default.relative(repo.localPath, candidate).split(node_path_1.default.sep).join('/'))
|
|
374
|
+
.sort((a, b) => {
|
|
375
|
+
const priorityDelta = markdownPriority(a) - markdownPriority(b);
|
|
376
|
+
if (priorityDelta !== 0) {
|
|
377
|
+
return priorityDelta;
|
|
378
|
+
}
|
|
379
|
+
return a.localeCompare(b);
|
|
380
|
+
});
|
|
381
|
+
const selected = candidates.slice(0, maxFiles);
|
|
382
|
+
const docs = [];
|
|
383
|
+
for (const relativePath of selected) {
|
|
384
|
+
const absolutePath = node_path_1.default.join(repo.localPath, relativePath);
|
|
385
|
+
const raw = (0, fs_1.safeReadText)(absolutePath);
|
|
386
|
+
const body = raw.slice(0, maxChars);
|
|
387
|
+
if (normalizeMarkdown(body).length === 0) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
docs.push({
|
|
391
|
+
path: relativePath,
|
|
392
|
+
body,
|
|
393
|
+
source: 'local',
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
return docs;
|
|
397
|
+
}
|
|
398
|
+
async function collectRemoteMarkdownDocs(repo, token) {
|
|
399
|
+
if (!token) {
|
|
400
|
+
return [];
|
|
401
|
+
}
|
|
402
|
+
const ownerRepo = parseOwnerRepo(repo.fullName);
|
|
403
|
+
if (!ownerRepo) {
|
|
404
|
+
return [];
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
const files = await (0, github_1.fetchRepositoryMarkdownDocs)({
|
|
408
|
+
owner: ownerRepo.owner,
|
|
409
|
+
repo: ownerRepo.repo,
|
|
410
|
+
defaultBranch: repo.defaultBranch ?? 'main',
|
|
411
|
+
token,
|
|
412
|
+
maxFiles: 25,
|
|
413
|
+
maxBytesPerFile: 120_000,
|
|
414
|
+
});
|
|
415
|
+
return files.map((entry) => ({
|
|
416
|
+
path: entry.path,
|
|
417
|
+
body: entry.body,
|
|
418
|
+
source: 'remote',
|
|
419
|
+
referenceUrl: entry.referenceUrl,
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function inferStoresFromDocs(docs) {
|
|
427
|
+
const keywords = ['postgres', 'mysql', 'mongodb', 'dynamodb', 'redis', 'cassandra', 'sqlite', 'kafka', 'sqs', 'rabbitmq'];
|
|
428
|
+
const matches = [];
|
|
429
|
+
for (const doc of docs) {
|
|
430
|
+
const normalized = normalizeMarkdown(doc.body).toLowerCase();
|
|
431
|
+
for (const keyword of keywords) {
|
|
432
|
+
if (normalized.includes(keyword)) {
|
|
433
|
+
matches.push(keyword);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return topUnique(matches.sort((a, b) => a.localeCompare(b)), 6);
|
|
438
|
+
}
|
|
439
|
+
function collectByHeading(docs, matcher, limit) {
|
|
440
|
+
const collected = [];
|
|
441
|
+
for (const doc of docs) {
|
|
442
|
+
const sections = markdownSections(doc.body);
|
|
443
|
+
for (const section of sections) {
|
|
444
|
+
if (!matcher.test(section.heading)) {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
collected.push(...splitSentences(section.body));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return topUnique(collected, limit);
|
|
451
|
+
}
|
|
452
|
+
function collectCommands(docs, limit) {
|
|
453
|
+
const commands = [];
|
|
454
|
+
for (const doc of docs) {
|
|
455
|
+
const matches = doc.body.match(/```(?:bash|sh|zsh|shell)?\n([\s\S]*?)```/gi) ?? [];
|
|
456
|
+
for (const block of matches) {
|
|
457
|
+
const body = block
|
|
458
|
+
.replace(/```(?:bash|sh|zsh|shell)?\n?/i, '')
|
|
459
|
+
.replace(/```$/i, '')
|
|
460
|
+
.split('\n')
|
|
461
|
+
.map((line) => line.trim())
|
|
462
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
|
463
|
+
for (const line of body) {
|
|
464
|
+
commands.push(line);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return topUnique(commands, limit);
|
|
469
|
+
}
|
|
470
|
+
function extractGlossaryTerms(docs) {
|
|
471
|
+
const terms = [];
|
|
472
|
+
for (const doc of docs) {
|
|
473
|
+
const glossarySections = markdownSections(doc.body).filter((section) => /glossary|terms/.test(section.heading));
|
|
474
|
+
for (const section of glossarySections) {
|
|
475
|
+
for (const line of section.body.split('\n')) {
|
|
476
|
+
const match = line.match(/^\s*[-*]\s*\*\*([^*]+)\*\*:/);
|
|
477
|
+
if (match) {
|
|
478
|
+
terms.push(match[1].trim());
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return topUnique(terms.sort((a, b) => a.localeCompare(b)), 12);
|
|
484
|
+
}
|
|
485
|
+
async function buildRepoInsights(repo, token) {
|
|
486
|
+
const localDocs = collectLocalMarkdownDocs(repo);
|
|
487
|
+
const remoteDocs = localDocs.length > 0 ? [] : await collectRemoteMarkdownDocs(repo, token);
|
|
488
|
+
const docs = [...localDocs, ...remoteDocs];
|
|
489
|
+
const summary = firstParagraph(docs.find((doc) => /(^|\/)readme\.mdx?$/i.test(doc.path))?.body ?? '') ??
|
|
490
|
+
firstParagraph(docs[0]?.body ?? '') ??
|
|
491
|
+
'Unknown';
|
|
492
|
+
const responsibilities = collectByHeading(docs, /(overview|purpose|responsibilit|architecture|what .*does)/, 5);
|
|
493
|
+
const interfaces = collectByHeading(docs, /(api|endpoint|contract|schema|graphql|grpc|openapi)/, 5);
|
|
494
|
+
const asyncPatterns = collectByHeading(docs, /(event|async|queue|topic|stream|kafka|pubsub)/, 5);
|
|
495
|
+
const deployment = collectByHeading(docs, /(deploy|environment|infrastructure|release|production)/, 5);
|
|
496
|
+
const runbooks = collectByHeading(docs, /(runbook|incident|on.?call|troubleshoot|escalation)/, 5);
|
|
497
|
+
const security = collectByHeading(docs, /(security|auth|authorization|compliance|privacy|encryption|secret)/, 5);
|
|
498
|
+
const adrs = docs
|
|
499
|
+
.filter((doc) => /(\/|^)docs\/adr\/.+\.mdx?$/i.test(doc.path) || /(\/|^)adr\/.+\.mdx?$/i.test(doc.path))
|
|
500
|
+
.map((doc) => doc.referenceUrl ?? doc.path);
|
|
501
|
+
const localDevelopment = collectCommands(docs, 8);
|
|
502
|
+
const dataStores = inferStoresFromDocs(docs);
|
|
503
|
+
const glossary = extractGlossaryTerms(docs);
|
|
504
|
+
const docReferences = topUnique(docs
|
|
505
|
+
.map((doc) => doc.referenceUrl ?? doc.path)
|
|
506
|
+
.sort((a, b) => a.localeCompare(b)), 8);
|
|
507
|
+
return {
|
|
508
|
+
summary,
|
|
509
|
+
responsibilities,
|
|
510
|
+
interfaces,
|
|
511
|
+
asyncPatterns,
|
|
512
|
+
deployment,
|
|
513
|
+
runbooks,
|
|
514
|
+
localDevelopment,
|
|
515
|
+
security,
|
|
516
|
+
adrs,
|
|
517
|
+
dataStores,
|
|
518
|
+
glossary,
|
|
519
|
+
docReferences,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
async function buildAllRepoInsights(selectedRepos, repoMap, token) {
|
|
523
|
+
const out = new Map();
|
|
524
|
+
await Promise.all(selectedRepos.map(async (repoName) => {
|
|
525
|
+
const repo = repoMap.get(repoName);
|
|
526
|
+
if (!repo) {
|
|
527
|
+
out.set(repoName, EMPTY_REPO_INSIGHTS);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const insights = await buildRepoInsights(repo, token);
|
|
531
|
+
out.set(repoName, insights);
|
|
532
|
+
}));
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
259
535
|
function filterReposForReadme(scope, config) {
|
|
260
536
|
const base = [...scope.effective];
|
|
261
537
|
const include = new Set((config.repos?.include ?? []).map((value) => normalizeRepoName(value)));
|
|
@@ -606,6 +882,84 @@ function formatList(values) {
|
|
|
606
882
|
}
|
|
607
883
|
return values.join(', ');
|
|
608
884
|
}
|
|
885
|
+
function insightsForRepo(repoName, context) {
|
|
886
|
+
return context.repoInsights.get(repoName) ?? EMPTY_REPO_INSIGHTS;
|
|
887
|
+
}
|
|
888
|
+
function shorten(text, max = 180) {
|
|
889
|
+
const normalized = normalizeMarkdown(text);
|
|
890
|
+
if (normalized.length <= max) {
|
|
891
|
+
return normalized;
|
|
892
|
+
}
|
|
893
|
+
return `${normalized.slice(0, max - 1).trimEnd()}…`;
|
|
894
|
+
}
|
|
895
|
+
function formatLinks(links, limit = 3) {
|
|
896
|
+
if (links.length === 0) {
|
|
897
|
+
return 'Unknown';
|
|
898
|
+
}
|
|
899
|
+
return links
|
|
900
|
+
.slice(0, limit)
|
|
901
|
+
.map((value) => {
|
|
902
|
+
if (/^https?:\/\//.test(value)) {
|
|
903
|
+
return `[doc](${value})`;
|
|
904
|
+
}
|
|
905
|
+
return `\`${value}\``;
|
|
906
|
+
})
|
|
907
|
+
.join(', ');
|
|
908
|
+
}
|
|
909
|
+
function inferRuntimeFromInsights(insights) {
|
|
910
|
+
const corpus = [
|
|
911
|
+
insights.summary,
|
|
912
|
+
...insights.responsibilities,
|
|
913
|
+
...insights.interfaces,
|
|
914
|
+
...insights.localDevelopment,
|
|
915
|
+
]
|
|
916
|
+
.join(' ')
|
|
917
|
+
.toLowerCase();
|
|
918
|
+
if (corpus.includes('next.js') || corpus.includes('nextjs')) {
|
|
919
|
+
return 'Node.js (Next.js)';
|
|
920
|
+
}
|
|
921
|
+
if (corpus.includes('nestjs')) {
|
|
922
|
+
return 'Node.js (NestJS)';
|
|
923
|
+
}
|
|
924
|
+
if (corpus.includes('express')) {
|
|
925
|
+
return 'Node.js (Express)';
|
|
926
|
+
}
|
|
927
|
+
if (corpus.includes('fastapi')) {
|
|
928
|
+
return 'Python (FastAPI)';
|
|
929
|
+
}
|
|
930
|
+
if (corpus.includes('django')) {
|
|
931
|
+
return 'Python (Django)';
|
|
932
|
+
}
|
|
933
|
+
if (corpus.includes('spring boot') || corpus.includes('spring')) {
|
|
934
|
+
return 'JVM (Spring)';
|
|
935
|
+
}
|
|
936
|
+
if (corpus.includes('golang') || corpus.includes('go service') || corpus.includes('go microservice')) {
|
|
937
|
+
return 'Go';
|
|
938
|
+
}
|
|
939
|
+
if (corpus.includes('rust')) {
|
|
940
|
+
return 'Rust';
|
|
941
|
+
}
|
|
942
|
+
return undefined;
|
|
943
|
+
}
|
|
944
|
+
function inferDeployTargetFromInsights(insights) {
|
|
945
|
+
const corpus = [...insights.deployment, insights.summary].join(' ').toLowerCase();
|
|
946
|
+
if (corpus.includes('kubernetes') || corpus.includes('helm') || corpus.includes('k8s')) {
|
|
947
|
+
return 'Kubernetes';
|
|
948
|
+
}
|
|
949
|
+
if (corpus.includes('vercel')) {
|
|
950
|
+
return 'Vercel';
|
|
951
|
+
}
|
|
952
|
+
if (corpus.includes('ecs') || corpus.includes('fargate')) {
|
|
953
|
+
return 'AWS ECS/Fargate';
|
|
954
|
+
}
|
|
955
|
+
if (corpus.includes('lambda') || corpus.includes('serverless')) {
|
|
956
|
+
return 'Serverless';
|
|
957
|
+
}
|
|
958
|
+
if (corpus.includes('docker') || corpus.includes('container')) {
|
|
959
|
+
return 'Container';
|
|
960
|
+
}
|
|
961
|
+
return undefined;
|
|
962
|
+
}
|
|
609
963
|
function ownerForService(serviceId, context) {
|
|
610
964
|
const overrides = context.config.ownerTeamOverrides ?? {};
|
|
611
965
|
if (overrides[serviceId]) {
|
|
@@ -633,17 +987,20 @@ function criticalityForService(serviceId, context) {
|
|
|
633
987
|
}
|
|
634
988
|
function apiSurfaceForService(serviceId, context) {
|
|
635
989
|
const serviceContracts = context.contracts.filter((record) => record.repo === serviceId);
|
|
636
|
-
if (serviceContracts.length === 0) {
|
|
637
|
-
return 'Unknown';
|
|
638
|
-
}
|
|
639
990
|
const byType = new Map();
|
|
640
991
|
for (const contract of serviceContracts) {
|
|
641
992
|
byType.set(contract.type, (byType.get(contract.type) ?? 0) + 1);
|
|
642
993
|
}
|
|
643
|
-
|
|
994
|
+
const typed = [...byType.entries()]
|
|
644
995
|
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
645
|
-
.map(([type, count]) => `${type} (${count})`)
|
|
646
|
-
|
|
996
|
+
.map(([type, count]) => `${type} (${count})`);
|
|
997
|
+
const insights = insightsForRepo(serviceId, context);
|
|
998
|
+
const inferred = topUnique([...insights.interfaces, ...insights.asyncPatterns].map((line) => shorten(line, 70)), 2);
|
|
999
|
+
const composed = [...typed, ...inferred];
|
|
1000
|
+
if (composed.length === 0) {
|
|
1001
|
+
return 'Unknown';
|
|
1002
|
+
}
|
|
1003
|
+
return composed.join(', ');
|
|
647
1004
|
}
|
|
648
1005
|
function dependenciesForService(serviceId, context) {
|
|
649
1006
|
const sourceId = `service:${serviceId}`;
|
|
@@ -684,79 +1041,41 @@ function serviceCatalog(context) {
|
|
|
684
1041
|
.sort((a, b) => a.localeCompare(b));
|
|
685
1042
|
return serviceIds.map((serviceId) => {
|
|
686
1043
|
const repo = context.repoMap.get(serviceId);
|
|
1044
|
+
const insights = insightsForRepo(serviceId, context);
|
|
1045
|
+
const runtime = repo ? inferRuntimeFramework(repo) : 'Unknown';
|
|
1046
|
+
const runtimeFromDocs = inferRuntimeFromInsights(insights);
|
|
1047
|
+
const deploy = repo ? inferDeployTarget(repo) : 'Unknown';
|
|
1048
|
+
const deployFromDocs = inferDeployTargetFromInsights(insights);
|
|
1049
|
+
const serviceDataStores = [...new Set([...datastoresForService(serviceId, context).split(', ').filter((entry) => entry !== 'Unknown'), ...insights.dataStores])]
|
|
1050
|
+
.filter((entry) => entry.length > 0)
|
|
1051
|
+
.sort((a, b) => a.localeCompare(b));
|
|
687
1052
|
return {
|
|
688
1053
|
serviceName: serviceId,
|
|
689
1054
|
repository: repo?.fullName ?? serviceId,
|
|
690
1055
|
ownerTeam: ownerForService(serviceId, context),
|
|
691
|
-
runtime:
|
|
1056
|
+
runtime: runtime !== 'Unknown' ? runtime : runtimeFromDocs ?? 'Unknown',
|
|
692
1057
|
apiEventSurface: apiSurfaceForService(serviceId, context),
|
|
693
1058
|
dependencies: dependenciesForService(serviceId, context),
|
|
694
|
-
dataStores:
|
|
695
|
-
deployTarget:
|
|
1059
|
+
dataStores: formatList(serviceDataStores),
|
|
1060
|
+
deployTarget: deploy !== 'Unknown' ? deploy : deployFromDocs ?? 'Unknown',
|
|
696
1061
|
tier: criticalityForService(serviceId, context),
|
|
697
1062
|
status: statusForService(serviceId, context),
|
|
698
1063
|
};
|
|
699
1064
|
});
|
|
700
1065
|
}
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
.
|
|
705
|
-
.
|
|
706
|
-
|
|
707
|
-
function renderSourceBlock(sourceRefs) {
|
|
708
|
-
const lines = ['### Sources', ''];
|
|
709
|
-
if (sourceRefs.length === 0) {
|
|
710
|
-
lines.push('- Unknown');
|
|
711
|
-
lines.push('');
|
|
712
|
-
return lines;
|
|
713
|
-
}
|
|
714
|
-
for (const source of sourceRefs) {
|
|
715
|
-
const generated = source.generatedAt ?? 'Unknown';
|
|
716
|
-
const freshness = source.stale ? 'stale' : 'fresh';
|
|
717
|
-
const suffix = source.note ? ` (${source.note})` : '';
|
|
718
|
-
lines.push(`- ${source.label}: \`${source.path}\` (generated: ${generated}, ${freshness})${suffix}`);
|
|
719
|
-
}
|
|
720
|
-
lines.push('');
|
|
721
|
-
return lines;
|
|
722
|
-
}
|
|
723
|
-
function renderStaleWarning(sourceRefs) {
|
|
724
|
-
const stale = sourceRefs.filter((source) => source.required && (source.stale || !source.exists));
|
|
725
|
-
if (stale.length === 0) {
|
|
726
|
-
return [];
|
|
727
|
-
}
|
|
728
|
-
const lines = ['> [!WARNING]', '> Stale or missing source data detected for this section:', ...stale.map((source) => `> - ${source.label}`), ''];
|
|
729
|
-
return lines;
|
|
730
|
-
}
|
|
731
|
-
function defaultManualBlockText(sectionId) {
|
|
732
|
-
return `\nAdd team-specific notes for \`${sectionId}\` here.\n`;
|
|
1066
|
+
function sectionAnchor(title) {
|
|
1067
|
+
return title
|
|
1068
|
+
.toLowerCase()
|
|
1069
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
1070
|
+
.trim()
|
|
1071
|
+
.replace(/\s+/g, '-');
|
|
733
1072
|
}
|
|
734
|
-
function
|
|
735
|
-
const out = new Map();
|
|
736
|
-
for (const sectionId of exports.README_SECTION_ORDER) {
|
|
737
|
-
const escaped = sectionId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
738
|
-
const regex = new RegExp(`<!-- SDX:SECTION:${escaped}:MANUAL:START -->([\\s\\S]*?)<!-- SDX:SECTION:${escaped}:MANUAL:END -->`, 'm');
|
|
739
|
-
const match = existingContent.match(regex);
|
|
740
|
-
if (match) {
|
|
741
|
-
out.set(sectionId, match[1]);
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
return out;
|
|
745
|
-
}
|
|
746
|
-
function renderSection(section, sources, manualContent) {
|
|
1073
|
+
function renderSection(section) {
|
|
747
1074
|
const lines = [];
|
|
748
|
-
lines.push(`<!-- SDX:SECTION:${section.id}:START -->`);
|
|
749
1075
|
lines.push(`## ${section.title}`);
|
|
750
1076
|
lines.push('');
|
|
751
1077
|
lines.push(...section.body);
|
|
752
1078
|
lines.push('');
|
|
753
|
-
const sourceRefs = resolveSectionSources(section, sources);
|
|
754
|
-
lines.push(...renderStaleWarning(sourceRefs));
|
|
755
|
-
lines.push(...renderSourceBlock(sourceRefs));
|
|
756
|
-
const manualBody = manualContent ?? defaultManualBlockText(section.id);
|
|
757
|
-
lines.push(`<!-- SDX:SECTION:${section.id}:MANUAL:START -->${manualBody}<!-- SDX:SECTION:${section.id}:MANUAL:END -->`);
|
|
758
|
-
lines.push(`<!-- SDX:SECTION:${section.id}:END -->`);
|
|
759
|
-
lines.push('');
|
|
760
1079
|
return lines.join('\n');
|
|
761
1080
|
}
|
|
762
1081
|
function splitLines(input) {
|
|
@@ -874,7 +1193,7 @@ function unifiedDiff(oldText, newText, oldLabel, newLabel) {
|
|
|
874
1193
|
}
|
|
875
1194
|
return `${lines.join('\n')}\n`;
|
|
876
1195
|
}
|
|
877
|
-
function buildReadmeContext(mapId, db, cwd, outputPath, config) {
|
|
1196
|
+
async function buildReadmeContext(mapId, db, cwd, outputPath, config) {
|
|
878
1197
|
const now = new Date();
|
|
879
1198
|
const threshold = config.staleThresholdHours ?? 72;
|
|
880
1199
|
const scope = (0, scope_1.loadScopeManifest)(mapId, cwd);
|
|
@@ -898,6 +1217,16 @@ function buildReadmeContext(mapId, db, cwd, outputPath, config) {
|
|
|
898
1217
|
sourceRefs.push(sourceFromFile('architecture-model', 'Architecture model', node_path_1.default.join(cwd, 'maps', mapId, 'architecture', 'model.json'), cwd, threshold, now, false));
|
|
899
1218
|
sourceRefs.push(sourceFromFile('architecture-validation', 'Architecture validation', node_path_1.default.join(cwd, 'maps', mapId, 'architecture', 'validation.json'), cwd, threshold, now, false));
|
|
900
1219
|
sourceRefs.push(sourceFromRepoSync(repos, selectedRepos, threshold, now));
|
|
1220
|
+
let githubToken;
|
|
1221
|
+
try {
|
|
1222
|
+
const sdxConfig = (0, config_1.loadConfig)(cwd);
|
|
1223
|
+
const tokenEnv = sdxConfig.github?.tokenEnv?.trim() || 'GITHUB_TOKEN';
|
|
1224
|
+
githubToken = process.env[tokenEnv];
|
|
1225
|
+
}
|
|
1226
|
+
catch {
|
|
1227
|
+
githubToken = process.env['GITHUB_TOKEN'];
|
|
1228
|
+
}
|
|
1229
|
+
const repoInsights = await buildAllRepoInsights(selectedRepos, repoMap, githubToken);
|
|
901
1230
|
return {
|
|
902
1231
|
cwd,
|
|
903
1232
|
mapId,
|
|
@@ -916,6 +1245,7 @@ function buildReadmeContext(mapId, db, cwd, outputPath, config) {
|
|
|
916
1245
|
sourceSnapshotAt: computeSnapshotTimestamp(sourceRefs, now),
|
|
917
1246
|
coreRequestPath: findCoreRequestPath(model, serviceMap),
|
|
918
1247
|
sourceRepoSyncAt: sourceRefs.find((source) => source.id === 'repo-sync')?.generatedAt,
|
|
1248
|
+
repoInsights,
|
|
919
1249
|
};
|
|
920
1250
|
}
|
|
921
1251
|
function buildSections(context) {
|
|
@@ -931,18 +1261,10 @@ function buildSections(context) {
|
|
|
931
1261
|
};
|
|
932
1262
|
const catalogRows = serviceCatalog(context);
|
|
933
1263
|
const asyncContracts = context.contracts.filter((record) => record.type === 'asyncapi');
|
|
934
|
-
const
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
fullName: repo?.fullName ?? repoName,
|
|
939
|
-
owner: ownerForService(repoName, context),
|
|
940
|
-
source: repo?.source ?? 'Unknown',
|
|
941
|
-
branch: repo?.defaultBranch ?? 'Unknown',
|
|
942
|
-
localPath: repo?.localPath ?? 'Unknown',
|
|
943
|
-
domain: domainForRepo(repoName, context.config),
|
|
944
|
-
};
|
|
945
|
-
});
|
|
1264
|
+
const allInsights = context.selectedRepos.map((repoName) => ({
|
|
1265
|
+
repoName,
|
|
1266
|
+
insights: insightsForRepo(repoName, context),
|
|
1267
|
+
}));
|
|
946
1268
|
const datastoreNodes = context.architectureModel.nodes.filter((node) => node.type === 'datastore');
|
|
947
1269
|
const adrDir = node_path_1.default.join(context.cwd, 'docs', 'adr');
|
|
948
1270
|
const adrFiles = (0, fs_1.fileExists)(adrDir)
|
|
@@ -952,25 +1274,81 @@ function buildSections(context) {
|
|
|
952
1274
|
.map((entry) => entry.name)
|
|
953
1275
|
.sort((a, b) => a.localeCompare(b))
|
|
954
1276
|
: [];
|
|
1277
|
+
const summaryHighlights = allInsights
|
|
1278
|
+
.filter((entry) => entry.insights.summary !== 'Unknown')
|
|
1279
|
+
.sort((a, b) => a.repoName.localeCompare(b.repoName))
|
|
1280
|
+
.slice(0, 8)
|
|
1281
|
+
.map((entry) => `- **${entry.repoName}**: ${shorten(entry.insights.summary, 220)}`);
|
|
955
1282
|
const coreFlowLines = context.coreRequestPath.length > 0
|
|
956
1283
|
? context.coreRequestPath.map((edge) => {
|
|
957
1284
|
const from = edge.from.replace('service:', '');
|
|
958
1285
|
const to = edge.to.replace('service:', '');
|
|
959
|
-
return `- ${from} -> ${to}
|
|
1286
|
+
return `- ${from} -> ${to}`;
|
|
960
1287
|
})
|
|
961
1288
|
: ['- Unknown'];
|
|
1289
|
+
const coreFlowServices = topUnique(context.coreRequestPath.flatMap((edge) => [edge.from.replace('service:', ''), edge.to.replace('service:', '')]), 8);
|
|
1290
|
+
const coreFlowNotes = coreFlowServices.map((serviceId) => {
|
|
1291
|
+
const insights = insightsForRepo(serviceId, context);
|
|
1292
|
+
const line = insights.responsibilities[0] ?? insights.interfaces[0] ?? insights.summary;
|
|
1293
|
+
return `- **${serviceId}**: ${shorten(line, 180)}`;
|
|
1294
|
+
});
|
|
1295
|
+
const asyncHighlights = topUnique(allInsights.flatMap((entry) => entry.insights.asyncPatterns.map((line) => `- **${entry.repoName}**: ${shorten(line, 180)}`)), 12);
|
|
1296
|
+
const securityHighlights = topUnique(allInsights.flatMap((entry) => entry.insights.security.map((line) => `- **${entry.repoName}**: ${shorten(line, 180)}`)), 10);
|
|
1297
|
+
const runbookHighlights = topUnique(allInsights.flatMap((entry) => entry.insights.runbooks.map((line) => `- **${entry.repoName}**: ${shorten(line, 180)}`)), 10);
|
|
1298
|
+
const localDevCommands = topUnique(allInsights.flatMap((entry) => entry.insights.localDevelopment), 12);
|
|
1299
|
+
const glossaryTerms = topUnique(allInsights.flatMap((entry) => entry.insights.glossary), 20).sort((a, b) => a.localeCompare(b));
|
|
1300
|
+
const repoRows = context.selectedRepos
|
|
1301
|
+
.map((repoName) => {
|
|
1302
|
+
const repo = context.repoMap.get(repoName);
|
|
1303
|
+
const insights = insightsForRepo(repoName, context);
|
|
1304
|
+
return {
|
|
1305
|
+
fullName: repo?.fullName ?? repoName,
|
|
1306
|
+
owner: ownerForService(repoName, context),
|
|
1307
|
+
domain: domainForRepo(repoName, context.config),
|
|
1308
|
+
role: shorten(insights.responsibilities[0] ?? insights.summary, 140),
|
|
1309
|
+
docs: formatLinks(insights.docReferences, 3),
|
|
1310
|
+
};
|
|
1311
|
+
})
|
|
1312
|
+
.sort((a, b) => a.fullName.localeCompare(b.fullName));
|
|
1313
|
+
const environmentRows = catalogRows.map((row) => {
|
|
1314
|
+
const insights = insightsForRepo(row.serviceName, context);
|
|
1315
|
+
const note = insights.deployment[0] ?? insights.runbooks[0] ?? 'Unknown';
|
|
1316
|
+
return `| ${row.serviceName} | ${row.deployTarget} | ${row.runtime} | ${shorten(note, 140)} |`;
|
|
1317
|
+
});
|
|
1318
|
+
const contractsByRepo = new Map();
|
|
1319
|
+
for (const contract of context.contracts) {
|
|
1320
|
+
const entries = contractsByRepo.get(contract.repo) ?? [];
|
|
1321
|
+
entries.push(contract);
|
|
1322
|
+
contractsByRepo.set(contract.repo, entries);
|
|
1323
|
+
}
|
|
1324
|
+
const contractRows = context.selectedRepos.map((repoName) => {
|
|
1325
|
+
const contracts = contractsByRepo.get(repoName) ?? [];
|
|
1326
|
+
const summary = contracts.length > 0
|
|
1327
|
+
? contracts.map((contract) => `${contract.type}:${contract.path}`).slice(0, 4).join('<br/>')
|
|
1328
|
+
: 'Unknown';
|
|
1329
|
+
return `| ${repoName} | ${summary} |`;
|
|
1330
|
+
});
|
|
1331
|
+
const adrLinks = topUnique([
|
|
1332
|
+
...adrFiles.map((fileName) => `- [${fileName}](./docs/adr/${fileName})`),
|
|
1333
|
+
...allInsights.flatMap((entry) => entry.insights.adrs.map((candidate) => /^https?:\/\//.test(candidate)
|
|
1334
|
+
? `- [${entry.repoName} ADR](${candidate})`
|
|
1335
|
+
: `- ${entry.repoName}: \`${candidate}\``)),
|
|
1336
|
+
], 30);
|
|
962
1337
|
const sectionById = {
|
|
963
1338
|
what_is_this_system: {
|
|
964
1339
|
id: 'what_is_this_system',
|
|
965
1340
|
title: SECTION_TITLES['what_is_this_system'],
|
|
966
1341
|
body: [
|
|
967
1342
|
context.config.customIntro ??
|
|
968
|
-
'
|
|
1343
|
+
'SDX traverses repository documentation, contracts, and dependency signals to keep this architecture guide current.',
|
|
1344
|
+
'',
|
|
1345
|
+
`- Scope: \`${context.scope.org}\` org, map \`${context.mapId}\``,
|
|
1346
|
+
`- Repositories in scope: ${context.selectedRepos.length}`,
|
|
1347
|
+
`- Services identified: ${catalogRows.length}`,
|
|
969
1348
|
'',
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
`- Services detected: ${catalogRows.length}`,
|
|
1349
|
+
...(summaryHighlights.length > 0
|
|
1350
|
+
? ['### Service purpose highlights', '', ...summaryHighlights]
|
|
1351
|
+
: ['### Service purpose highlights', '', '- Unknown']),
|
|
974
1352
|
],
|
|
975
1353
|
sourceIds: ['scope', 'repo-sync', 'service-map-json'],
|
|
976
1354
|
},
|
|
@@ -994,6 +1372,11 @@ function buildSections(context) {
|
|
|
994
1372
|
? `- [Optional C4 container](${links.optionalContainer})`
|
|
995
1373
|
: '- Optional C4 container: Not available');
|
|
996
1374
|
}
|
|
1375
|
+
if (coreFlowLines.length > 0) {
|
|
1376
|
+
lines.push('');
|
|
1377
|
+
lines.push('### Primary request path');
|
|
1378
|
+
lines.push(...coreFlowLines);
|
|
1379
|
+
}
|
|
997
1380
|
return lines;
|
|
998
1381
|
})(),
|
|
999
1382
|
sourceIds: [
|
|
@@ -1015,6 +1398,9 @@ function buildSections(context) {
|
|
|
1015
1398
|
...(catalogRows.length > 0
|
|
1016
1399
|
? catalogRows.map((row) => `| ${row.serviceName} | ${row.repository} | ${row.ownerTeam} | ${row.runtime} | ${row.apiEventSurface} | ${row.dependencies} | ${row.dataStores} | ${row.deployTarget} | ${row.tier} | ${row.status} |`)
|
|
1017
1400
|
: ['| Unknown | Unknown | Unknown | Unknown | Unknown | Unknown | Unknown | Unknown | Unknown | Unknown |']),
|
|
1401
|
+
'',
|
|
1402
|
+
'### Service briefs',
|
|
1403
|
+
...(summaryHighlights.length > 0 ? summaryHighlights : ['- Unknown']),
|
|
1018
1404
|
],
|
|
1019
1405
|
sourceIds: ['service-map-json', 'contracts-json', 'architecture-model', 'repo-sync'],
|
|
1020
1406
|
},
|
|
@@ -1023,8 +1409,11 @@ function buildSections(context) {
|
|
|
1023
1409
|
title: SECTION_TITLES['critical_flows'],
|
|
1024
1410
|
body: [
|
|
1025
1411
|
`- Primary sequence diagram: [core-request-flow.mmd](${links.sequence})`,
|
|
1026
|
-
'-
|
|
1412
|
+
'- Current highest-confidence request chain:',
|
|
1027
1413
|
...coreFlowLines,
|
|
1414
|
+
'',
|
|
1415
|
+
'### Service responsibilities in this flow',
|
|
1416
|
+
...(coreFlowNotes.length > 0 ? coreFlowNotes : ['- Unknown']),
|
|
1028
1417
|
],
|
|
1029
1418
|
sourceIds: ['architecture-model', 'service-map-json', 'docs-dependencies', 'diagram-core-sequence'],
|
|
1030
1419
|
},
|
|
@@ -1037,6 +1426,9 @@ function buildSections(context) {
|
|
|
1037
1426
|
...(asyncContracts.length > 0
|
|
1038
1427
|
? asyncContracts.map((record) => `| ${record.path} | ${record.repo} | ${record.version ?? 'Unknown'} | ${record.compatibilityStatus} | ${formatList(record.producers)} | ${formatList(record.consumers)} |`)
|
|
1039
1428
|
: ['| Unknown | Unknown | Unknown | Unknown | Unknown | Unknown |']),
|
|
1429
|
+
'',
|
|
1430
|
+
'### Async behavior from repository docs',
|
|
1431
|
+
...(asyncHighlights.length > 0 ? asyncHighlights : ['- Unknown']),
|
|
1040
1432
|
],
|
|
1041
1433
|
sourceIds: ['contracts-json', 'architecture-model'],
|
|
1042
1434
|
},
|
|
@@ -1044,11 +1436,9 @@ function buildSections(context) {
|
|
|
1044
1436
|
id: 'contracts_index',
|
|
1045
1437
|
title: SECTION_TITLES['contracts_index'],
|
|
1046
1438
|
body: [
|
|
1047
|
-
'| Repository |
|
|
1048
|
-
'
|
|
1049
|
-
...(
|
|
1050
|
-
? context.contracts.map((record) => `| ${record.repo} | ${record.type} | ${record.path} | ${record.version ?? 'Unknown'} | ${record.compatibilityStatus} |`)
|
|
1051
|
-
: ['| Unknown | Unknown | Unknown | Unknown | Unknown |']),
|
|
1439
|
+
'| Repository | Contract surfaces |',
|
|
1440
|
+
'|---|---|',
|
|
1441
|
+
...(contractRows.length > 0 ? contractRows : ['| Unknown | Unknown |']),
|
|
1052
1442
|
],
|
|
1053
1443
|
sourceIds: ['contracts-json', 'contracts-md'],
|
|
1054
1444
|
},
|
|
@@ -1056,11 +1446,11 @@ function buildSections(context) {
|
|
|
1056
1446
|
id: 'repository_index',
|
|
1057
1447
|
title: SECTION_TITLES['repository_index'],
|
|
1058
1448
|
body: [
|
|
1059
|
-
'| Repository | Owner/team | Domain |
|
|
1060
|
-
'
|
|
1449
|
+
'| Repository | Owner/team | Domain | Role in system | Key docs |',
|
|
1450
|
+
'|---|---|---|---|---|',
|
|
1061
1451
|
...(repoRows.length > 0
|
|
1062
|
-
? repoRows.map((row) => `| ${row.fullName} | ${row.owner} | ${row.domain} | ${row.
|
|
1063
|
-
: ['| Unknown | Unknown | Unknown | Unknown | Unknown |
|
|
1452
|
+
? repoRows.map((row) => `| ${row.fullName} | ${row.owner} | ${row.domain} | ${row.role} | ${row.docs} |`)
|
|
1453
|
+
: ['| Unknown | Unknown | Unknown | Unknown | Unknown |']),
|
|
1064
1454
|
],
|
|
1065
1455
|
sourceIds: ['scope', 'repo-sync'],
|
|
1066
1456
|
},
|
|
@@ -1070,9 +1460,7 @@ function buildSections(context) {
|
|
|
1070
1460
|
body: [
|
|
1071
1461
|
'| Service | Deploy target | Runtime/framework | Environment notes |',
|
|
1072
1462
|
'|---|---|---|---|',
|
|
1073
|
-
...(
|
|
1074
|
-
? catalogRows.map((row) => `| ${row.serviceName} | ${row.deployTarget} | ${row.runtime} | ${row.deployTarget === 'Unknown' ? 'Unknown' : 'Validate env parity in deployment pipeline'} |`)
|
|
1075
|
-
: ['| Unknown | Unknown | Unknown | Unknown |']),
|
|
1463
|
+
...(environmentRows.length > 0 ? environmentRows : ['| Unknown | Unknown | Unknown | Unknown |']),
|
|
1076
1464
|
],
|
|
1077
1465
|
sourceIds: ['service-map-json', 'repo-sync', 'architecture-model'],
|
|
1078
1466
|
},
|
|
@@ -1100,11 +1488,13 @@ function buildSections(context) {
|
|
|
1100
1488
|
id: 'security_compliance',
|
|
1101
1489
|
title: SECTION_TITLES['security_compliance'],
|
|
1102
1490
|
body: [
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1491
|
+
...(securityHighlights.length > 0
|
|
1492
|
+
? securityHighlights
|
|
1493
|
+
: [
|
|
1494
|
+
'- Authentication and authorization approach: Unknown',
|
|
1495
|
+
'- Data classification and compliance posture: Unknown',
|
|
1496
|
+
'- Secret management and key handling: Unknown',
|
|
1497
|
+
]),
|
|
1108
1498
|
],
|
|
1109
1499
|
sourceIds: ['architecture-model', 'contracts-json'],
|
|
1110
1500
|
},
|
|
@@ -1114,13 +1504,17 @@ function buildSections(context) {
|
|
|
1114
1504
|
body: [
|
|
1115
1505
|
'```bash',
|
|
1116
1506
|
'./scripts/sdx status',
|
|
1507
|
+
`./scripts/sdx repo sync --org ${context.scope.org}`,
|
|
1117
1508
|
`./scripts/sdx map build ${context.mapId}`,
|
|
1118
1509
|
`./scripts/sdx contracts extract --map ${context.mapId}`,
|
|
1119
|
-
`./scripts/sdx
|
|
1510
|
+
`./scripts/sdx architecture generate --map ${context.mapId}`,
|
|
1120
1511
|
`./scripts/sdx docs readme --map ${context.mapId}`,
|
|
1121
1512
|
'```',
|
|
1122
1513
|
'',
|
|
1123
|
-
'
|
|
1514
|
+
'### Commands found in service docs',
|
|
1515
|
+
...(localDevCommands.length > 0
|
|
1516
|
+
? ['```bash', ...localDevCommands.map((command) => command.replace(/`/g, '')), '```']
|
|
1517
|
+
: ['- Unknown']),
|
|
1124
1518
|
],
|
|
1125
1519
|
sourceIds: ['scope', 'service-map-json', 'contracts-json'],
|
|
1126
1520
|
},
|
|
@@ -1128,32 +1522,25 @@ function buildSections(context) {
|
|
|
1128
1522
|
id: 'runbooks_escalation',
|
|
1129
1523
|
title: SECTION_TITLES['runbooks_escalation'],
|
|
1130
1524
|
body: [
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
'- Required action: populate escalation ownership in manual block.',
|
|
1525
|
+
...(runbookHighlights.length > 0
|
|
1526
|
+
? runbookHighlights
|
|
1527
|
+
: ['- Runbooks and escalation notes are not explicitly documented in scanned repositories.']),
|
|
1135
1528
|
],
|
|
1136
1529
|
sourceIds: ['architecture-model', 'repo-sync'],
|
|
1137
1530
|
},
|
|
1138
1531
|
adr_index: {
|
|
1139
1532
|
id: 'adr_index',
|
|
1140
1533
|
title: SECTION_TITLES['adr_index'],
|
|
1141
|
-
body: [
|
|
1142
|
-
...(adrFiles.length > 0
|
|
1143
|
-
? adrFiles.map((fileName) => `- [${fileName}](./docs/adr/${fileName})`)
|
|
1144
|
-
: ['- Unknown (no ADR markdown files found under `docs/adr/`)']),
|
|
1145
|
-
],
|
|
1534
|
+
body: [...(adrLinks.length > 0 ? adrLinks : ['- Unknown'])],
|
|
1146
1535
|
sourceIds: ['docs-architecture'],
|
|
1147
1536
|
},
|
|
1148
1537
|
glossary: {
|
|
1149
1538
|
id: 'glossary',
|
|
1150
1539
|
title: SECTION_TITLES['glossary'],
|
|
1151
1540
|
body: [
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
'- **Override**: Manual architecture hints in `maps/<map-id>/architecture-overrides.json`.',
|
|
1156
|
-
'- **Unknown**: Field not currently derivable from SDX artifacts; requires manual completion.',
|
|
1541
|
+
...(glossaryTerms.length > 0
|
|
1542
|
+
? glossaryTerms.map((term) => `- **${term}**: Refer to service-level docs for context.`)
|
|
1543
|
+
: ['- Unknown']),
|
|
1157
1544
|
],
|
|
1158
1545
|
sourceIds: ['scope', 'service-map-json', 'contracts-json', 'architecture-model'],
|
|
1159
1546
|
},
|
|
@@ -1161,33 +1548,32 @@ function buildSections(context) {
|
|
|
1161
1548
|
id: 'changelog_metadata',
|
|
1162
1549
|
title: SECTION_TITLES['changelog_metadata'],
|
|
1163
1550
|
body: [
|
|
1164
|
-
`-
|
|
1165
|
-
`-
|
|
1166
|
-
`-
|
|
1167
|
-
`- CLI
|
|
1168
|
-
|
|
1169
|
-
`- Repo sync baseline: ${context.sourceRepoSyncAt ?? 'Unknown'}`,
|
|
1170
|
-
'- Source refs used:',
|
|
1171
|
-
...context.sources.map((source) => ` - ${source.label}: \`${source.path}\``),
|
|
1551
|
+
`- Last generated: ${context.sourceSnapshotAt}`,
|
|
1552
|
+
`- Source snapshot: ${context.sourceSnapshotAt}`,
|
|
1553
|
+
`- Map: ${context.mapId}`,
|
|
1554
|
+
`- Tooling: SDX CLI ${(0, version_1.getCliPackageVersion)()} (schema ${constants_1.SCHEMA_VERSION})`,
|
|
1555
|
+
'- Run `./scripts/sdx docs readme --map <map-id> --check` in CI to verify freshness and drift.',
|
|
1172
1556
|
],
|
|
1173
1557
|
sourceIds: context.sources.map((source) => source.id),
|
|
1174
1558
|
},
|
|
1175
1559
|
};
|
|
1176
1560
|
return exports.README_SECTION_ORDER.map((sectionId) => sectionById[sectionId]);
|
|
1177
1561
|
}
|
|
1178
|
-
function renderReadme(sections, context
|
|
1179
|
-
const manualBlocks = extractManualBlocks(existingContent);
|
|
1562
|
+
function renderReadme(sections, context) {
|
|
1180
1563
|
const lines = [
|
|
1181
|
-
|
|
1564
|
+
`# ${context.scope.org} System Architecture`,
|
|
1565
|
+
'',
|
|
1566
|
+
`This README is the architecture entry point for the \`${context.scope.org}\` engineering organization.`,
|
|
1567
|
+
'',
|
|
1568
|
+
`It is generated by SDX from repository docs, contracts, and map artifacts using \`${context.mapId}\`.`,
|
|
1182
1569
|
'',
|
|
1183
|
-
|
|
1570
|
+
'## Table of contents',
|
|
1184
1571
|
'',
|
|
1185
|
-
|
|
1572
|
+
...sections.map((section) => `- [${section.title}](#${sectionAnchor(section.title)})`),
|
|
1186
1573
|
'',
|
|
1187
1574
|
];
|
|
1188
1575
|
for (const section of sections) {
|
|
1189
|
-
|
|
1190
|
-
lines.push(renderSection(section, context.sources, manual));
|
|
1576
|
+
lines.push(renderSection(section));
|
|
1191
1577
|
}
|
|
1192
1578
|
return {
|
|
1193
1579
|
content: `${lines.join('\n').trimEnd()}\n`,
|
|
@@ -1217,7 +1603,7 @@ function summarizeResult(outputPath, staleSources, missingSources, changed, chec
|
|
|
1217
1603
|
}
|
|
1218
1604
|
return lines.join('\n');
|
|
1219
1605
|
}
|
|
1220
|
-
function generateReadme(options) {
|
|
1606
|
+
async function generateReadme(options) {
|
|
1221
1607
|
const cwd = options.cwd ?? process.cwd();
|
|
1222
1608
|
const outputPath = node_path_1.default.resolve(cwd, options.output ?? 'README.md');
|
|
1223
1609
|
const includeSections = options.includeSections ?? [];
|
|
@@ -1227,7 +1613,7 @@ function generateReadme(options) {
|
|
|
1227
1613
|
}
|
|
1228
1614
|
const { config, sourcePath } = loadReadmeConfig(cwd);
|
|
1229
1615
|
const selectedSections = selectSections(config, includeSections, excludeSections);
|
|
1230
|
-
const context = buildReadmeContext(options.mapId, options.db, cwd, outputPath, config);
|
|
1616
|
+
const context = await buildReadmeContext(options.mapId, options.db, cwd, outputPath, config);
|
|
1231
1617
|
if (sourcePath) {
|
|
1232
1618
|
context.sources.push(sourceFromFile('readme-config', 'README config', sourcePath, cwd, context.staleThresholdHours, context.now, false));
|
|
1233
1619
|
}
|
|
@@ -1240,7 +1626,7 @@ function generateReadme(options) {
|
|
|
1240
1626
|
context.sourceSnapshotAt = computeSnapshotTimestamp(context.sources, context.now);
|
|
1241
1627
|
const orderedSections = buildSections(context).filter((section) => selectedSections.includes(section.id));
|
|
1242
1628
|
const existingContent = (0, fs_1.safeReadText)(outputPath);
|
|
1243
|
-
const rendered = renderReadme(orderedSections, context
|
|
1629
|
+
const rendered = renderReadme(orderedSections, context);
|
|
1244
1630
|
const { stale, missing, changed } = checkFailures(existingContent, rendered.content, rendered.sourceRefs);
|
|
1245
1631
|
const shouldWrite = writeEnabled && changed;
|
|
1246
1632
|
if (shouldWrite) {
|
package/dist/lib/repoRegistry.js
CHANGED
|
@@ -61,15 +61,20 @@ function upsertRepos(db, repos) {
|
|
|
61
61
|
}
|
|
62
62
|
function setLocalRepoPath(db, name, localPath, org) {
|
|
63
63
|
const normalized = node_path_1.default.resolve(localPath);
|
|
64
|
+
const now = new Date().toISOString();
|
|
64
65
|
const existing = db
|
|
65
66
|
.prepare('SELECT * FROM repo_registry WHERE name = ?')
|
|
66
67
|
.get(name);
|
|
67
68
|
if (existing) {
|
|
68
|
-
db.prepare(`UPDATE repo_registry
|
|
69
|
+
db.prepare(`UPDATE repo_registry
|
|
70
|
+
SET local_path = ?,
|
|
71
|
+
source = CASE WHEN source='github' THEN 'hybrid' ELSE 'local' END,
|
|
72
|
+
last_synced_at = ?
|
|
73
|
+
WHERE name = ?`).run(normalized, now, name);
|
|
69
74
|
}
|
|
70
75
|
else {
|
|
71
|
-
db.prepare(`INSERT INTO repo_registry (name, full_name, org, archived, fork, local_path, source)
|
|
72
|
-
VALUES (?, ?, ?, 0, 0, ?, 'local')`).run(name, org ? `${org}/${name}` : name, org ?? 'local', normalized);
|
|
76
|
+
db.prepare(`INSERT INTO repo_registry (name, full_name, org, archived, fork, local_path, source, last_synced_at)
|
|
77
|
+
VALUES (?, ?, ?, 0, 0, ?, 'local', ?)`).run(name, org ? `${org}/${name}` : name, org ?? 'local', normalized, now);
|
|
73
78
|
}
|
|
74
79
|
return getRepoByName(db, name);
|
|
75
80
|
}
|