@chappibunny/repolens 1.3.1 โ†’ 1.4.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  All notable changes to RepoLens will be documented in this file.
4
4
 
5
+ ## 1.4.0
6
+
7
+ ### ๐Ÿ› Bug Fixes (Tier 1 โ€” Production)
8
+
9
+ - **Confluence CDATA injection**: Code blocks containing `]]>` no longer break Confluence XML storage format. Applied standard CDATA escape pattern (`]]]]><![CDATA[>`).
10
+ - **Discord branding**: Replaced 3 leftover "RABITAI" references with "RepoLens" in Discord webhook embeds (title, footer, error title).
11
+ - **CLI branding**: Updated 2 remaining "RABITAI" banner strings to "RepoLens" in CLI output and help text.
12
+ - **Markdown publisher missing mappings**: Added 9 missing document-type filename mappings (`executive_summary`, `business_domains`, `architecture_overview`, `data_flows`, `developer_onboarding`, `graphql_schema`, `type_graph`, `dependency_graph`, `architecture_drift`). All 15 document plan keys now map to output filenames.
13
+ - **Relative link rewriting**: Notion and Confluence publishers now strip relative markdown links (`./path.md`, `../path.md`) that can't resolve in external platforms. Links are replaced with their display text.
14
+
15
+ ### ๐Ÿ”ง Improvements (Tier 2 โ€” Robustness)
16
+
17
+ - **Generic domain defaults**: Replaced 12 fintech-specific domain hints with 15 universally applicable domains (Authentication, Analytics, Content Management, Search, Notifications, Payments, API Layer, UI Components, Hooks, State, Utilities, Data Layer, Config, Testing, Background Jobs). Domain inference no longer maps "chart" to "Market Data" in non-finance apps.
18
+ - **Real import-based system map**: `renderSystemMap()` now uses actual import edges from the dependency graph analyzer when available, instead of heuristic guessing. Diagram labels indicate whether relationships are from "Real import analysis" or "Heuristic inference".
19
+ - **AI context size limiting**: Added `truncateContext()` function to AI prompts with a 12K character cap. Progressive pruning: routes/pages to 15, domains to 8, top modules to 10, then hard-truncate. Prevents token overflow on large codebases.
20
+ - **Doctor env var validation**: `repolens doctor` now checks for publisher-specific environment variables (NOTION_TOKEN, CONFLUENCE_URL/EMAIL/API_TOKEN, GITHUB_TOKEN, REPOLENS_AI_API_KEY) and warns when required vars are missing for configured publishers.
21
+ - **Truncation warnings**: Renderers now display notes when output is truncated โ€” modules >100 in catalog, routes >200 in route map, files/routes >25 and modules >40 in architecture diff.
22
+ - **Watch mode tests**: Added 3 new tests for `src/watch.js` covering no-directory handling, watcher setup, and node_modules filtering.
23
+
24
+ ### ๐Ÿ“Š Test Coverage
25
+
26
+ - **188 tests** passing across **16 test files** (up from 185/15).
27
+
5
28
  ## 1.3.1
6
29
 
7
30
  ### ๐Ÿ“ Documentation Overhaul
package/README.md CHANGED
@@ -9,6 +9,7 @@
9
9
  ```
10
10
 
11
11
  [![npm version](https://img.shields.io/npm/v/@chappibunny/repolens)](https://www.npmjs.com/package/@chappibunny/repolens)
12
+ [![VS Code Extension](https://img.shields.io/visual-studio-marketplace/v/CHAPIBUNNY.repolens-architecture?label=VS%20Code)](https://marketplace.visualstudio.com/items?itemName=CHAPIBUNNY.repolens-architecture)
12
13
  [![Tests](https://img.shields.io/badge/tests-185%20passing-brightgreen)](https://github.com/CHAPIBUNNY/repolens/actions)
13
14
  [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
14
15
 
@@ -16,7 +17,7 @@
16
17
 
17
18
  RepoLens scans your repository, generates living architecture documentation, and publishes it to Notion, Confluence, GitHub Wiki, or Markdown โ€” automatically on every push. Engineers get technical docs. Stakeholders get readable system overviews. Nobody writes a word.
18
19
 
19
- > Stable as of v1.0 โ€” [API guarantees](STABILITY.md) ยท [Security hardened](SECURITY.md) ยท v1.3.1
20
+ > Stable as of v1.0 โ€” [API guarantees](STABILITY.md) ยท [Security hardened](SECURITY.md) ยท v1.4.0
20
21
 
21
22
  ---
22
23
 
@@ -113,6 +114,28 @@ For alternative methods, see [INSTALLATION.md](INSTALLATION.md).
113
114
 
114
115
  ---
115
116
 
117
+ ## ๐ŸŽจ VS Code Extension
118
+
119
+ **View your architecture directly in VS Code** โ€” browse modules, visualize dependencies, and explore your codebase structure without leaving the editor.
120
+
121
+ **Install from Marketplace:**
122
+ ```
123
+ ext install CHAPIBUNNY.repolens-architecture
124
+ ```
125
+
126
+ [**โ†’ Get it on Visual Studio Marketplace**](https://marketplace.visualstudio.com/items?itemName=CHAPIBUNNY.repolens-architecture)
127
+
128
+ **Features:**
129
+ - ๐Ÿ—๏ธ **Architecture Explorer** โ€” Tree view of your system structure
130
+ - ๐Ÿ“Š **Dependency Visualizer** โ€” Interactive dependency graphs
131
+ - ๐Ÿ“ **Module Browser** โ€” Navigate modules by domain and function
132
+ - ๐Ÿ” **Command Palette** โ€” Quick access to architecture insights
133
+ - ๐Ÿ“ˆ **System Metrics** โ€” Real-time architecture health indicators
134
+
135
+ The extension reads your `.repolens.yml` configuration and provides an interactive UI for exploring the documentation that RepoLens generates.
136
+
137
+ ---
138
+
116
139
  ## ๐ŸŽ“ Onboarding Guide
117
140
 
118
141
  Step-by-step setup for publishers, AI features, Notion, Confluence, GitHub Wiki, Discord, and CI/CD automation.
@@ -238,9 +261,11 @@ When you open a pull request, RepoLens posts:
238
261
 
239
262
  v1.0+ features complete โ€” CLI, config schema, and plugin interface are frozen.
240
263
 
264
+ **Completed:**
265
+ - [x] VS Code extension ([available on Marketplace](https://marketplace.visualstudio.com/items?itemName=CHAPIBUNNY.repolens-architecture))
266
+
241
267
  **Next:**
242
268
  - [ ] Obsidian publisher
243
- - [ ] VS Code extension
244
269
  - [ ] GitHub App
245
270
 
246
271
  See [ROADMAP.md](ROADMAP.md) for detailed planning.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/ai/prompts.js CHANGED
@@ -1,5 +1,48 @@
1
1
  // Strict prompt templates for AI-generated documentation sections
2
2
 
3
+ const MAX_CONTEXT_CHARS = 12000; // ~3000 tokens, safe for all models
4
+
5
+ /**
6
+ * Truncate a context object to fit within token limits.
7
+ * Prunes large arrays (routes, modules, domains) to keep context compact.
8
+ */
9
+ function truncateContext(context) {
10
+ let json = JSON.stringify(context, null, 2);
11
+ if (json.length <= MAX_CONTEXT_CHARS) return json;
12
+
13
+ // Progressively shrink: reduce array sizes
14
+ const trimmed = { ...context };
15
+
16
+ // Trim routes
17
+ if (trimmed.routes) {
18
+ if (trimmed.routes.pages && trimmed.routes.pages.length > 15) {
19
+ trimmed.routes = { ...trimmed.routes, pages: trimmed.routes.pages.slice(0, 15) };
20
+ }
21
+ if (trimmed.routes.apis && trimmed.routes.apis.length > 15) {
22
+ trimmed.routes = { ...trimmed.routes, apis: trimmed.routes.apis.slice(0, 15) };
23
+ }
24
+ }
25
+
26
+ // Trim domains
27
+ if (Array.isArray(trimmed.domains) && trimmed.domains.length > 8) {
28
+ trimmed.domains = trimmed.domains.slice(0, 8);
29
+ }
30
+
31
+ // Trim top modules
32
+ if (Array.isArray(trimmed.topModules) && trimmed.topModules.length > 10) {
33
+ trimmed.topModules = trimmed.topModules.slice(0, 10);
34
+ }
35
+
36
+ json = JSON.stringify(trimmed, null, 2);
37
+
38
+ // Final hard truncation if still over limit
39
+ if (json.length > MAX_CONTEXT_CHARS) {
40
+ json = json.slice(0, MAX_CONTEXT_CHARS) + "\n... (context truncated for token limit)";
41
+ }
42
+
43
+ return json;
44
+ }
45
+
3
46
  export const SYSTEM_PROMPT = `You are a senior software architect and technical writer.
4
47
  Your job is to turn structured repository analysis into clear documentation.
5
48
 
@@ -20,7 +63,7 @@ export function createExecutiveSummaryPrompt(context) {
20
63
  return `Write an executive summary for a mixed audience of technical and non-technical readers.
21
64
 
22
65
  Use this context:
23
- ${JSON.stringify(context, null, 2)}
66
+ ${truncateContext(context)}
24
67
 
25
68
  Requirements:
26
69
  - Explain what the system appears to do based on the modules and routes.
@@ -53,7 +96,7 @@ export function createSystemOverviewPrompt(context) {
53
96
  return `Write a system overview for a mixed audience.
54
97
 
55
98
  Use this context:
56
- ${JSON.stringify(context, null, 2)}
99
+ ${truncateContext(context)}
57
100
 
58
101
  Requirements:
59
102
  - Provide a concise, high-level orientation to the codebase.
@@ -81,7 +124,7 @@ export function createBusinessDomainsPrompt(context) {
81
124
  return `Write business domain documentation for a mixed audience, especially non-technical readers.
82
125
 
83
126
  Use this context:
84
- ${JSON.stringify(context, null, 2)}
127
+ ${truncateContext(context)}
85
128
 
86
129
  Requirements:
87
130
  - Translate codebase structure into business language.
@@ -113,7 +156,7 @@ export function createArchitectureOverviewPrompt(context) {
113
156
  return `Write an architecture overview for engineers, architects, and technical PMs.
114
157
 
115
158
  Use this context:
116
- ${JSON.stringify(context, null, 2)}
159
+ ${truncateContext(context)}
117
160
 
118
161
  Requirements:
119
162
  - Explain the layered architecture based on observable patterns.
@@ -147,10 +190,10 @@ export function createDataFlowsPrompt(flows, context) {
147
190
  return `Write data flow documentation for a mixed audience.
148
191
 
149
192
  Use this flow information:
150
- ${JSON.stringify(flows, null, 2)}
193
+ ${truncateContext(flows)}
151
194
 
152
195
  And this context:
153
- ${JSON.stringify(context, null, 2)}
196
+ ${truncateContext(context)}
154
197
 
155
198
  Requirements:
156
199
  - Explain major information flows in plain language.
@@ -179,7 +222,7 @@ export function createDeveloperOnboardingPrompt(context) {
179
222
  return `Write developer onboarding documentation to help new engineers get productive quickly.
180
223
 
181
224
  Use this context:
182
- ${JSON.stringify(context, null, 2)}
225
+ ${truncateContext(context)}
183
226
 
184
227
  Requirements:
185
228
  - Guide new developers through the codebase structure.
@@ -218,7 +261,7 @@ Type: ${module.type}
218
261
  Domain: ${module.domain}
219
262
 
220
263
  Additional context:
221
- ${JSON.stringify(context, null, 2)}
264
+ ${truncateContext(context)}
222
265
 
223
266
  Requirements:
224
267
  - Explain the module's likely purpose.
@@ -252,7 +295,7 @@ File: ${route.file}
252
295
  Type: ${route.type}
253
296
 
254
297
  Additional context:
255
- ${JSON.stringify(context, null, 2)}
298
+ ${truncateContext(context)}
256
299
 
257
300
  Requirements:
258
301
  - Explain the user purpose of this route.
@@ -283,7 +326,7 @@ API: ${api.methods.join(", ")} ${api.path}
283
326
  File: ${api.file}
284
327
 
285
328
  Additional context:
286
- ${JSON.stringify(context, null, 2)}
329
+ ${truncateContext(context)}
287
330
 
288
331
  Requirements:
289
332
  - Explain the purpose in plain language and technical language.
@@ -2,42 +2,42 @@
2
2
 
3
3
  const DEFAULT_DOMAIN_HINTS = [
4
4
  {
5
- match: ["auth", "login", "signup", "session", "user", "account"],
6
- domain: "Authentication",
7
- description: "User authentication and identity flows"
5
+ match: ["auth", "login", "signup", "session", "user", "account", "oauth", "sso"],
6
+ domain: "Authentication & Identity",
7
+ description: "User authentication, authorization, and identity management"
8
8
  },
9
9
  {
10
- match: ["stock", "chart", "price", "market", "watchlist", "ticker", "quote"],
11
- domain: "Market Data & Analysis",
12
- description: "Market data retrieval, analysis, and visualization"
10
+ match: ["dashboard", "analytics", "chart", "report", "metric", "stat", "insight"],
11
+ domain: "Analytics & Reporting",
12
+ description: "Data visualization, reporting, and analytics dashboards"
13
13
  },
14
14
  {
15
- match: ["article", "newsletter", "news", "research", "content", "blog", "post"],
16
- domain: "Content & Research",
17
- description: "Content publishing, research, and insight delivery"
15
+ match: ["article", "newsletter", "news", "research", "content", "blog", "post", "cms"],
16
+ domain: "Content Management",
17
+ description: "Content publishing, management, and delivery"
18
18
  },
19
19
  {
20
- match: ["portfolio", "positions", "holdings", "trades", "orders"],
21
- domain: "Portfolio Management",
22
- description: "Portfolio tracking and trading functionality"
20
+ match: ["search", "filter", "query", "index", "catalog", "browse"],
21
+ domain: "Search & Discovery",
22
+ description: "Search functionality, filtering, and content discovery"
23
23
  },
24
24
  {
25
- match: ["alert", "notification", "email", "sms", "webhook"],
26
- domain: "Alerts & Notifications",
27
- description: "User notification and alerting system"
25
+ match: ["alert", "notification", "email", "sms", "webhook", "push", "message"],
26
+ domain: "Notifications",
27
+ description: "User notifications and messaging system"
28
28
  },
29
29
  {
30
- match: ["payment", "subscription", "billing", "stripe", "checkout"],
30
+ match: ["payment", "subscription", "billing", "stripe", "checkout", "invoice"],
31
31
  domain: "Payments & Billing",
32
32
  description: "Payment processing and subscription management"
33
33
  },
34
34
  {
35
- match: ["api", "endpoint", "route", "handler"],
35
+ match: ["api", "endpoint", "route", "handler", "controller", "middleware"],
36
36
  domain: "API Layer",
37
37
  description: "Backend API endpoints and request handling"
38
38
  },
39
39
  {
40
- match: ["component", "ui", "button", "form", "modal", "dialog"],
40
+ match: ["component", "ui", "button", "form", "modal", "dialog", "layout", "widget"],
41
41
  domain: "UI Components",
42
42
  description: "Reusable user interface components"
43
43
  },
@@ -47,19 +47,34 @@ const DEFAULT_DOMAIN_HINTS = [
47
47
  description: "Custom React hooks for state and behavior"
48
48
  },
49
49
  {
50
- match: ["store", "state", "redux", "zustand", "context"],
50
+ match: ["store", "state", "redux", "zustand", "context", "atom"],
51
51
  domain: "State Management",
52
52
  description: "Application state management"
53
53
  },
54
54
  {
55
- match: ["lib", "util", "helper", "common", "shared"],
55
+ match: ["lib", "util", "helper", "common", "shared", "tool"],
56
56
  domain: "Shared Utilities",
57
57
  description: "Common utilities and helper functions"
58
58
  },
59
59
  {
60
- match: ["data", "database", "db", "prisma", "sql"],
60
+ match: ["data", "database", "db", "prisma", "sql", "model", "schema", "migration", "seed"],
61
61
  domain: "Data Layer",
62
- description: "Database access and data persistence"
62
+ description: "Database access, models, and data persistence"
63
+ },
64
+ {
65
+ match: ["config", "setting", "env", "constant"],
66
+ domain: "Configuration",
67
+ description: "Application configuration and environment settings"
68
+ },
69
+ {
70
+ match: ["test", "spec", "fixture", "mock", "stub", "e2e", "cypress", "playwright"],
71
+ domain: "Testing",
72
+ description: "Test suites, fixtures, and testing utilities"
73
+ },
74
+ {
75
+ match: ["job", "queue", "worker", "cron", "task", "scheduler", "background"],
76
+ domain: "Background Jobs",
77
+ description: "Background processing, job queues, and scheduled tasks"
63
78
  }
64
79
  ];
65
80
 
package/src/cli.js CHANGED
@@ -89,7 +89,7 @@ async function printBanner() {
89
89
  โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘
90
90
  โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘
91
91
  โ•šโ•โ• โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•
92
- ๐Ÿ” Repository Intelligence by RABITAI ๐Ÿฐ
92
+ ๐Ÿ” Repository Intelligence by RepoLens
93
93
  v${version}
94
94
  `);
95
95
  console.log("โ”€".repeat(70));
@@ -133,7 +133,7 @@ async function findConfig(startDir = process.cwd()) {
133
133
 
134
134
  function printHelp() {
135
135
  console.log(`
136
- RepoLens โ€” Repository Intelligence CLI by RABITAI ๐Ÿฐ
136
+ RepoLens โ€” Repository Intelligence CLI
137
137
 
138
138
  Usage:
139
139
  repolens <command> [options]
@@ -200,7 +200,7 @@ async function generateDocument(docPlan, context) {
200
200
 
201
201
  case "system_map":
202
202
  // Hybrid: deterministic diagram + AI explanation (for now, just diagram)
203
- return renderSystemMap(scanResult, config);
203
+ return renderSystemMap(scanResult, config, depGraph);
204
204
 
205
205
  case "developer_onboarding":
206
206
  return await generateDeveloperOnboarding(aiContext);
package/src/doctor.js CHANGED
@@ -151,6 +151,57 @@ export async function runDoctor(targetDir = process.cwd()) {
151
151
  }
152
152
  }
153
153
 
154
+ // Validate environment variables for configured publishers
155
+ if (cfg && Array.isArray(cfg.publishers)) {
156
+ info("Environment:");
157
+ info("");
158
+
159
+ const envChecks = [];
160
+
161
+ if (cfg.publishers.includes("notion")) {
162
+ envChecks.push(
163
+ { key: "NOTION_TOKEN", required: true, publisher: "Notion" },
164
+ { key: "NOTION_PARENT_PAGE_ID", required: true, publisher: "Notion" },
165
+ );
166
+ }
167
+
168
+ if (cfg.publishers.includes("confluence")) {
169
+ envChecks.push(
170
+ { key: "CONFLUENCE_URL", required: true, publisher: "Confluence" },
171
+ { key: "CONFLUENCE_EMAIL", required: true, publisher: "Confluence" },
172
+ { key: "CONFLUENCE_API_TOKEN", required: true, publisher: "Confluence" },
173
+ { key: "CONFLUENCE_SPACE_KEY", required: true, publisher: "Confluence" },
174
+ { key: "CONFLUENCE_PARENT_PAGE_ID", required: true, publisher: "Confluence" },
175
+ );
176
+ }
177
+
178
+ if (cfg.publishers.includes("github_wiki")) {
179
+ envChecks.push(
180
+ { key: "GITHUB_TOKEN", required: true, publisher: "GitHub Wiki" },
181
+ );
182
+ }
183
+
184
+ if (cfg.ai?.enabled || process.env.REPOLENS_AI_ENABLED === "true") {
185
+ envChecks.push(
186
+ { key: "REPOLENS_AI_API_KEY", required: true, publisher: "AI" },
187
+ );
188
+ }
189
+
190
+ if (envChecks.length === 0) {
191
+ ok("No publisher-specific env vars required (Markdown only)");
192
+ }
193
+
194
+ for (const check of envChecks) {
195
+ if (process.env[check.key]) {
196
+ ok(`${check.key} is set (${check.publisher})`);
197
+ } else {
198
+ warn(`${check.key} is not set โ€” required for ${check.publisher} publishing`);
199
+ }
200
+ }
201
+
202
+ info("");
203
+ }
204
+
154
205
  info("");
155
206
 
156
207
  const detectedRoots = await detectRepoRoots(repoRoot);
@@ -71,7 +71,7 @@ function buildEmbed(payload) {
71
71
  };
72
72
 
73
73
  return {
74
- title: title || "RABITAI Documentation Updated",
74
+ title: title || "RepoLens Documentation Updated",
75
75
  description: description || "Documentation has been regenerated",
76
76
  color: typeof color === "string" ? colorMap[color] || colorMap.info : color || colorMap.info,
77
77
  fields: fields.map((field) => ({
@@ -82,7 +82,7 @@ function buildEmbed(payload) {
82
82
  timestamp,
83
83
  footer: footer
84
84
  ? { text: footer }
85
- : { text: "RABITAI ๐Ÿฐ" },
85
+ : { text: "RepoLens ๐Ÿ”" },
86
86
  url: url || undefined,
87
87
  };
88
88
  }
@@ -237,7 +237,7 @@ export function buildErrorNotification(options) {
237
237
  }
238
238
 
239
239
  return {
240
- title: "๐Ÿšจ RABITAI Error",
240
+ title: "๐Ÿšจ RepoLens Error",
241
241
  description: "Documentation generation failed",
242
242
  color: "error",
243
243
  fields,
@@ -136,7 +136,12 @@ async function writeCache(cache) {
136
136
 
137
137
  // Convert Markdown to Confluence Storage Format
138
138
  function markdownToConfluenceStorage(markdown) {
139
- const lines = markdown.split("\n");
139
+ // Rewrite relative file links that can't resolve in Confluence
140
+ const rewritten = markdown.replace(
141
+ /\[([^\]]+)\]\(\.{1,2}\/[^)]+\)/g,
142
+ "$1"
143
+ );
144
+ const lines = rewritten.split("\n");
140
145
  const output = [];
141
146
 
142
147
  let i = 0;
@@ -161,10 +166,12 @@ function markdownToConfluenceStorage(markdown) {
161
166
  }
162
167
  i++; // skip closing ```
163
168
  const code = codeLines.join("\n");
169
+ // Escape ]]> inside code to prevent CDATA injection
170
+ const safeCode = code.replace(/]]>/g, "]]]]><![CDATA[>");
164
171
  output.push(
165
172
  `<ac:structured-macro ac:name="code">` +
166
173
  `<ac:parameter ac:name="language">${escapeHtml(language)}</ac:parameter>` +
167
- `<ac:plain-text-body><![CDATA[${code}]]></ac:plain-text-body>` +
174
+ `<ac:plain-text-body><![CDATA[${safeCode}]]></ac:plain-text-body>` +
168
175
  `</ac:structured-macro>`
169
176
  );
170
177
  continue;
@@ -8,12 +8,21 @@ function outputDir(cfg) {
8
8
 
9
9
  function pageFileName(key) {
10
10
  const mapping = {
11
+ executive_summary: "executive_summary.md",
11
12
  system_overview: "system_overview.md",
13
+ business_domains: "business_domains.md",
14
+ architecture_overview: "architecture_overview.md",
12
15
  module_catalog: "module_catalog.md",
13
16
  api_surface: "api_surface.md",
17
+ data_flows: "data_flows.md",
14
18
  arch_diff: "architecture_diff.md",
15
19
  route_map: "route_map.md",
16
- system_map: "system_map.md"
20
+ system_map: "system_map.md",
21
+ developer_onboarding: "developer_onboarding.md",
22
+ graphql_schema: "graphql_schema.md",
23
+ type_graph: "type_graph.md",
24
+ dependency_graph: "dependency_graph.md",
25
+ architecture_drift: "architecture_drift.md"
17
26
  };
18
27
 
19
28
  return mapping[key] || `${key}.md`;
@@ -172,6 +172,18 @@ export async function clearPage(pageId) {
172
172
  }
173
173
  }
174
174
 
175
+ /**
176
+ * Rewrite relative file links that cannot resolve in external publishers.
177
+ * Converts [text](./path.md) and [text](../path.md) to just "text",
178
+ * while preserving absolute URLs like [text](https://example.com).
179
+ */
180
+ function rewriteRelativeLinks(markdown) {
181
+ return markdown.replace(
182
+ /\[([^\]]+)\]\(\.{1,2}\/[^)]+\)/g,
183
+ "$1"
184
+ );
185
+ }
186
+
175
187
  function parseInlineRichText(text) {
176
188
  // Parse inline markdown: **bold**, *italic*, `code` into Notion rich_text annotations
177
189
  const segments = [];
@@ -246,8 +258,11 @@ function markdownToNotionBlocks(markdown) {
246
258
  warn(`markdownToNotionBlocks received invalid markdown: ${typeof markdown}`);
247
259
  return [];
248
260
  }
261
+
262
+ // Rewrite relative file links that can't resolve in Notion
263
+ const rewritten = rewriteRelativeLinks(markdown);
249
264
 
250
- const lines = markdown.split("\n");
265
+ const lines = rewritten.split("\n");
251
266
  const blocks = [];
252
267
  let i = 0;
253
268
 
@@ -326,28 +341,88 @@ function markdownToNotionBlocks(markdown) {
326
341
 
327
342
  if (tableRows.length > 0) {
328
343
  const columnCount = tableRows[0].length;
329
- const tableBlock = {
330
- object: "block",
331
- type: "table",
332
- table: {
333
- table_width: columnCount,
334
- has_column_header: true,
335
- has_row_header: false,
336
- children: tableRows.map((row) => ({
337
- type: "table_row",
338
- table_row: {
339
- cells: row.slice(0, columnCount).map(cell => parseInlineRichText(cell))
344
+ const headerRow = tableRows[0];
345
+ const dataRows = tableRows.slice(1);
346
+
347
+ // Notion API limit: max 100 rows per table (including header)
348
+ // Split into chunks if needed
349
+ if (dataRows.length >= 100) {
350
+ const CHUNK_SIZE = 99; // 99 data rows + 1 header = 100 total
351
+
352
+ for (let chunkIdx = 0; chunkIdx < dataRows.length; chunkIdx += CHUNK_SIZE) {
353
+ const chunkRows = dataRows.slice(chunkIdx, chunkIdx + CHUNK_SIZE);
354
+ const allRows = [headerRow, ...chunkRows];
355
+
356
+ const tableBlock = {
357
+ object: "block",
358
+ type: "table",
359
+ table: {
360
+ table_width: columnCount,
361
+ has_column_header: true,
362
+ has_row_header: false,
363
+ children: allRows.map((row) => ({
364
+ type: "table_row",
365
+ table_row: {
366
+ cells: row.slice(0, columnCount).map(cell => parseInlineRichText(cell))
367
+ }
368
+ }))
340
369
  }
341
- }))
370
+ };
371
+
372
+ // Pad rows that have fewer cells than the header
373
+ for (const child of tableBlock.table.children) {
374
+ while (child.table_row.cells.length < columnCount) {
375
+ child.table_row.cells.push([{ type: "text", text: { content: "" } }]);
376
+ }
377
+ }
378
+
379
+ blocks.push(tableBlock);
380
+
381
+ // Add continuation note between chunks
382
+ if (chunkIdx + CHUNK_SIZE < dataRows.length) {
383
+ const remaining = dataRows.length - (chunkIdx + CHUNK_SIZE);
384
+ blocks.push({
385
+ object: "block",
386
+ type: "paragraph",
387
+ paragraph: {
388
+ rich_text: [{
389
+ type: "text",
390
+ text: {
391
+ content: `๐Ÿ“‹ Table continued below (${remaining} more rows)...`
392
+ },
393
+ annotations: { italic: true, color: "gray" }
394
+ }]
395
+ }
396
+ });
397
+ }
342
398
  }
343
- };
344
- // Pad rows that have fewer cells than the header
345
- for (const child of tableBlock.table.children) {
346
- while (child.table_row.cells.length < columnCount) {
347
- child.table_row.cells.push([{ type: "text", text: { content: "" } }]);
399
+ } else {
400
+ // Table fits in one block
401
+ const tableBlock = {
402
+ object: "block",
403
+ type: "table",
404
+ table: {
405
+ table_width: columnCount,
406
+ has_column_header: true,
407
+ has_row_header: false,
408
+ children: tableRows.map((row) => ({
409
+ type: "table_row",
410
+ table_row: {
411
+ cells: row.slice(0, columnCount).map(cell => parseInlineRichText(cell))
412
+ }
413
+ }))
414
+ }
415
+ };
416
+
417
+ // Pad rows that have fewer cells than the header
418
+ for (const child of tableBlock.table.children) {
419
+ while (child.table_row.cells.length < columnCount) {
420
+ child.table_row.cells.push([{ type: "text", text: { content: "" } }]);
421
+ }
348
422
  }
423
+
424
+ blocks.push(tableBlock);
349
425
  }
350
- blocks.push(tableBlock);
351
426
  }
352
427
  continue;
353
428
  }
@@ -448,7 +523,7 @@ function markdownToNotionBlocks(markdown) {
448
523
  }
449
524
 
450
525
  // Exported for testing
451
- export { markdownToNotionBlocks, parseInlineRichText };
526
+ export { markdownToNotionBlocks, parseInlineRichText, rewriteRelativeLinks };
452
527
 
453
528
  export async function replacePageContent(pageId, markdown) {
454
529
  // Ensure page is unarchived before editing
@@ -266,6 +266,10 @@ export function renderRouteMap(cfg, scan) {
266
266
  lines.push(`| \`${page.path}\` | \`${page.file}\` |`);
267
267
  }
268
268
 
269
+ if (scan.pages.length > 200) {
270
+ lines.push(``, `> **Note:** Showing 200 of ${scan.pages.length} pages. Configure \`scan.include\` to narrow scope.`);
271
+ }
272
+
269
273
  lines.push(``);
270
274
  }
271
275
 
@@ -283,6 +287,10 @@ export function renderRouteMap(cfg, scan) {
283
287
  lines.push(`| ${route.methods.join(", ")} | \`${route.path}\` | \`${route.file}\` |`);
284
288
  }
285
289
 
290
+ if (scan.api.length > 200) {
291
+ lines.push(``, `> **Note:** Showing 200 of ${scan.api.length} API endpoints.`);
292
+ }
293
+
286
294
  lines.push(``);
287
295
  }
288
296
 
@@ -88,6 +88,9 @@ export function renderArchitectureDiff(diff) {
88
88
  for (const route of data.addedRoutes.slice(0, 25)) {
89
89
  lines.push(`- \`${route}\``);
90
90
  }
91
+ if (data.addedRoutes.length > 25) {
92
+ lines.push(``, `> Showing 25 of ${data.addedRoutes.length} added routes.`);
93
+ }
91
94
  lines.push("");
92
95
  }
93
96
 
@@ -96,6 +99,9 @@ export function renderArchitectureDiff(diff) {
96
99
  for (const route of data.removedRoutes.slice(0, 25)) {
97
100
  lines.push(`- \`${route}\``);
98
101
  }
102
+ if (data.removedRoutes.length > 25) {
103
+ lines.push(``, `> Showing 25 of ${data.removedRoutes.length} removed routes.`);
104
+ }
99
105
  lines.push("");
100
106
  }
101
107
 
@@ -104,6 +110,9 @@ export function renderArchitectureDiff(diff) {
104
110
  for (const module of data.impactedModules.slice(0, 40)) {
105
111
  lines.push(`- \`${module}\``);
106
112
  }
113
+ if (data.impactedModules.length > 40) {
114
+ lines.push(``, `> Showing 40 of ${data.impactedModules.length} impacted modules.`);
115
+ }
107
116
  lines.push("");
108
117
  }
109
118
 
@@ -112,6 +121,9 @@ export function renderArchitectureDiff(diff) {
112
121
  for (const file of data.added.slice(0, 25)) {
113
122
  lines.push(`- \`${file}\``);
114
123
  }
124
+ if (data.added.length > 25) {
125
+ lines.push(``, `> Showing 25 of ${data.added.length} added files.`);
126
+ }
115
127
  lines.push("");
116
128
  }
117
129
 
@@ -120,6 +132,9 @@ export function renderArchitectureDiff(diff) {
120
132
  for (const file of data.removed.slice(0, 25)) {
121
133
  lines.push(`- \`${file}\``);
122
134
  }
135
+ if (data.removed.length > 25) {
136
+ lines.push(``, `> Showing 25 of ${data.removed.length} removed files.`);
137
+ }
123
138
  lines.push("");
124
139
  }
125
140
 
@@ -128,6 +143,9 @@ export function renderArchitectureDiff(diff) {
128
143
  for (const file of data.modified.slice(0, 25)) {
129
144
  lines.push(`- \`${file}\``);
130
145
  }
146
+ if (data.modified.length > 25) {
147
+ lines.push(``, `> Showing 25 of ${data.modified.length} modified files.`);
148
+ }
131
149
  lines.push("");
132
150
  }
133
151
 
@@ -10,7 +10,7 @@ function normalizeLabel(value) {
10
10
  .replace(/\/+/g, "/");
11
11
  }
12
12
 
13
- function buildModuleGraph(modules) {
13
+ function buildModuleGraph(modules, depGraph) {
14
14
  // Create nodes with module details
15
15
  const nodes = modules.map(mod => ({
16
16
  id: sanitizeNodeId(mod.key),
@@ -20,74 +20,66 @@ function buildModuleGraph(modules) {
20
20
  category: categorizeModule(mod.key)
21
21
  }));
22
22
 
23
- // Infer relationships based on common patterns
24
23
  const relationships = [];
25
24
 
26
- for (const source of nodes) {
27
- for (const target of nodes) {
28
- if (source.id === target.id) continue;
29
-
30
- // CLI imports from core, publishers, renderers, utils
31
- if (source.label === "bin" || source.label.startsWith("bin/")) {
32
- if (target.label.startsWith("src/")) {
33
- relationships.push({
34
- from: source.id,
35
- to: target.id,
36
- type: "uses"
37
- });
38
- }
39
- }
25
+ // Use real import edges from dependency graph when available
26
+ if (depGraph && depGraph.edges && depGraph.edges.length > 0) {
27
+ // Map file-level edges to module-level edges
28
+ const moduleEdges = new Map(); // "sourceModule->targetModule" โ†’ count
40
29
 
41
- // Core modules are foundational - others depend on them
42
- if (target.label.startsWith("src/core")) {
43
- if (source.label.startsWith("src/publishers") ||
44
- source.label.startsWith("src/renderers") ||
45
- source.label.startsWith("src/delivery")) {
46
- relationships.push({
47
- from: source.id,
48
- to: target.id,
49
- type: "depends-on"
50
- });
51
- }
30
+ for (const edge of depGraph.edges) {
31
+ const sourceModule = findModuleForFile(edge.from, modules);
32
+ const targetModule = findModuleForFile(edge.to, modules);
33
+
34
+ if (sourceModule && targetModule && sourceModule !== targetModule) {
35
+ const edgeKey = `${sanitizeNodeId(sourceModule)}:${sanitizeNodeId(targetModule)}`;
36
+ moduleEdges.set(edgeKey, (moduleEdges.get(edgeKey) || 0) + 1);
52
37
  }
38
+ }
53
39
 
54
- // Publishers use renderers
55
- if (source.label.startsWith("src/publishers") && target.label.startsWith("src/renderers")) {
40
+ for (const [edgeKey, count] of moduleEdges) {
41
+ const [fromId, toId] = edgeKey.split(":");
42
+ const sourceNode = nodes.find(n => n.id === fromId);
43
+ const targetNode = nodes.find(n => n.id === toId);
44
+ if (sourceNode && targetNode) {
56
45
  relationships.push({
57
- from: source.id,
58
- to: target.id,
59
- type: "renders"
46
+ from: fromId,
47
+ to: toId,
48
+ type: targetNode.category === "test" ? "tests" : "imports",
49
+ weight: count
60
50
  });
61
51
  }
52
+ }
53
+ } else {
54
+ // Fallback: infer relationships based on common patterns
55
+ for (const source of nodes) {
56
+ for (const target of nodes) {
57
+ if (source.id === target.id) continue;
58
+
59
+ if (source.label === "bin" || source.label.startsWith("bin/")) {
60
+ if (target.label.startsWith("src/")) {
61
+ relationships.push({ from: source.id, to: target.id, type: "uses" });
62
+ }
63
+ }
62
64
 
63
- // Everything uses utils
64
- if (target.label.startsWith("src/utils")) {
65
- if (source.label.startsWith("src/") && source.label !== target.label) {
66
- relationships.push({
67
- from: source.id,
68
- to: target.id,
69
- type: "uses"
70
- });
65
+ if (target.label.startsWith("src/core")) {
66
+ if (source.label.startsWith("src/publishers") ||
67
+ source.label.startsWith("src/renderers") ||
68
+ source.label.startsWith("src/delivery")) {
69
+ relationships.push({ from: source.id, to: target.id, type: "depends-on" });
70
+ }
71
71
  }
72
- }
73
72
 
74
- // Delivery uses publishers
75
- if (source.label.startsWith("src/delivery") && target.label.startsWith("src/publishers")) {
76
- relationships.push({
77
- from: source.id,
78
- to: target.id,
79
- type: "publishes-via"
80
- });
81
- }
73
+ if (target.label.startsWith("src/utils")) {
74
+ if (source.label.startsWith("src/") && source.label !== target.label) {
75
+ relationships.push({ from: source.id, to: target.id, type: "uses" });
76
+ }
77
+ }
82
78
 
83
- // Tests test everything
84
- if (source.label.startsWith("tests/") || source.label.startsWith("test/")) {
85
- if (!target.label.startsWith("tests/") && !target.label.startsWith("test/")) {
86
- relationships.push({
87
- from: source.id,
88
- to: target.id,
89
- type: "tests"
90
- });
79
+ if (source.label.startsWith("tests/") || source.label.startsWith("test/")) {
80
+ if (!target.label.startsWith("tests/") && !target.label.startsWith("test/")) {
81
+ relationships.push({ from: source.id, to: target.id, type: "tests" });
82
+ }
91
83
  }
92
84
  }
93
85
  }
@@ -96,6 +88,23 @@ function buildModuleGraph(modules) {
96
88
  return { nodes, relationships };
97
89
  }
98
90
 
91
+ /**
92
+ * Find which module a file belongs to.
93
+ */
94
+ function findModuleForFile(fileKey, modules) {
95
+ const normalized = fileKey.replace(/\\/g, "/");
96
+ // Find the most specific (longest) matching module key
97
+ let bestMatch = null;
98
+ for (const mod of modules) {
99
+ if (normalized.startsWith(mod.key) || normalized.startsWith(mod.key + "/")) {
100
+ if (!bestMatch || mod.key.length > bestMatch.length) {
101
+ bestMatch = mod.key;
102
+ }
103
+ }
104
+ }
105
+ return bestMatch;
106
+ }
107
+
99
108
  function categorizeModule(key) {
100
109
  const normalized = key.toLowerCase();
101
110
  if (normalized.includes("core")) return "core";
@@ -180,14 +189,14 @@ function generateUnicodeArchitectureDiagram(nodes, relationships) {
180
189
 
181
190
  lines.push("");
182
191
  lines.push("Legend:");
183
- lines.push(" โ†’ depends on / uses");
192
+ lines.push(" โ†’ imports / depends on");
184
193
  lines.push(" โ•Œโ†’ tests");
185
194
  lines.push("```");
186
195
 
187
196
  return lines.join("\n");
188
197
  }
189
198
 
190
- export function renderSystemMap(scan) {
199
+ export function renderSystemMap(scan, config, depGraph) {
191
200
  const modules = (scan.modules || []).slice(0, 30); // Limit for readability
192
201
 
193
202
  if (modules.length === 0) {
@@ -201,9 +210,13 @@ export function renderSystemMap(scan) {
201
210
  ].join("\n");
202
211
  }
203
212
 
204
- const { nodes, relationships } = buildModuleGraph(modules);
213
+ const { nodes, relationships } = buildModuleGraph(modules, depGraph);
205
214
  const architectureDiagram = generateUnicodeArchitectureDiagram(nodes, relationships);
206
215
 
216
+ const sourceLabel = depGraph && depGraph.edges && depGraph.edges.length > 0
217
+ ? "**Source:** Real import analysis"
218
+ : "**Source:** Heuristic inference (run full publish for import-based analysis)";
219
+
207
220
  // Build markdown output
208
221
  const lines = [
209
222
  "# ๐Ÿ—๏ธ System Map",
@@ -212,6 +225,8 @@ export function renderSystemMap(scan) {
212
225
  "",
213
226
  `Showing: ${nodes.length} modules and ${relationships.length} relationships`,
214
227
  "",
228
+ sourceLabel,
229
+ "",
215
230
  "---",
216
231
  "",
217
232
  "## Architecture Diagram",