@coralai/sps-cli 0.50.24 → 0.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +18 -1
  2. package/dist/commands/projectInit.d.ts +15 -0
  3. package/dist/commands/projectInit.d.ts.map +1 -1
  4. package/dist/commands/projectInit.js +191 -3
  5. package/dist/commands/projectInit.js.map +1 -1
  6. package/dist/commands/wikiCommand.d.ts +77 -0
  7. package/dist/commands/wikiCommand.d.ts.map +1 -0
  8. package/dist/commands/wikiCommand.js +489 -0
  9. package/dist/commands/wikiCommand.js.map +1 -0
  10. package/dist/console/routes/projects.d.ts.map +1 -1
  11. package/dist/console/routes/projects.js +1 -0
  12. package/dist/console/routes/projects.js.map +1 -1
  13. package/dist/console-assets/assets/index-DRhdpvew.css +10 -0
  14. package/dist/console-assets/assets/{index-QBai48VV.js → index-WUGCBcyb.js} +3 -3
  15. package/dist/console-assets/index.html +2 -2
  16. package/dist/core/taskPrompts.d.ts +12 -0
  17. package/dist/core/taskPrompts.d.ts.map +1 -1
  18. package/dist/core/taskPrompts.js +14 -0
  19. package/dist/core/taskPrompts.js.map +1 -1
  20. package/dist/core/wiki/frontmatter.d.ts +55 -0
  21. package/dist/core/wiki/frontmatter.d.ts.map +1 -0
  22. package/dist/core/wiki/frontmatter.js +109 -0
  23. package/dist/core/wiki/frontmatter.js.map +1 -0
  24. package/dist/core/wiki/hot.d.ts +27 -0
  25. package/dist/core/wiki/hot.d.ts.map +1 -0
  26. package/dist/core/wiki/hot.js +124 -0
  27. package/dist/core/wiki/hot.js.map +1 -0
  28. package/dist/core/wiki/index-builder.d.ts +37 -0
  29. package/dist/core/wiki/index-builder.d.ts.map +1 -0
  30. package/dist/core/wiki/index-builder.js +130 -0
  31. package/dist/core/wiki/index-builder.js.map +1 -0
  32. package/dist/core/wiki/linter.d.ts +76 -0
  33. package/dist/core/wiki/linter.d.ts.map +1 -0
  34. package/dist/core/wiki/linter.js +280 -0
  35. package/dist/core/wiki/linter.js.map +1 -0
  36. package/dist/core/wiki/log.d.ts +24 -0
  37. package/dist/core/wiki/log.d.ts.map +1 -0
  38. package/dist/core/wiki/log.js +107 -0
  39. package/dist/core/wiki/log.js.map +1 -0
  40. package/dist/core/wiki/manifest.d.ts +59 -0
  41. package/dist/core/wiki/manifest.d.ts.map +1 -0
  42. package/dist/core/wiki/manifest.js +180 -0
  43. package/dist/core/wiki/manifest.js.map +1 -0
  44. package/dist/core/wiki/page.d.ts +72 -0
  45. package/dist/core/wiki/page.d.ts.map +1 -0
  46. package/dist/core/wiki/page.js +221 -0
  47. package/dist/core/wiki/page.js.map +1 -0
  48. package/dist/core/wiki/reader.d.ts +102 -0
  49. package/dist/core/wiki/reader.d.ts.map +1 -0
  50. package/dist/core/wiki/reader.js +225 -0
  51. package/dist/core/wiki/reader.js.map +1 -0
  52. package/dist/core/wiki/scaffold.d.ts +42 -0
  53. package/dist/core/wiki/scaffold.d.ts.map +1 -0
  54. package/dist/core/wiki/scaffold.js +223 -0
  55. package/dist/core/wiki/scaffold.js.map +1 -0
  56. package/dist/core/wiki/searcher.d.ts +73 -0
  57. package/dist/core/wiki/searcher.d.ts.map +1 -0
  58. package/dist/core/wiki/searcher.js +216 -0
  59. package/dist/core/wiki/searcher.js.map +1 -0
  60. package/dist/core/wiki/sources.d.ts +84 -0
  61. package/dist/core/wiki/sources.d.ts.map +1 -0
  62. package/dist/core/wiki/sources.js +261 -0
  63. package/dist/core/wiki/sources.js.map +1 -0
  64. package/dist/core/wiki/types.d.ts +904 -0
  65. package/dist/core/wiki/types.d.ts.map +1 -0
  66. package/dist/core/wiki/types.js +109 -0
  67. package/dist/core/wiki/types.js.map +1 -0
  68. package/dist/engines/StageEngine.d.ts +17 -1
  69. package/dist/engines/StageEngine.d.ts.map +1 -1
  70. package/dist/engines/StageEngine.js +85 -0
  71. package/dist/engines/StageEngine.js.map +1 -1
  72. package/dist/main.js +78 -1
  73. package/dist/main.js.map +1 -1
  74. package/dist/services/ProjectService.d.ts +2 -0
  75. package/dist/services/ProjectService.d.ts.map +1 -1
  76. package/dist/services/ProjectService.js.map +1 -1
  77. package/dist/shared/wikiPaths.d.ts +38 -0
  78. package/dist/shared/wikiPaths.d.ts.map +1 -0
  79. package/dist/shared/wikiPaths.js +89 -0
  80. package/dist/shared/wikiPaths.js.map +1 -0
  81. package/package.json +1 -1
  82. package/skills/wiki-update/SKILL.md +300 -0
  83. package/dist/console-assets/assets/index-BgOHCIG1.css +0 -10
@@ -0,0 +1,225 @@
1
+ /**
2
+ * @module core/wiki/reader
3
+ * @description wikiRead():5 层确定性检索 + 类型优先级 + token 预算
4
+ *
5
+ * @layer core
6
+ *
7
+ * doc-28 §10 Wiki 读取原则的代码实现。**这是 Worker prompt 注入的入口**。
8
+ *
9
+ * 5 层叠加:
10
+ * L1 永远 — hot.md 全文 ~500 字
11
+ * L2 永远 — index.md 节选 top-N 行 ~500 字
12
+ * L3 优先 — pinned wiki_pages(card frontmatter) ~50×N 字
13
+ * L4 按 skill — 卡 skills ∩ 页 tags → top-3 ~50×3 字
14
+ * L5 按关键词 — BM25(card.title + card.desc)→top-3 ~50×3 字
15
+ *
16
+ * 优先级排序(命中多页):
17
+ * lesson = 3 / decision = 3 / concept = 2 / module = 1 / source = 1
18
+ * stale page → 跳过(status=stale 或 mtime 太老)
19
+ *
20
+ * Token 预算硬上限 1500 字(~2000 token),超出砍 L5 keyword 命中。
21
+ *
22
+ * 设计原则(Karpathy):
23
+ * - **确定性**——同输入同输出,纯函数(除文件 I/O)
24
+ * - **Push 而非 Pull**——Worker 不需要学怎么查,结果已经摆好
25
+ * - **TL;DR 而非全文**——让 Worker 自己决定要不要 Read 完整 page
26
+ */
27
+ import { readHot } from './hot.js';
28
+ import { readIndexSummary } from './index-builder.js';
29
+ import { getPageById, listValidPages } from './page.js';
30
+ import { extractTLDR, WikiSearcher } from './searcher.js';
31
+ const DEFAULT_OPTS = {
32
+ indexLines: 30,
33
+ skillTopN: 3,
34
+ keywordTopN: 3,
35
+ budgetTokens: 2000,
36
+ };
37
+ // 类型优先级(数字越大越靠前)
38
+ const TYPE_PRIORITY = {
39
+ lesson: 3,
40
+ decision: 3,
41
+ concept: 2,
42
+ module: 1,
43
+ source: 1,
44
+ };
45
+ // 启发式:1 个汉字/英文单词 ≈ 1.5 token(粗估)
46
+ const CHARS_PER_TOKEN = 1.5;
47
+ // ─── 主入口 ───────────────────────────────────────────────────────
48
+ /**
49
+ * 读取 wiki 注入到 Worker prompt 的上下文。
50
+ *
51
+ * 步骤:
52
+ * 1. Layer 1: hot.md 全文
53
+ * 2. Layer 2: index.md 节选
54
+ * 3. Layer 3: pinned pages(按 id 取)
55
+ * 4. 列所有 page 建临时 searcher
56
+ * 5. Layer 4: skill 匹配
57
+ * 6. Layer 5: BM25 关键词
58
+ * 7. 合并去重 → 类型优先级排序 → 预算截断
59
+ * 8. 装载 TL;DR 入 PageContextEntry
60
+ *
61
+ * 失败模式:任何 layer 失败(文件丢/解析错)单独 swallow,不阻塞其他 layer。
62
+ */
63
+ export function wikiRead(input, opts = {}) {
64
+ const cfg = { ...DEFAULT_OPTS, ...opts };
65
+ // L1
66
+ const hot = safeRead(() => readHot(input.repoDir), '');
67
+ // L2
68
+ const indexSummary = safeRead(() => readIndexSummary(input.repoDir, cfg.indexLines), '');
69
+ // 拉所有 page 一次(reader.ts 调用频率低 = 卡启动一次;现读 OK)
70
+ const pages = safeRead(() => listValidPages(input.repoDir), []);
71
+ // L3: pinned
72
+ const pinned = (input.pinnedPages ?? [])
73
+ .map((id) => getPageById(input.repoDir, id))
74
+ .filter((p) => p !== null);
75
+ // 临时 searcher 用 IndexedDoc(pageToIndexed 来自 searcher.ts)
76
+ const searcher = new WikiSearcher(pages.map(pageToIndexedAdapter));
77
+ // L4: skill
78
+ const bySkill = searcher
79
+ .searchByTags(input.cardSkills, cfg.skillTopN)
80
+ .map((r) => ({ pageId: r.pageId, source: 'skill' }));
81
+ // L5: keyword
82
+ const byKeyword = searcher
83
+ .search(`${input.cardTitle} ${input.cardDesc}`, cfg.keywordTopN)
84
+ .map((r) => ({ pageId: r.pageId, source: 'keyword' }));
85
+ // 合并去重(按 source 优先级:pinned > skill > keyword)
86
+ const dedup = new Map();
87
+ for (const p of pinned)
88
+ dedup.set(p.pageId, { source: 'pinned' });
89
+ for (const r of bySkill) {
90
+ if (!dedup.has(r.pageId))
91
+ dedup.set(r.pageId, { source: r.source });
92
+ }
93
+ for (const r of byKeyword) {
94
+ if (!dedup.has(r.pageId))
95
+ dedup.set(r.pageId, { source: r.source });
96
+ }
97
+ // 把每个 page 加载完整 + 排序
98
+ const entries = [];
99
+ for (const [pageId, meta] of dedup) {
100
+ const page = pages.find((p) => p.pageId === pageId);
101
+ if (!page)
102
+ continue;
103
+ if (isStalePage(page))
104
+ continue;
105
+ entries.push({
106
+ pageId,
107
+ title: page.frontmatter.title,
108
+ type: page.frontmatter.type,
109
+ tldr: extractTLDR(page.body),
110
+ source: meta.source,
111
+ priority: priorityOf(page.frontmatter.type, meta.source),
112
+ });
113
+ }
114
+ // 优先级排序
115
+ entries.sort((a, b) => b.priority - a.priority);
116
+ // 预算截断:估算 token,超出从尾部砍 keyword 命中(保 pinned + skill + 高优类型)
117
+ const trimmed = applyBudget(entries, hot, indexSummary, cfg.budgetTokens);
118
+ const tokensEstimate = estimateTokens(hot, indexSummary, trimmed);
119
+ return {
120
+ hot,
121
+ indexSummary,
122
+ pages: trimmed,
123
+ tokensEstimate,
124
+ };
125
+ }
126
+ // ─── Prompt 渲染 ──────────────────────────────────────────────────
127
+ /**
128
+ * WikiContext → prompt 注入 markdown。
129
+ *
130
+ * 格式(doc-28 §10):
131
+ * # 项目知识 - 当前状态
132
+ * <hot.md 全文>
133
+ * ---
134
+ * # 知识地图(节选)
135
+ * <index summary>
136
+ * ---
137
+ * # 与本任务相关的页
138
+ * ## [[id]] (type) [via source]
139
+ * TL;DR: ...
140
+ */
141
+ export function formatWikiContext(ctx) {
142
+ const sections = [];
143
+ if (ctx.hot.trim().length > 0) {
144
+ // hot 已经是带 frontmatter 的完整文档;展示时去掉 frontmatter 块
145
+ const hotBody = stripFrontmatter(ctx.hot).trim();
146
+ sections.push('# 项目知识 - 当前状态\n\n' + hotBody);
147
+ }
148
+ if (ctx.indexSummary.trim().length > 0) {
149
+ sections.push('# 知识地图(节选)\n\n' + ctx.indexSummary.trim());
150
+ }
151
+ if (ctx.pages.length > 0) {
152
+ const lines = ['# 与本任务相关的页', ''];
153
+ for (const p of ctx.pages) {
154
+ const tag = p.source === 'pinned' ? '📌 pinned' : p.source === 'skill' ? 'via skill' : 'via keyword';
155
+ lines.push(`## [[${p.pageId}]] (${p.type}, ${tag})`);
156
+ lines.push(`TL;DR: ${p.tldr.replace(/\s+/g, ' ').trim().slice(0, 300)}`);
157
+ lines.push('');
158
+ }
159
+ lines.push('完整内容:直接 Read 文件,或 `sps wiki read "<keyword>"` 找更多。');
160
+ sections.push(lines.join('\n'));
161
+ }
162
+ return sections.join('\n\n---\n\n');
163
+ }
164
+ // ─── Helpers (private) ────────────────────────────────────────────
165
+ function pageToIndexedAdapter(p) {
166
+ // 复用 searcher 的工厂;这里手写一份避免循环引用复杂度
167
+ return {
168
+ pageId: p.pageId,
169
+ title: p.frontmatter.title,
170
+ tags: p.frontmatter.tags,
171
+ tldr: extractTLDR(p.body),
172
+ body: p.body,
173
+ type: p.frontmatter.type,
174
+ };
175
+ }
176
+ function priorityOf(type, source) {
177
+ // pinned 永远最高(用户/Worker 显式指定)
178
+ // 其他按类型权重
179
+ const sourceBonus = source === 'pinned' ? 100 : source === 'skill' ? 10 : 0;
180
+ return sourceBonus + (TYPE_PRIORITY[type] ?? 1);
181
+ }
182
+ function isStalePage(page) {
183
+ return page.frontmatter.status === 'stale';
184
+ }
185
+ function applyBudget(entries, hot, indexSummary, budgetTokens) {
186
+ const baseTokens = estimateChars(hot) + estimateChars(indexSummary);
187
+ const baseTok = baseTokens / CHARS_PER_TOKEN;
188
+ if (baseTok >= budgetTokens) {
189
+ // hot+index 已超预算 → 不加任何 page
190
+ return [];
191
+ }
192
+ const remaining = budgetTokens - baseTok;
193
+ // 每页 TL;DR 估 ~80 token(300 字符 / 1.5 + 一些 metadata)
194
+ const PER_PAGE_TOKENS = 80;
195
+ const maxPages = Math.max(0, Math.floor(remaining / PER_PAGE_TOKENS));
196
+ if (entries.length <= maxPages)
197
+ return entries.slice();
198
+ // 砍法:保留 pinned + skill,只砍 keyword 末尾
199
+ const pinned = entries.filter((e) => e.source === 'pinned');
200
+ const skill = entries.filter((e) => e.source === 'skill');
201
+ const keyword = entries.filter((e) => e.source === 'keyword');
202
+ const need = Math.max(0, maxPages - pinned.length - skill.length);
203
+ return [...pinned, ...skill, ...keyword.slice(0, need)];
204
+ }
205
+ function estimateTokens(hot, indexSummary, entries) {
206
+ const charCount = estimateChars(hot) +
207
+ estimateChars(indexSummary) +
208
+ entries.reduce((sum, e) => sum + e.title.length + e.tldr.length + 30, 0);
209
+ return Math.ceil(charCount / CHARS_PER_TOKEN);
210
+ }
211
+ function estimateChars(s) {
212
+ return s.length;
213
+ }
214
+ function stripFrontmatter(content) {
215
+ return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
216
+ }
217
+ function safeRead(fn, fallback) {
218
+ try {
219
+ return fn();
220
+ }
221
+ catch {
222
+ return fallback;
223
+ }
224
+ }
225
+ //# sourceMappingURL=reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reader.js","sourceRoot":"","sources":["../../../src/core/wiki/reader.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,42 @@
1
+ export interface InitWikiOptions {
2
+ /** Project name used in templates */
3
+ readonly projectName: string;
4
+ /** ISO date for created/updated fields (default: today) */
5
+ readonly today?: string;
6
+ /** Skip writing .gitignore (default: false) */
7
+ readonly skipGitignore?: boolean;
8
+ }
9
+ export interface InitWikiReport {
10
+ /** Wiki dir absolute path */
11
+ readonly wikiDir: string;
12
+ /** Subdirs created (already-existing skipped) */
13
+ readonly created: string[];
14
+ /** Files written (already-existing skipped) */
15
+ readonly filesWritten: string[];
16
+ /** Files left untouched because they already existed */
17
+ readonly filesSkipped: string[];
18
+ /** Was .gitignore touched? */
19
+ readonly gitignoreUpdated: boolean;
20
+ }
21
+ /**
22
+ * Idempotently scaffold wiki/ in the given repo.
23
+ *
24
+ * Creates:
25
+ * - wiki/, wiki/{modules,concepts,decisions,lessons,sources}/, wiki/.raw/, wiki/_attachments/
26
+ * - wiki/WIKI.md, wiki/index.md, wiki/overview.md, wiki/.hot.md
27
+ * - .gitignore: appends per-instance drift entries if missing
28
+ *
29
+ * Does NOT write .manifest.json — that's owned by `sps wiki update` and is empty
30
+ * until first ingest.
31
+ *
32
+ * Re-runnable safely: existing files are preserved.
33
+ */
34
+ export declare function initWiki(repoDir: string, opts: InitWikiOptions): InitWikiReport;
35
+ /**
36
+ * Append wiki drift entries to .gitignore if missing.
37
+ *
38
+ * Returns true when at least one line was appended.
39
+ * Creates .gitignore if not present.
40
+ */
41
+ export declare function ensureGitignoreEntries(repoDir: string): boolean;
42
+ //# sourceMappingURL=scaffold.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../../../src/core/wiki/scaffold.ts"],"names":[],"mappings":"AAyIA,MAAM,WAAW,eAAe;IAC9B,qCAAqC;IACrC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,cAAc;IAC7B,6BAA6B;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC;IAC3B,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,wDAAwD;IACxD,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,CAAC;IAChC,8BAA8B;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,eAAe,GAAG,cAAc,CAiD/E;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAsC/D"}
@@ -0,0 +1,223 @@
1
+ /**
2
+ * @module core/wiki/scaffold
3
+ * @description `sps wiki init` 的物理脚手架:建目录、写模板、安装 .gitignore 条目
4
+ *
5
+ * @layer core
6
+ *
7
+ * doc-28 §4 目录结构 + §3 三层架构(per-repo)的实施面。
8
+ *
9
+ * 设计原则:
10
+ * - **幂等**:重复跑不破坏现有内容
11
+ * - **不覆盖**:所有写入前先 existsSync 检查
12
+ * - **单向同步 .gitignore**:只 append 缺失行,不重排已有内容
13
+ *
14
+ * 失败模式:mkdir 失败 → throw(让命令层报错)。
15
+ */
16
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
17
+ import { resolve } from 'node:path';
18
+ import { wikiAttachmentsDir, wikiDir, wikiHotFile, wikiIndexFile, wikiMetaFile, wikiOverviewFile, wikiPageDir, wikiRawDir, } from '../../shared/wikiPaths.js';
19
+ // ─── Constants ────────────────────────────────────────────────────
20
+ const ALL_PAGE_TYPES = [
21
+ 'module',
22
+ 'concept',
23
+ 'decision',
24
+ 'lesson',
25
+ 'source',
26
+ ];
27
+ /** Lines that wiki ownership requires in .gitignore */
28
+ const GITIGNORE_LINES = [
29
+ '# Wiki per-instance drift (added by sps wiki init)',
30
+ 'wiki/.hot.md',
31
+ 'wiki/.log.md',
32
+ 'wiki/.manifest.json',
33
+ ];
34
+ /**
35
+ * Default WIKI.md template. Contains schema version + sources config.
36
+ * Worker reads `sources:` to know what to ingest.
37
+ */
38
+ function wikiMetaTemplate(projectName, today) {
39
+ return `---
40
+ type: meta
41
+ title: ${projectName} Wiki
42
+ version: 1
43
+ created: ${today}
44
+ updated: ${today}
45
+ sources:
46
+ code:
47
+ - "src/**/*.ts"
48
+ doc:
49
+ - "docs/**/*.md"
50
+ - "README.md"
51
+ raw:
52
+ - "wiki/.raw/**/*"
53
+ ---
54
+
55
+ # ${projectName} Wiki
56
+
57
+ Project knowledge base. See [doc-28](../docs/design/28-wiki-system.md) for design.
58
+
59
+ ## Page Types
60
+
61
+ - **modules/** — Code modules / services (auto-derived from \`src/\`)
62
+ - **concepts/** — Domain concepts and patterns
63
+ - **decisions/** — Architecture decisions
64
+ - **lessons/** — Bug post-mortems and gotchas
65
+ - **sources/** — External references (PDFs, articles, transcripts)
66
+
67
+ ## Workflow
68
+
69
+ \`\`\`bash
70
+ sps wiki update <project> # ingest source diff via Worker + skill
71
+ sps wiki read <project> "query" # 5-layer retrieval for prompt injection
72
+ \`\`\`
73
+ `;
74
+ }
75
+ const INDEX_PLACEHOLDER = `---
76
+ type: meta
77
+ title: Wiki Index
78
+ updated: 1970-01-01T00:00:00Z
79
+ ---
80
+
81
+ # Wiki Index
82
+
83
+ (empty — run \`sps wiki update\` to ingest sources)
84
+ `;
85
+ const OVERVIEW_TEMPLATE = (projectName, today) => `---
86
+ type: meta
87
+ title: ${projectName} Overview
88
+ created: ${today}
89
+ updated: ${today}
90
+ ---
91
+
92
+ # ${projectName} Overview
93
+
94
+ (autogenerated summary — first \`sps wiki update\` will fill this in)
95
+ `;
96
+ const HOT_PLACEHOLDER = `---
97
+ type: meta
98
+ title: Hot Cache
99
+ updated: 1970-01-01T00:00:00Z
100
+ ---
101
+
102
+ # Recent Context
103
+
104
+ ## Last Updated
105
+ (尚无活动。第一次 \`sps wiki update\` 或卡片完成后会自动填充。)
106
+
107
+ ## Key Recent Facts
108
+ (none yet)
109
+
110
+ ## Recent Changes
111
+ (none yet)
112
+
113
+ ## Active Threads
114
+ (none yet)
115
+ `;
116
+ /**
117
+ * Idempotently scaffold wiki/ in the given repo.
118
+ *
119
+ * Creates:
120
+ * - wiki/, wiki/{modules,concepts,decisions,lessons,sources}/, wiki/.raw/, wiki/_attachments/
121
+ * - wiki/WIKI.md, wiki/index.md, wiki/overview.md, wiki/.hot.md
122
+ * - .gitignore: appends per-instance drift entries if missing
123
+ *
124
+ * Does NOT write .manifest.json — that's owned by `sps wiki update` and is empty
125
+ * until first ingest.
126
+ *
127
+ * Re-runnable safely: existing files are preserved.
128
+ */
129
+ export function initWiki(repoDir, opts) {
130
+ const today = opts.today ?? new Date().toISOString().slice(0, 10);
131
+ const created = [];
132
+ const filesWritten = [];
133
+ const filesSkipped = [];
134
+ // 1) Subdirs
135
+ const dirs = [
136
+ wikiDir(repoDir),
137
+ ...ALL_PAGE_TYPES.map((t) => wikiPageDir(repoDir, t)),
138
+ wikiRawDir(repoDir),
139
+ wikiAttachmentsDir(repoDir),
140
+ ];
141
+ for (const d of dirs) {
142
+ if (!existsSync(d)) {
143
+ mkdirSync(d, { recursive: true });
144
+ created.push(d);
145
+ }
146
+ }
147
+ // 2) Templated files (don't overwrite existing user content)
148
+ const templated = [
149
+ [wikiMetaFile(repoDir), wikiMetaTemplate(opts.projectName, today)],
150
+ [wikiIndexFile(repoDir), INDEX_PLACEHOLDER],
151
+ [wikiOverviewFile(repoDir), OVERVIEW_TEMPLATE(opts.projectName, today)],
152
+ [wikiHotFile(repoDir), HOT_PLACEHOLDER],
153
+ ];
154
+ for (const [path, body] of templated) {
155
+ if (existsSync(path)) {
156
+ filesSkipped.push(path);
157
+ }
158
+ else {
159
+ writeFileSync(path, body, { encoding: 'utf-8', mode: 0o644 });
160
+ filesWritten.push(path);
161
+ }
162
+ }
163
+ // 3) .gitignore — only append missing lines
164
+ let gitignoreUpdated = false;
165
+ if (!opts.skipGitignore) {
166
+ gitignoreUpdated = ensureGitignoreEntries(repoDir);
167
+ }
168
+ return {
169
+ wikiDir: wikiDir(repoDir),
170
+ created,
171
+ filesWritten,
172
+ filesSkipped,
173
+ gitignoreUpdated,
174
+ };
175
+ }
176
+ /**
177
+ * Append wiki drift entries to .gitignore if missing.
178
+ *
179
+ * Returns true when at least one line was appended.
180
+ * Creates .gitignore if not present.
181
+ */
182
+ export function ensureGitignoreEntries(repoDir) {
183
+ const gitignorePath = resolve(repoDir, '.gitignore');
184
+ let existing = '';
185
+ if (existsSync(gitignorePath)) {
186
+ existing = readFileSync(gitignorePath, 'utf-8');
187
+ }
188
+ const existingLines = new Set(existing.split('\n').map((l) => l.trim()));
189
+ const toAppend = [];
190
+ for (const line of GITIGNORE_LINES) {
191
+ // skip comment header if any drift line already present (we don't want to
192
+ // re-add the comment if user organized their .gitignore)
193
+ if (line.startsWith('#'))
194
+ continue;
195
+ if (!existingLines.has(line)) {
196
+ toAppend.push(line);
197
+ }
198
+ }
199
+ if (toAppend.length === 0)
200
+ return false;
201
+ // If .gitignore is brand new or empty, include the comment header.
202
+ const block = [];
203
+ const needsHeader = !existing.includes('# Wiki per-instance drift');
204
+ if (existing.length > 0 && !existing.endsWith('\n'))
205
+ block.push('');
206
+ if (existing.length > 0)
207
+ block.push('');
208
+ if (needsHeader)
209
+ block.push(GITIGNORE_LINES[0]);
210
+ block.push(...toAppend);
211
+ block.push(''); // trailing newline
212
+ if (existsSync(gitignorePath)) {
213
+ appendFileSync(gitignorePath, block.join('\n'), { encoding: 'utf-8' });
214
+ }
215
+ else {
216
+ writeFileSync(gitignorePath, block.join('\n').replace(/^\n+/, ''), {
217
+ encoding: 'utf-8',
218
+ mode: 0o644,
219
+ });
220
+ }
221
+ return true;
222
+ }
223
+ //# sourceMappingURL=scaffold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../../../src/core/wiki/scaffold.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @module core/wiki/searcher
3
+ * @description Wiki BM25F 全文检索(field-weighted Best Matching 25)
4
+ *
5
+ * @layer core
6
+ *
7
+ * 实现选择:
8
+ * - **不**用 lunr.js / minisearch / fuse.js —— 多一个 50KB 依赖不值得
9
+ * - **不**做词干提取(stemming)—— SPS 是技术文档库,词形变化少;stemming
10
+ * 反而把 "Pipeline" 和 "PipelineService" 合并成同一 token,丢辨别力
11
+ * - **不**做向量 embedding —— v0 BM25 够用;v1 视召回率决定
12
+ *
13
+ * BM25F 通过 field tiling 实现:title 3x / tags 2x / tldr 2x / body 1x
14
+ * 加权方式 = 该 field 内 token 重复 n 次(等价于 BM25F 的 boosting)。
15
+ *
16
+ * 中文支持:ASCII 按词切;中文按字切(朴素 unigram)。够用且无外部依赖。
17
+ *
18
+ * 数据规模:项目 wiki 一般 < 1000 page,~MB 级 corpus。in-memory 索引
19
+ * 占用几 MB,构建 < 100ms,查询 < 10ms。不需要持久化(每次进程启动重建)。
20
+ */
21
+ import type { Page, PageType } from './types.js';
22
+ export interface BM25Options {
23
+ /** 词频饱和参数;标准值 1.2-2.0 */
24
+ k1?: number;
25
+ /** 长度归一化;0=关 / 0.75=标准 / 1=最大归一 */
26
+ b?: number;
27
+ /** 各字段权重 */
28
+ fieldWeights?: {
29
+ title?: number;
30
+ tags?: number;
31
+ tldr?: number;
32
+ body?: number;
33
+ };
34
+ }
35
+ export declare function tokenize(text: string): string[];
36
+ export interface IndexedDoc {
37
+ readonly pageId: string;
38
+ readonly title: string;
39
+ readonly tags: readonly string[];
40
+ /** 第一段(## TL;DR 之后到下个 ## 之前);用作字段权重 + 返回值预览 */
41
+ readonly tldr: string;
42
+ /** Body 剩余部分(除 TL;DR 之外) */
43
+ readonly body: string;
44
+ readonly type: PageType;
45
+ }
46
+ /**
47
+ * 从 Page 对象抽 IndexedDoc。
48
+ * TL;DR 提取规则:找 `## TL;DR\n...\n##` 之间的内容;找不到取 body 前 200 字符。
49
+ */
50
+ export declare function pageToIndexed(page: Page): IndexedDoc;
51
+ export declare function extractTLDR(body: string): string;
52
+ export interface SearchResult {
53
+ readonly pageId: string;
54
+ readonly score: number;
55
+ }
56
+ export declare class WikiSearcher {
57
+ readonly docs: readonly IndexedDoc[];
58
+ private readonly opts;
59
+ private readonly postings;
60
+ private readonly docLengths;
61
+ private readonly avgDocLength;
62
+ constructor(docs: readonly IndexedDoc[], opts?: BM25Options);
63
+ /**
64
+ * 全字段加权的 token stream。同一个 token 在 title 出现一次 = title weight 个副本。
65
+ */
66
+ private tokenizeDoc;
67
+ search(query: string, limit?: number): SearchResult[];
68
+ /** 按 tag 集合过滤(OR 语义)—— 用于 reader.ts 的 skill-match layer */
69
+ searchByTags(tags: readonly string[], limit?: number): SearchResult[];
70
+ /** 按 type 列出(不打分;reader.ts 排序用) */
71
+ byType(type: PageType): IndexedDoc[];
72
+ }
73
+ //# sourceMappingURL=searcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"searcher.d.ts","sourceRoot":"","sources":["../../../src/core/wiki/searcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAIjD,MAAM,WAAW,WAAW;IAC1B,yBAAyB;IACzB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,YAAY;IACZ,YAAY,CAAC,EAAE;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;CACH;AAqED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAW/C;AAID,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,+CAA+C;IAC/C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4BAA4B;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;CACzB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,UAAU,CASpD;AAID,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMhD;AAID,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAID,qBAAa,YAAY;aASL,IAAI,EAAE,SAAS,UAAU,EAAE;IAR7C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAEnB;IACF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0C;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;gBAGpB,IAAI,EAAE,SAAS,UAAU,EAAE,EAC3C,IAAI,GAAE,WAAgB;IAqBxB;;OAEG;IACH,OAAO,CAAC,WAAW;IAUnB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,YAAY,EAAE;IA2BjD,2DAA2D;IAC3D,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,SAAK,GAAG,YAAY,EAAE;IAejE,mCAAmC;IACnC,MAAM,CAAC,IAAI,EAAE,QAAQ,GAAG,UAAU,EAAE;CAGrC"}