gsd-pi 2.38.0-dev.add4f78 → 2.38.0-dev.d533afb

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 (117) hide show
  1. package/dist/resource-loader.js +34 -1
  2. package/dist/resources/extensions/github-sync/cli.js +284 -0
  3. package/dist/resources/extensions/github-sync/index.js +73 -0
  4. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  5. package/dist/resources/extensions/github-sync/sync.js +424 -0
  6. package/dist/resources/extensions/github-sync/templates.js +118 -0
  7. package/dist/resources/extensions/github-sync/types.js +7 -0
  8. package/dist/resources/extensions/gsd/auto/session.js +3 -23
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  10. package/dist/resources/extensions/gsd/auto-loop.js +292 -263
  11. package/dist/resources/extensions/gsd/auto-post-unit.js +28 -3
  12. package/dist/resources/extensions/gsd/auto-prompts.js +23 -43
  13. package/dist/resources/extensions/gsd/auto-start.js +7 -1
  14. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  15. package/dist/resources/extensions/gsd/auto.js +143 -80
  16. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  17. package/dist/resources/extensions/gsd/commands.js +2 -1
  18. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  19. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  20. package/dist/resources/extensions/gsd/doctor-providers.js +27 -11
  21. package/dist/resources/extensions/gsd/doctor.js +20 -1
  22. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  23. package/dist/resources/extensions/gsd/files.js +4 -0
  24. package/dist/resources/extensions/gsd/git-service.js +15 -12
  25. package/dist/resources/extensions/gsd/guided-flow.js +82 -32
  26. package/dist/resources/extensions/gsd/index.js +22 -19
  27. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  28. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  29. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  30. package/dist/resources/extensions/gsd/preferences-validation.js +58 -10
  31. package/dist/resources/extensions/gsd/preferences.js +4 -2
  32. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  33. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  34. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  35. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  36. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  37. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  38. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  39. package/dist/resources/extensions/gsd/prompts/run-uat.md +27 -10
  40. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  41. package/dist/resources/extensions/gsd/repo-identity.js +19 -3
  42. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  43. package/dist/resources/extensions/mcp-client/index.js +14 -1
  44. package/package.json +1 -1
  45. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  46. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  47. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  48. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  49. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  50. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  51. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  52. package/src/resources/extensions/github-sync/cli.ts +364 -0
  53. package/src/resources/extensions/github-sync/index.ts +93 -0
  54. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  55. package/src/resources/extensions/github-sync/sync.ts +556 -0
  56. package/src/resources/extensions/github-sync/templates.ts +183 -0
  57. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  58. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  59. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  60. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  61. package/src/resources/extensions/github-sync/types.ts +47 -0
  62. package/src/resources/extensions/gsd/auto/session.ts +3 -25
  63. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  64. package/src/resources/extensions/gsd/auto-loop.ts +382 -360
  65. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  66. package/src/resources/extensions/gsd/auto-prompts.ts +25 -45
  67. package/src/resources/extensions/gsd/auto-start.ts +11 -1
  68. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  69. package/src/resources/extensions/gsd/auto.ts +139 -86
  70. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  71. package/src/resources/extensions/gsd/commands.ts +2 -2
  72. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  73. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  74. package/src/resources/extensions/gsd/doctor-providers.ts +26 -9
  75. package/src/resources/extensions/gsd/doctor.ts +22 -1
  76. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  77. package/src/resources/extensions/gsd/files.ts +3 -1
  78. package/src/resources/extensions/gsd/git-service.ts +20 -10
  79. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  80. package/src/resources/extensions/gsd/index.ts +21 -16
  81. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  82. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  83. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  84. package/src/resources/extensions/gsd/preferences-validation.ts +50 -10
  85. package/src/resources/extensions/gsd/preferences.ts +3 -2
  86. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  87. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  88. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  89. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  90. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  91. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  92. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  93. package/src/resources/extensions/gsd/prompts/run-uat.md +27 -10
  94. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  95. package/src/resources/extensions/gsd/repo-identity.ts +20 -3
  96. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  97. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  98. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  99. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  100. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  101. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  102. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  103. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  104. package/src/resources/extensions/gsd/types.ts +0 -1
  105. package/src/resources/extensions/mcp-client/index.ts +17 -1
  106. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  107. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  108. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  109. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  110. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  111. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  112. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  113. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  114. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  115. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  116. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  117. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -1,254 +0,0 @@
1
- // GSD Extension — Semantic Chunker with TF-IDF Relevance Scoring
2
- // Splits code/text into semantic chunks and selects the most relevant ones for a given task.
3
- // Pure TypeScript — no external dependencies.
4
- // ─── Constants ──────────────────────────────────────────────────────────────
5
- const CODE_BOUNDARY_RE = /^(export\s+)?(async\s+)?(function|class|interface|type|const|enum)\s/;
6
- const MARKDOWN_HEADING_RE = /^#{1,6}\s/;
7
- const STOP_WORDS = new Set([
8
- "the", "a", "an", "is", "are", "was", "were", "be", "to", "of", "in",
9
- "for", "on", "with", "at", "by", "from", "this", "that", "it", "as",
10
- "or", "and", "not", "but", "if", "do", "no", "so", "up", "its", "has",
11
- "had", "get", "set", "can", "may", "all", "use", "new", "one", "two",
12
- "also", "each", "than", "been", "into", "most", "only", "over", "such",
13
- "how", "some", "any", "our", "his", "her", "out", "did", "let", "say", "she",
14
- ]);
15
- const DEFAULT_MIN_LINES = 3;
16
- const DEFAULT_MAX_LINES = 80;
17
- const DEFAULT_MAX_CHUNKS = 5;
18
- const DEFAULT_MIN_SCORE = 0.1;
19
- function detectContentType(lines) {
20
- let codeSignals = 0;
21
- let mdSignals = 0;
22
- const sampleSize = Math.min(lines.length, 50);
23
- for (let i = 0; i < sampleSize; i++) {
24
- const line = lines[i];
25
- if (CODE_BOUNDARY_RE.test(line) || /^\s*import\s/.test(line)) {
26
- codeSignals++;
27
- }
28
- if (MARKDOWN_HEADING_RE.test(line)) {
29
- mdSignals++;
30
- }
31
- }
32
- if (mdSignals >= 2 && mdSignals > codeSignals)
33
- return "markdown";
34
- if (codeSignals >= 2)
35
- return "code";
36
- return "text";
37
- }
38
- // ─── Tokenizer ──────────────────────────────────────────────────────────────
39
- function tokenize(text) {
40
- return text
41
- .toLowerCase()
42
- .split(/[\s\W]+/)
43
- .filter((w) => w.length >= 2 && !STOP_WORDS.has(w));
44
- }
45
- // ─── splitIntoChunks ────────────────────────────────────────────────────────
46
- export function splitIntoChunks(content, options) {
47
- if (!content || content.trim().length === 0)
48
- return [];
49
- const minLines = options?.minLines ?? DEFAULT_MIN_LINES;
50
- const maxLines = options?.maxLines ?? DEFAULT_MAX_LINES;
51
- const lines = content.split("\n");
52
- if (lines.length === 0)
53
- return [];
54
- const contentType = detectContentType(lines);
55
- let boundaries;
56
- switch (contentType) {
57
- case "code":
58
- boundaries = findCodeBoundaries(lines);
59
- break;
60
- case "markdown":
61
- boundaries = findMarkdownBoundaries(lines);
62
- break;
63
- default:
64
- boundaries = findTextBoundaries(lines);
65
- break;
66
- }
67
- // Always include 0 as first boundary
68
- if (boundaries.length === 0 || boundaries[0] !== 0) {
69
- boundaries.unshift(0);
70
- }
71
- // Build raw chunks from boundaries
72
- const rawChunks = [];
73
- for (let i = 0; i < boundaries.length; i++) {
74
- const start = boundaries[i];
75
- const end = i + 1 < boundaries.length ? boundaries[i + 1] - 1 : lines.length - 1;
76
- const chunkLines = lines.slice(start, end + 1);
77
- rawChunks.push({
78
- content: chunkLines.join("\n"),
79
- startLine: start + 1, // 1-based
80
- endLine: end + 1, // 1-based
81
- score: 0,
82
- });
83
- }
84
- // Split oversized chunks at maxLines
85
- const splitChunks = [];
86
- for (const chunk of rawChunks) {
87
- const chunkLineCount = chunk.endLine - chunk.startLine + 1;
88
- if (chunkLineCount <= maxLines) {
89
- splitChunks.push(chunk);
90
- }
91
- else {
92
- const chunkLines = chunk.content.split("\n");
93
- for (let offset = 0; offset < chunkLines.length; offset += maxLines) {
94
- const slice = chunkLines.slice(offset, offset + maxLines);
95
- splitChunks.push({
96
- content: slice.join("\n"),
97
- startLine: chunk.startLine + offset,
98
- endLine: chunk.startLine + offset + slice.length - 1,
99
- score: 0,
100
- });
101
- }
102
- }
103
- }
104
- // Merge tiny chunks into predecessor
105
- const merged = [];
106
- for (const chunk of splitChunks) {
107
- const chunkLineCount = chunk.endLine - chunk.startLine + 1;
108
- if (chunkLineCount < minLines && merged.length > 0) {
109
- const prev = merged[merged.length - 1];
110
- prev.content += "\n" + chunk.content;
111
- prev.endLine = chunk.endLine;
112
- }
113
- else {
114
- merged.push({ ...chunk });
115
- }
116
- }
117
- return merged;
118
- }
119
- function findCodeBoundaries(lines) {
120
- const boundaries = [];
121
- for (let i = 0; i < lines.length; i++) {
122
- if (CODE_BOUNDARY_RE.test(lines[i])) {
123
- // Also consider a blank line before a boundary marker
124
- if (i > 0 && lines[i - 1].trim() === "" && !boundaries.includes(i)) {
125
- boundaries.push(i);
126
- }
127
- else if (!boundaries.includes(i)) {
128
- boundaries.push(i);
129
- }
130
- }
131
- }
132
- return boundaries;
133
- }
134
- function findMarkdownBoundaries(lines) {
135
- const boundaries = [];
136
- for (let i = 0; i < lines.length; i++) {
137
- if (MARKDOWN_HEADING_RE.test(lines[i])) {
138
- boundaries.push(i);
139
- }
140
- }
141
- return boundaries;
142
- }
143
- function findTextBoundaries(lines) {
144
- const boundaries = [0];
145
- for (let i = 1; i < lines.length; i++) {
146
- if (lines[i - 1].trim() === "" && lines[i].trim() !== "") {
147
- boundaries.push(i);
148
- }
149
- }
150
- return boundaries;
151
- }
152
- // ─── scoreChunks ────────────────────────────────────────────────────────────
153
- export function scoreChunks(chunks, query) {
154
- if (chunks.length === 0)
155
- return [];
156
- const queryTerms = tokenize(query);
157
- if (queryTerms.length === 0) {
158
- return chunks.map((c) => ({ ...c, score: 0 }));
159
- }
160
- const totalChunks = chunks.length;
161
- // Pre-compute IDF for each query term
162
- const termChunkCounts = new Map();
163
- const chunkTokenSets = [];
164
- for (const chunk of chunks) {
165
- const tokens = new Set(tokenize(chunk.content));
166
- chunkTokenSets.push(tokens);
167
- for (const term of queryTerms) {
168
- if (tokens.has(term)) {
169
- termChunkCounts.set(term, (termChunkCounts.get(term) ?? 0) + 1);
170
- }
171
- }
172
- }
173
- const idf = new Map();
174
- for (const term of queryTerms) {
175
- const df = termChunkCounts.get(term) ?? 0;
176
- idf.set(term, Math.log(1 + totalChunks / (1 + df)));
177
- }
178
- // Score each chunk
179
- const scored = chunks.map((chunk, idx) => {
180
- const chunkTokens = tokenize(chunk.content);
181
- const totalTerms = chunkTokens.length;
182
- if (totalTerms === 0)
183
- return { ...chunk, score: 0 };
184
- // Count term frequencies
185
- const termFreq = new Map();
186
- for (const token of chunkTokens) {
187
- termFreq.set(token, (termFreq.get(token) ?? 0) + 1);
188
- }
189
- let score = 0;
190
- for (const term of queryTerms) {
191
- const tf = (termFreq.get(term) ?? 0) / totalTerms;
192
- const termIdf = idf.get(term) ?? 0;
193
- score += tf * termIdf;
194
- }
195
- return { ...chunk, score };
196
- });
197
- // Normalize to 0-1
198
- const maxScore = Math.max(...scored.map((c) => c.score));
199
- if (maxScore > 0) {
200
- for (const chunk of scored) {
201
- chunk.score = chunk.score / maxScore;
202
- }
203
- }
204
- return scored;
205
- }
206
- // ─── chunkByRelevance ───────────────────────────────────────────────────────
207
- export function chunkByRelevance(content, query, options) {
208
- const maxChunks = options?.maxChunks ?? DEFAULT_MAX_CHUNKS;
209
- const minScore = options?.minScore ?? DEFAULT_MIN_SCORE;
210
- const minLines = options?.minChunkLines ?? DEFAULT_MIN_LINES;
211
- const maxLines = options?.maxChunkLines ?? DEFAULT_MAX_LINES;
212
- const rawChunks = splitIntoChunks(content, { minLines, maxLines });
213
- if (rawChunks.length === 0) {
214
- return { chunks: [], totalChunks: 0, omittedChunks: 0, savingsPercent: 0 };
215
- }
216
- const scored = scoreChunks(rawChunks, query);
217
- // Filter by minScore and take top maxChunks by score
218
- const qualifying = scored
219
- .filter((c) => c.score >= minScore)
220
- .sort((a, b) => b.score - a.score)
221
- .slice(0, maxChunks);
222
- // Return in original document order (by startLine)
223
- const selected = qualifying.sort((a, b) => a.startLine - b.startLine);
224
- const totalChars = content.length;
225
- const selectedChars = selected.reduce((sum, c) => sum + c.content.length, 0);
226
- const savingsPercent = totalChars > 0
227
- ? Math.round(((totalChars - selectedChars) / totalChars) * 100)
228
- : 0;
229
- return {
230
- chunks: selected,
231
- totalChunks: rawChunks.length,
232
- omittedChunks: rawChunks.length - selected.length,
233
- savingsPercent: Math.max(0, savingsPercent),
234
- };
235
- }
236
- // ─── formatChunks ───────────────────────────────────────────────────────────
237
- export function formatChunks(result, filePath) {
238
- if (result.chunks.length === 0) {
239
- return `[${filePath}: empty or no relevant chunks]`;
240
- }
241
- const parts = [];
242
- let lastEndLine = 0;
243
- for (const chunk of result.chunks) {
244
- // Show omission gap
245
- if (lastEndLine > 0 && chunk.startLine > lastEndLine + 1) {
246
- const gapLines = chunk.startLine - lastEndLine - 1;
247
- parts.push(`[...${gapLines} lines omitted...]`);
248
- }
249
- parts.push(`[Lines ${chunk.startLine}-${chunk.endLine}]`);
250
- parts.push(chunk.content);
251
- lastEndLine = chunk.endLine;
252
- }
253
- return parts.join("\n");
254
- }
@@ -1,212 +0,0 @@
1
- /**
2
- * Summary distiller — extracts essential structured data from SUMMARY.md files,
3
- * dropping verbose prose to save context budget.
4
- */
5
- // ─── Frontmatter parsing ─────────────────────────────────────────────────────
6
- function parseFrontmatter(raw) {
7
- const result = {
8
- id: "",
9
- provides: [],
10
- requires: [],
11
- key_files: [],
12
- key_decisions: [],
13
- patterns_established: [],
14
- };
15
- // Extract frontmatter block between --- markers
16
- const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---/);
17
- if (!fmMatch)
18
- return result;
19
- const fmBlock = fmMatch[1];
20
- const lines = fmBlock.split(/\r?\n/);
21
- let currentKey = null;
22
- for (const line of lines) {
23
- // Scalar value: key: value
24
- const scalarMatch = line.match(/^(\w[\w_]*):\s*(.+)$/);
25
- if (scalarMatch) {
26
- const [, key, value] = scalarMatch;
27
- currentKey = key;
28
- setScalar(result, key, value.trim());
29
- continue;
30
- }
31
- // Array-start key with empty value: key:\n or key: []\n
32
- const arrayStartMatch = line.match(/^(\w[\w_]*):\s*(\[\])?\s*$/);
33
- if (arrayStartMatch) {
34
- currentKey = arrayStartMatch[1];
35
- continue;
36
- }
37
- // Array item: - value
38
- const itemMatch = line.match(/^\s+-\s+(.+)$/);
39
- if (itemMatch && currentKey) {
40
- pushItem(result, currentKey, itemMatch[1].trim());
41
- continue;
42
- }
43
- }
44
- return result;
45
- }
46
- function setScalar(fm, key, value) {
47
- if (key === "id")
48
- fm.id = value;
49
- }
50
- function pushItem(fm, key, value) {
51
- switch (key) {
52
- case "provides":
53
- fm.provides.push(value);
54
- break;
55
- case "requires":
56
- fm.requires.push(value);
57
- break;
58
- case "key_files":
59
- fm.key_files.push(value);
60
- break;
61
- case "key_decisions":
62
- fm.key_decisions.push(value);
63
- break;
64
- case "patterns_established":
65
- fm.patterns_established.push(value);
66
- break;
67
- }
68
- }
69
- // ─── Body parsing ────────────────────────────────────────────────────────────
70
- function extractTitleAndOneLiner(body) {
71
- const lines = body.split(/\r?\n/);
72
- let titleId = "";
73
- let oneLiner = "";
74
- let foundTitle = false;
75
- for (const line of lines) {
76
- const titleMatch = line.match(/^#\s+(\S+):\s*(.*)$/);
77
- if (titleMatch && !foundTitle) {
78
- titleId = titleMatch[1];
79
- // If the title line itself has text after "S01: ", use that as a fallback
80
- if (titleMatch[2].trim()) {
81
- oneLiner = titleMatch[2].trim();
82
- }
83
- foundTitle = true;
84
- continue;
85
- }
86
- // First non-empty line after the title is the one-liner
87
- if (foundTitle && !oneLiner && line.trim() && !line.startsWith("#")) {
88
- oneLiner = line.trim();
89
- break;
90
- }
91
- }
92
- return { id: titleId, oneLiner };
93
- }
94
- function getBodyAfterFrontmatter(raw) {
95
- const fmMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/);
96
- if (fmMatch) {
97
- return raw.slice(fmMatch[0].length);
98
- }
99
- return raw;
100
- }
101
- // ─── Public API ──────────────────────────────────────────────────────────────
102
- /**
103
- * Distill a single SUMMARY.md content string into a compact structured block.
104
- */
105
- export function distillSingle(summary) {
106
- const fm = parseFrontmatter(summary);
107
- const body = getBodyAfterFrontmatter(summary);
108
- const { id: titleId, oneLiner } = extractTitleAndOneLiner(body);
109
- const id = fm.id || titleId || "???";
110
- return formatEntry({
111
- id,
112
- oneLiner,
113
- provides: fm.provides,
114
- requires: fm.requires,
115
- key_files: fm.key_files,
116
- key_decisions: fm.key_decisions,
117
- patterns: fm.patterns_established,
118
- });
119
- }
120
- function formatEntry(entry) {
121
- return formatEntryWithDropLevel(entry, 0);
122
- }
123
- /**
124
- * Format an entry, progressively dropping fields based on dropLevel:
125
- * 0 = full output
126
- * 1 = drop patterns
127
- * 2 = drop patterns + key_decisions
128
- * 3 = drop patterns + key_decisions + key_files
129
- */
130
- function formatEntryWithDropLevel(entry, dropLevel) {
131
- const lines = [];
132
- lines.push(`## ${entry.id}: ${entry.oneLiner}`);
133
- if (entry.provides.length > 0) {
134
- lines.push(`provides: ${entry.provides.join(", ")}`);
135
- }
136
- if (entry.requires.length > 0) {
137
- lines.push(`requires: ${entry.requires.join(", ")}`);
138
- }
139
- if (dropLevel < 3 && entry.key_files.length > 0) {
140
- lines.push(`key_files: ${entry.key_files.join(", ")}`);
141
- }
142
- if (dropLevel < 2 && entry.key_decisions.length > 0) {
143
- lines.push(`key_decisions: ${entry.key_decisions.join(", ")}`);
144
- }
145
- if (dropLevel < 1 && entry.patterns.length > 0) {
146
- lines.push(`patterns: ${entry.patterns.join(", ")}`);
147
- }
148
- return lines.join("\n");
149
- }
150
- /**
151
- * Distill multiple SUMMARY.md contents into a budget-constrained output.
152
- */
153
- export function distillSummaries(summaries, budgetChars) {
154
- const originalChars = summaries.reduce((sum, s) => sum + s.length, 0);
155
- if (summaries.length === 0) {
156
- return {
157
- content: "",
158
- summaryCount: 0,
159
- savingsPercent: 0,
160
- originalChars: 0,
161
- distilledChars: 0,
162
- };
163
- }
164
- // Parse all entries up front
165
- const entries = summaries.map((summary) => {
166
- const fm = parseFrontmatter(summary);
167
- const body = getBodyAfterFrontmatter(summary);
168
- const { id: titleId, oneLiner } = extractTitleAndOneLiner(body);
169
- return {
170
- id: fm.id || titleId || "???",
171
- oneLiner,
172
- provides: fm.provides,
173
- requires: fm.requires,
174
- key_files: fm.key_files,
175
- key_decisions: fm.key_decisions,
176
- patterns: fm.patterns_established,
177
- };
178
- });
179
- // Try progressively more aggressive dropping until it fits
180
- for (let dropLevel = 0; dropLevel <= 3; dropLevel++) {
181
- const blocks = entries.map((e) => formatEntryWithDropLevel(e, dropLevel));
182
- const content = blocks.join("\n\n");
183
- if (content.length <= budgetChars) {
184
- const distilledChars = content.length;
185
- return {
186
- content,
187
- summaryCount: summaries.length,
188
- savingsPercent: originalChars > 0
189
- ? Math.round((1 - distilledChars / originalChars) * 100)
190
- : 0,
191
- originalChars,
192
- distilledChars,
193
- };
194
- }
195
- }
196
- // Even at max drop level it doesn't fit — truncate
197
- const blocks = entries.map((e) => formatEntryWithDropLevel(e, 3));
198
- let content = blocks.join("\n\n");
199
- if (content.length > budgetChars) {
200
- content = content.slice(0, Math.max(0, budgetChars - 15)) + "\n[...truncated]";
201
- }
202
- const distilledChars = content.length;
203
- return {
204
- content,
205
- summaryCount: summaries.length,
206
- savingsPercent: originalChars > 0
207
- ? Math.round((1 - distilledChars / originalChars) * 100)
208
- : 0,
209
- originalChars,
210
- distilledChars,
211
- };
212
- }