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 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 sources, missing sources, or README drift)
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 and freshness summary
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,
@@ -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
+ }
@@ -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
- return [...byType.entries()]
994
+ const typed = [...byType.entries()]
644
995
  .sort((a, b) => a[0].localeCompare(b[0]))
645
- .map(([type, count]) => `${type} (${count})`)
646
- .join(', ');
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: repo ? inferRuntimeFramework(repo) : 'Unknown',
1056
+ runtime: runtime !== 'Unknown' ? runtime : runtimeFromDocs ?? 'Unknown',
692
1057
  apiEventSurface: apiSurfaceForService(serviceId, context),
693
1058
  dependencies: dependenciesForService(serviceId, context),
694
- dataStores: datastoresForService(serviceId, context),
695
- deployTarget: repo ? inferDeployTarget(repo) : 'Unknown',
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 resolveSectionSources(section, sources) {
702
- const ids = new Set(section.sourceIds);
703
- return sources
704
- .filter((source) => ids.has(source.id))
705
- .sort((a, b) => a.label.localeCompare(b.label));
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 extractManualBlocks(existingContent) {
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 repoRows = context.selectedRepos.map((repoName) => {
935
- const repo = context.repoMap.get(repoName);
936
- return {
937
- name: repoName,
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} (confidence ${edge.provenance.confidence.toFixed(2)})`;
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
- 'This README is generated by SDX as the canonical architecture onboarding guide for this org workspace.',
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
- `- Organization: \`${context.scope.org}\``,
971
- `- Map: \`${context.mapId}\``,
972
- `- Repositories selected for this README: ${context.selectedRepos.length}`,
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
- '- Highest-confidence path:',
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 | Type | Path | Version | Compatibility |',
1048
- '|---|---|---|---|---|',
1049
- ...(context.contracts.length > 0
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 | Source | Default branch | Local path |',
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.source} | ${row.branch} | ${row.localPath.replace(/\|/g, '\\|')} |`)
1063
- : ['| Unknown | 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
- ...(catalogRows.length > 0
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
- '- Authentication/authorization model: Unknown',
1104
- '- Data classification posture: Unknown',
1105
- '- Compliance scope (SOC2/PCI/HIPAA/etc.): Unknown',
1106
- '- Secret management baseline: Unknown',
1107
- '- Required action: populate this section via manual block with org security standards.',
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 docs generate --map ${context.mapId}`,
1510
+ `./scripts/sdx architecture generate --map ${context.mapId}`,
1120
1511
  `./scripts/sdx docs readme --map ${context.mapId}`,
1121
1512
  '```',
1122
1513
  '',
1123
- '- Use `--check` in CI to enforce freshness and deterministic output.',
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
- '- Runbook root: `docs/runbooks/` (Unknown if not present)',
1132
- '- Escalation path: Unknown',
1133
- '- Incident channel: Unknown',
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
- '- **Service**: A deployable unit represented by a repository in the selected map scope.',
1153
- '- **Contract**: API/event interface artifact (OpenAPI, GraphQL, Protobuf, AsyncAPI).',
1154
- '- **Map**: A named SDX scope manifest that defines discovered/included/excluded repos.',
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
- `- Generated timestamp: ${context.sourceSnapshotAt}`,
1165
- `- Map id: ${context.mapId}`,
1166
- `- Schema version: ${constants_1.SCHEMA_VERSION}`,
1167
- `- CLI version: ${(0, version_1.getCliPackageVersion)()}`,
1168
- `- Freshness threshold (hours): ${context.staleThresholdHours}`,
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, existingContent) {
1179
- const manualBlocks = extractManualBlocks(existingContent);
1562
+ function renderReadme(sections, context) {
1180
1563
  const lines = [
1181
- '# SDX Organization Architecture Workspace',
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
- `> Generated for org \`${context.scope.org}\` using map \`${context.mapId}\`.`,
1570
+ '## Table of contents',
1184
1571
  '',
1185
- `> Source snapshot timestamp: ${context.sourceSnapshotAt}`,
1572
+ ...sections.map((section) => `- [${section.title}](#${sectionAnchor(section.title)})`),
1186
1573
  '',
1187
1574
  ];
1188
1575
  for (const section of sections) {
1189
- const manual = manualBlocks.get(section.id);
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, existingContent);
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) {
@@ -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 SET local_path = ?, source = CASE WHEN source='github' THEN 'hybrid' ELSE 'local' END WHERE name = ?`).run(normalized, name);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdx-cli",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "System Design Intelligence CLI",
5
5
  "type": "commonjs",
6
6
  "bin": {