@chappibunny/repolens 1.3.0 → 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 +36 -1
- package/README.md +95 -1109
- package/RELEASE.md +1 -1
- 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/RELEASE.md
CHANGED
|
@@ -22,7 +22,7 @@ RepoLens uses semantic versioning:
|
|
|
22
22
|
8. Push branch and tag: `git push --follow-tags`
|
|
23
23
|
9. GitHub Actions `release.yml` runs automatically:
|
|
24
24
|
- Security audit (dependency + secrets scanning)
|
|
25
|
-
- Test suite (
|
|
25
|
+
- Test suite (185 tests)
|
|
26
26
|
- Create GitHub Release with tarball
|
|
27
27
|
- Publish to npm (`npm publish --access public`)
|
|
28
28
|
10. Verify on npm: `npm view @chappibunny/repolens version`
|
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
|
|