sdx-cli 0.3.0 → 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
@@ -20,6 +20,7 @@
20
20
  <a href="#install-from-npm">Install from npm</a> •
21
21
  <a href="#one-command-setup">One-Command Setup</a> •
22
22
  <a href="#architecture-pack-org--service-deep-dives">Architecture Pack</a> •
23
+ <a href="#canonical-root-readme-generation">Canonical README</a> •
23
24
  <a href="#daily-workflow">Daily Workflow</a> •
24
25
  <a href="#for-codex-agents">For Codex Agents</a> •
25
26
  <a href="#release-process">Release Process</a>
@@ -133,6 +134,7 @@ From your SDX workspace root:
133
134
  ./scripts/sdx contracts extract --map platform-core
134
135
  ./scripts/sdx docs generate --map platform-core
135
136
  ./scripts/sdx architecture generate --map platform-core
137
+ ./scripts/sdx docs readme --map platform-core
136
138
  ```
137
139
 
138
140
  For planning and rollout:
@@ -175,6 +177,76 @@ Use overrides to:
175
177
  - suppress incorrect inferred edges,
176
178
  - attach service owner/criticality/business context metadata.
177
179
 
180
+ ### Canonical Root README Generation
181
+ Generate a complete root `README.md` as the canonical onboarding and architecture overview for your org workspace.
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
+
193
+ ```bash
194
+ # generate/update root README.md
195
+ ./scripts/sdx docs readme --map platform-core
196
+
197
+ # write to a different output file
198
+ ./scripts/sdx docs readme --map platform-core --output ARCHITECTURE.md
199
+
200
+ # check mode for CI (non-zero on stale/missing required artifacts or README drift)
201
+ ./scripts/sdx docs readme --map platform-core --check
202
+
203
+ # dry-run preview with unified diff + readiness summary
204
+ ./scripts/sdx docs readme --map platform-core --dry-run
205
+
206
+ # selective sections
207
+ ./scripts/sdx docs readme --map platform-core \
208
+ --include what_is_this_system,architecture_glance,service_catalog \
209
+ --exclude glossary
210
+ ```
211
+
212
+ Supported section IDs (baseline order):
213
+ - `what_is_this_system`
214
+ - `architecture_glance`
215
+ - `service_catalog`
216
+ - `critical_flows`
217
+ - `event_async_topology`
218
+ - `contracts_index`
219
+ - `repository_index`
220
+ - `environments_deployment`
221
+ - `data_stores_boundaries`
222
+ - `security_compliance`
223
+ - `local_dev_contribution`
224
+ - `runbooks_escalation`
225
+ - `adr_index`
226
+ - `glossary`
227
+ - `changelog_metadata`
228
+
229
+ README config file support (first existing file wins):
230
+ - `.sdx/readme.config.json`
231
+ - `.sdx/readme.config.yaml`
232
+ - `.sdx/readme.config.yml`
233
+
234
+ Config capabilities:
235
+ - section toggles (`sections.include`, `sections.exclude`, `sections.enabled`)
236
+ - repo include/exclude filters (`repos.include`, `repos.exclude`)
237
+ - domain grouping (`domainGroups`)
238
+ - owner/team overrides (`ownerTeamOverrides`)
239
+ - diagram behavior (`diagram.autoGenerateMissing`, `diagram.includeC4Links`)
240
+ - custom intro text (`customIntro`)
241
+ - stale threshold override in hours (`staleThresholdHours`, default `72`)
242
+
243
+ CI automation example:
244
+ - copy [`docs/examples/readme-refresh.yml`](./docs/examples/readme-refresh.yml) into your consumer workspace repo under `.github/workflows/`.
245
+ - set repo/org variables:
246
+ - `SDX_ORG` (required)
247
+ - `SDX_MAP` (optional, defaults to `all-services` in the workflow)
248
+ - the workflow runs `repo sync`, `map build`, `contracts extract`, `docs generate`, and `docs readme`, then opens a PR.
249
+
178
250
  ## Cross-Repo Tech-Lead PRs (Spec-System Native)
179
251
  Use this flow when SDX should create real `CC-*` contract-change PRs in downstream repos that have spec-system initialized.
180
252
 
@@ -233,7 +305,8 @@ Use this minimal runbook when an agent needs architecture context quickly:
233
305
  3. `./scripts/sdx map build <map-id>`
234
306
  4. `./scripts/sdx contracts extract --map <map-id>`
235
307
  5. `./scripts/sdx architecture generate --map <map-id>`
236
- 6. `./scripts/sdx codex run <task-type> --map <map-id> --input <file>`
308
+ 6. `./scripts/sdx docs readme --map <map-id>`
309
+ 7. `./scripts/sdx codex run <task-type> --map <map-id> --input <file>`
237
310
 
238
311
  Where outputs land:
239
312
  - `maps/<map-id>/service-map.json|md|mmd`
@@ -260,7 +333,7 @@ sdx prompt
260
333
 
261
334
  sdx architecture generate|validate
262
335
  sdx contracts extract
263
- sdx docs generate
336
+ sdx docs generate|readme
264
337
  sdx plan review
265
338
  sdx service propose
266
339
  sdx handoff draft
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const readme_1 = require("../../lib/readme");
5
+ const project_1 = require("../../lib/project");
6
+ class DocsReadmeCommand extends core_1.Command {
7
+ static description = 'Generate or validate the canonical root README from SDX artifacts';
8
+ static flags = {
9
+ map: core_1.Flags.string({ required: true, description: 'Map identifier' }),
10
+ output: core_1.Flags.string({ required: false, default: 'README.md', description: 'README output path' }),
11
+ check: core_1.Flags.boolean({ required: false, default: false, description: 'Check mode (no writes, non-zero on stale/missing/diff)' }),
12
+ 'dry-run': core_1.Flags.boolean({ required: false, default: false, description: 'Preview mode (no writes, print unified diff + summary)' }),
13
+ include: core_1.Flags.string({
14
+ required: false,
15
+ description: 'Comma-separated section IDs to include (baseline order preserved)',
16
+ }),
17
+ exclude: core_1.Flags.string({
18
+ required: false,
19
+ description: 'Comma-separated section IDs to exclude (applied after include; exclude wins)',
20
+ }),
21
+ };
22
+ async run() {
23
+ const { flags } = await this.parse(DocsReadmeCommand);
24
+ const context = (0, project_1.loadProject)(process.cwd());
25
+ const includeSections = (0, readme_1.parseReadmeSectionList)(flags.include);
26
+ const excludeSections = (0, readme_1.parseReadmeSectionList)(flags.exclude);
27
+ const result = await (0, readme_1.generateReadme)({
28
+ mapId: flags.map,
29
+ db: context.db,
30
+ cwd: context.cwd,
31
+ output: flags.output,
32
+ includeSections,
33
+ excludeSections,
34
+ check: flags.check,
35
+ dryRun: flags['dry-run'],
36
+ });
37
+ const status = result.checkPassed ? 'ok' : 'error';
38
+ (0, project_1.recordRun)(context.db, 'docs_readme', status, flags.map, {
39
+ outputPath: result.outputPath,
40
+ sections: result.sections,
41
+ changed: result.changed,
42
+ stale: result.stale,
43
+ staleSources: result.staleSources.map((source) => source.label),
44
+ missingSources: result.missingSources.map((source) => source.label),
45
+ dryRun: flags['dry-run'],
46
+ check: flags.check,
47
+ });
48
+ context.db.close();
49
+ this.log(result.summary);
50
+ if (result.diff && result.diff.trim().length > 0) {
51
+ this.log('');
52
+ this.log(result.diff.trimEnd());
53
+ }
54
+ if (flags.check && !result.checkPassed) {
55
+ this.error('README check failed: stale/missing sources or content drift detected.', { exit: 1 });
56
+ }
57
+ if (!flags.check && !flags['dry-run']) {
58
+ this.log(`Wrote README: ${result.outputPath}`);
59
+ }
60
+ }
61
+ }
62
+ exports.default = DocsReadmeCommand;
@@ -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
+ }