@chappibunny/repolens 1.4.0 โ†’ 1.5.1

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,38 @@
2
2
 
3
3
  All notable changes to RepoLens will be documented in this file.
4
4
 
5
+ ## 1.5.1
6
+
7
+ ### ๐Ÿ› Bug Fixes
8
+
9
+ - **[object Object] rendering fix**: All 6 structured AI renderers now use safe coercion helpers (`safeStr`, `toBulletList`, `toHeadingSections`) that handle strings, arrays, objects, and null values robustly. Prevents `[object Object]` from appearing in Notion/Confluence/Markdown output when AI returns unexpected shapes.
10
+
11
+ ### ๐Ÿ”ง Improvements โ€” Robustness
12
+
13
+ - **Fetch timeout**: All HTTP requests via `fetchWithRetry` now enforce a 30-second timeout (configurable via `timeoutMs`). Prevents publishers from hanging indefinitely on stalled connections. `AbortError` is converted to a friendly timeout message.
14
+ - **Per-document error isolation**: Extended analysis (GraphQL, TypeScript, dependency graph, drift detection) now wraps each phase in try/catch. A failing analyzer no longer blocks the entire doc generation pipeline.
15
+ - **Partial-success publishing**: Publisher orchestration no longer throws on the first failure. If Notion fails but Confluence/Markdown succeed, all remaining publishers still run. Failures are logged as warnings. Only throws if *all* publishers fail.
16
+
17
+ ### ๐Ÿ“Š Test Coverage
18
+
19
+ - **251 tests** passing across **18 test files** (up from 224/17).
20
+ - New `tests/robustness.test.js` with 27 tests covering: rate limiter, context builder, flow inference, Discord integration, PR comment module, telemetry, write-doc-set file I/O, fetch timeout, doc generation error isolation, and partial-success publishing.
21
+
22
+ ## 1.5.0
23
+
24
+ ### ๐Ÿš€ New Features (Tier 3 โ€” Differentiation)
25
+
26
+ - **Document caching**: Hash-based caching skips redundant API calls for unchanged documents. Notion, Confluence, and GitHub Wiki publishers now receive only changed pages; Markdown always gets the full set. Cache persists in `.repolens/doc-hashes.json`.
27
+ - **Structured AI output**: AI sections now request JSON-mode responses with schema validation. If JSON parsing or schema validation fails, a single re-prompt is attempted before falling back to plain-text AI, then deterministic generation. All 6 AI document types have JSON schemas and Markdown renderers.
28
+ - **Multi-provider AI**: Added native adapters for Anthropic (Messages API) and Google Gemini alongside existing OpenAI-compatible support. Set `REPOLENS_AI_PROVIDER` to `anthropic`, `google`, or `openai_compatible` (default). Azure OpenAI uses the OpenAI-compatible adapter.
29
+ - **Monorepo awareness**: Automatic detection of npm/yarn workspaces, pnpm workspaces, and Lerna configurations. Scan results include workspace metadata. System Overview renderer shows package inventory table. AI context includes monorepo structure.
30
+ - **CODEOWNERS integration**: Parses `CODEOWNERS` / `.github/CODEOWNERS` / `docs/CODEOWNERS` files. Maps file ownership to modules via last-match-wins pattern matching. Module Catalog now displays an "Owners" column when CODEOWNERS is present. Ownership data is included in artifacts.
31
+
32
+ ### ๐Ÿ“Š Test Coverage
33
+
34
+ - **219 tests** passing across **17 test files** (up from 188/16).
35
+ - New `tests/tier3.test.js` with 31 tests covering caching, monorepo detection, CODEOWNERS parsing, multi-provider AI config, and structured output rendering.
36
+
5
37
  ## 1.4.0
6
38
 
7
39
  ### ๐Ÿ› Bug Fixes (Tier 1 โ€” Production)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Charl Van Zyl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
 
18
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.
19
19
 
20
- > Stable as of v1.0 โ€” [API guarantees](STABILITY.md) ยท [Security hardened](SECURITY.md) ยท v1.4.0
20
+ > Stable as of v1.0 โ€” [API guarantees](STABILITY.md) ยท [Security hardened](SECURITY.md) ยท v1.5.0
21
21
 
22
22
  ---
23
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -11,129 +11,109 @@ import {
11
11
  createDeveloperOnboardingPrompt,
12
12
  createModuleSummaryPrompt,
13
13
  createRouteSummaryPrompt,
14
- createAPIDocumentationPrompt
14
+ createAPIDocumentationPrompt,
15
+ AI_SCHEMAS,
16
+ renderStructuredToMarkdown,
15
17
  } from "./prompts.js";
16
18
  import { info, warn } from "../utils/logger.js";
17
19
 
18
- export async function generateExecutiveSummary(context) {
19
- if (!isAIEnabled()) {
20
- return getFallbackExecutiveSummary(context);
20
+ /**
21
+ * Try structured JSON mode first, fall back to plain-text AI, then deterministic.
22
+ */
23
+ async function generateWithStructuredFallback(key, promptText, maxTokens, fallbackFn) {
24
+ if (!isAIEnabled()) return fallbackFn();
25
+
26
+ const schema = AI_SCHEMAS[key];
27
+
28
+ // Try structured JSON mode
29
+ if (schema) {
30
+ info(`Generating ${key} with structured AI...`);
31
+ const jsonPrompt = promptText + `\n\nRespond ONLY with a JSON object matching this schema: ${JSON.stringify({ required: schema.required })}. No markdown, no explanation โ€” just the JSON object.`;
32
+
33
+ const result = await generateText({
34
+ system: SYSTEM_PROMPT,
35
+ user: jsonPrompt,
36
+ maxTokens,
37
+ jsonMode: true,
38
+ jsonSchema: schema,
39
+ });
40
+
41
+ if (result.success && result.parsed) {
42
+ const md = renderStructuredToMarkdown(key, result.parsed);
43
+ if (md) return md;
44
+ }
45
+ // If structured mode failed, fall through to plain-text
46
+ warn(`Structured AI failed for ${key}, trying plain-text mode...`);
21
47
  }
22
-
23
- info("Generating executive summary with AI...");
24
-
48
+
49
+ // Plain-text AI fallback
50
+ info(`Generating ${key} with AI...`);
25
51
  const result = await generateText({
26
52
  system: SYSTEM_PROMPT,
27
- user: createExecutiveSummaryPrompt(context),
28
- maxTokens: 1500
53
+ user: promptText,
54
+ maxTokens,
29
55
  });
30
-
56
+
31
57
  if (!result.success) {
32
58
  warn("AI generation failed, using fallback");
33
- return getFallbackExecutiveSummary(context);
59
+ return fallbackFn();
34
60
  }
35
-
61
+
36
62
  return result.text;
37
63
  }
38
64
 
65
+ export async function generateExecutiveSummary(context) {
66
+ return generateWithStructuredFallback(
67
+ "executive_summary",
68
+ createExecutiveSummaryPrompt(context),
69
+ 1500,
70
+ () => getFallbackExecutiveSummary(context),
71
+ );
72
+ }
73
+
39
74
  export async function generateSystemOverview(context) {
40
- if (!isAIEnabled()) {
41
- return getFallbackSystemOverview(context);
42
- }
43
-
44
- info("Generating system overview with AI...");
45
-
46
- const result = await generateText({
47
- system: SYSTEM_PROMPT,
48
- user: createSystemOverviewPrompt(context),
49
- maxTokens: 1200
50
- });
51
-
52
- if (!result.success) {
53
- return getFallbackSystemOverview(context);
54
- }
55
-
56
- return result.text;
75
+ return generateWithStructuredFallback(
76
+ "system_overview",
77
+ createSystemOverviewPrompt(context),
78
+ 1200,
79
+ () => getFallbackSystemOverview(context),
80
+ );
57
81
  }
58
82
 
59
83
  export async function generateBusinessDomains(context) {
60
- if (!isAIEnabled()) {
61
- return getFallbackBusinessDomains(context);
62
- }
63
-
64
- info("Generating business domains with AI...");
65
-
66
- const result = await generateText({
67
- system: SYSTEM_PROMPT,
68
- user: createBusinessDomainsPrompt(context),
69
- maxTokens: 2000
70
- });
71
-
72
- if (!result.success) {
73
- return getFallbackBusinessDomains(context);
74
- }
75
-
76
- return result.text;
84
+ return generateWithStructuredFallback(
85
+ "business_domains",
86
+ createBusinessDomainsPrompt(context),
87
+ 2000,
88
+ () => getFallbackBusinessDomains(context),
89
+ );
77
90
  }
78
91
 
79
92
  export async function generateArchitectureOverview(context) {
80
- if (!isAIEnabled()) {
81
- return getFallbackArchitectureOverview(context);
82
- }
83
-
84
- info("Generating architecture overview with AI...");
85
-
86
- const result = await generateText({
87
- system: SYSTEM_PROMPT,
88
- user: createArchitectureOverviewPrompt(context),
89
- maxTokens: 1800
90
- });
91
-
92
- if (!result.success) {
93
- return getFallbackArchitectureOverview(context);
94
- }
95
-
96
- return result.text;
93
+ return generateWithStructuredFallback(
94
+ "architecture_overview",
95
+ createArchitectureOverviewPrompt(context),
96
+ 1800,
97
+ () => getFallbackArchitectureOverview(context),
98
+ );
97
99
  }
98
100
 
99
101
  export async function generateDataFlows(flows, context) {
100
- if (!isAIEnabled()) {
101
- return getFallbackDataFlows(flows);
102
- }
103
-
104
- info("Generating data flows with AI...");
105
-
106
- const result = await generateText({
107
- system: SYSTEM_PROMPT,
108
- user: createDataFlowsPrompt(flows, context),
109
- maxTokens: 1800
110
- });
111
-
112
- if (!result.success) {
113
- return getFallbackDataFlows(flows);
114
- }
115
-
116
- return result.text;
102
+ return generateWithStructuredFallback(
103
+ "data_flows",
104
+ createDataFlowsPrompt(flows, context),
105
+ 1800,
106
+ () => getFallbackDataFlows(flows),
107
+ );
117
108
  }
118
109
 
119
110
  export async function generateDeveloperOnboarding(context) {
120
- if (!isAIEnabled()) {
121
- return getFallbackDeveloperOnboarding(context);
122
- }
123
-
124
- info("Generating developer onboarding with AI...");
125
-
126
- const result = await generateText({
127
- system: SYSTEM_PROMPT,
128
- user: createDeveloperOnboardingPrompt(context),
129
- maxTokens: 2200
130
- });
131
-
132
- if (!result.success) {
133
- return getFallbackDeveloperOnboarding(context);
134
- }
135
-
136
- return result.text;
111
+ return generateWithStructuredFallback(
112
+ "developer_onboarding",
113
+ createDeveloperOnboardingPrompt(context),
114
+ 2200,
115
+ () => getFallbackDeveloperOnboarding(context),
116
+ );
137
117
  }
138
118
 
139
119
  // Fallback generators (deterministic, no AI)
package/src/ai/prompts.js CHANGED
@@ -353,3 +353,218 @@ Dependencies:
353
353
  Risks:
354
354
  [if applicable]`;
355
355
  }
356
+
357
+ // --- JSON schemas for structured AI output ---
358
+
359
+ export const AI_SCHEMAS = {
360
+ executive_summary: {
361
+ required: ["whatItDoes", "whoItServes", "coreCapabilities", "mainAreas", "risks"],
362
+ description: "Executive summary for mixed audience",
363
+ },
364
+ system_overview: {
365
+ required: ["snapshot", "layers", "domains", "patterns", "observations"],
366
+ description: "High-level system overview",
367
+ },
368
+ business_domains: {
369
+ required: ["domains"],
370
+ description: "Business domain breakdown",
371
+ },
372
+ architecture_overview: {
373
+ required: ["style", "layers", "strengths", "weaknesses"],
374
+ description: "Architecture overview for engineers",
375
+ },
376
+ data_flows: {
377
+ required: ["flows"],
378
+ description: "Data flow documentation",
379
+ },
380
+ developer_onboarding: {
381
+ required: ["startHere", "mainFolders", "coreFlows", "complexityHotspots"],
382
+ description: "Developer onboarding guide",
383
+ },
384
+ };
385
+
386
+ /**
387
+ * Render a structured JSON response into Markdown for the given document type.
388
+ */
389
+ export function renderStructuredToMarkdown(key, parsed) {
390
+ switch (key) {
391
+ case "executive_summary":
392
+ return renderExecutiveSummaryJSON(parsed);
393
+ case "system_overview":
394
+ return renderSystemOverviewJSON(parsed);
395
+ case "business_domains":
396
+ return renderBusinessDomainsJSON(parsed);
397
+ case "architecture_overview":
398
+ return renderArchitectureOverviewJSON(parsed);
399
+ case "data_flows":
400
+ return renderDataFlowsJSON(parsed);
401
+ case "developer_onboarding":
402
+ return renderDeveloperOnboardingJSON(parsed);
403
+ default:
404
+ return null;
405
+ }
406
+ }
407
+
408
+ /**
409
+ * Safely convert any AI response value to a readable string.
410
+ * Handles: strings, arrays (of strings or objects), plain objects, and other types.
411
+ */
412
+ function safeStr(val) {
413
+ if (val == null) return "";
414
+ if (typeof val === "string") return val;
415
+ if (Array.isArray(val)) return val.map(safeStr).join(", ");
416
+ if (typeof val === "object") {
417
+ // Try common field patterns the AI might use
418
+ if (val.name) return val.description ? `${val.name}: ${val.description}` : val.name;
419
+ if (val.title) return val.description ? `${val.title}: ${val.description}` : val.title;
420
+ // Fallback: render object key/value pairs
421
+ return Object.entries(val).map(([k, v]) => `${k}: ${typeof v === "string" ? v : safeStr(v)}`).join("; ");
422
+ }
423
+ return String(val);
424
+ }
425
+
426
+ /**
427
+ * Convert a value to a bullet list.
428
+ * Handles strings, arrays of strings, arrays of objects, and plain objects.
429
+ */
430
+ function toBulletList(val) {
431
+ if (val == null) return "";
432
+ if (typeof val === "string") return val;
433
+ if (Array.isArray(val)) {
434
+ return val.map(item => {
435
+ if (typeof item === "string") return `- ${item}`;
436
+ if (typeof item === "object" && item !== null) {
437
+ const label = item.name || item.title || Object.keys(item)[0] || "";
438
+ const desc = item.description || item[Object.keys(item)[0]];
439
+ if (label && desc && label !== desc) return `- **${label}**: ${desc}`;
440
+ return `- ${safeStr(item)}`;
441
+ }
442
+ return `- ${String(item)}`;
443
+ }).join("\n");
444
+ }
445
+ if (typeof val === "object") {
446
+ // Object with key/value pairs โ†’ render as list
447
+ return Object.entries(val).map(([k, v]) => `- **${k}**: ${typeof v === "string" ? v : safeStr(v)}`).join("\n");
448
+ }
449
+ return String(val);
450
+ }
451
+
452
+ /**
453
+ * Convert a value to a heading-based section list (### heading per item).
454
+ */
455
+ function toHeadingSections(val) {
456
+ if (val == null) return "";
457
+ if (typeof val === "string") return val;
458
+ if (Array.isArray(val)) {
459
+ return val.map(item => {
460
+ if (typeof item === "string") return `### ${item}`;
461
+ if (typeof item === "object" && item !== null) {
462
+ const label = item.name || item.title || Object.keys(item)[0] || "Section";
463
+ const desc = item.description || item[Object.keys(item)[0]] || "";
464
+ return `### ${label}\n\n${typeof desc === "string" ? desc : safeStr(desc)}`;
465
+ }
466
+ return `### ${String(item)}`;
467
+ }).join("\n\n");
468
+ }
469
+ if (typeof val === "object") {
470
+ return Object.entries(val).map(([k, v]) => `### ${k}\n\n${typeof v === "string" ? v : safeStr(v)}`).join("\n\n");
471
+ }
472
+ return String(val);
473
+ }
474
+
475
+ function renderExecutiveSummaryJSON(d) {
476
+ let md = `# Executive Summary\n\n`;
477
+ md += `## What This System Does\n\n${safeStr(d.whatItDoes)}\n\n`;
478
+ md += `## Who It Serves\n\n${safeStr(d.whoItServes)}\n\n`;
479
+ md += `## Core Capabilities\n\n${toBulletList(d.coreCapabilities)}\n\n`;
480
+ md += `## Main System Areas\n\n${toBulletList(d.mainAreas)}\n\n`;
481
+ if (d.dependencies) md += `## Key Dependencies\n\n${toBulletList(d.dependencies)}\n\n`;
482
+ md += `## Operational and Architectural Risks\n\n${toBulletList(d.risks)}\n\n`;
483
+ if (d.focusAreas) md += `## Recommended Focus Areas\n\n${toBulletList(d.focusAreas)}\n`;
484
+ return md;
485
+ }
486
+
487
+ function renderSystemOverviewJSON(d) {
488
+ let md = `# System Overview\n\n`;
489
+ md += `## Repository Snapshot\n\n${safeStr(d.snapshot)}\n\n`;
490
+ md += `## Main Architectural Layers\n\n${toBulletList(d.layers)}\n\n`;
491
+ md += `## Dominant Domains\n\n${toBulletList(d.domains)}\n\n`;
492
+ md += `## Main Technology Patterns\n\n${toBulletList(d.patterns)}\n\n`;
493
+ md += `## Key Observations\n\n${toBulletList(d.observations)}\n`;
494
+ return md;
495
+ }
496
+
497
+ function renderBusinessDomainsJSON(d) {
498
+ let md = `# Business Domains\n\n`;
499
+ if (!Array.isArray(d.domains)) {
500
+ // Handle object-style domains: { "Auth": { description: "..." }, ... }
501
+ if (typeof d.domains === "object" && d.domains !== null) {
502
+ for (const [name, info] of Object.entries(d.domains)) {
503
+ const desc = typeof info === "string" ? info : info?.description || safeStr(info);
504
+ md += `## ${name}\n\n${desc}\n\n`;
505
+ if (info?.modules) md += `**Key modules:** ${safeStr(info.modules)}\n\n`;
506
+ if (info?.userFunctionality) md += `**User-visible functionality:** ${info.userFunctionality}\n\n`;
507
+ if (info?.dependencies) md += `**Dependencies:** ${safeStr(info.dependencies)}\n\n`;
508
+ }
509
+ return md;
510
+ }
511
+ return md + safeStr(d.domains);
512
+ }
513
+ for (const dom of d.domains) {
514
+ const name = dom.name || dom.title || safeStr(dom);
515
+ md += `## ${name}\n\n${dom.description || ""}\n\n`;
516
+ if (dom.modules) md += `**Key modules:** ${safeStr(dom.modules)}\n\n`;
517
+ if (dom.userFunctionality) md += `**User-visible functionality:** ${dom.userFunctionality}\n\n`;
518
+ if (dom.dependencies) md += `**Dependencies:** ${safeStr(dom.dependencies)}\n\n`;
519
+ }
520
+ return md;
521
+ }
522
+
523
+ function renderArchitectureOverviewJSON(d) {
524
+ let md = `# Architecture Overview\n\n`;
525
+ md += `## Architecture Style\n\n${safeStr(d.style)}\n\n`;
526
+ md += `## Layers\n\n${toHeadingSections(d.layers)}\n\n`;
527
+ md += `## Architectural Strengths\n\n${toBulletList(d.strengths)}\n\n`;
528
+ md += `## Architectural Weaknesses\n\n${toBulletList(d.weaknesses)}\n`;
529
+ return md;
530
+ }
531
+
532
+ function renderDataFlowsJSON(d) {
533
+ let md = `# Data Flows\n\n`;
534
+ if (!Array.isArray(d.flows)) {
535
+ if (typeof d.flows === "object" && d.flows !== null) {
536
+ for (const [name, info] of Object.entries(d.flows)) {
537
+ const desc = typeof info === "string" ? info : info?.description || safeStr(info);
538
+ md += `## ${name}\n\n${desc}\n\n`;
539
+ if (info?.steps) md += `**Steps:**\n${toBulletList(info.steps)}\n\n`;
540
+ if (info?.modules) md += `**Involved modules:** ${safeStr(info.modules)}\n\n`;
541
+ }
542
+ return md;
543
+ }
544
+ return md + safeStr(d.flows);
545
+ }
546
+ for (const flow of d.flows) {
547
+ const name = flow.name || flow.title || safeStr(flow);
548
+ md += `## ${name}\n\n${flow.description || ""}\n\n`;
549
+ if (flow.steps) {
550
+ const steps = Array.isArray(flow.steps)
551
+ ? flow.steps.map((s, i) => `${i + 1}. ${safeStr(s)}`).join("\n")
552
+ : safeStr(flow.steps);
553
+ md += `**Steps:**\n${steps}\n\n`;
554
+ }
555
+ if (flow.modules) md += `**Involved modules:** ${safeStr(flow.modules)}\n\n`;
556
+ if (flow.criticalDependencies) md += `**Critical dependencies:** ${safeStr(flow.criticalDependencies)}\n\n`;
557
+ }
558
+ return md;
559
+ }
560
+
561
+ function renderDeveloperOnboardingJSON(d) {
562
+ let md = `# Developer Onboarding\n\n`;
563
+ md += `## Start Here\n\n${safeStr(d.startHere)}\n\n`;
564
+ md += `## Main Folders\n\n${toBulletList(d.mainFolders)}\n\n`;
565
+ md += `## Core Product Flows\n\n${toBulletList(d.coreFlows)}\n\n`;
566
+ if (d.importantRoutes) md += `## Important Routes\n\n${toBulletList(d.importantRoutes)}\n\n`;
567
+ if (d.sharedLibraries) md += `## Important Shared Libraries\n\n${toBulletList(d.sharedLibraries)}\n\n`;
568
+ md += `## Known Complexity Hotspots\n\n${toBulletList(d.complexityHotspots)}\n`;
569
+ return md;
570
+ }