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,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REPOWIKI_MAX_TOP_SECTIONS = void 0;
|
|
4
|
+
exports.comprehensiveOutlineNeedsStructureRetry = comprehensiveOutlineNeedsStructureRetry;
|
|
5
|
+
exports.syncPageParentSectionsFromGroups = syncPageParentSectionsFromGroups;
|
|
6
|
+
exports.patchWikiStructureSectionsAndParents = patchWikiStructureSectionsAndParents;
|
|
7
|
+
exports.normalizeComprehensiveWikiStructure = normalizeComprehensiveWikiStructure;
|
|
8
|
+
exports.structureRepairUserAppendix = structureRepairUserAppendix;
|
|
9
|
+
const repowikiStructureNav_1 = require("./repowikiStructureNav");
|
|
10
|
+
/** Top-level category folders under each `lang/` (inclusive cap). */
|
|
11
|
+
exports.REPOWIKI_MAX_TOP_SECTIONS = 7;
|
|
12
|
+
function escapeXmlText(s) {
|
|
13
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
14
|
+
}
|
|
15
|
+
function escapeXmlAttr(s) {
|
|
16
|
+
return escapeXmlText(s).replace(/"/g, """);
|
|
17
|
+
}
|
|
18
|
+
function comprehensiveOutlineNeedsStructureRetry(comprehensive, structureXml, pages) {
|
|
19
|
+
if (!comprehensive || pages.length <= 1)
|
|
20
|
+
return false;
|
|
21
|
+
return (0, repowikiStructureNav_1.parseSectionGroups)(structureXml).length < 2;
|
|
22
|
+
}
|
|
23
|
+
function capSectionCount(groups, max) {
|
|
24
|
+
if (groups.length <= max)
|
|
25
|
+
return groups.map((g) => ({ ...g, pageIds: [...g.pageIds] }));
|
|
26
|
+
const head = groups.slice(0, max - 1).map((g) => ({ ...g, pageIds: [...g.pageIds] }));
|
|
27
|
+
const tail = groups.slice(max - 1);
|
|
28
|
+
const merged = {
|
|
29
|
+
id: tail[0].id,
|
|
30
|
+
title: tail[0].title,
|
|
31
|
+
pageIds: tail.flatMap((t) => t.pageIds),
|
|
32
|
+
};
|
|
33
|
+
return [...head, merged];
|
|
34
|
+
}
|
|
35
|
+
function dedupeSectionPageRefsAcross(groups) {
|
|
36
|
+
const claimed = new Set();
|
|
37
|
+
for (const g of groups) {
|
|
38
|
+
const next = [];
|
|
39
|
+
const seenLocal = new Set();
|
|
40
|
+
for (const pid of g.pageIds) {
|
|
41
|
+
if (seenLocal.has(pid))
|
|
42
|
+
continue;
|
|
43
|
+
seenLocal.add(pid);
|
|
44
|
+
if (claimed.has(pid))
|
|
45
|
+
continue;
|
|
46
|
+
claimed.add(pid);
|
|
47
|
+
next.push(pid);
|
|
48
|
+
}
|
|
49
|
+
g.pageIds = next;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function assignOrphanPagesRoundRobin(groups, pageIds) {
|
|
53
|
+
if (groups.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
const inSections = new Set(groups.flatMap((g) => g.pageIds));
|
|
56
|
+
const orphans = pageIds.filter((id) => !inSections.has(id));
|
|
57
|
+
if (!orphans.length)
|
|
58
|
+
return;
|
|
59
|
+
for (let i = 0; i < orphans.length; i++) {
|
|
60
|
+
groups[i % groups.length].pageIds.push(orphans[i]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function syntheticSectionsForPages(pages, langFolder) {
|
|
64
|
+
const n = pages.length;
|
|
65
|
+
const numSections = Math.min(exports.REPOWIKI_MAX_TOP_SECTIONS, Math.max(2, Math.ceil(n / 4)));
|
|
66
|
+
const sections = [];
|
|
67
|
+
for (let s = 0; s < numSections; s++) {
|
|
68
|
+
const title = langFolder === "zh" ? `主题 ${s + 1}` : `Topic ${s + 1}`;
|
|
69
|
+
sections.push({ id: `auto-part-${s + 1}`, title, pageIds: [] });
|
|
70
|
+
}
|
|
71
|
+
for (let i = 0; i < pages.length; i++) {
|
|
72
|
+
sections[i % numSections].pageIds.push(pages[i].id);
|
|
73
|
+
}
|
|
74
|
+
return sections;
|
|
75
|
+
}
|
|
76
|
+
function syncPageParentSectionsFromGroups(pages, groups) {
|
|
77
|
+
const pageToSection = new Map();
|
|
78
|
+
for (const g of groups) {
|
|
79
|
+
for (const pid of g.pageIds) {
|
|
80
|
+
if (!pageToSection.has(pid))
|
|
81
|
+
pageToSection.set(pid, g.id);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const p of pages) {
|
|
85
|
+
const sid = pageToSection.get(p.id);
|
|
86
|
+
if (sid)
|
|
87
|
+
p.parentSection = sid;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function serializeSectionsElement(groups) {
|
|
91
|
+
const lines = [" <sections>"];
|
|
92
|
+
for (const g of groups) {
|
|
93
|
+
lines.push(` <section id="${escapeXmlAttr(g.id)}">`);
|
|
94
|
+
lines.push(` <title>${escapeXmlText(g.title)}</title>`);
|
|
95
|
+
lines.push(` <pages>`);
|
|
96
|
+
for (const pid of g.pageIds) {
|
|
97
|
+
lines.push(` <page_ref>${escapeXmlText(pid)}</page_ref>`);
|
|
98
|
+
}
|
|
99
|
+
lines.push(` </pages>`);
|
|
100
|
+
lines.push(` <subsections/>`);
|
|
101
|
+
lines.push(` </section>`);
|
|
102
|
+
}
|
|
103
|
+
lines.push(` </sections>`);
|
|
104
|
+
return lines.join("\n");
|
|
105
|
+
}
|
|
106
|
+
function upsertPageParentSection(xml, pageId, sectionId) {
|
|
107
|
+
const escId = pageId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
108
|
+
const pageRe = new RegExp(`<page\\s+([^>]*\\bid\\s*=\\s*"${escId}"[^>]*)>([\\s\\S]*?)</page>`, "i");
|
|
109
|
+
return xml.replace(pageRe, (_full, attrs, inner) => {
|
|
110
|
+
const tagOpen = `<page ${attrs}>`;
|
|
111
|
+
let body = inner;
|
|
112
|
+
if (/<parent_section>/i.test(body)) {
|
|
113
|
+
body = body.replace(/<parent_section>\s*[\s\S]*?\s*<\/parent_section>/i, `<parent_section>${escapeXmlText(sectionId)}</parent_section>`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const rfClose = body.search(/<\/relevant_files>/i);
|
|
117
|
+
if (rfClose !== -1) {
|
|
118
|
+
const end = body.indexOf(">", rfClose) + 1;
|
|
119
|
+
body =
|
|
120
|
+
body.slice(0, end) +
|
|
121
|
+
`\n <parent_section>${escapeXmlText(sectionId)}</parent_section>` +
|
|
122
|
+
body.slice(end);
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
body = `${body}\n <parent_section>${escapeXmlText(sectionId)}</parent_section>`;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return `${tagOpen}${body}</page>`;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function patchWikiStructureSectionsAndParents(xml, groups, pages) {
|
|
132
|
+
let out = xml;
|
|
133
|
+
const serialized = serializeSectionsElement(groups);
|
|
134
|
+
if (/<sections>\s*[\s\S]*?<\/sections>/i.test(out)) {
|
|
135
|
+
out = out.replace(/<sections>\s*[\s\S]*?<\/sections>/i, serialized);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
const descClose = out.match(/<\/description>\s*/i);
|
|
139
|
+
if (descClose && descClose.index !== undefined) {
|
|
140
|
+
const insertAt = descClose.index + descClose[0].length;
|
|
141
|
+
out = `${out.slice(0, insertAt)}\n${serialized}\n${out.slice(insertAt)}`;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
const titleClose = out.match(/<\/title>\s*/i);
|
|
145
|
+
if (titleClose && titleClose.index !== undefined) {
|
|
146
|
+
const insertAt = titleClose.index + titleClose[0].length;
|
|
147
|
+
out = `${out.slice(0, insertAt)}\n${serialized}\n${out.slice(insertAt)}`;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const p of pages) {
|
|
152
|
+
if (p.parentSection) {
|
|
153
|
+
out = upsertPageParentSection(out, p.id, p.parentSection);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
function normalizeComprehensiveWikiStructure(opts) {
|
|
159
|
+
const { comprehensive, langFolder, xml } = opts;
|
|
160
|
+
const pages = opts.pages;
|
|
161
|
+
if (!comprehensive) {
|
|
162
|
+
return { xml, pages, groups: (0, repowikiStructureNav_1.parseSectionGroups)(xml) };
|
|
163
|
+
}
|
|
164
|
+
if (pages.length === 0) {
|
|
165
|
+
return { xml, pages, groups: (0, repowikiStructureNav_1.parseSectionGroups)(xml) };
|
|
166
|
+
}
|
|
167
|
+
if (pages.length === 1) {
|
|
168
|
+
let groups = capSectionCount((0, repowikiStructureNav_1.parseSectionGroups)(xml), exports.REPOWIKI_MAX_TOP_SECTIONS);
|
|
169
|
+
dedupeSectionPageRefsAcross(groups);
|
|
170
|
+
assignOrphanPagesRoundRobin(groups, pages.map((p) => p.id));
|
|
171
|
+
const pid = pages[0].id;
|
|
172
|
+
const pick = groups[0] ?? { id: "section-overview", title: langFolder === "zh" ? "概述" : "Overview", pageIds: [] };
|
|
173
|
+
groups = [{ id: pick.id, title: pick.title, pageIds: [pid] }];
|
|
174
|
+
syncPageParentSectionsFromGroups(pages, groups);
|
|
175
|
+
return { xml: patchWikiStructureSectionsAndParents(xml, groups, pages), pages, groups };
|
|
176
|
+
}
|
|
177
|
+
let groups = capSectionCount((0, repowikiStructureNav_1.parseSectionGroups)(xml), exports.REPOWIKI_MAX_TOP_SECTIONS);
|
|
178
|
+
dedupeSectionPageRefsAcross(groups);
|
|
179
|
+
assignOrphanPagesRoundRobin(groups, pages.map((p) => p.id));
|
|
180
|
+
if (groups.length < 2) {
|
|
181
|
+
groups = syntheticSectionsForPages(pages, langFolder);
|
|
182
|
+
}
|
|
183
|
+
syncPageParentSectionsFromGroups(pages, groups);
|
|
184
|
+
return {
|
|
185
|
+
xml: patchWikiStructureSectionsAndParents(xml, groups, pages),
|
|
186
|
+
pages,
|
|
187
|
+
groups,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function structureRepairUserAppendix(previousRaw, langFolder) {
|
|
191
|
+
const preview = previousRaw.slice(0, 8_000);
|
|
192
|
+
if (langFolder === "zh") {
|
|
193
|
+
return `
|
|
194
|
+
|
|
195
|
+
【修订要求 — 上一条纲要无效】上一次的 wiki_structure **小节数量不足**(少于 2 个顶层 <section>,或缺失 <sections>),但需要 **2 至 7 个** 顶层小节。请重新输出 **完整** 的 wiki_structure XML(不要用"见上文"省略)。
|
|
196
|
+
|
|
197
|
+
硬性要求:
|
|
198
|
+
- 在 <sections> 下提供 **至少 2 个、至多 7 个** 平级 <section>,每个有唯一 id。
|
|
199
|
+
- **禁止** 把所有 <page> 放进同一个小节;将 ≤20 页分配到多个小节。
|
|
200
|
+
- 每个 <page> 必须有 <parent_section>,其值必须等于拥有该页的 section 的 id(且该 section 的 <page_ref> 引用此页)。
|
|
201
|
+
- 仍须符合此前的模板(先 <sections>,再完整 <pages> 块等)。
|
|
202
|
+
|
|
203
|
+
上一次输出摘要(仅供参考,请修正而非照抄):
|
|
204
|
+
${preview}`;
|
|
205
|
+
}
|
|
206
|
+
return `
|
|
207
|
+
|
|
208
|
+
【Revision required — previous outline invalid】The last wiki_structure had **too few** top-level <section> blocks under <sections> (or <sections> was missing). Return a **complete new** wiki_structure XML with **between 2 and 7** sibling <section> elements.
|
|
209
|
+
|
|
210
|
+
Hard requirements:
|
|
211
|
+
- Provide **at least 2 and at most 7** <section> elements under <sections>, each with a **unique** id.
|
|
212
|
+
- **Do not** place every <page> in a single section; distribute up to 20 pages across multiple sections.
|
|
213
|
+
- Every <page> MUST include <parent_section> equal to the owning section id (that section MUST list the page via <page_ref>).
|
|
214
|
+
- Follow the same template as before (<sections> first, then a full <pages> block).
|
|
215
|
+
|
|
216
|
+
Previous output (for reference only — fix it, do not copy):
|
|
217
|
+
${preview}`;
|
|
218
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REPOWIKI_MAX_PAGES = void 0;
|
|
4
|
+
exports.decodeXmlEntities = decodeXmlEntities;
|
|
5
|
+
exports.extractWikiStructureXml = extractWikiStructureXml;
|
|
6
|
+
exports.extractSectionsInnerXml = extractSectionsInnerXml;
|
|
7
|
+
exports.parsePageRefsFromSections = parsePageRefsFromSections;
|
|
8
|
+
exports.parseWikiPages = parseWikiPages;
|
|
9
|
+
exports.sanitizeWikiOutlineId = sanitizeWikiOutlineId;
|
|
10
|
+
exports.slugifyFileStem = slugifyFileStem;
|
|
11
|
+
exports.slugifyWikiTitle = slugifyWikiTitle;
|
|
12
|
+
exports.allocateWikiFileStems = allocateWikiFileStems;
|
|
13
|
+
exports.allocateWikiFileStemsWithTargetPaths = allocateWikiFileStemsWithTargetPaths;
|
|
14
|
+
/**
|
|
15
|
+
* Decode XML/HTML entities in text extracted from tags (parser does not use a real XML decoder).
|
|
16
|
+
* Runs until stable so double-escaped values (e.g. `&amp;`) become a single `&`.
|
|
17
|
+
*/
|
|
18
|
+
function decodeXmlEntities(s) {
|
|
19
|
+
if (!s)
|
|
20
|
+
return s;
|
|
21
|
+
let prev = "";
|
|
22
|
+
let out = s;
|
|
23
|
+
for (let i = 0; i < 8 && out !== prev; i++) {
|
|
24
|
+
prev = out;
|
|
25
|
+
out = out
|
|
26
|
+
.replace(/&/g, "&")
|
|
27
|
+
.replace(/</g, "<")
|
|
28
|
+
.replace(/>/g, ">")
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/'/g, "'")
|
|
31
|
+
.replace(/&#(\d+);/g, (_, n) => {
|
|
32
|
+
const cp = parseInt(n, 10);
|
|
33
|
+
return Number.isFinite(cp) ? String.fromCodePoint(cp) : _;
|
|
34
|
+
})
|
|
35
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => {
|
|
36
|
+
const cp = parseInt(h, 16);
|
|
37
|
+
return Number.isFinite(cp) ? String.fromCodePoint(cp) : _;
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** Accept closed document or truncated model output (missing closing </wiki_structure>). */
|
|
43
|
+
function extractWikiStructureXml(fullText) {
|
|
44
|
+
const closed = fullText.match(/<wiki_structure>[\s\S]*?<\/wiki_structure>/im);
|
|
45
|
+
if (closed)
|
|
46
|
+
return closed[0];
|
|
47
|
+
const open = fullText.match(/<wiki_structure>[\s\S]*/im);
|
|
48
|
+
return open?.[0] ?? "";
|
|
49
|
+
}
|
|
50
|
+
function textContent(inner, tag) {
|
|
51
|
+
const re = new RegExp(`<${tag}>[\\s\\S]*?<\\/${tag}>`, "i");
|
|
52
|
+
const m = inner.match(re);
|
|
53
|
+
if (!m)
|
|
54
|
+
return "";
|
|
55
|
+
const open = `<${tag}>`;
|
|
56
|
+
const close = `</${tag}>`;
|
|
57
|
+
const s = m[0];
|
|
58
|
+
const start = s.toLowerCase().indexOf(open.toLowerCase());
|
|
59
|
+
const end = s.toLowerCase().lastIndexOf(close.toLowerCase());
|
|
60
|
+
if (start === -1 || end === -1)
|
|
61
|
+
return "";
|
|
62
|
+
const body = s.slice(start + open.length, end).trim();
|
|
63
|
+
const stripped = body.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
64
|
+
return decodeXmlEntities(stripped);
|
|
65
|
+
}
|
|
66
|
+
function getAttr(inner, attr) {
|
|
67
|
+
const re = new RegExp(`${attr}="([^"]*)"`, "i");
|
|
68
|
+
const m = inner.match(re);
|
|
69
|
+
return decodeXmlEntities(m?.[1]?.trim() ?? "");
|
|
70
|
+
}
|
|
71
|
+
function collectFilePaths(block) {
|
|
72
|
+
const out = [];
|
|
73
|
+
const re = /<file_path>([\s\S]*?)<\/file_path>/gi;
|
|
74
|
+
let x;
|
|
75
|
+
while ((x = re.exec(block)) !== null) {
|
|
76
|
+
const raw = x[1]?.trim().replace(/\s+/g, "") ?? "";
|
|
77
|
+
const p = decodeXmlEntities(raw);
|
|
78
|
+
if (p)
|
|
79
|
+
out.push(p.replace(/\\/g, "/"));
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* `<sections>...</sections>` inner XML; supports truncation (missing `</sections>`).
|
|
85
|
+
* `page_ref` extraction is scoped here so stray tags elsewhere do not pollute.
|
|
86
|
+
*/
|
|
87
|
+
function extractSectionsInnerXml(wikiStructureXml) {
|
|
88
|
+
const closed = wikiStructureXml.match(/<sections>([\s\S]*?)<\/sections>/i);
|
|
89
|
+
if (closed)
|
|
90
|
+
return closed[1].trim();
|
|
91
|
+
const open = wikiStructureXml.match(/<sections>([\s\S]*)/i);
|
|
92
|
+
return open?.[1]?.trim() ?? "";
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* All `<page_ref>` ids under `<sections>`, deduped, first-seen order.
|
|
96
|
+
*/
|
|
97
|
+
function parsePageRefsFromSections(wikiStructureXml) {
|
|
98
|
+
const inner = extractSectionsInnerXml(wikiStructureXml);
|
|
99
|
+
if (!inner)
|
|
100
|
+
return [];
|
|
101
|
+
const re = /<page_ref>([\s\S]*?)<\/page_ref>/gi;
|
|
102
|
+
const seen = new Set();
|
|
103
|
+
const out = [];
|
|
104
|
+
let m;
|
|
105
|
+
while ((m = re.exec(inner)) !== null) {
|
|
106
|
+
const raw = decodeXmlEntities((m[1] ?? "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim());
|
|
107
|
+
const id = sanitizeId(raw);
|
|
108
|
+
if (!id || seen.has(id))
|
|
109
|
+
continue;
|
|
110
|
+
seen.add(id);
|
|
111
|
+
out.push(id);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
function humanizeIdFallback(id) {
|
|
116
|
+
const s = id.replace(/[-_.]+/g, " ").trim();
|
|
117
|
+
if (!s)
|
|
118
|
+
return id;
|
|
119
|
+
return s.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
120
|
+
}
|
|
121
|
+
function makeSyntheticPage(id) {
|
|
122
|
+
return {
|
|
123
|
+
id,
|
|
124
|
+
title: humanizeIdFallback(id) || id,
|
|
125
|
+
description: "",
|
|
126
|
+
relevantFiles: [],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Collect `<page id="...">` openings even when the tag was cut off before `>` (truncated model output).
|
|
131
|
+
* Skips ids already present in `alreadyHave` (full `<page>...</page>` parses).
|
|
132
|
+
*/
|
|
133
|
+
function salvagePartialPageOpeningIds(wikiStructureXml, alreadyHave) {
|
|
134
|
+
const re = /<page\b[^>]*?\bid\s*=\s*"([^"]+)"/gi;
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
const out = [];
|
|
137
|
+
let m;
|
|
138
|
+
while ((m = re.exec(wikiStructureXml)) !== null) {
|
|
139
|
+
const id = sanitizeId((m[1] ?? "").trim());
|
|
140
|
+
if (!id || alreadyHave.has(id) || seen.has(id))
|
|
141
|
+
continue;
|
|
142
|
+
seen.add(id);
|
|
143
|
+
out.push(id);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Full `<page id="...">...</page>` entries only.
|
|
149
|
+
*/
|
|
150
|
+
function parseFullWikiPages(wikiStructureXml) {
|
|
151
|
+
const pageRe = /<page\s+([^>]*?)>([\s\S]*?)<\/page>/gi;
|
|
152
|
+
const list = [];
|
|
153
|
+
let pm;
|
|
154
|
+
while ((pm = pageRe.exec(wikiStructureXml)) !== null) {
|
|
155
|
+
const openAttrs = pm[1] ?? "";
|
|
156
|
+
const inner = pm[2] ?? "";
|
|
157
|
+
const id = getAttr(openAttrs, "id");
|
|
158
|
+
if (!id)
|
|
159
|
+
continue;
|
|
160
|
+
const title = textContent(inner, "title");
|
|
161
|
+
const description = textContent(inner, "description");
|
|
162
|
+
const parentSection = textContent(inner, "parent_section").trim();
|
|
163
|
+
const files = collectFilePaths(inner);
|
|
164
|
+
list.push({
|
|
165
|
+
id: sanitizeId(id),
|
|
166
|
+
title: title || id,
|
|
167
|
+
description,
|
|
168
|
+
relevantFiles: files.slice(0, 25),
|
|
169
|
+
...(parentSection ? { parentSection } : {}),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return dedupeById(list);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Find all `<page id="...">...</page>` in the full structure, then merge in outline-only data when the
|
|
176
|
+
* outer `<pages>` block is missing or truncated: `<page_ref>` under `<sections>` and optional salvage
|
|
177
|
+
* of cut-off `<page id="...">` openings.
|
|
178
|
+
*/
|
|
179
|
+
function parseWikiPages(wikiStructureXml) {
|
|
180
|
+
if (!wikiStructureXml)
|
|
181
|
+
return [];
|
|
182
|
+
const fullList = parseFullWikiPages(wikiStructureXml);
|
|
183
|
+
const mergedIds = new Set(fullList.map((p) => p.id));
|
|
184
|
+
const refIds = parsePageRefsFromSections(wikiStructureXml);
|
|
185
|
+
const salvageIds = salvagePartialPageOpeningIds(wikiStructureXml, mergedIds);
|
|
186
|
+
const extra = [];
|
|
187
|
+
for (const id of refIds) {
|
|
188
|
+
if (mergedIds.has(id))
|
|
189
|
+
continue;
|
|
190
|
+
mergedIds.add(id);
|
|
191
|
+
extra.push(makeSyntheticPage(id));
|
|
192
|
+
}
|
|
193
|
+
for (const id of salvageIds) {
|
|
194
|
+
if (mergedIds.has(id))
|
|
195
|
+
continue;
|
|
196
|
+
mergedIds.add(id);
|
|
197
|
+
extra.push(makeSyntheticPage(id));
|
|
198
|
+
}
|
|
199
|
+
return [...fullList, ...extra];
|
|
200
|
+
}
|
|
201
|
+
function dedupeById(pages) {
|
|
202
|
+
const seen = new Set();
|
|
203
|
+
const out = [];
|
|
204
|
+
for (const p of pages) {
|
|
205
|
+
if (seen.has(p.id))
|
|
206
|
+
continue;
|
|
207
|
+
seen.add(p.id);
|
|
208
|
+
out.push(p);
|
|
209
|
+
}
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
function sanitizeWikiOutlineId(id) {
|
|
213
|
+
return id.replace(/[^a-zA-Z0-9_.-]/g, "-").slice(0, 80);
|
|
214
|
+
}
|
|
215
|
+
function sanitizeId(id) {
|
|
216
|
+
return sanitizeWikiOutlineId(id);
|
|
217
|
+
}
|
|
218
|
+
function slugifyFileStem(pageId) {
|
|
219
|
+
return pageId.replace(/[^a-zA-Z0-9_.-]/g, "-") || "page";
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Stem from outline / page title: filesystem-safe, keeps letters (incl. CJK), digits,
|
|
223
|
+
* hyphen, underscore, dot; drops path/control chars and trims trailing dot/space (Windows).
|
|
224
|
+
*/
|
|
225
|
+
function slugifyWikiTitle(title) {
|
|
226
|
+
let s = title.trim();
|
|
227
|
+
if (!s)
|
|
228
|
+
return "";
|
|
229
|
+
s = s.replace(/[\x00-\x1f<>:"/\\|?*]/g, "");
|
|
230
|
+
s = s.replace(/\s+/g, "-");
|
|
231
|
+
s = s.replace(/-+/g, "-");
|
|
232
|
+
s = s.replace(/^[-.]+|[-.]+$/g, "");
|
|
233
|
+
if (!s)
|
|
234
|
+
return "";
|
|
235
|
+
if (s.length > 120)
|
|
236
|
+
s = s.slice(0, 120).replace(/[-.]+$/g, "");
|
|
237
|
+
s = s.replace(/[. ]+$/g, "").replace(/^[. ]+/g, "");
|
|
238
|
+
return s || "";
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Unique `.md` file stem per page id for a language folder. Title-derived when possible.
|
|
242
|
+
*/
|
|
243
|
+
function allocateWikiFileStems(pages) {
|
|
244
|
+
const used = new Set();
|
|
245
|
+
const map = new Map();
|
|
246
|
+
for (const p of pages) {
|
|
247
|
+
const base = slugifyWikiTitle(p.title) || slugifyFileStem(p.id);
|
|
248
|
+
let stem = base;
|
|
249
|
+
if (used.has(stem)) {
|
|
250
|
+
const withId = `${base}-${slugifyFileStem(p.id)}`;
|
|
251
|
+
if (!used.has(withId))
|
|
252
|
+
stem = withId;
|
|
253
|
+
else {
|
|
254
|
+
let n = 2;
|
|
255
|
+
while (used.has(`${base}-${n}`))
|
|
256
|
+
n++;
|
|
257
|
+
stem = `${base}-${n}`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
used.add(stem);
|
|
261
|
+
map.set(p.id, stem);
|
|
262
|
+
}
|
|
263
|
+
return map;
|
|
264
|
+
}
|
|
265
|
+
function shortPageIdSuffix(pageId) {
|
|
266
|
+
const s = slugifyFileStem(pageId).replace(/^page-?/i, "");
|
|
267
|
+
return (s || slugifyFileStem(pageId)).slice(-8) || "p";
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Resolve final `.md` stems per page given section folder segments, so paths do not collide with
|
|
271
|
+
* unrelated files on disk (e.g. orphans from an old wiki-title folder layout) or other page ids.
|
|
272
|
+
*/
|
|
273
|
+
function allocateWikiFileStemsWithTargetPaths(pages, bucketByPageId, pathToOwnerPageId, physicalExists) {
|
|
274
|
+
const baseById = allocateWikiFileStems(pages);
|
|
275
|
+
const out = new Map();
|
|
276
|
+
/** rel posix path -> page id assigned in this pass */
|
|
277
|
+
const claimed = new Map();
|
|
278
|
+
const conflicts = (rel, pageId) => {
|
|
279
|
+
const metaOwner = pathToOwnerPageId.get(rel);
|
|
280
|
+
const stepOwner = claimed.get(rel);
|
|
281
|
+
const ex = physicalExists(rel);
|
|
282
|
+
if (stepOwner && stepOwner !== pageId)
|
|
283
|
+
return true;
|
|
284
|
+
if (metaOwner === pageId)
|
|
285
|
+
return false;
|
|
286
|
+
if (metaOwner && metaOwner !== pageId)
|
|
287
|
+
return true;
|
|
288
|
+
if (ex)
|
|
289
|
+
return true;
|
|
290
|
+
return false;
|
|
291
|
+
};
|
|
292
|
+
for (const p of pages) {
|
|
293
|
+
const bucket = bucketByPageId.get(p.id) ?? "wiki";
|
|
294
|
+
const baseStem = baseById.get(p.id);
|
|
295
|
+
let stem = baseStem;
|
|
296
|
+
let attempt = 0;
|
|
297
|
+
while (true) {
|
|
298
|
+
const rel = `${bucket}/${stem}.md`.replace(/\\/g, "/");
|
|
299
|
+
if (!conflicts(rel, p.id)) {
|
|
300
|
+
claimed.set(rel, p.id);
|
|
301
|
+
out.set(p.id, stem);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
attempt++;
|
|
305
|
+
if (attempt === 1) {
|
|
306
|
+
stem = `${baseStem}-${shortPageIdSuffix(p.id)}`;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
stem = `${baseStem}-${attempt + 1}`;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return out;
|
|
314
|
+
}
|
|
315
|
+
/** Max wiki pages per index run (structure prompt + truncation). */
|
|
316
|
+
exports.REPOWIKI_MAX_PAGES = 20;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Writes a `TASKS.md` inside `<wikiBase>/` based on an outline computed by
|
|
4
|
+
* `runRepowikiOutlineOnly`. The file is consumed by the downstream
|
|
5
|
+
* harness-build-skill-knowledge-builder skill which fills page content iteratively.
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
|
+
exports.writeWikiTasks = writeWikiTasks;
|
|
42
|
+
const fs = __importStar(require("fs/promises"));
|
|
43
|
+
const path = __importStar(require("path"));
|
|
44
|
+
/**
|
|
45
|
+
* Group pages by their `sectionFolder` bucket, preserving first-seen order.
|
|
46
|
+
*/
|
|
47
|
+
function groupBySection(pages) {
|
|
48
|
+
const buckets = new Map();
|
|
49
|
+
for (const p of pages) {
|
|
50
|
+
const list = buckets.get(p.sectionFolder);
|
|
51
|
+
if (list)
|
|
52
|
+
list.push(p);
|
|
53
|
+
else
|
|
54
|
+
buckets.set(p.sectionFolder, [p]);
|
|
55
|
+
}
|
|
56
|
+
return [...buckets.entries()].map(([folder, list]) => ({ folder, pages: list }));
|
|
57
|
+
}
|
|
58
|
+
function renderTasksMarkdown(input) {
|
|
59
|
+
const { outline } = input;
|
|
60
|
+
const lines = [];
|
|
61
|
+
const groups = groupBySection(outline.pages);
|
|
62
|
+
lines.push(`# Baseline Wiki Tasks — ${input.projectName}`);
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push(`Generated by svharness v${input.cliVersion} on ${input.generatedAtIso}`);
|
|
65
|
+
lines.push(`Source tree: \`${input.sourceRootRel}\``);
|
|
66
|
+
lines.push(`Outline model: \`${input.model}\``);
|
|
67
|
+
lines.push(`Outline XML: \`./structure.xml\``);
|
|
68
|
+
lines.push(`Primary language: \`${outline.primaryLang}\``);
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push("> 下游 agent(harness-build-skill-wiki-writer)按本清单逐页生成 wiki:");
|
|
71
|
+
lines.push("> 逐条完成 Pages 列表中的任务,每完成一页把 `[ ]` 改为 `[x]`。");
|
|
72
|
+
lines.push("> 每页应产出 Markdown 文件到对应 `Output` 路径,内容严格基于基线源码。");
|
|
73
|
+
lines.push("");
|
|
74
|
+
lines.push("> **源码引用规范(强制,与 --generate-wiki 产出口径保持一致)**:");
|
|
75
|
+
lines.push("> 所有源码链接必须指向 `assets/baseline/code/` 下的路径,允许两种写法(可混用):");
|
|
76
|
+
lines.push("> 1. 行内链接:`[Foo.java:10-30](../../../assets/baseline/code/<repo-relative>/Foo.java#L10-L30)`");
|
|
77
|
+
lines.push("> 2. Sources 行:`Sources: <repo-relative>/Foo.java:10-30, <repo-relative>/Bar.kt:5-8`");
|
|
78
|
+
lines.push("> `<repo-relative>` 指**相对 `assets/baseline/code/` 的路径**,既非仓库原始绝对路径,");
|
|
79
|
+
lines.push("> 也非 wiki md 文件的相对路径。行号使用 `start-end`(闭区间)。");
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push("## Sections");
|
|
82
|
+
for (const g of groups) {
|
|
83
|
+
const ids = g.pages.map((p) => p.id).join(", ");
|
|
84
|
+
lines.push(`- \`${g.folder}\`: ${ids}`);
|
|
85
|
+
}
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push(`## Pages (${outline.pages.length})`);
|
|
88
|
+
lines.push("");
|
|
89
|
+
for (const p of outline.pages) {
|
|
90
|
+
lines.push(`### [ ] ${p.id} — ${p.title}`);
|
|
91
|
+
lines.push(`- Output: \`${p.relativeOutputPath}\``);
|
|
92
|
+
lines.push(`- Section: \`${p.sectionFolder}\``);
|
|
93
|
+
if (p.parentSection) {
|
|
94
|
+
lines.push(`- Parent section id: \`${p.parentSection}\``);
|
|
95
|
+
}
|
|
96
|
+
if (p.description) {
|
|
97
|
+
lines.push(`- Description: ${p.description}`);
|
|
98
|
+
}
|
|
99
|
+
if (p.relevantFiles.length > 0) {
|
|
100
|
+
lines.push(`- Relevant files:`);
|
|
101
|
+
for (const f of p.relevantFiles) {
|
|
102
|
+
lines.push(` - \`${f}\``);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
lines.push(`- Relevant files: _(none declared — agent should infer from tree)_`);
|
|
107
|
+
}
|
|
108
|
+
lines.push(`- Acceptance:`);
|
|
109
|
+
lines.push(` - 文件存在且非空`);
|
|
110
|
+
lines.push(` - 首段含页面标题(H1 与 title 一致)`);
|
|
111
|
+
lines.push(` - 至少引用 1 个 Relevant files 中的源码片段或路径`);
|
|
112
|
+
lines.push(` - Sources 行或行内链接的路径必须定位到 \`assets/baseline/code/\` 下的真实文件`);
|
|
113
|
+
lines.push("");
|
|
114
|
+
}
|
|
115
|
+
return lines.join("\n");
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Write `TASKS.md` under the wiki base directory and return the absolute path.
|
|
119
|
+
*/
|
|
120
|
+
async function writeWikiTasks(input) {
|
|
121
|
+
const wikiBase = path.join(input.targetRoot, input.wikiRelPath.replace(/^[/\\]+/, ""));
|
|
122
|
+
await fs.mkdir(wikiBase, { recursive: true });
|
|
123
|
+
const out = path.join(wikiBase, "TASKS.md");
|
|
124
|
+
const md = renderTasksMarkdown(input);
|
|
125
|
+
await fs.writeFile(out, md, "utf8");
|
|
126
|
+
return out;
|
|
127
|
+
}
|