legacyver 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.agent/skills/openspec-apply-change/SKILL.md +156 -0
  2. package/.agent/skills/openspec-archive-change/SKILL.md +114 -0
  3. package/.agent/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  4. package/.agent/skills/openspec-continue-change/SKILL.md +118 -0
  5. package/.agent/skills/openspec-explore/SKILL.md +290 -0
  6. package/.agent/skills/openspec-ff-change/SKILL.md +101 -0
  7. package/.agent/skills/openspec-new-change/SKILL.md +74 -0
  8. package/.agent/skills/openspec-onboard/SKILL.md +529 -0
  9. package/.agent/skills/openspec-sync-specs/SKILL.md +138 -0
  10. package/.agent/skills/openspec-verify-change/SKILL.md +168 -0
  11. package/.agent/workflows/opsx-apply.md +149 -0
  12. package/.agent/workflows/opsx-archive.md +154 -0
  13. package/.agent/workflows/opsx-bulk-archive.md +239 -0
  14. package/.agent/workflows/opsx-continue.md +111 -0
  15. package/.agent/workflows/opsx-explore.md +171 -0
  16. package/.agent/workflows/opsx-ff.md +91 -0
  17. package/.agent/workflows/opsx-new.md +66 -0
  18. package/.agent/workflows/opsx-onboard.md +522 -0
  19. package/.agent/workflows/opsx-sync.md +131 -0
  20. package/.agent/workflows/opsx-verify.md +161 -0
  21. package/.github/prompts/opsx-apply.prompt.md +149 -0
  22. package/.github/prompts/opsx-archive.prompt.md +154 -0
  23. package/.github/prompts/opsx-bulk-archive.prompt.md +239 -0
  24. package/.github/prompts/opsx-continue.prompt.md +111 -0
  25. package/.github/prompts/opsx-explore.prompt.md +171 -0
  26. package/.github/prompts/opsx-ff.prompt.md +91 -0
  27. package/.github/prompts/opsx-new.prompt.md +66 -0
  28. package/.github/prompts/opsx-onboard.prompt.md +522 -0
  29. package/.github/prompts/opsx-sync.prompt.md +131 -0
  30. package/.github/prompts/opsx-verify.prompt.md +161 -0
  31. package/.github/skills/openspec-apply-change/SKILL.md +156 -0
  32. package/.github/skills/openspec-archive-change/SKILL.md +114 -0
  33. package/.github/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  34. package/.github/skills/openspec-continue-change/SKILL.md +118 -0
  35. package/.github/skills/openspec-explore/SKILL.md +290 -0
  36. package/.github/skills/openspec-ff-change/SKILL.md +101 -0
  37. package/.github/skills/openspec-new-change/SKILL.md +74 -0
  38. package/.github/skills/openspec-onboard/SKILL.md +529 -0
  39. package/.github/skills/openspec-sync-specs/SKILL.md +138 -0
  40. package/.github/skills/openspec-verify-change/SKILL.md +168 -0
  41. package/.legacyverignore.example +43 -0
  42. package/.legacyverrc +7 -0
  43. package/.opencode/command/opsx-apply.md +149 -0
  44. package/.opencode/command/opsx-archive.md +154 -0
  45. package/.opencode/command/opsx-bulk-archive.md +239 -0
  46. package/.opencode/command/opsx-continue.md +111 -0
  47. package/.opencode/command/opsx-explore.md +171 -0
  48. package/.opencode/command/opsx-ff.md +91 -0
  49. package/.opencode/command/opsx-new.md +66 -0
  50. package/.opencode/command/opsx-onboard.md +522 -0
  51. package/.opencode/command/opsx-sync.md +131 -0
  52. package/.opencode/command/opsx-verify.md +161 -0
  53. package/.opencode/skills/openspec-apply-change/SKILL.md +156 -0
  54. package/.opencode/skills/openspec-archive-change/SKILL.md +114 -0
  55. package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  56. package/.opencode/skills/openspec-continue-change/SKILL.md +118 -0
  57. package/.opencode/skills/openspec-explore/SKILL.md +290 -0
  58. package/.opencode/skills/openspec-ff-change/SKILL.md +101 -0
  59. package/.opencode/skills/openspec-new-change/SKILL.md +74 -0
  60. package/.opencode/skills/openspec-onboard/SKILL.md +529 -0
  61. package/.opencode/skills/openspec-sync-specs/SKILL.md +138 -0
  62. package/.opencode/skills/openspec-verify-change/SKILL.md +168 -0
  63. package/LICENSE +1 -1
  64. package/README.md +128 -83
  65. package/bin/legacyver.js +48 -25
  66. package/legacyver-docs/SUMMARY.md +3 -0
  67. package/legacyver-docs/components.md +57 -0
  68. package/legacyver-docs/index.md +15 -0
  69. package/nul +2 -0
  70. package/package.json +23 -25
  71. package/src/cache/hash.js +9 -10
  72. package/src/cache/index.js +43 -65
  73. package/src/cli/commands/analyze.js +212 -190
  74. package/src/cli/commands/cache.js +15 -35
  75. package/src/cli/commands/init.js +63 -107
  76. package/src/cli/commands/providers.js +56 -81
  77. package/src/cli/commands/version.js +7 -10
  78. package/src/cli/ui.js +58 -77
  79. package/src/crawler/filters.js +41 -40
  80. package/src/crawler/index.js +52 -36
  81. package/src/crawler/manifest.js +31 -43
  82. package/src/crawler/walk.js +32 -38
  83. package/src/llm/chunker.js +34 -56
  84. package/src/llm/cost-estimator.js +68 -51
  85. package/src/llm/free-model.js +67 -0
  86. package/src/llm/index.js +22 -43
  87. package/src/llm/prompts.js +45 -33
  88. package/src/llm/providers/gemini.js +94 -0
  89. package/src/llm/providers/groq.js +55 -40
  90. package/src/llm/providers/ollama.js +38 -65
  91. package/src/llm/providers/openrouter.js +67 -0
  92. package/src/llm/queue.js +59 -88
  93. package/src/llm/re-prompter.js +41 -0
  94. package/src/llm/validator.js +72 -0
  95. package/src/parser/ast/generic.js +45 -222
  96. package/src/parser/ast/go.js +86 -205
  97. package/src/parser/ast/java.js +76 -146
  98. package/src/parser/ast/javascript.js +173 -241
  99. package/src/parser/ast/laravel/blade.js +56 -0
  100. package/src/parser/ast/laravel/classifier.js +30 -0
  101. package/src/parser/ast/laravel/controller.js +35 -0
  102. package/src/parser/ast/laravel/index.js +54 -0
  103. package/src/parser/ast/laravel/model.js +41 -0
  104. package/src/parser/ast/laravel/provider.js +28 -0
  105. package/src/parser/ast/laravel/routes.js +45 -0
  106. package/src/parser/ast/php.js +129 -0
  107. package/src/parser/ast/python.js +76 -199
  108. package/src/parser/ast/typescript.js +10 -244
  109. package/src/parser/body-extractor.js +40 -0
  110. package/src/parser/call-graph.js +50 -67
  111. package/src/parser/complexity-scorer.js +59 -0
  112. package/src/parser/index.js +61 -86
  113. package/src/parser/pattern-detector.js +71 -0
  114. package/src/parser/pkg-builder.js +36 -83
  115. package/src/renderer/html.js +63 -135
  116. package/src/renderer/index.js +23 -35
  117. package/src/renderer/json.js +17 -35
  118. package/src/renderer/markdown.js +83 -117
  119. package/src/utils/config.js +52 -53
  120. package/src/utils/errors.js +26 -41
  121. package/src/utils/logger.js +32 -53
  122. package/src/cli/flags.js +0 -87
  123. package/src/llm/providers/anthropic.js +0 -57
  124. package/src/llm/providers/google.js +0 -65
  125. package/src/llm/providers/openai.js +0 -52
  126. package/src/parser/ast/tree-sitter-init.js +0 -80
@@ -0,0 +1,71 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Domain pattern detector — scans function body text for known patterns.
5
+ */
6
+
7
+ const PATTERNS = [
8
+ {
9
+ name: 'arithmetic',
10
+ test: (body) =>
11
+ /[\*\/\%\*\*]/.test(body) || /Math\./.test(body),
12
+ },
13
+ {
14
+ name: 'mqtt',
15
+ test: (body) =>
16
+ /\.publish\s*\(/.test(body) ||
17
+ /\.subscribe\s*\(/.test(body) ||
18
+ /\.connect\s*\(/.test(body) ||
19
+ /mqtts?:\/\//.test(body),
20
+ },
21
+ {
22
+ name: 'http_call',
23
+ test: (body) =>
24
+ /\bfetch\s*\(/.test(body) ||
25
+ /\baxios\b/.test(body) ||
26
+ /\bhttp\.get\s*\(/.test(body) ||
27
+ /\bcurl\b/.test(body) ||
28
+ /\bGuzzle\b/.test(body),
29
+ },
30
+ {
31
+ name: 'database_query',
32
+ test: (body) =>
33
+ /\.query\s*\(/.test(body) ||
34
+ /->where\s*\(/.test(body) ||
35
+ /->select\s*\(/.test(body) ||
36
+ /\bDB::/.test(body) ||
37
+ /\bPDO\b/.test(body) ||
38
+ /\bmongoose\b/.test(body) ||
39
+ /\bprisma\b/.test(body),
40
+ },
41
+ {
42
+ name: 'event_emit',
43
+ test: (body) =>
44
+ /\.emit\s*\(/.test(body) ||
45
+ /\.dispatch\s*\(/.test(body) ||
46
+ /\bevent\s*\(/.test(body) ||
47
+ /Event::dispatch\s*\(/.test(body),
48
+ },
49
+ {
50
+ name: 'caching',
51
+ test: (body) =>
52
+ /\bcache\s*\(/.test(body) ||
53
+ /\bCache::/.test(body) ||
54
+ /\bRedis::/.test(body),
55
+ },
56
+ {
57
+ name: 'queue_job',
58
+ test: (body) =>
59
+ /\bdispatch\s*\(/.test(body) ||
60
+ /Queue::push\s*\(/.test(body) ||
61
+ /->delay\s*\(/.test(body) ||
62
+ /\.queue\s*\(/.test(body),
63
+ },
64
+ ];
65
+
66
+ function detectPatterns(bodyText) {
67
+ if (!bodyText) return [];
68
+ return PATTERNS.filter((p) => p.test(bodyText)).map((p) => p.name);
69
+ }
70
+
71
+ module.exports = { detectPatterns, PATTERNS };
@@ -1,105 +1,58 @@
1
- /**
2
- * PKG (Project Knowledge Graph) builder.
3
- * Aggregates all FileFacts into a single ProjectKnowledgeGraph.
4
- * Infers project name from package.json or directory name.
5
- * Detects entry points (files with empty calledBy).
6
- */
7
-
8
- import { readFileSync, existsSync } from 'node:fs';
9
- import { join, basename } from 'node:path';
1
+ 'use strict';
10
2
 
11
3
  /**
12
- * @typedef {object} ProjectKnowledgeGraph
13
- * @property {object} meta
14
- * @property {string} meta.name
15
- * @property {string} meta.primaryLanguage
16
- * @property {number} meta.totalFiles
17
- * @property {string} meta.analyzedAt
18
- * @property {Object<string, import('./index.js').FileFacts>} files
19
- * @property {string[]} entryPoints
20
- * @property {Object<string, string[]>} graph
4
+ * Assemble all FileFacts into a Project Knowledge Graph (PKG).
21
5
  */
22
-
23
- /**
24
- * Build a ProjectKnowledgeGraph from all FileFacts.
25
- * @param {import('./index.js').FileFacts[]} allFacts
26
- * @param {string} targetDir - Base directory
27
- * @returns {ProjectKnowledgeGraph}
28
- */
29
- export function buildPKG(allFacts, targetDir) {
30
- const name = inferProjectName(targetDir);
31
- const primaryLanguage = detectPrimaryLanguage(allFacts);
32
-
6
+ function buildPKG(allFacts, meta) {
33
7
  const files = {};
34
8
  const graph = {};
9
+ const entryPoints = [];
35
10
 
36
11
  for (const facts of allFacts) {
37
12
  files[facts.relativePath] = facts;
38
- if (facts.callsTo.length > 0) {
39
- graph[facts.relativePath] = facts.callsTo;
13
+ graph[facts.relativePath] = facts.callsTo || [];
14
+ if (!facts.calledBy || facts.calledBy.length === 0) {
15
+ entryPoints.push(facts.relativePath);
40
16
  }
41
17
  }
42
18
 
43
- // Entry points: files not called by anyone
44
- const entryPoints = allFacts
45
- .filter((f) => f.calledBy.length === 0)
46
- .map((f) => f.relativePath);
19
+ // Laravel meta aggregation
20
+ let laravelMeta = null;
21
+ if (meta && meta.framework === 'laravel') {
22
+ const routeMap = [];
23
+ const relationships = [];
24
+ const providerBindings = [];
25
+ const modelNames = [];
26
+
27
+ for (const facts of allFacts) {
28
+ if (facts.laravelContext) {
29
+ const ctx = facts.laravelContext;
30
+ if (ctx.type === 'route_file' && ctx.routes) {
31
+ routeMap.push(...ctx.routes);
32
+ }
33
+ if (ctx.type === 'model') {
34
+ if (facts.classes && facts.classes.length > 0) modelNames.push(facts.classes[0].name);
35
+ if (ctx.relationships) relationships.push(...ctx.relationships.map(r => ({ ...r, fromModel: facts.classes && facts.classes[0] ? facts.classes[0].name : facts.relativePath })));
36
+ }
37
+ if (ctx.type === 'provider' && ctx.registerBindings) {
38
+ providerBindings.push({ provider: facts.relativePath, bindings: ctx.registerBindings });
39
+ }
40
+ }
41
+ }
42
+
43
+ laravelMeta = { routeMap, relationships, providerBindings, modelNames };
44
+ }
47
45
 
48
46
  return {
49
47
  meta: {
50
- name,
51
- primaryLanguage,
48
+ ...meta,
52
49
  totalFiles: allFacts.length,
53
- analyzedAt: new Date().toISOString(),
54
50
  },
55
51
  files,
56
52
  entryPoints,
57
53
  graph,
54
+ laravelMeta,
58
55
  };
59
56
  }
60
57
 
61
- function inferProjectName(targetDir) {
62
- // Try package.json
63
- const pkgJsonPath = join(targetDir, 'package.json');
64
- if (existsSync(pkgJsonPath)) {
65
- try {
66
- const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
67
- if (pkg.name) return pkg.name;
68
- } catch {
69
- // Ignore parse errors
70
- }
71
- }
72
-
73
- // Try go.mod
74
- const goModPath = join(targetDir, 'go.mod');
75
- if (existsSync(goModPath)) {
76
- try {
77
- const content = readFileSync(goModPath, 'utf-8');
78
- const match = content.match(/module\s+(\S+)/);
79
- if (match) return match[1];
80
- } catch {
81
- // Ignore
82
- }
83
- }
84
-
85
- // Fall back to directory name
86
- return basename(targetDir);
87
- }
88
-
89
- function detectPrimaryLanguage(allFacts) {
90
- const counts = {};
91
- for (const facts of allFacts) {
92
- counts[facts.language] = (counts[facts.language] || 0) + 1;
93
- }
94
-
95
- let maxLang = 'unknown';
96
- let maxCount = 0;
97
- for (const [lang, count] of Object.entries(counts)) {
98
- if (count > maxCount) {
99
- maxLang = lang;
100
- maxCount = count;
101
- }
102
- }
103
-
104
- return maxLang;
105
- }
58
+ module.exports = { buildPKG };
@@ -1,152 +1,80 @@
1
- /**
2
- * HTML renderer.
3
- * Converts all Markdown fragments to HTML using marked.
4
- * Injects sidebar navigation.
5
- * Outputs single self-contained index.html.
6
- */
1
+ 'use strict';
7
2
 
8
- import { mkdirSync, writeFileSync } from 'node:fs';
9
- import { join, dirname } from 'node:path';
10
- import { marked } from 'marked';
3
+ const { writeFileSync, mkdirSync } = require('fs');
4
+ const path = require('path');
5
+ const { marked } = require('marked');
11
6
 
12
7
  /**
13
- * Render documentation as a single self-contained HTML file.
14
- * @param {import('../llm/queue.js').DocFragment[]} fragments
15
- * @param {import('../parser/pkg-builder.js').ProjectKnowledgeGraph} pkg
16
- * @param {string} outputDir - Output directory path
8
+ * HTML renderer single self-contained index.html with lunr.js search.
17
9
  */
18
- export async function renderHTML(fragments, pkg, outputDir) {
10
+ async function render(fragments, pkg, outputDir, config) {
19
11
  mkdirSync(outputDir, { recursive: true });
20
12
 
21
- const sidebar = buildSidebar(fragments, pkg);
22
- const content = buildContent(fragments, pkg);
13
+ const nav = fragments.map(f => {
14
+ const id = f.relativePath.replace(/[^a-zA-Z0-9]/g, '-');
15
+ return `<li><a href="#${id}">${f.relativePath}</a></li>`;
16
+ }).join('\n');
17
+
18
+ const content = fragments.map(f => {
19
+ const id = f.relativePath.replace(/[^a-zA-Z0-9]/g, '-');
20
+ const html = marked(f.content || '');
21
+ return `<section id="${id}"><h2>${f.relativePath}</h2>${html}</section>`;
22
+ }).join('\n');
23
+
24
+ const lunrDocs = fragments.map(f => ({
25
+ id: f.relativePath,
26
+ title: f.relativePath,
27
+ body: (f.content || '').replace(/[#`*_\[\]()]/g, ' '),
28
+ }));
23
29
 
24
30
  const html = `<!DOCTYPE html>
25
31
  <html lang="en">
26
32
  <head>
27
- <meta charset="UTF-8">
28
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
29
- <title>${pkg.meta.name} - Documentation</title>
30
- <style>
31
- * { margin: 0; padding: 0; box-sizing: border-box; }
32
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: flex; min-height: 100vh; }
33
-
34
- .sidebar {
35
- width: 280px; min-width: 280px; background: #1e1e2e; color: #cdd6f4;
36
- padding: 20px; overflow-y: auto; position: fixed; top: 0; left: 0; bottom: 0;
37
- }
38
- .sidebar h2 { color: #89b4fa; font-size: 16px; margin-bottom: 16px; }
39
- .sidebar ul { list-style: none; }
40
- .sidebar li { margin-bottom: 4px; }
41
- .sidebar a { color: #a6adc8; text-decoration: none; font-size: 13px; display: block; padding: 4px 8px; border-radius: 4px; }
42
- .sidebar a:hover { background: #313244; color: #cdd6f4; }
43
- .sidebar .dir-label { color: #6c7086; font-size: 11px; text-transform: uppercase; margin-top: 12px; margin-bottom: 4px; padding-left: 8px; }
44
-
45
- .search-box { width: 100%; padding: 8px 12px; background: #313244; border: 1px solid #45475a; border-radius: 6px; color: #cdd6f4; font-size: 13px; margin-bottom: 16px; }
46
- .search-box::placeholder { color: #6c7086; }
47
-
48
- .content { margin-left: 280px; padding: 40px; max-width: 900px; flex: 1; }
49
- .content h1 { color: #1e1e2e; border-bottom: 2px solid #89b4fa; padding-bottom: 8px; margin-bottom: 24px; }
50
- .content h2 { color: #313244; margin-top: 32px; margin-bottom: 12px; }
51
- .content h3 { color: #45475a; margin-top: 20px; margin-bottom: 8px; }
52
- .content p { line-height: 1.6; margin-bottom: 12px; color: #313244; }
53
- .content code { background: #f5f5f5; padding: 2px 6px; border-radius: 3px; font-size: 14px; }
54
- .content pre { background: #1e1e2e; color: #cdd6f4; padding: 16px; border-radius: 8px; overflow-x: auto; margin-bottom: 16px; }
55
- .content pre code { background: none; padding: 0; }
56
- .content table { border-collapse: collapse; width: 100%; margin-bottom: 16px; }
57
- .content th, .content td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
58
- .content th { background: #f5f5f5; }
59
- .content ul, .content ol { margin-left: 24px; margin-bottom: 12px; }
60
- .content li { margin-bottom: 4px; line-height: 1.5; }
61
-
62
- .file-section { border-top: 1px solid #e0e0e0; padding-top: 32px; margin-top: 32px; }
63
- .file-section:first-child { border-top: none; padding-top: 0; margin-top: 0; }
64
-
65
- .meta-table { font-size: 14px; }
66
- .meta-table td:first-child { font-weight: 600; width: 150px; }
67
- </style>
33
+ <meta charset="UTF-8">
34
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
35
+ <title>${pkg.meta && pkg.meta.name || 'Legacyver'} Documentation</title>
36
+ <script src="https://unpkg.com/lunr/lunr.js"></script>
37
+ <style>
38
+ body { font-family: system-ui, sans-serif; display: flex; margin: 0; }
39
+ nav { width: 250px; min-height: 100vh; background: #1e1e2e; color: #cdd6f4; padding: 1rem; overflow-y: auto; position: sticky; top: 0; }
40
+ nav ul { list-style: none; padding: 0; }
41
+ nav a { color: #89b4fa; text-decoration: none; font-size: 0.85rem; }
42
+ nav a:hover { color: #cba6f7; }
43
+ main { flex: 1; padding: 2rem; max-width: 900px; }
44
+ pre { background: #1e1e2e; color: #cdd6f4; padding: 1rem; border-radius: 6px; overflow-x: auto; }
45
+ code { background: #313244; padding: 2px 4px; border-radius: 3px; }
46
+ #search { width: 100%; padding: 0.5rem; margin-bottom: 1rem; background: #313244; border: none; color: #cdd6f4; border-radius: 4px; }
47
+ #search-results { background: #313244; padding: 0.5rem; border-radius: 4px; margin-bottom: 1rem; display: none; }
48
+ </style>
68
49
  </head>
69
50
  <body>
70
- <nav class="sidebar">
71
- <h2>${pkg.meta.name}</h2>
72
- <input type="text" class="search-box" placeholder="Search files..." id="search">
73
- ${sidebar}
74
- </nav>
75
- <main class="content">
76
- ${content}
77
- </main>
78
- <script>
79
- document.getElementById('search').addEventListener('input', function(e) {
80
- const query = e.target.value.toLowerCase();
81
- document.querySelectorAll('.sidebar li').forEach(function(li) {
82
- const text = li.textContent.toLowerCase();
83
- li.style.display = text.includes(query) ? '' : 'none';
84
- });
85
- });
86
-
87
- document.querySelectorAll('.sidebar a').forEach(function(a) {
88
- a.addEventListener('click', function(e) {
89
- e.preventDefault();
90
- var target = document.getElementById(this.getAttribute('href').substring(1));
91
- if (target) target.scrollIntoView({ behavior: 'smooth' });
92
- });
93
- });
94
- </script>
51
+ <nav>
52
+ <input id="search" type="search" placeholder="Search..." />
53
+ <div id="search-results"></div>
54
+ <ul>${nav}</ul>
55
+ </nav>
56
+ <main>${content}</main>
57
+ <script>
58
+ const docs = ${JSON.stringify(lunrDocs)};
59
+ const idx = lunr(function() {
60
+ this.field('title'); this.field('body');
61
+ docs.forEach(d => this.add(d));
62
+ });
63
+ document.getElementById('search').addEventListener('input', function() {
64
+ const q = this.value.trim();
65
+ const res = document.getElementById('search-results');
66
+ if (!q) { res.style.display = 'none'; return; }
67
+ try {
68
+ const results = idx.search(q);
69
+ res.innerHTML = results.map(r => '<div><a href="#' + r.ref.replace(/[^a-zA-Z0-9]/g,'-') + '">' + r.ref + '</a></div>').join('') || '<div>No results</div>';
70
+ res.style.display = 'block';
71
+ } catch(e) { res.style.display = 'none'; }
72
+ });
73
+ </script>
95
74
  </body>
96
75
  </html>`;
97
76
 
98
- writeFileSync(join(outputDir, 'index.html'), html, 'utf-8');
77
+ writeFileSync(path.join(outputDir, 'index.html'), html, 'utf8');
99
78
  }
100
79
 
101
- function buildSidebar(fragments, pkg) {
102
- const dirs = new Map();
103
-
104
- for (const fragment of fragments) {
105
- const dir = dirname(fragment.relativePath);
106
- if (!dirs.has(dir)) {
107
- dirs.set(dir, []);
108
- }
109
- dirs.get(dir).push(fragment);
110
- }
111
-
112
- let html = '<ul>';
113
-
114
- const sortedDirs = [...dirs.keys()].sort();
115
- for (const dir of sortedDirs) {
116
- if (dir !== '.') {
117
- html += `<li><div class="dir-label">${dir}</div></li>`;
118
- }
119
- const files = dirs.get(dir).sort((a, b) => a.relativePath.localeCompare(b.relativePath));
120
- for (const f of files) {
121
- const id = f.relativePath.replace(/[^a-zA-Z0-9]/g, '-');
122
- html += `<li><a href="#${id}">${f.relativePath}</a></li>`;
123
- }
124
- }
125
-
126
- html += '</ul>';
127
- return html;
128
- }
129
-
130
- function buildContent(fragments, pkg) {
131
- let html = '';
132
-
133
- // Project overview
134
- html += `<h1>${pkg.meta.name} Documentation</h1>`;
135
- html += `<table class="meta-table">`;
136
- html += `<tr><td>Primary Language</td><td>${pkg.meta.primaryLanguage}</td></tr>`;
137
- html += `<tr><td>Total Files</td><td>${pkg.meta.totalFiles}</td></tr>`;
138
- html += `<tr><td>Analyzed At</td><td>${pkg.meta.analyzedAt}</td></tr>`;
139
- html += `</table>`;
140
-
141
- // Individual file docs
142
- const sorted = [...fragments].sort((a, b) => a.relativePath.localeCompare(b.relativePath));
143
- for (const fragment of sorted) {
144
- const id = fragment.relativePath.replace(/[^a-zA-Z0-9]/g, '-');
145
- html += `<div class="file-section" id="${id}">`;
146
- html += `<h2>${fragment.relativePath}</h2>`;
147
- html += marked(fragment.content);
148
- html += `</div>`;
149
- }
150
-
151
- return html;
152
- }
80
+ module.exports = { render };
@@ -1,44 +1,32 @@
1
- /**
2
- * Renderer dispatcher.
3
- * Reads format from config, calls correct renderer.
4
- * Ensures output directory exists.
5
- */
1
+ 'use strict';
6
2
 
7
- import { mkdirSync } from 'node:fs';
8
- import { renderMarkdown } from './markdown.js';
9
- import { renderHTML } from './html.js';
10
- import { renderJSON } from './json.js';
11
- import { RenderError } from '../utils/errors.js';
12
-
13
- const RENDERERS = {
14
- markdown: renderMarkdown,
15
- html: renderHTML,
16
- json: renderJSON,
17
- };
3
+ const { mkdirSync } = require('fs');
4
+ const { RenderError } = require('../utils/errors');
18
5
 
19
6
  /**
20
- * Render documentation using the specified format.
21
- * @param {import('../llm/queue.js').DocFragment[]} fragments
22
- * @param {import('../parser/pkg-builder.js').ProjectKnowledgeGraph} pkg
23
- * @param {object} options
24
- * @param {string} options.format - Output format (markdown, html, json)
25
- * @param {string} options.outputDir - Output directory
7
+ * Renderer dispatcher.
26
8
  */
27
- export async function render(fragments, pkg, options) {
28
- const { format = 'markdown', outputDir } = options;
29
-
30
- const renderer = RENDERERS[format];
31
- if (!renderer) {
32
- throw new RenderError(format, `Unsupported format "${format}". Supported: ${Object.keys(RENDERERS).join(', ')}`);
33
- }
34
-
35
- // Ensure output directory exists
9
+ async function render(fragments, pkg, outputDir, config) {
36
10
  mkdirSync(outputDir, { recursive: true });
37
11
 
12
+ const format = (config.format || 'markdown').toLowerCase();
13
+
38
14
  try {
39
- await renderer(fragments, pkg, outputDir);
40
- } catch (err) {
41
- if (err instanceof RenderError) throw err;
42
- throw new RenderError(format, err.message);
15
+ switch (format) {
16
+ case 'html':
17
+ await require('./html').render(fragments, pkg, outputDir, config);
18
+ break;
19
+ case 'json':
20
+ await require('./json').render(fragments, pkg, outputDir, config);
21
+ break;
22
+ case 'markdown':
23
+ default:
24
+ await require('./markdown').render(fragments, pkg, outputDir, config);
25
+ break;
26
+ }
27
+ } catch (e) {
28
+ throw new RenderError(format, e);
43
29
  }
44
30
  }
31
+
32
+ module.exports = { render };
@@ -1,46 +1,28 @@
1
- /**
2
- * JSON renderer.
3
- * Outputs full PKG with llmDescription field added to each file entry.
4
- * Writes legacyver-docs/documentation.json.
5
- */
1
+ 'use strict';
6
2
 
7
- import { mkdirSync, writeFileSync } from 'node:fs';
8
- import { join } from 'node:path';
3
+ const { writeFileSync, mkdirSync } = require('fs');
4
+ const path = require('path');
9
5
 
10
6
  /**
11
- * Render documentation as a JSON file.
12
- * @param {import('../llm/queue.js').DocFragment[]} fragments
13
- * @param {import('../parser/pkg-builder.js').ProjectKnowledgeGraph} pkg
14
- * @param {string} outputDir - Output directory path
7
+ * JSON renderer outputs PKG enriched with LLM descriptions.
15
8
  */
16
- export async function renderJSON(fragments, pkg, outputDir) {
9
+ async function render(fragments, pkg, outputDir, config) {
17
10
  mkdirSync(outputDir, { recursive: true });
18
11
 
19
- // Create a fragment lookup by relative path
20
- const fragmentMap = new Map();
21
- for (const fragment of fragments) {
22
- fragmentMap.set(fragment.relativePath, fragment);
23
- }
24
-
25
- // Build output: PKG + llmDescription per file
26
- const output = {
27
- meta: pkg.meta,
28
- entryPoints: pkg.entryPoints,
29
- graph: pkg.graph,
30
- files: {},
31
- };
32
-
33
- for (const [filePath, facts] of Object.entries(pkg.files)) {
34
- const fragment = fragmentMap.get(filePath);
35
- output.files[filePath] = {
36
- ...facts,
37
- llmDescription: fragment ? fragment.content : null,
38
- };
12
+ // Enrich PKG with LLM descriptions
13
+ const enrichedPkg = JSON.parse(JSON.stringify(pkg));
14
+ for (const frag of fragments) {
15
+ if (enrichedPkg.files[frag.relativePath]) {
16
+ enrichedPkg.files[frag.relativePath].llmDescription = frag.content;
17
+ enrichedPkg.files[frag.relativePath]._qualityWarnings = frag._qualityWarnings || [];
18
+ }
39
19
  }
40
20
 
41
21
  writeFileSync(
42
- join(outputDir, 'documentation.json'),
43
- JSON.stringify(output, null, 2),
44
- 'utf-8'
22
+ path.join(outputDir, 'documentation.json'),
23
+ JSON.stringify(enrichedPkg, null, 2),
24
+ 'utf8'
45
25
  );
46
26
  }
27
+
28
+ module.exports = { render };