@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 +23 -0
- package/README.md +27 -2
- package/package.json +1 -1
- package/src/ai/prompts.js +53 -10
- package/src/analyzers/domain-inference.js +37 -22
- package/src/cli.js +2 -2
- package/src/docs/generate-doc-set.js +1 -1
- package/src/doctor.js +51 -0
- package/src/integrations/discord.js +3 -3
- package/src/publishers/confluence.js +9 -2
- package/src/publishers/markdown.js +10 -1
- package/src/publishers/notion.js +95 -20
- package/src/renderers/render.js +8 -0
- package/src/renderers/renderDiff.js +18 -0
- package/src/renderers/renderMap.js +75 -60
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 (`]]]]><](https://www.npmjs.com/package/@chappibunny/repolens)
|
|
12
|
+
[](https://marketplace.visualstudio.com/items?itemName=CHAPIBUNNY.repolens-architecture)
|
|
12
13
|
[](https://github.com/CHAPIBUNNY/repolens/actions)
|
|
13
14
|
[](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.
|
|
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
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
193
|
+
${truncateContext(flows)}
|
|
151
194
|
|
|
152
195
|
And this context:
|
|
153
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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
|
|
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: ["
|
|
11
|
-
domain: "
|
|
12
|
-
description: "
|
|
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
|
|
17
|
-
description: "Content publishing,
|
|
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: ["
|
|
21
|
-
domain: "
|
|
22
|
-
description: "
|
|
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: "
|
|
27
|
-
description: "User
|
|
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
|
|
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
|
|
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 || "
|
|
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: "
|
|
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: "๐จ
|
|
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
|
-
|
|
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[${
|
|
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`;
|
package/src/publishers/notion.js
CHANGED
|
@@ -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 =
|
|
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
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
package/src/renderers/render.js
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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:
|
|
58
|
-
to:
|
|
59
|
-
type: "
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
from: source.id,
|
|
68
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
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",
|