svharness 0.8.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/README.md +531 -0
- package/bin/cli.js +3 -0
- package/dist/adapters/_frontmatter.js +24 -0
- package/dist/adapters/claude-code.js +12 -0
- package/dist/adapters/codechat.js +12 -0
- package/dist/adapters/cursor.js +19 -0
- package/dist/adapters/generic.js +19 -0
- package/dist/adapters/index.js +26 -0
- package/dist/adapters/qoder.js +12 -0
- package/dist/commands/apply.js +272 -0
- package/dist/commands/init.js +420 -0
- package/dist/core/agent-injector.js +192 -0
- package/dist/core/next-steps.js +91 -0
- package/dist/core/render-meta.js +81 -0
- package/dist/core/repomix-pack.js +54 -0
- package/dist/core/scaffold.js +93 -0
- package/dist/core/state.js +80 -0
- package/dist/index.js +239 -0
- package/dist/types.js +5 -0
- package/dist/utils/baseline-copy.js +591 -0
- package/dist/utils/baseline-defaults.js +106 -0
- package/dist/utils/logger.js +56 -0
- package/dist/utils/validate-args.js +132 -0
- package/dist/utils/version.js +23 -0
- package/dist/wiki/abort.js +30 -0
- package/dist/wiki/config.js +79 -0
- package/dist/wiki/defaults.js +16 -0
- package/dist/wiki/envLoader.js +78 -0
- package/dist/wiki/index.js +29 -0
- package/dist/wiki/openaiCompat.js +219 -0
- package/dist/wiki/repowikiCanonicalSections.js +67 -0
- package/dist/wiki/repowikiCheckpoint.js +106 -0
- package/dist/wiki/repowikiConfig.js +9 -0
- package/dist/wiki/repowikiGit.js +73 -0
- package/dist/wiki/repowikiIndexer.js +824 -0
- package/dist/wiki/repowikiMarkdownPost.js +123 -0
- package/dist/wiki/repowikiMetadataContent.js +64 -0
- package/dist/wiki/repowikiMetadataJson.js +15 -0
- package/dist/wiki/repowikiScanner.js +156 -0
- package/dist/wiki/repowikiStructureNav.js +286 -0
- package/dist/wiki/repowikiStructureNormalize.js +218 -0
- package/dist/wiki/wikiStructureXml.js +316 -0
- package/dist/wiki/wikiTasksWriter.js +127 -0
- package/package.json +57 -0
- package/templates/_shared/apply-skills/harness-apply-skills-main.md +91 -0
- package/templates/_shared/build-rules/harness-build-rule-agent-agnostic.md +35 -0
- package/templates/_shared/build-rules/harness-build-rule-chinese-only.md +49 -0
- package/templates/_shared/build-rules/harness-build-rule-memory-write.md +31 -0
- package/templates/_shared/build-rules/harness-build-rule-orchestrator-flow.md +25 -0
- package/templates/_shared/build-rules/harness-build-rule-skills-tasks-output.md +35 -0
- package/templates/_shared/build-rules/harness-build-rule-specs-schema.md +32 -0
- package/templates/_shared/build-rules/harness-build-rule-user-interaction.md +63 -0
- package/templates/_shared/build-skills/harness-build-skill-knowledge-builder.md +120 -0
- package/templates/_shared/build-skills/harness-build-skill-orchestrator.md +87 -0
- package/templates/_shared/build-skills/harness-build-skill-spec-builder.md +85 -0
- package/templates/_shared/build-skills/harness-build-skill-wiki-writer.md +77 -0
- package/templates/_shared/meta/AGENTS.md.ejs +53 -0
- package/templates/_shared/meta/CHANGELOG.md.ejs +15 -0
- package/templates/_shared/meta/README.md.ejs +51 -0
- package/templates/_shared/meta/VERSION.ejs +1 -0
- package/templates/_shared/meta/harness.yaml.ejs +52 -0
- package/templates/_shared/skeleton/agent-env/memory/categories/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/memory/inbox/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/skills/.gitkeep +1 -0
- package/templates/_shared/skeleton/agent-env/tools/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/code/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/repomix/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/baseline/wiki/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/raw/.gitkeep +1 -0
- package/templates/_shared/skeleton/assets/requirements/.gitkeep +1 -0
- package/templates/_shared/skeleton/commands/install/.gitkeep +1 -0
- package/templates/_shared/skeleton/commands/update/.gitkeep +1 -0
- package/templates/_shared/skeleton/specs/behavior/schema.json +39 -0
- package/templates/_shared/skeleton/specs/interfaces/schema.json +38 -0
- package/templates/_shared/skeleton/specs/signals/schema.json +37 -0
- package/templates/_shared/skeleton/specs/ui/schema.json +44 -0
- package/templates/_shared/skeleton/tasks/templates/.gitkeep +0 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-compose-mandatory.mdc +49 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-coroutines-scope.mdc +52 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-hilt-injection.mdc +47 -0
- package/templates/android-compose/skeleton/agent-env/rules/harness-mvi-layering.mdc +58 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/SKILL.md +260 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/gradle-module-patterns.md +66 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/implementation-checklist.md +45 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-architecture/references/udf-data-flow.md +80 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/SKILL.md +79 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/interact.md +83 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-android-cli/references/journeys.md +97 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/SKILL.md +162 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/canonical-sources.md +116 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/diagnostics.md +182 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/report-template.md +135 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/scoring.md +277 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/references/search-playbook.md +303 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-audit/scripts/compose-reports.init.gradle +58 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-state/SKILL.md +196 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/SKILL.md +192 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/composable-api-guide.md +123 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/performance-recipes.md +97 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-compose-ui/references/state-patterns.md +93 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-kotlin-coroutines/SKILL.md +167 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/SKILL.md +45 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/CONFIGURATION.md +44 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md +83 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REDUNDANT-RULES.md +222 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/REFLECTION-GUIDE.md +139 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md +176 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-r8-analyzer/references/android/training/testing/other-components/ui-automator.md +312 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/SKILL.md +87 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/analysis-of-the-project-and-layout.md +42 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/android/develop/ui/compose/designsystems/migrate-xml-theme-to-compose.md +168 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/android/develop/ui/compose/setup-compose-dependencies-and-compiler.md +183 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/identify-optimal-xml-candidate.md +31 -0
- package/templates/android-compose/skeleton/agent-env/skills/harness-xml-to-compose/references/xml-layout-migration.md +86 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-aidl-thread.md +29 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-lifecycle-awareness.md +32 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-mvc-layering.md +32 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-view-binding.md +33 -0
- package/templates/android-xml/skeleton/agent-env/rules/seed-xml-styling.md +27 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-cmake-explicit-sources.md +31 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-header-guards.md +34 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-include-layering.md +39 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-no-cyclic-deps.md +29 -0
- package/templates/cpp/skeleton/agent-env/rules/seed-raii.md +30 -0
- package/templates/python/skeleton/agent-env/rules/seed-context-managers.md +60 -0
- package/templates/python/skeleton/agent-env/rules/seed-docstrings.md +48 -0
- package/templates/python/skeleton/agent-env/rules/seed-import-order.md +49 -0
- package/templates/python/skeleton/agent-env/rules/seed-pep8-naming.md +45 -0
- package/templates/python/skeleton/agent-env/rules/seed-type-annotations.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-controlled-component.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-effect-cleanup.md +43 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-hook-rules.md +42 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-key-stability.md +39 -0
- package/templates/web-react/skeleton/agent-env/rules/seed-no-props-drilling.md +43 -0
|
@@ -0,0 +1,824 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.REPOWIKI_LANG_ZH = exports.REPOWIKI_LANG_EN = void 0;
|
|
37
|
+
exports.langRunForRepowikiTarget = langRunForRepowikiTarget;
|
|
38
|
+
exports.runRepowikiIndex = runRepowikiIndex;
|
|
39
|
+
exports.runRepowikiOutlineOnly = runRepowikiOutlineOnly;
|
|
40
|
+
const crypto_1 = require("crypto");
|
|
41
|
+
const fs_1 = require("fs");
|
|
42
|
+
const fs = __importStar(require("fs/promises"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
const abort_1 = require("./abort");
|
|
45
|
+
const openaiCompat_1 = require("./openaiCompat");
|
|
46
|
+
const repowikiScanner_1 = require("./repowikiScanner");
|
|
47
|
+
const repowikiCanonicalSections_1 = require("./repowikiCanonicalSections");
|
|
48
|
+
const repowikiCheckpoint_1 = require("./repowikiCheckpoint");
|
|
49
|
+
const repowikiGit_1 = require("./repowikiGit");
|
|
50
|
+
const repowikiConfig_1 = require("./repowikiConfig");
|
|
51
|
+
const repowikiStructureNav_1 = require("./repowikiStructureNav");
|
|
52
|
+
const repowikiStructureNormalize_1 = require("./repowikiStructureNormalize");
|
|
53
|
+
const repowikiMarkdownPost_1 = require("./repowikiMarkdownPost");
|
|
54
|
+
const repowikiMetadataContent_1 = require("./repowikiMetadataContent");
|
|
55
|
+
const repowikiMetadataJson_1 = require("./repowikiMetadataJson");
|
|
56
|
+
const wikiStructureXml_1 = require("./wikiStructureXml");
|
|
57
|
+
/** Canonical labels for outline + page generation (structure step uses `label`). */
|
|
58
|
+
exports.REPOWIKI_LANG_EN = {
|
|
59
|
+
folder: "en",
|
|
60
|
+
label: "English (en)",
|
|
61
|
+
};
|
|
62
|
+
exports.REPOWIKI_LANG_ZH = {
|
|
63
|
+
folder: "zh",
|
|
64
|
+
label: "简体中文(zh-CN)",
|
|
65
|
+
};
|
|
66
|
+
function langRunForRepowikiTarget(target) {
|
|
67
|
+
return target === "en" ? exports.REPOWIKI_LANG_EN : exports.REPOWIKI_LANG_ZH;
|
|
68
|
+
}
|
|
69
|
+
/** When `languages` is omitted, generate Simplified Chinese only. */
|
|
70
|
+
const DEFAULT_LANGS = [exports.REPOWIKI_LANG_ZH];
|
|
71
|
+
/** AbortSignal that never aborts — used as default when caller passes nothing. */
|
|
72
|
+
const NEVER_ABORT = new AbortController().signal;
|
|
73
|
+
async function chatCompletionTextForIndex(signal, params) {
|
|
74
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
75
|
+
const ac = new AbortController();
|
|
76
|
+
const onAbort = () => ac.abort();
|
|
77
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
78
|
+
try {
|
|
79
|
+
const text = await (0, openaiCompat_1.chatCompletionText)({ ...params, signal: ac.signal });
|
|
80
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
81
|
+
return text;
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
if (signal.aborted || ac.signal.aborted || (0, openaiCompat_1.isFetchAbortError)(e)) {
|
|
85
|
+
throw new abort_1.WikiAbortError();
|
|
86
|
+
}
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
signal.removeEventListener("abort", onAbort);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function normFsPosix(p) {
|
|
94
|
+
return path.normalize(p).replace(/\\/g, "/");
|
|
95
|
+
}
|
|
96
|
+
function invertFilesMap(files) {
|
|
97
|
+
const m = new Map();
|
|
98
|
+
if (!files)
|
|
99
|
+
return m;
|
|
100
|
+
for (const [pageId, rel] of Object.entries(files)) {
|
|
101
|
+
m.set(rel.replace(/\\/g, "/"), pageId);
|
|
102
|
+
}
|
|
103
|
+
return m;
|
|
104
|
+
}
|
|
105
|
+
async function pathExists(abs) {
|
|
106
|
+
try {
|
|
107
|
+
await fs.access(abs);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function structurePrompt(repoName, treeText, readmeChunk, structureLang, comprehensive) {
|
|
115
|
+
const zhOutlineExtra = structureLang.folder === "zh"
|
|
116
|
+
? `
|
|
117
|
+
|
|
118
|
+
【简体中文硬性要求 — 纲要步骤】凡是给人看的文字(仓库级 <title>/ <description>、每个 <section> 的 <title>、每个 <page> 的 <title> 与 <description>)必须全部是简体中文(zh-CN)。禁止使用英文句子或段落;仅路径、文件名、代码标识符/API 符号可保留英文。`
|
|
119
|
+
: "";
|
|
120
|
+
const enOutlineExtra = structureLang.folder === "en"
|
|
121
|
+
? `
|
|
122
|
+
|
|
123
|
+
【English hard requirement — outline step】All human-readable text in this XML (repo-level <title>/<description>, each <section>'s <title>, each <page>'s <title> and <description>) MUST be fluent English. Do not write Chinese, Japanese, or other non-English prose for those fields. Only code identifiers, filenames, paths, API symbols, and unavoidable proper nouns may stay non-English where appropriate.`
|
|
124
|
+
: "";
|
|
125
|
+
const sectionBlock = comprehensive
|
|
126
|
+
? `Use **multiple** top-level \`<section>\` blocks under \`<sections>\`: **between 2 and 7** (inclusive). **Never** return only **one** section when the wiki has **two or more** pages — split topics into **distinct** sections (do **not** collapse everything into a single bucket named after the wiki title).
|
|
127
|
+
Use **at most 7** sections total. For very small repos with only **one** page, **one** section is acceptable.
|
|
128
|
+
Each section MUST have a **unique** \`id\` attribute (letters, digits, hyphens only — stable identifier).
|
|
129
|
+
Each section's \`<title>\` MUST be **in the outline language** (${structureLang.label}), **repo-specific**, and suitable as a section heading (this becomes the subdirectory name under each language folder).
|
|
130
|
+
**Filesystem folders** under the language directory are derived from each section \`<title>\` (slugified), **not** from \`id\`.
|
|
131
|
+
Distribute pages across sections: each section's \`<pages>\` lists \`<page_ref>\` ids; use **at most ${wikiStructureXml_1.REPOWIKI_MAX_PAGES}** pages **total** across all sections.
|
|
132
|
+
|
|
133
|
+
Return ONLY valid XML in this shape:
|
|
134
|
+
|
|
135
|
+
<wiki_structure>
|
|
136
|
+
<title>[Overall title]</title>
|
|
137
|
+
<description>[Brief]</description>
|
|
138
|
+
<sections>
|
|
139
|
+
<section id="getting-started">
|
|
140
|
+
<title>[Section title in outline language]</title>
|
|
141
|
+
<pages>
|
|
142
|
+
<page_ref>page-1</page_ref>
|
|
143
|
+
</pages>
|
|
144
|
+
<subsections/>
|
|
145
|
+
</section>
|
|
146
|
+
<!-- more sections with unique ids -->
|
|
147
|
+
</sections>
|
|
148
|
+
<pages>
|
|
149
|
+
<page id="page-1">
|
|
150
|
+
<title>[Title]</title>
|
|
151
|
+
<description>[What this covers]</description>
|
|
152
|
+
<importance>high</importance>
|
|
153
|
+
<relevant_files>
|
|
154
|
+
<file_path>[repo-relative/path]</file_path>
|
|
155
|
+
</relevant_files>
|
|
156
|
+
<related_pages/>
|
|
157
|
+
<parent_section>getting-started</parent_section>
|
|
158
|
+
</page>
|
|
159
|
+
</pages>
|
|
160
|
+
</wiki_structure>`
|
|
161
|
+
: `<wiki_structure>
|
|
162
|
+
<title>[Overall title]</title>
|
|
163
|
+
<description>[Brief]</description>
|
|
164
|
+
<pages>
|
|
165
|
+
<page id="page-1">
|
|
166
|
+
<title>[Title]</title>
|
|
167
|
+
<description>[What]</description>
|
|
168
|
+
<importance>high</importance>
|
|
169
|
+
<relevant_files>
|
|
170
|
+
<file_path>[path]</file_path>
|
|
171
|
+
</relevant_files>
|
|
172
|
+
<related_pages/>
|
|
173
|
+
</page>
|
|
174
|
+
</pages>
|
|
175
|
+
</wiki_structure>`;
|
|
176
|
+
return `Analyze the repository "${repoName}" and propose a concise developer wiki outline.
|
|
177
|
+
|
|
178
|
+
<file_tree>
|
|
179
|
+
${treeText}
|
|
180
|
+
</file_tree>
|
|
181
|
+
|
|
182
|
+
<readme_excerpt>
|
|
183
|
+
${readmeChunk.slice(0, 12_000)}
|
|
184
|
+
</readme_excerpt>
|
|
185
|
+
|
|
186
|
+
Target language for human-readable titles/descriptions ONLY in this outline step: ${structureLang.label}.${zhOutlineExtra}${enOutlineExtra}
|
|
187
|
+
|
|
188
|
+
${sectionBlock}
|
|
189
|
+
|
|
190
|
+
Rules:
|
|
191
|
+
- Produce **at most ${wikiStructureXml_1.REPOWIKI_MAX_PAGES}** wiki pages total with **stable** ids \`page-1\`, \`page-2\`, …, \`page-${wikiStructureXml_1.REPOWIKI_MAX_PAGES}\` (use consecutive ids; fewer pages is OK).
|
|
192
|
+
${comprehensive ? `- Every \`<page>\` MUST include \`<parent_section>\` with the **exact** \`id\` of the section that owns this page (the section whose \`<page_ref>\` lists this page id).\n- Each page id must appear in exactly one section's \`page_ref\` list.` : `- Without a \`<sections>\` block, every page is stored under **one** folder derived from the wiki \`<title>\`; omit \`<parent_section>\` or ignore it for paths.`}
|
|
193
|
+
${comprehensive ? `- Each section \`<title>\` becomes a subdirectory name (slugified); keep titles concise and filesystem-friendly where possible.` : `- The wiki \`<title>\` alone determines that single subdirectory name (slugified).`}
|
|
194
|
+
- Ensure \`relevant_files\` cite real paths from file_tree where possible (skip generated/vendor dirs).
|
|
195
|
+
${comprehensive ? `- CRITICAL: Every tag must be closed. After \`<sections>\`, you MUST include one final \`<pages>...</pages>\` block containing every full \`<page id="...">...</page>\` with title, description, parent_section, and relevant_files. Section-level \`<pages>\` may only list \`<page_ref>\` — never omit the outer \`<page>\` definitions.` : `- CRITICAL: Every tag must be closed. Include a \`<pages>\` block listing every full \`<page id="...">...</page>\` with title, description, and relevant_files.`}
|
|
196
|
+
- Return ONLY XML. No markdown fences. No HTML. Start with <wiki_structure> end </wiki_structure>.`;
|
|
197
|
+
}
|
|
198
|
+
function pageMarkdownPrompt(langFolder, langDescriptor, pageTopic, ctx) {
|
|
199
|
+
const zhExtra = langFolder === "zh"
|
|
200
|
+
? `
|
|
201
|
+
|
|
202
|
+
【简体中文硬性要求 — 正文】全部正文、各级标题(# / ## / ###)、表格单元格、Mermaid 与其它图中的说明文字一律使用简体中文(zh-CN)。禁止英文句子,除非直接引用代码、路径、标识符或 API 名称。`
|
|
203
|
+
: "";
|
|
204
|
+
const enExtra = langFolder === "en"
|
|
205
|
+
? `
|
|
206
|
+
|
|
207
|
+
【English hard requirement — body】All prose, headings (# / ## / ###), table cell text, and explanatory labels inside Mermaid/diagrams MUST be English. Do not write Chinese prose (or mixed-language explanations) unless you are quoting literal content from repo files/strings. Preserve non-English only in fenced code excerpts, paths, identifiers, and API symbols as they appear in the repo.`
|
|
208
|
+
: "";
|
|
209
|
+
const h1Rule = langFolder === "zh"
|
|
210
|
+
? `2. Start with '# [title]' (the H1 must match the topic; for a zh-CN wiki, use Simplified Chinese for that title line).`
|
|
211
|
+
: langFolder === "en"
|
|
212
|
+
? `2. Start with '# [title]' (the H1 must match the topic in English prose).`
|
|
213
|
+
: `2. Start with '# [title]' (the H1 must match the topic).`;
|
|
214
|
+
return `You write accurate technical Markdown documentation for developers.
|
|
215
|
+
|
|
216
|
+
Output language: ${langDescriptor}.${zhExtra}${enExtra}
|
|
217
|
+
|
|
218
|
+
Repository context bundled as tagged files:
|
|
219
|
+
|
|
220
|
+
${ctx}
|
|
221
|
+
|
|
222
|
+
Write ONE wiki page for topic: "${pageTopic}".
|
|
223
|
+
|
|
224
|
+
Strict rules:
|
|
225
|
+
1. Ground claims only in supplied files.
|
|
226
|
+
${h1Rule}
|
|
227
|
+
3. After the introduction (first paragraph or short opener) and **before** the first \`##\`, add a concise **table of contents**: a bullet or numbered list of links to the \`##\` sections that follow (use markdown links with anchors, e.g. \`[Section](#section-slug)\`, matching your real headings). **Omit this TOC** if the page is very short or only has a single \`##\` section (avoid a redundant mini-TOC).
|
|
228
|
+
4. Use '##','###', tables, fenced code from files when helpful.
|
|
229
|
+
5. Prefer Mermaid with \`\`\`mermaid diagrams using TOP-DOWN flow (\`graph TD\`, not LR).
|
|
230
|
+
6. Where useful cite repository files on one line: \`Sources: path/to/file:line-range\` (comma-separated if multiple). Paths are repo-relative with \`/\` separators.
|
|
231
|
+
|
|
232
|
+
If bundled files lack coverage, acknowledge gaps briefly — do not invent APIs.`;
|
|
233
|
+
}
|
|
234
|
+
function pageGenerationSystemPrompt(langFolder) {
|
|
235
|
+
if (langFolder === "zh") {
|
|
236
|
+
return "你是技术文档撰写者,只根据提供的 <file path=…> 摘录作答。Wiki 页的各级标题、正文、表格文字、图中的文字说明必须全部是简体中文(zh-CN)。除代码摘录、仓库路径与标识符/API 英文名外禁止使用英文 prose。";
|
|
237
|
+
}
|
|
238
|
+
if (langFolder === "en") {
|
|
239
|
+
return "You are a technical writer. Answer ONLY from bundled <file path=…> excerpts. The entire Markdown page MUST be English: headings, body, table text, diagram labels/explanations. Use non-English prose only inside literal code fences or when verbatim-quoting user-facing strings from the repository; identifiers, paths, and API symbols follow the codebase.";
|
|
240
|
+
}
|
|
241
|
+
return "You write factual Markdown grounded strictly in bundled <file path=…> excerpts.";
|
|
242
|
+
}
|
|
243
|
+
function normalizeLeadingH1(md, canonicalTitle) {
|
|
244
|
+
const t = canonicalTitle.trim();
|
|
245
|
+
if (!t)
|
|
246
|
+
return md;
|
|
247
|
+
const text = md.replace(/^\uFEFF/, "");
|
|
248
|
+
const lines = text.split(/\r?\n/);
|
|
249
|
+
let i = 0;
|
|
250
|
+
while (i < lines.length && /^\s*$/.test(lines[i]))
|
|
251
|
+
i++;
|
|
252
|
+
if (i < lines.length && /^#\s/.test(lines[i])) {
|
|
253
|
+
lines[i] = `# ${t}`;
|
|
254
|
+
return lines.join("\n");
|
|
255
|
+
}
|
|
256
|
+
return md;
|
|
257
|
+
}
|
|
258
|
+
async function mkdirp(p) {
|
|
259
|
+
await fs.mkdir(p, { recursive: true });
|
|
260
|
+
}
|
|
261
|
+
function bucketAndStemFromRel(relPosix) {
|
|
262
|
+
const n = relPosix.replace(/\\/g, "/");
|
|
263
|
+
const slash = n.lastIndexOf("/");
|
|
264
|
+
const file = slash === -1 ? n : n.slice(slash + 1);
|
|
265
|
+
const stem = file.replace(/\.md$/i, "");
|
|
266
|
+
const bucket = slash === -1 ? "" : n.slice(0, slash);
|
|
267
|
+
return { bucket, stem };
|
|
268
|
+
}
|
|
269
|
+
async function mergePathOwners(wikiBase, checkpoint) {
|
|
270
|
+
const meta = await (0, repowikiConfig_1.loadRepowikiMetadata)(wikiBase);
|
|
271
|
+
const m = new Map();
|
|
272
|
+
for (const [k, v] of invertFilesMap(meta?.files).entries()) {
|
|
273
|
+
m.set(k, v);
|
|
274
|
+
}
|
|
275
|
+
if (checkpoint?.fileByPageId) {
|
|
276
|
+
for (const [pid, rel] of Object.entries(checkpoint.fileByPageId)) {
|
|
277
|
+
const n = rel.replace(/\\/g, "/");
|
|
278
|
+
m.set(n, pid);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return m;
|
|
282
|
+
}
|
|
283
|
+
function pageFullyOnDisk(wikiBase, langFolders, relPosix) {
|
|
284
|
+
return langFolders.map(async (lg) => pathExists(path.join(wikiBase, lg, ...relPosix.split("/").filter(Boolean))));
|
|
285
|
+
}
|
|
286
|
+
async function shouldSkipCompletedPage(wikiBase, langFolders, checkpoint, pageId) {
|
|
287
|
+
if (!checkpoint?.completed.includes(pageId))
|
|
288
|
+
return false;
|
|
289
|
+
const rel = checkpoint.fileByPageId[pageId]?.replace(/\\/g, "/");
|
|
290
|
+
if (!rel)
|
|
291
|
+
return false;
|
|
292
|
+
const checks = pageFullyOnDisk(wikiBase, langFolders, rel);
|
|
293
|
+
const results = await Promise.all(checks);
|
|
294
|
+
return results.every(Boolean);
|
|
295
|
+
}
|
|
296
|
+
async function runRepowikiIndex(opts) {
|
|
297
|
+
const log = opts.onLog ?? (() => { });
|
|
298
|
+
const report = opts.onProgress;
|
|
299
|
+
const signal = opts.signal ?? NEVER_ABORT;
|
|
300
|
+
const wikiBase = path.join(opts.wikiOutputRootFs, opts.wikiRelPath.replace(/^[/\\]+/, ""));
|
|
301
|
+
const langs = opts.languages?.length ? opts.languages : DEFAULT_LANGS;
|
|
302
|
+
const resumeEnabled = opts.resumeEnabled !== false;
|
|
303
|
+
const forceFull = Boolean(opts.forceFull);
|
|
304
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
305
|
+
const head = await (0, repowikiGit_1.gitHeadAt)(opts.repoRootFs);
|
|
306
|
+
const primaryLang = langs[0].folder;
|
|
307
|
+
await mkdirp(wikiBase);
|
|
308
|
+
for (const lg of langs) {
|
|
309
|
+
await mkdirp(path.join(wikiBase, lg.folder));
|
|
310
|
+
}
|
|
311
|
+
const repoName = path.basename(opts.repoRootFs.replace(/[/\\]+$/, ""));
|
|
312
|
+
const fileList = await (0, repowikiScanner_1.collectRepoFileList)(opts.repoRootFs);
|
|
313
|
+
const treeText = (0, repowikiScanner_1.summarizeTreeLines)(fileList, 260);
|
|
314
|
+
const readme = await (0, repowikiScanner_1.tryReadFirstReadme)(opts.repoRootFs);
|
|
315
|
+
const structureLang = langs[0] ?? exports.REPOWIKI_LANG_ZH;
|
|
316
|
+
const langFolders = langs.map((l) => l.folder);
|
|
317
|
+
let existingCp = forceFull ? null : await (0, repowikiCheckpoint_1.readRepowikiCheckpoint)(wikiBase);
|
|
318
|
+
if (forceFull && existingCp) {
|
|
319
|
+
await (0, repowikiCheckpoint_1.clearRepowikiCheckpoint)(wikiBase);
|
|
320
|
+
existingCp = null;
|
|
321
|
+
}
|
|
322
|
+
if (existingCp &&
|
|
323
|
+
!(0, repowikiCheckpoint_1.checkpointMatchesRun)(existingCp, opts.repoRootFs, wikiBase, primaryLang)) {
|
|
324
|
+
log("[repowiki] Clearing stale checkpoint (paths or language differ).");
|
|
325
|
+
await (0, repowikiCheckpoint_1.clearRepowikiCheckpoint)(wikiBase);
|
|
326
|
+
existingCp = null;
|
|
327
|
+
}
|
|
328
|
+
let runId = (0, crypto_1.randomUUID)();
|
|
329
|
+
let xml = "";
|
|
330
|
+
let pages = [];
|
|
331
|
+
let resumed = false;
|
|
332
|
+
const tryResume = resumeEnabled &&
|
|
333
|
+
!forceFull &&
|
|
334
|
+
existingCp &&
|
|
335
|
+
(0, repowikiCheckpoint_1.checkpointMatchesRun)(existingCp, opts.repoRootFs, wikiBase, primaryLang);
|
|
336
|
+
if (tryResume && existingCp) {
|
|
337
|
+
const structurePath = path.join(wikiBase, "structure.xml");
|
|
338
|
+
const rawStructure = await fs.readFile(structurePath, "utf8").catch(() => "");
|
|
339
|
+
if (rawStructure.trim()) {
|
|
340
|
+
const parsedXml = (0, wikiStructureXml_1.extractWikiStructureXml)(rawStructure) || rawStructure;
|
|
341
|
+
const parsedPages = (0, wikiStructureXml_1.parseWikiPages)(parsedXml);
|
|
342
|
+
if (parsedPages.length > 0) {
|
|
343
|
+
xml = parsedXml;
|
|
344
|
+
pages = parsedPages.slice(0, wikiStructureXml_1.REPOWIKI_MAX_PAGES);
|
|
345
|
+
const comprehensiveResume = opts.comprehensiveSections ?? false;
|
|
346
|
+
const normalizedResume = (0, repowikiStructureNormalize_1.normalizeComprehensiveWikiStructure)({
|
|
347
|
+
comprehensive: comprehensiveResume,
|
|
348
|
+
langFolder: structureLang.folder,
|
|
349
|
+
xml,
|
|
350
|
+
pages,
|
|
351
|
+
});
|
|
352
|
+
if (normalizedResume.xml !== xml) {
|
|
353
|
+
await fs.writeFile(structurePath, normalizedResume.xml.replace(/^\uFEFF/, ""), "utf8");
|
|
354
|
+
log("[repowiki] Updated structure.xml (normalized sections / parent_section).");
|
|
355
|
+
}
|
|
356
|
+
xml = normalizedResume.xml;
|
|
357
|
+
pages = normalizedResume.pages;
|
|
358
|
+
runId = existingCp.runId;
|
|
359
|
+
resumed = true;
|
|
360
|
+
log(`[repowiki] Resuming run ${runId} (${existingCp.completed.length} page(s) already completed).`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (!resumed) {
|
|
365
|
+
report?.({ done: 0, total: 0, detail: "Requesting wiki outline from model…" });
|
|
366
|
+
log("[repowiki] Requesting wiki structure…");
|
|
367
|
+
const structureSystem = structureLang.folder === "zh"
|
|
368
|
+
? "你只输出格式正确的 wiki_structure XML(符合用户给定模板);不要使用 markdown 代码围栏。纲要中凡是给人阅读的标题与描述必须使用简体中文(zh-CN)。"
|
|
369
|
+
: structureLang.folder === "en"
|
|
370
|
+
? "You output ONLY well-formed wiki_structure XML matching the user's template. Never use markdown fences. Every repo title, wiki title/description, section titles, page titles, and page descriptions meant for humans must be fluent English (no Chinese prose in those strings unless it is unavoidable quoted content from the repository)."
|
|
371
|
+
: "You output ONLY well-formed wiki_structure XML matching the user's template. Never use markdown fences.";
|
|
372
|
+
const comprehensive = opts.comprehensiveSections ?? false;
|
|
373
|
+
let structureRaw = await chatCompletionTextForIndex(signal, {
|
|
374
|
+
baseUrl: opts.baseUrl,
|
|
375
|
+
apiKey: opts.apiKey,
|
|
376
|
+
model: opts.model,
|
|
377
|
+
temperature: 0.2,
|
|
378
|
+
messages: [
|
|
379
|
+
{ role: "system", content: structureSystem },
|
|
380
|
+
{
|
|
381
|
+
role: "user",
|
|
382
|
+
content: structurePrompt(repoName, treeText, readme, structureLang, comprehensive),
|
|
383
|
+
},
|
|
384
|
+
],
|
|
385
|
+
});
|
|
386
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
387
|
+
xml = (0, wikiStructureXml_1.extractWikiStructureXml)(structureRaw) ?? "";
|
|
388
|
+
if (!xml) {
|
|
389
|
+
throw new Error(`Model returned no <wiki_structure> XML. Preview: ${structureRaw.slice(0, 400)}`);
|
|
390
|
+
}
|
|
391
|
+
pages = (0, wikiStructureXml_1.parseWikiPages)(xml);
|
|
392
|
+
if (!pages.length) {
|
|
393
|
+
throw new Error(`Could not parse wiki pages from XML. Preview: ${structureRaw.slice(0, 500)}`);
|
|
394
|
+
}
|
|
395
|
+
if (pages.length > wikiStructureXml_1.REPOWIKI_MAX_PAGES) {
|
|
396
|
+
log(`[repowiki] Truncating outline from ${pages.length} to ${wikiStructureXml_1.REPOWIKI_MAX_PAGES} pages.`);
|
|
397
|
+
pages = pages.slice(0, wikiStructureXml_1.REPOWIKI_MAX_PAGES);
|
|
398
|
+
}
|
|
399
|
+
if (comprehensive && (0, repowikiStructureNormalize_1.comprehensiveOutlineNeedsStructureRetry)(comprehensive, xml, pages)) {
|
|
400
|
+
log("[repowiki] Outline has too few <section> blocks — requesting revised outline…");
|
|
401
|
+
structureRaw = await chatCompletionTextForIndex(signal, {
|
|
402
|
+
baseUrl: opts.baseUrl,
|
|
403
|
+
apiKey: opts.apiKey,
|
|
404
|
+
model: opts.model,
|
|
405
|
+
temperature: 0.2,
|
|
406
|
+
messages: [
|
|
407
|
+
{ role: "system", content: structureSystem },
|
|
408
|
+
{
|
|
409
|
+
role: "user",
|
|
410
|
+
content: structurePrompt(repoName, treeText, readme, structureLang, comprehensive) +
|
|
411
|
+
(0, repowikiStructureNormalize_1.structureRepairUserAppendix)(structureRaw, structureLang.folder),
|
|
412
|
+
},
|
|
413
|
+
],
|
|
414
|
+
});
|
|
415
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
416
|
+
const xmlRetry = (0, wikiStructureXml_1.extractWikiStructureXml)(structureRaw) ?? "";
|
|
417
|
+
if (!xmlRetry) {
|
|
418
|
+
throw new Error(`Revision request returned no <wiki_structure> XML. Preview: ${structureRaw.slice(0, 400)}`);
|
|
419
|
+
}
|
|
420
|
+
xml = xmlRetry;
|
|
421
|
+
pages = (0, wikiStructureXml_1.parseWikiPages)(xml);
|
|
422
|
+
if (!pages.length) {
|
|
423
|
+
throw new Error(`Could not parse wiki pages from revised XML. Preview: ${structureRaw.slice(0, 500)}`);
|
|
424
|
+
}
|
|
425
|
+
if (pages.length > wikiStructureXml_1.REPOWIKI_MAX_PAGES) {
|
|
426
|
+
log(`[repowiki] Truncating revised outline from ${pages.length} to ${wikiStructureXml_1.REPOWIKI_MAX_PAGES} pages.`);
|
|
427
|
+
pages = pages.slice(0, wikiStructureXml_1.REPOWIKI_MAX_PAGES);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const normalized = (0, repowikiStructureNormalize_1.normalizeComprehensiveWikiStructure)({
|
|
431
|
+
comprehensive,
|
|
432
|
+
langFolder: structureLang.folder,
|
|
433
|
+
xml,
|
|
434
|
+
pages,
|
|
435
|
+
});
|
|
436
|
+
xml = normalized.xml;
|
|
437
|
+
pages = normalized.pages;
|
|
438
|
+
await fs.writeFile(path.join(wikiBase, "structure.xml"), xml.replace(/^\uFEFF/, ""), "utf8");
|
|
439
|
+
log("[repowiki] Wrote structure.xml");
|
|
440
|
+
const freshCp = {
|
|
441
|
+
version: 1,
|
|
442
|
+
runId,
|
|
443
|
+
status: "running",
|
|
444
|
+
sourceRootFs: normFsPosix(opts.repoRootFs),
|
|
445
|
+
wikiBaseFs: normFsPosix(wikiBase),
|
|
446
|
+
indexedCommit: head ?? null,
|
|
447
|
+
targetLang: primaryLang,
|
|
448
|
+
languages: langFolders,
|
|
449
|
+
completed: [],
|
|
450
|
+
fileByPageId: {},
|
|
451
|
+
};
|
|
452
|
+
await (0, repowikiCheckpoint_1.writeRepowikiCheckpoint)(wikiBase, freshCp);
|
|
453
|
+
}
|
|
454
|
+
let checkpoint = (await (0, repowikiCheckpoint_1.readRepowikiCheckpoint)(wikiBase)) ?? {
|
|
455
|
+
version: 1,
|
|
456
|
+
runId,
|
|
457
|
+
status: "running",
|
|
458
|
+
sourceRootFs: normFsPosix(opts.repoRootFs),
|
|
459
|
+
wikiBaseFs: normFsPosix(wikiBase),
|
|
460
|
+
indexedCommit: head ?? null,
|
|
461
|
+
targetLang: primaryLang,
|
|
462
|
+
languages: langFolders,
|
|
463
|
+
completed: [],
|
|
464
|
+
fileByPageId: {},
|
|
465
|
+
};
|
|
466
|
+
const sectionGroups = (0, repowikiStructureNav_1.parseSectionGroups)(xml);
|
|
467
|
+
const outlineFolderSegment = (0, repowikiCanonicalSections_1.defaultWikiFolderSegment)((0, repowikiStructureNav_1.extractWikiOutlineTitle)(xml), structureLang.folder);
|
|
468
|
+
const folderBySectionId = (0, repowikiCanonicalSections_1.buildUniqueSectionFolderSegments)(sectionGroups);
|
|
469
|
+
const folderByPageId = new Map();
|
|
470
|
+
for (const p of pages) {
|
|
471
|
+
folderByPageId.set(p.id, (0, repowikiCanonicalSections_1.sectionFolderSegmentForPage)(p, sectionGroups, outlineFolderSegment, folderBySectionId));
|
|
472
|
+
}
|
|
473
|
+
const sectionMap = (0, repowikiCanonicalSections_1.formatSectionFolderMapSummary)(folderByPageId, pages.map((p) => p.id));
|
|
474
|
+
log(`[repowiki] Section map: ${sectionMap}`);
|
|
475
|
+
const pathOwners = await mergePathOwners(wikiBase, checkpoint);
|
|
476
|
+
const primaryLangDir = path.join(wikiBase, primaryLang);
|
|
477
|
+
const physicalExists = (rel) => (0, fs_1.existsSync)(path.join(primaryLangDir, ...rel.split("/").filter(Boolean)));
|
|
478
|
+
const filesRelByPageId = new Map();
|
|
479
|
+
for (const p of pages) {
|
|
480
|
+
if (await shouldSkipCompletedPage(wikiBase, langFolders, checkpoint, p.id)) {
|
|
481
|
+
const relCp = checkpoint.fileByPageId[p.id]?.replace(/\\/g, "/");
|
|
482
|
+
if (relCp) {
|
|
483
|
+
filesRelByPageId.set(p.id, relCp);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const pagesNeedingPaths = pages.filter((p) => !filesRelByPageId.has(p.id));
|
|
488
|
+
const stemAlloc = (0, wikiStructureXml_1.allocateWikiFileStemsWithTargetPaths)(pagesNeedingPaths, folderByPageId, pathOwners, physicalExists);
|
|
489
|
+
for (const p of pagesNeedingPaths) {
|
|
490
|
+
const bucket = folderByPageId.get(p.id);
|
|
491
|
+
const stem = stemAlloc.get(p.id);
|
|
492
|
+
filesRelByPageId.set(p.id, `${bucket}/${stem}.md`.replace(/\\/g, "/"));
|
|
493
|
+
}
|
|
494
|
+
for (const p of pages) {
|
|
495
|
+
if (!filesRelByPageId.has(p.id)) {
|
|
496
|
+
throw new Error(`[repowiki] Missing path mapping for page ${p.id}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const pagesToGenerate = [];
|
|
500
|
+
for (const p of pages) {
|
|
501
|
+
if (await shouldSkipCompletedPage(wikiBase, langFolders, checkpoint, p.id)) {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
pagesToGenerate.push(p);
|
|
505
|
+
}
|
|
506
|
+
const totalSteps = 2 + pages.length * langs.length;
|
|
507
|
+
let done = 1;
|
|
508
|
+
report?.({
|
|
509
|
+
done,
|
|
510
|
+
total: totalSteps,
|
|
511
|
+
detail: resumed
|
|
512
|
+
? `Resumed — ${pagesToGenerate.length} page(s) remaining`
|
|
513
|
+
: `Outline ready (${pages.length} page × ${langs.length} languages)`,
|
|
514
|
+
});
|
|
515
|
+
async function persistCheckpoint(completedList) {
|
|
516
|
+
const next = {
|
|
517
|
+
...checkpoint,
|
|
518
|
+
runId,
|
|
519
|
+
status: "running",
|
|
520
|
+
completed: completedList,
|
|
521
|
+
fileByPageId: { ...checkpoint.fileByPageId },
|
|
522
|
+
languages: langFolders,
|
|
523
|
+
indexedCommit: head ?? checkpoint.indexedCommit ?? null,
|
|
524
|
+
};
|
|
525
|
+
for (const p of pages) {
|
|
526
|
+
const rel = filesRelByPageId.get(p.id);
|
|
527
|
+
if (rel && completedList.includes(p.id)) {
|
|
528
|
+
next.fileByPageId[p.id] = rel;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
await (0, repowikiCheckpoint_1.writeRepowikiCheckpoint)(wikiBase, next);
|
|
532
|
+
Object.assign(checkpoint, next);
|
|
533
|
+
}
|
|
534
|
+
let completedSoFar = [...checkpoint.completed].filter((id) => pages.some((p) => p.id === id));
|
|
535
|
+
for (let i = 0; i < pages.length; i++) {
|
|
536
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
537
|
+
const page = pages[i];
|
|
538
|
+
if (await shouldSkipCompletedPage(wikiBase, langFolders, checkpoint, page.id)) {
|
|
539
|
+
log(`[repowiki] Skip ${page.id} (already completed).`);
|
|
540
|
+
done += langs.length;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
const relForPage = filesRelByPageId.get(page.id);
|
|
544
|
+
const { bucket, stem } = bucketAndStemFromRel(relForPage);
|
|
545
|
+
if (!bucket) {
|
|
546
|
+
throw new Error(`[repowiki] Invalid path (missing section folder): ${relForPage}`);
|
|
547
|
+
}
|
|
548
|
+
const existing = await resolveExistingPaths(opts.repoRootFs, fileList, page.relevantFiles).catch(() => page.relevantFiles);
|
|
549
|
+
page.relevantFiles = existing.slice(0, 12);
|
|
550
|
+
let ctxBundle = await (0, repowikiScanner_1.bundleSourceContext)(opts.repoRootFs, page.relevantFiles, 24_000);
|
|
551
|
+
if (!ctxBundle.trim()) {
|
|
552
|
+
const fallbackPick = sampleFallbackPaths(fileList, 12);
|
|
553
|
+
const fb = await (0, repowikiScanner_1.bundleSourceContext)(opts.repoRootFs, fallbackPick, 20_000);
|
|
554
|
+
log(`[repowiki] Page ${page.id}: thin context — using sampled repo files.`);
|
|
555
|
+
ctxBundle = fb;
|
|
556
|
+
}
|
|
557
|
+
if (!ctxBundle.trim()) {
|
|
558
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
559
|
+
for (const lg of langs) {
|
|
560
|
+
await writeMarkdown(wikiBase, lg.folder, bucket, stem, lg.folder === "zh"
|
|
561
|
+
? `# ${page.title}\n\n_未找到可读的仓库文件用于摘要。_\n`
|
|
562
|
+
: `# ${page.title}\n\n_No readable repository files found to summarize._\n`, opts.repoRootFs);
|
|
563
|
+
done++;
|
|
564
|
+
report?.({
|
|
565
|
+
done,
|
|
566
|
+
total: totalSteps,
|
|
567
|
+
detail: `Stub ${lg.folder}/${bucket}/${stem}.md (${i + 1}/${pages.length})`,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if (!completedSoFar.includes(page.id)) {
|
|
571
|
+
completedSoFar.push(page.id);
|
|
572
|
+
await persistCheckpoint(completedSoFar);
|
|
573
|
+
}
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
for (const lg of langs) {
|
|
577
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
578
|
+
report?.({
|
|
579
|
+
done,
|
|
580
|
+
total: totalSteps,
|
|
581
|
+
detail: `Waiting for model: ${lg.folder}/${bucket}/${stem}.md (${i + 1}/${pages.length})`,
|
|
582
|
+
});
|
|
583
|
+
const md = await chatCompletionTextForIndex(signal, {
|
|
584
|
+
baseUrl: opts.baseUrl,
|
|
585
|
+
apiKey: opts.apiKey,
|
|
586
|
+
model: opts.model,
|
|
587
|
+
temperature: 0.25,
|
|
588
|
+
messages: [
|
|
589
|
+
{ role: "system", content: pageGenerationSystemPrompt(lg.folder) },
|
|
590
|
+
{
|
|
591
|
+
role: "user",
|
|
592
|
+
content: pageMarkdownPrompt(lg.folder, lg.label, `${page.title}\n\n${page.description}`, ctxBundle),
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
});
|
|
596
|
+
await writeMarkdown(wikiBase, lg.folder, bucket, stem, normalizeLeadingH1(md, page.title), opts.repoRootFs);
|
|
597
|
+
log(`[repowiki] ${lg.folder}/${bucket}/${stem}.md (${i + 1}/${pages.length})`);
|
|
598
|
+
done++;
|
|
599
|
+
report?.({
|
|
600
|
+
done,
|
|
601
|
+
total: totalSteps,
|
|
602
|
+
detail: `Saved ${lg.folder}/${bucket}/${stem}.md`,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
if (!completedSoFar.includes(page.id)) {
|
|
606
|
+
completedSoFar.push(page.id);
|
|
607
|
+
}
|
|
608
|
+
await persistCheckpoint(completedSoFar);
|
|
609
|
+
}
|
|
610
|
+
done++;
|
|
611
|
+
report?.({
|
|
612
|
+
done,
|
|
613
|
+
total: totalSteps,
|
|
614
|
+
detail: "Writing metadata.json…",
|
|
615
|
+
});
|
|
616
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
617
|
+
const files = {};
|
|
618
|
+
for (const p of pages) {
|
|
619
|
+
const rel = filesRelByPageId.get(p.id);
|
|
620
|
+
files[p.id] = rel;
|
|
621
|
+
}
|
|
622
|
+
const embedBase64 = opts.repowikiMetadataEmbedMarkdownBase64 !== false;
|
|
623
|
+
let contentBase64ByLang;
|
|
624
|
+
if (embedBase64) {
|
|
625
|
+
contentBase64ByLang = await (0, repowikiMetadataContent_1.collectContentBase64ByLang)({
|
|
626
|
+
wikiBase,
|
|
627
|
+
langFolders,
|
|
628
|
+
pageIds: pages.map((p) => p.id),
|
|
629
|
+
filesRelByPageId,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
const outlineTitle = (0, repowikiStructureNav_1.extractWikiOutlineTitle)(xml).trim();
|
|
633
|
+
const meta = {
|
|
634
|
+
schema: repowikiMetadataJson_1.REPOWIKI_METADATA_SCHEMA_ID,
|
|
635
|
+
version: repowikiMetadataJson_1.REPOWIKI_METADATA_SCHEMA_VERSION,
|
|
636
|
+
metadataSchemaVersion: repowikiMetadataJson_1.REPOWIKI_METADATA_SCHEMA_VERSION,
|
|
637
|
+
indexedAt: new Date().toISOString(),
|
|
638
|
+
indexedCommit: head ?? null,
|
|
639
|
+
wikiTitle: outlineTitle ? outlineTitle : null,
|
|
640
|
+
model: opts.model,
|
|
641
|
+
langFolders: [...langFolders],
|
|
642
|
+
pages: pages.map((p) => ({ id: p.id, title: p.title })),
|
|
643
|
+
pageIds: pages.map((p) => p.id),
|
|
644
|
+
sourceRootFs: opts.repoRootFs.replace(/\\/g, "/"),
|
|
645
|
+
wikiBaseFs: wikiBase.replace(/\\/g, "/"),
|
|
646
|
+
files,
|
|
647
|
+
...(contentBase64ByLang ? { contentBase64ByLang } : {}),
|
|
648
|
+
};
|
|
649
|
+
const metaJson = JSON.stringify(meta, null, 2);
|
|
650
|
+
await fs.writeFile(path.join(wikiBase, "metadata.json"), metaJson, "utf8");
|
|
651
|
+
log("[repowiki] Wrote metadata.json");
|
|
652
|
+
await (0, repowikiCheckpoint_1.clearRepowikiCheckpoint)(wikiBase);
|
|
653
|
+
log("[repowiki] Index complete (checkpoint cleared).");
|
|
654
|
+
}
|
|
655
|
+
async function writeMarkdown(wikiBase, langFolder, sectionFolderSlug, stem, md, repoRootFs) {
|
|
656
|
+
const dir = path.join(wikiBase, langFolder, sectionFolderSlug);
|
|
657
|
+
await mkdirp(dir);
|
|
658
|
+
const target = path.join(dir, `${stem}.md`);
|
|
659
|
+
const linked = (0, repowikiMarkdownPost_1.linkifySourcesLinesInMarkdown)(md, target, repoRootFs);
|
|
660
|
+
await fs.writeFile(target, linked.replace(/^\uFEFF/, ""), "utf8");
|
|
661
|
+
}
|
|
662
|
+
async function resolveExistingPaths(_repoRootFs, allRel, requested) {
|
|
663
|
+
const set = new Set(allRel.map((p) => p.replace(/\\/g, "/")));
|
|
664
|
+
const out = [];
|
|
665
|
+
for (let raw of requested) {
|
|
666
|
+
raw = raw.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
667
|
+
if (!raw)
|
|
668
|
+
continue;
|
|
669
|
+
if (set.has(raw)) {
|
|
670
|
+
out.push(raw);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
const baseName = raw.split("/").pop();
|
|
674
|
+
const hit = [...set].find((p) => p === raw || p.endsWith("/" + baseName) || p.split("/").pop() === baseName) ?? "";
|
|
675
|
+
if (hit)
|
|
676
|
+
out.push(hit);
|
|
677
|
+
}
|
|
678
|
+
return [...new Set(out)].slice(0, 14);
|
|
679
|
+
}
|
|
680
|
+
function sampleFallbackPaths(allRel, n) {
|
|
681
|
+
const priority = /\.(tsx?|jsx?|py|go|rs|java|kt|kts|gradle|swift|rb|yaml|yml|json|md)$/i;
|
|
682
|
+
const scored = allRel.filter((p) => priority.test(p));
|
|
683
|
+
const pick = scored.length >= n ? scored.slice(0, n) : [...scored, ...allRel].slice(0, n);
|
|
684
|
+
return [...new Set(pick)];
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Run only the outline stage of repowiki: call the LLM once to produce
|
|
688
|
+
* `structure.xml`, compute section folders, and resolve target file paths
|
|
689
|
+
* for every page. Does NOT generate per-page Markdown — intended to power
|
|
690
|
+
* the `--wiki-tasks-only` CLI mode so downstream agents can fill pages.
|
|
691
|
+
*/
|
|
692
|
+
async function runRepowikiOutlineOnly(opts) {
|
|
693
|
+
const log = opts.onLog ?? (() => { });
|
|
694
|
+
const report = opts.onProgress;
|
|
695
|
+
const signal = opts.signal ?? NEVER_ABORT;
|
|
696
|
+
const wikiBase = path.join(opts.wikiOutputRootFs, opts.wikiRelPath.replace(/^[/\\]+/, ""));
|
|
697
|
+
const langs = opts.languages?.length ? opts.languages : DEFAULT_LANGS;
|
|
698
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
699
|
+
const primaryLang = langs[0].folder;
|
|
700
|
+
await mkdirp(wikiBase);
|
|
701
|
+
for (const lg of langs) {
|
|
702
|
+
await mkdirp(path.join(wikiBase, lg.folder));
|
|
703
|
+
}
|
|
704
|
+
const repoName = path.basename(opts.repoRootFs.replace(/[/\\]+$/, ""));
|
|
705
|
+
const fileList = await (0, repowikiScanner_1.collectRepoFileList)(opts.repoRootFs);
|
|
706
|
+
const treeText = (0, repowikiScanner_1.summarizeTreeLines)(fileList, 260);
|
|
707
|
+
const readme = await (0, repowikiScanner_1.tryReadFirstReadme)(opts.repoRootFs);
|
|
708
|
+
const structureLang = langs[0] ?? exports.REPOWIKI_LANG_ZH;
|
|
709
|
+
report?.({ done: 0, total: 0, detail: "Requesting wiki outline from model…" });
|
|
710
|
+
log("[repowiki] Requesting wiki structure…");
|
|
711
|
+
const structureSystem = structureLang.folder === "zh"
|
|
712
|
+
? "你只输出格式正确的 wiki_structure XML(符合用户给定模板);不要使用 markdown 代码围栏。纲要中凡是给人阅读的标题与描述必须使用简体中文(zh-CN)。"
|
|
713
|
+
: structureLang.folder === "en"
|
|
714
|
+
? "You output ONLY well-formed wiki_structure XML matching the user's template. Never use markdown fences. Every repo title, wiki title/description, section titles, page titles, and page descriptions meant for humans must be fluent English (no Chinese prose in those strings unless it is unavoidable quoted content from the repository)."
|
|
715
|
+
: "You output ONLY well-formed wiki_structure XML matching the user's template. Never use markdown fences.";
|
|
716
|
+
const comprehensive = opts.comprehensiveSections ?? false;
|
|
717
|
+
let structureRaw = await chatCompletionTextForIndex(signal, {
|
|
718
|
+
baseUrl: opts.baseUrl,
|
|
719
|
+
apiKey: opts.apiKey,
|
|
720
|
+
model: opts.model,
|
|
721
|
+
temperature: 0.2,
|
|
722
|
+
messages: [
|
|
723
|
+
{ role: "system", content: structureSystem },
|
|
724
|
+
{
|
|
725
|
+
role: "user",
|
|
726
|
+
content: structurePrompt(repoName, treeText, readme, structureLang, comprehensive),
|
|
727
|
+
},
|
|
728
|
+
],
|
|
729
|
+
});
|
|
730
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
731
|
+
let xml = (0, wikiStructureXml_1.extractWikiStructureXml)(structureRaw) ?? "";
|
|
732
|
+
if (!xml) {
|
|
733
|
+
throw new Error(`Model returned no <wiki_structure> XML. Preview: ${structureRaw.slice(0, 400)}`);
|
|
734
|
+
}
|
|
735
|
+
let pages = (0, wikiStructureXml_1.parseWikiPages)(xml);
|
|
736
|
+
if (!pages.length) {
|
|
737
|
+
throw new Error(`Could not parse wiki pages from XML. Preview: ${structureRaw.slice(0, 500)}`);
|
|
738
|
+
}
|
|
739
|
+
if (pages.length > wikiStructureXml_1.REPOWIKI_MAX_PAGES) {
|
|
740
|
+
log(`[repowiki] Truncating outline from ${pages.length} to ${wikiStructureXml_1.REPOWIKI_MAX_PAGES} pages.`);
|
|
741
|
+
pages = pages.slice(0, wikiStructureXml_1.REPOWIKI_MAX_PAGES);
|
|
742
|
+
}
|
|
743
|
+
if (comprehensive && (0, repowikiStructureNormalize_1.comprehensiveOutlineNeedsStructureRetry)(comprehensive, xml, pages)) {
|
|
744
|
+
log("[repowiki] Outline has too few <section> blocks — requesting revised outline…");
|
|
745
|
+
structureRaw = await chatCompletionTextForIndex(signal, {
|
|
746
|
+
baseUrl: opts.baseUrl,
|
|
747
|
+
apiKey: opts.apiKey,
|
|
748
|
+
model: opts.model,
|
|
749
|
+
temperature: 0.2,
|
|
750
|
+
messages: [
|
|
751
|
+
{ role: "system", content: structureSystem },
|
|
752
|
+
{
|
|
753
|
+
role: "user",
|
|
754
|
+
content: structurePrompt(repoName, treeText, readme, structureLang, comprehensive) +
|
|
755
|
+
(0, repowikiStructureNormalize_1.structureRepairUserAppendix)(structureRaw, structureLang.folder),
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
});
|
|
759
|
+
(0, abort_1.assertNotAborted)(signal);
|
|
760
|
+
const xmlRetry = (0, wikiStructureXml_1.extractWikiStructureXml)(structureRaw) ?? "";
|
|
761
|
+
if (!xmlRetry) {
|
|
762
|
+
throw new Error(`Revision request returned no <wiki_structure> XML. Preview: ${structureRaw.slice(0, 400)}`);
|
|
763
|
+
}
|
|
764
|
+
xml = xmlRetry;
|
|
765
|
+
pages = (0, wikiStructureXml_1.parseWikiPages)(xml);
|
|
766
|
+
if (!pages.length) {
|
|
767
|
+
throw new Error(`Could not parse wiki pages from revised XML. Preview: ${structureRaw.slice(0, 500)}`);
|
|
768
|
+
}
|
|
769
|
+
if (pages.length > wikiStructureXml_1.REPOWIKI_MAX_PAGES) {
|
|
770
|
+
log(`[repowiki] Truncating revised outline from ${pages.length} to ${wikiStructureXml_1.REPOWIKI_MAX_PAGES} pages.`);
|
|
771
|
+
pages = pages.slice(0, wikiStructureXml_1.REPOWIKI_MAX_PAGES);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const normalized = (0, repowikiStructureNormalize_1.normalizeComprehensiveWikiStructure)({
|
|
775
|
+
comprehensive,
|
|
776
|
+
langFolder: structureLang.folder,
|
|
777
|
+
xml,
|
|
778
|
+
pages,
|
|
779
|
+
});
|
|
780
|
+
xml = normalized.xml;
|
|
781
|
+
pages = normalized.pages;
|
|
782
|
+
const structureXmlPath = path.join(wikiBase, "structure.xml");
|
|
783
|
+
await fs.writeFile(structureXmlPath, xml.replace(/^\uFEFF/, ""), "utf8");
|
|
784
|
+
log("[repowiki] Wrote structure.xml");
|
|
785
|
+
const sectionGroups = (0, repowikiStructureNav_1.parseSectionGroups)(xml);
|
|
786
|
+
const outlineFolderSegment = (0, repowikiCanonicalSections_1.defaultWikiFolderSegment)((0, repowikiStructureNav_1.extractWikiOutlineTitle)(xml), structureLang.folder);
|
|
787
|
+
const folderBySectionId = (0, repowikiCanonicalSections_1.buildUniqueSectionFolderSegments)(sectionGroups);
|
|
788
|
+
const folderByPageId = new Map();
|
|
789
|
+
for (const p of pages) {
|
|
790
|
+
folderByPageId.set(p.id, (0, repowikiCanonicalSections_1.sectionFolderSegmentForPage)(p, sectionGroups, outlineFolderSegment, folderBySectionId));
|
|
791
|
+
}
|
|
792
|
+
const sectionMap = (0, repowikiCanonicalSections_1.formatSectionFolderMapSummary)(folderByPageId, pages.map((p) => p.id));
|
|
793
|
+
log(`[repowiki] Section map: ${sectionMap}`);
|
|
794
|
+
// In tasks-only mode there is no checkpoint / prior metadata to honor,
|
|
795
|
+
// so pass empty owners and a `never-exists` physical probe: we just want
|
|
796
|
+
// deterministic target paths for the tasks list.
|
|
797
|
+
const stemAlloc = (0, wikiStructureXml_1.allocateWikiFileStemsWithTargetPaths)(pages, folderByPageId, new Map(), () => false);
|
|
798
|
+
const outlinePages = pages.map((p) => {
|
|
799
|
+
const bucket = folderByPageId.get(p.id);
|
|
800
|
+
const stem = stemAlloc.get(p.id);
|
|
801
|
+
const relUnderLang = `${bucket}/${stem}.md`.replace(/\\/g, "/");
|
|
802
|
+
return {
|
|
803
|
+
id: p.id,
|
|
804
|
+
title: p.title,
|
|
805
|
+
description: p.description,
|
|
806
|
+
parentSection: p.parentSection,
|
|
807
|
+
sectionFolder: bucket,
|
|
808
|
+
relativeOutputPath: `${primaryLang}/${relUnderLang}`,
|
|
809
|
+
relevantFiles: p.relevantFiles.slice(),
|
|
810
|
+
};
|
|
811
|
+
});
|
|
812
|
+
report?.({
|
|
813
|
+
done: 1,
|
|
814
|
+
total: 1,
|
|
815
|
+
detail: `Outline ready (${outlinePages.length} page, tasks-only mode)`,
|
|
816
|
+
});
|
|
817
|
+
return {
|
|
818
|
+
wikiBaseFs: wikiBase,
|
|
819
|
+
primaryLang,
|
|
820
|
+
structureXmlPath,
|
|
821
|
+
sectionMapSummary: sectionMap,
|
|
822
|
+
pages: outlinePages,
|
|
823
|
+
};
|
|
824
|
+
}
|