@entelligentsia/forgecli 1.0.2 → 1.0.3
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/CHANGELOG.md +23 -0
- package/dist/CHANGELOG-forge-plugin.md +24 -0
- package/dist/extensions/forgecli/audience-gate.js +1 -1
- package/dist/extensions/forgecli/audience-gate.js.map +1 -1
- package/dist/extensions/forgecli/fix-bug.d.ts +1 -2
- package/dist/extensions/forgecli/fix-bug.js +678 -609
- package/dist/extensions/forgecli/fix-bug.js.map +1 -1
- package/dist/extensions/forgecli/forge-artifact-tool.js +15 -3
- package/dist/extensions/forgecli/forge-artifact-tool.js.map +1 -1
- package/dist/extensions/forgecli/forge-subagent.d.ts +17 -0
- package/dist/extensions/forgecli/forge-subagent.js +31 -12
- package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
- package/dist/extensions/forgecli/forge-tools.d.ts +6 -0
- package/dist/extensions/forgecli/forge-tools.js +69 -6
- package/dist/extensions/forgecli/forge-tools.js.map +1 -1
- package/dist/extensions/forgecli/run-task.js +461 -391
- package/dist/extensions/forgecli/run-task.js.map +1 -1
- package/dist/extensions/forgecli/session-registry.d.ts +12 -0
- package/dist/extensions/forgecli/session-registry.js +23 -0
- package/dist/extensions/forgecli/session-registry.js.map +1 -1
- package/dist/extensions/forgecli/subagent/caller-context.d.ts +35 -11
- package/dist/extensions/forgecli/subagent/caller-context.js +49 -21
- package/dist/extensions/forgecli/subagent/caller-context.js.map +1 -1
- package/dist/extensions/forgecli/subagent/orchestrator-transcript.d.ts +66 -0
- package/dist/extensions/forgecli/subagent/orchestrator-transcript.js +66 -0
- package/dist/extensions/forgecli/subagent/orchestrator-transcript.js.map +1 -0
- package/dist/extensions/forgecli/subagent/phase-guard.d.ts +34 -0
- package/dist/extensions/forgecli/subagent/phase-guard.js +139 -0
- package/dist/extensions/forgecli/subagent/phase-guard.js.map +1 -0
- package/dist/extensions/forgecli/subagent/phase-summary-map.d.ts +1 -0
- package/dist/extensions/forgecli/subagent/phase-summary-map.js +22 -0
- package/dist/extensions/forgecli/subagent/phase-summary-map.js.map +1 -0
- package/dist/extensions/forgecli/thread-switcher.js +2 -2
- package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
- package/dist/extensions/forgecli/viewport-events.d.ts +4 -0
- package/dist/extensions/forgecli/viewport-events.js +18 -1
- package/dist/extensions/forgecli/viewport-events.js.map +1 -1
- package/dist/extensions/forgecli/viewport-renderer.d.ts +12 -2
- package/dist/extensions/forgecli/viewport-renderer.js +8 -6
- package/dist/extensions/forgecli/viewport-renderer.js.map +1 -1
- package/dist/forge-payload/.base-pack/workflows/fix_bug.md +10 -28
- package/dist/forge-payload/.base-pack/workflows/triage.md +190 -0
- package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
- package/dist/forge-payload/.schemas/enum-catalog.json +1 -1
- package/dist/forge-payload/.schemas/migrations.json +9 -0
- package/dist/forge-payload/integrity.json +3 -3
- package/dist/forge-payload/meta/fragments/tool-discipline.md +21 -2
- package/dist/forge-payload/meta/workflows/meta-bug-triage.md +210 -0
- package/dist/forge-payload/meta/workflows/meta-fix-bug.md +10 -28
- package/dist/forge-payload/schemas/enum-catalog.json +1 -1
- package/dist/forge-payload/schemas/structure-manifest.json +20 -1
- package/dist/forge-payload/tools/artifact.cjs +34 -5
- package/node_modules/@entelligentsia/forge-compress/dist/compressor.d.ts +6 -0
- package/node_modules/@entelligentsia/forge-compress/dist/compressor.js +137 -0
- package/node_modules/@entelligentsia/forge-compress/dist/entropy.d.ts +3 -0
- package/node_modules/@entelligentsia/forge-compress/dist/entropy.js +99 -0
- package/node_modules/@entelligentsia/forge-compress/dist/forge/entity.d.ts +8 -0
- package/node_modules/@entelligentsia/forge-compress/dist/forge/entity.js +149 -0
- package/node_modules/@entelligentsia/forge-compress/dist/forge/index.d.ts +7 -0
- package/node_modules/@entelligentsia/forge-compress/dist/forge/index.js +4 -0
- package/node_modules/@entelligentsia/forge-compress/dist/forge/markdown.d.ts +5 -0
- package/node_modules/@entelligentsia/forge-compress/dist/forge/markdown.js +92 -0
- package/node_modules/@entelligentsia/forge-compress/dist/forge/query.d.ts +7 -0
- package/node_modules/@entelligentsia/forge-compress/dist/forge/query.js +60 -0
- package/node_modules/@entelligentsia/forge-compress/dist/forge/validate.d.ts +1 -0
- package/node_modules/@entelligentsia/forge-compress/dist/forge/validate.js +82 -0
- package/node_modules/@entelligentsia/forge-compress/dist/index.d.ts +6 -0
- package/node_modules/@entelligentsia/forge-compress/dist/index.js +5 -0
- package/node_modules/@entelligentsia/forge-compress/dist/progressive.d.ts +1 -0
- package/node_modules/@entelligentsia/forge-compress/dist/progressive.js +108 -0
- package/node_modules/@entelligentsia/forge-compress/dist/strip.d.ts +4 -0
- package/node_modules/@entelligentsia/forge-compress/dist/strip.js +55 -0
- package/node_modules/@entelligentsia/forge-compress/dist/tokens.d.ts +2 -0
- package/node_modules/@entelligentsia/forge-compress/dist/tokens.js +17 -0
- package/node_modules/@entelligentsia/forge-compress/package.json +45 -0
- package/node_modules/@entelligentsia/forge-compress/src/__tests__/compress.test.ts +409 -0
- package/node_modules/@entelligentsia/forge-compress/src/compressor.ts +147 -0
- package/node_modules/@entelligentsia/forge-compress/src/entropy.ts +105 -0
- package/node_modules/@entelligentsia/forge-compress/src/forge/entity.ts +184 -0
- package/node_modules/@entelligentsia/forge-compress/src/forge/index.ts +10 -0
- package/node_modules/@entelligentsia/forge-compress/src/forge/markdown.ts +122 -0
- package/node_modules/@entelligentsia/forge-compress/src/forge/query.ts +105 -0
- package/node_modules/@entelligentsia/forge-compress/src/forge/validate.ts +86 -0
- package/node_modules/@entelligentsia/forge-compress/src/index.ts +22 -0
- package/node_modules/@entelligentsia/forge-compress/src/progressive.ts +123 -0
- package/node_modules/@entelligentsia/forge-compress/src/strip.ts +58 -0
- package/node_modules/@entelligentsia/forge-compress/src/tokens.ts +19 -0
- package/package.json +5 -10
|
@@ -29,6 +29,9 @@ const ARTIFACT_CATALOG = {
|
|
|
29
29
|
'triage': { filename: 'TRIAGE.md', type: 'md' },
|
|
30
30
|
'bug-report': { filename: 'BUG_REPORT.md', type: 'md' },
|
|
31
31
|
'index': { filename: 'INDEX.md', type: 'md' },
|
|
32
|
+
'task-prompt': { filename: 'TASK_PROMPT.md', type: 'md' },
|
|
33
|
+
'sprint-requirements': { filename: 'SPRINT_REQUIREMENTS.md', type: 'md' },
|
|
34
|
+
'sprint-completion-review': { filename: 'SPRINT_COMPLETION_REVIEW.md', type: 'md' },
|
|
32
35
|
'cost-report': { filename: 'COST_REPORT.md', type: 'md' },
|
|
33
36
|
'timesheet': { filename: 'TIMESHEET.md', type: 'md' },
|
|
34
37
|
'plan-summary': { filename: 'PLAN-SUMMARY.json', type: 'json' },
|
|
@@ -44,6 +47,25 @@ const ARTIFACT_CATALOG = {
|
|
|
44
47
|
'collation-summary': { filename: 'COLLATION-SUMMARY.json', type: 'json' },
|
|
45
48
|
};
|
|
46
49
|
|
|
50
|
+
// Per-entity filename overrides. Bug-mode plans and plan-summaries use the
|
|
51
|
+
// BUG_FIX_PLAN prefix to match the long-standing forge convention and the
|
|
52
|
+
// preflight-gate.cjs expectations for review-plan in bug mode. Without this
|
|
53
|
+
// override, plan-fix (routed via plan_task.md post FORGE-BUG-040) writes
|
|
54
|
+
// PLAN.md and review-plan preflight then fails "artifact missing:
|
|
55
|
+
// BUG_FIX_PLAN.md" — see FORGE-BUG-041.
|
|
56
|
+
const ARTIFACT_FILENAME_OVERRIDES = {
|
|
57
|
+
bug: {
|
|
58
|
+
'plan': 'BUG_FIX_PLAN.md',
|
|
59
|
+
'plan-summary': 'BUG-FIX-PLAN-SUMMARY.json',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
function resolveArtifactFilename(entity, artifactName) {
|
|
64
|
+
const override = ARTIFACT_FILENAME_OVERRIDES[entity];
|
|
65
|
+
if (override && override[artifactName]) return override[artifactName];
|
|
66
|
+
return ARTIFACT_CATALOG[artifactName].filename;
|
|
67
|
+
}
|
|
68
|
+
|
|
47
69
|
const ARTIFACT_NAMES = Object.keys(ARTIFACT_CATALOG).sort();
|
|
48
70
|
|
|
49
71
|
// ── Summary JSON validation ──────────────────────────────────────────────────
|
|
@@ -200,7 +222,13 @@ if (subcmd === 'list') {
|
|
|
200
222
|
const known = [];
|
|
201
223
|
const other = [];
|
|
202
224
|
for (const f of files) {
|
|
203
|
-
|
|
225
|
+
// Recognise the per-entity overrides as well, so e.g. BUG_FIX_PLAN.md
|
|
226
|
+
// surfaces as the `plan` artifact in bug-mode listings.
|
|
227
|
+
const overrideEntry = ARTIFACT_FILENAME_OVERRIDES[entity]
|
|
228
|
+
? Object.entries(ARTIFACT_FILENAME_OVERRIDES[entity]).find(([, fn]) => fn === f)
|
|
229
|
+
: undefined;
|
|
230
|
+
const catalogEntry = overrideEntry
|
|
231
|
+
|| Object.entries(ARTIFACT_CATALOG).find(([, v]) => v.filename === f);
|
|
204
232
|
if (catalogEntry) {
|
|
205
233
|
known.push(` ${catalogEntry[0]} → ${f}`);
|
|
206
234
|
} else {
|
|
@@ -233,13 +261,14 @@ if (!catalogEntry) {
|
|
|
233
261
|
process.exit(1);
|
|
234
262
|
}
|
|
235
263
|
|
|
236
|
-
const
|
|
264
|
+
const resolvedFilename = resolveArtifactFilename(entity, artifactName);
|
|
265
|
+
const filePath = path.join(absDir, resolvedFilename);
|
|
237
266
|
|
|
238
267
|
// ── read ─────────────────────────────────────────────────────────────────────
|
|
239
268
|
|
|
240
269
|
if (subcmd === 'read') {
|
|
241
270
|
if (!fs.existsSync(filePath)) {
|
|
242
|
-
process.stderr.write(`Artifact not found: ${path.join(entityDir,
|
|
271
|
+
process.stderr.write(`Artifact not found: ${path.join(entityDir, resolvedFilename)}\n`);
|
|
243
272
|
process.exit(2);
|
|
244
273
|
}
|
|
245
274
|
process.stdout.write(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -272,7 +301,7 @@ if (subcmd === 'write') {
|
|
|
272
301
|
const validationError = validateSummaryJson(content);
|
|
273
302
|
if (validationError) {
|
|
274
303
|
process.stderr.write(
|
|
275
|
-
`Summary validation failed for ${
|
|
304
|
+
`Summary validation failed for ${resolvedFilename}: ${validationError}. ` +
|
|
276
305
|
`Required fields: ${SUMMARY_REQUIRED.join(', ')}.\n`
|
|
277
306
|
);
|
|
278
307
|
process.exit(1);
|
|
@@ -282,7 +311,7 @@ if (subcmd === 'write') {
|
|
|
282
311
|
fs.mkdirSync(absDir, { recursive: true });
|
|
283
312
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
284
313
|
process.stdout.write(
|
|
285
|
-
`Wrote ${Buffer.byteLength(content, 'utf8')} bytes to ${path.join(entityDir,
|
|
314
|
+
`Wrote ${Buffer.byteLength(content, 'utf8')} bytes to ${path.join(entityDir, resolvedFilename)}\n`
|
|
286
315
|
);
|
|
287
316
|
process.exit(0);
|
|
288
317
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { stripAnsi } from "./strip.js";
|
|
2
|
+
export declare function lightweightCleanup(content: string): string;
|
|
3
|
+
export declare function verbatimCompact(text: string): string;
|
|
4
|
+
export declare function aggressiveCompress(content: string, ext?: string): string;
|
|
5
|
+
export declare function safeguardRatio(original: string, compressed: string): string;
|
|
6
|
+
export { stripAnsi };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { countTokens } from "./tokens.js";
|
|
2
|
+
import { stripAnsi, stripTimestampsAndHashes, normalizeWhitespace, isBoilerplate, } from "./strip.js";
|
|
3
|
+
export function lightweightCleanup(content) {
|
|
4
|
+
const lines = content.split("\n");
|
|
5
|
+
const total = lines.length;
|
|
6
|
+
const result = [];
|
|
7
|
+
let blankCount = 0;
|
|
8
|
+
let braceRun = [];
|
|
9
|
+
const flushBraceRun = () => {
|
|
10
|
+
if (total <= 200 || braceRun.length <= 5) {
|
|
11
|
+
result.push(...braceRun);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
result.push(braceRun[0], braceRun[1]);
|
|
15
|
+
result.push(`[${braceRun.length - 2} brace-only lines collapsed]`);
|
|
16
|
+
}
|
|
17
|
+
braceRun = [];
|
|
18
|
+
};
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed) {
|
|
22
|
+
flushBraceRun();
|
|
23
|
+
blankCount++;
|
|
24
|
+
if (blankCount <= 1)
|
|
25
|
+
result.push("");
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
blankCount = 0;
|
|
29
|
+
if (/^[}\]);]+$/.test(trimmed)) {
|
|
30
|
+
braceRun.push(trimmed);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
flushBraceRun();
|
|
34
|
+
result.push(line);
|
|
35
|
+
}
|
|
36
|
+
flushBraceRun();
|
|
37
|
+
return result.join("\n");
|
|
38
|
+
}
|
|
39
|
+
export function verbatimCompact(text) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
let blankCount = 0;
|
|
42
|
+
let prevLine = null;
|
|
43
|
+
let repeatCount = 0;
|
|
44
|
+
const flushRepeats = () => {
|
|
45
|
+
if (repeatCount > 1 && prevLine !== null) {
|
|
46
|
+
const lastIdx = lines.length - 1;
|
|
47
|
+
if (lastIdx >= 0) {
|
|
48
|
+
lines[lastIdx] = `[${repeatCount}x] ${prevLine}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
repeatCount = 0;
|
|
52
|
+
prevLine = null;
|
|
53
|
+
};
|
|
54
|
+
for (const line of text.split("\n")) {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
if (!trimmed) {
|
|
57
|
+
blankCount++;
|
|
58
|
+
if (blankCount <= 1) {
|
|
59
|
+
flushRepeats();
|
|
60
|
+
lines.push("");
|
|
61
|
+
}
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
blankCount = 0;
|
|
65
|
+
if (isBoilerplate(trimmed))
|
|
66
|
+
continue;
|
|
67
|
+
const normalized = normalizeWhitespace(trimmed);
|
|
68
|
+
const stripped = stripTimestampsAndHashes(normalized);
|
|
69
|
+
if (prevLine !== null && prevLine === stripped) {
|
|
70
|
+
repeatCount++;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
flushRepeats();
|
|
74
|
+
prevLine = stripped;
|
|
75
|
+
repeatCount = 1;
|
|
76
|
+
lines.push(stripped);
|
|
77
|
+
}
|
|
78
|
+
flushRepeats();
|
|
79
|
+
return lines.join("\n");
|
|
80
|
+
}
|
|
81
|
+
export function aggressiveCompress(content, ext) {
|
|
82
|
+
const result = [];
|
|
83
|
+
const isPython = ext === "py";
|
|
84
|
+
const isHtml = ext === "html" || ext === "htm" || ext === "xml" || ext === "svg";
|
|
85
|
+
const isSql = ext === "sql";
|
|
86
|
+
const isShell = ext === "sh" || ext === "bash" || ext === "zsh" || ext === "fish";
|
|
87
|
+
let inBlockComment = false;
|
|
88
|
+
for (const line of content.split("\n")) {
|
|
89
|
+
const trimmed = line.trim();
|
|
90
|
+
if (!trimmed)
|
|
91
|
+
continue;
|
|
92
|
+
if (inBlockComment) {
|
|
93
|
+
if (trimmed.includes("*/") || (isHtml && trimmed.includes("-->"))) {
|
|
94
|
+
inBlockComment = false;
|
|
95
|
+
}
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (trimmed.startsWith("/*") || (isHtml && trimmed.startsWith("<!--"))) {
|
|
99
|
+
if (!(trimmed.includes("*/") || trimmed.includes("-->"))) {
|
|
100
|
+
inBlockComment = true;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (trimmed.startsWith("//") && !trimmed.startsWith("///"))
|
|
105
|
+
continue;
|
|
106
|
+
if (trimmed.startsWith("*") || trimmed.startsWith("*/"))
|
|
107
|
+
continue;
|
|
108
|
+
if (isPython && trimmed.startsWith("#"))
|
|
109
|
+
continue;
|
|
110
|
+
if (isSql && trimmed.startsWith("--"))
|
|
111
|
+
continue;
|
|
112
|
+
if (isShell && trimmed.startsWith("#") && !trimmed.startsWith("#!"))
|
|
113
|
+
continue;
|
|
114
|
+
if (/^[}\]);]+$/.test(trimmed)) {
|
|
115
|
+
const last = result[result.length - 1];
|
|
116
|
+
if (last && /^[}\]);]+$/.test(last.trim())) {
|
|
117
|
+
result[result.length - 1] = last + trimmed;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
result.push(trimmed);
|
|
122
|
+
}
|
|
123
|
+
return result.join("\n");
|
|
124
|
+
}
|
|
125
|
+
export function safeguardRatio(original, compressed) {
|
|
126
|
+
const origTokens = countTokens(original);
|
|
127
|
+
const compTokens = countTokens(compressed);
|
|
128
|
+
if (origTokens === 0)
|
|
129
|
+
return compressed;
|
|
130
|
+
if (compTokens > origTokens)
|
|
131
|
+
return original;
|
|
132
|
+
const ratio = compTokens / origTokens;
|
|
133
|
+
if (ratio < 0.05 && origTokens < 2000)
|
|
134
|
+
return original;
|
|
135
|
+
return compressed;
|
|
136
|
+
}
|
|
137
|
+
export { stripAnsi };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { countTokens } from "./tokens.js";
|
|
2
|
+
export function shannonEntropy(text) {
|
|
3
|
+
if (!text)
|
|
4
|
+
return 0;
|
|
5
|
+
const freq = new Map();
|
|
6
|
+
for (const ch of text) {
|
|
7
|
+
freq.set(ch, (freq.get(ch) ?? 0) + 1);
|
|
8
|
+
}
|
|
9
|
+
const len = text.length;
|
|
10
|
+
let entropy = 0;
|
|
11
|
+
for (const count of freq.values()) {
|
|
12
|
+
const p = count / len;
|
|
13
|
+
if (p > 0)
|
|
14
|
+
entropy -= p * Math.log2(p);
|
|
15
|
+
}
|
|
16
|
+
return entropy;
|
|
17
|
+
}
|
|
18
|
+
export function normalizedTokenEntropy(text) {
|
|
19
|
+
if (!text || text.trim().length === 0)
|
|
20
|
+
return 0;
|
|
21
|
+
const words = text.trim().split(/\s+/);
|
|
22
|
+
if (words.length <= 1) {
|
|
23
|
+
const charEntropy = shannonEntropy(text.trim());
|
|
24
|
+
return Math.min(1, charEntropy / 4.5);
|
|
25
|
+
}
|
|
26
|
+
const freq = new Map();
|
|
27
|
+
for (const w of words) {
|
|
28
|
+
freq.set(w, (freq.get(w) ?? 0) + 1);
|
|
29
|
+
}
|
|
30
|
+
const n = words.length;
|
|
31
|
+
const uniqueRatio = freq.size / n;
|
|
32
|
+
let entropy = 0;
|
|
33
|
+
for (const count of freq.values()) {
|
|
34
|
+
const p = count / n;
|
|
35
|
+
if (p > 0)
|
|
36
|
+
entropy -= p * Math.log2(p);
|
|
37
|
+
}
|
|
38
|
+
const maxEntropy = Math.log2(n);
|
|
39
|
+
const normalized = maxEntropy > 0 ? entropy / maxEntropy : 0;
|
|
40
|
+
return Math.min(1, normalized * 0.7 + uniqueRatio * 0.3);
|
|
41
|
+
}
|
|
42
|
+
function renderIb(lines, scores, threshold) {
|
|
43
|
+
const out = [];
|
|
44
|
+
let omitRun = 0;
|
|
45
|
+
for (let i = 0; i < lines.length; i++) {
|
|
46
|
+
if (scores[i] >= threshold) {
|
|
47
|
+
if (omitRun > 0) {
|
|
48
|
+
out.push(`// ... ${omitRun} low-info lines omitted`);
|
|
49
|
+
omitRun = 0;
|
|
50
|
+
}
|
|
51
|
+
out.push(lines[i]);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
omitRun++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (omitRun > 0) {
|
|
58
|
+
out.push(`// ... ${omitRun} low-info lines omitted`);
|
|
59
|
+
}
|
|
60
|
+
return out.join("\n");
|
|
61
|
+
}
|
|
62
|
+
export function compressIb(text, targetRatio) {
|
|
63
|
+
if (!text)
|
|
64
|
+
return "";
|
|
65
|
+
const inputTokens = countTokens(text);
|
|
66
|
+
if (inputTokens === 0)
|
|
67
|
+
return text;
|
|
68
|
+
const ratio = Math.max(0.02, Math.min(1.0, targetRatio));
|
|
69
|
+
const lines = text.split("\n");
|
|
70
|
+
const scores = lines.map((ln) => normalizedTokenEntropy(ln));
|
|
71
|
+
let lo = 0;
|
|
72
|
+
let hi = 1;
|
|
73
|
+
let best = renderIb(lines, scores, 0);
|
|
74
|
+
let bestDiff = Infinity;
|
|
75
|
+
const consider = (thr) => {
|
|
76
|
+
const cand = renderIb(lines, scores, thr);
|
|
77
|
+
const r = countTokens(cand) / inputTokens;
|
|
78
|
+
const diff = Math.abs(r - ratio);
|
|
79
|
+
if (diff < bestDiff) {
|
|
80
|
+
bestDiff = diff;
|
|
81
|
+
best = cand;
|
|
82
|
+
}
|
|
83
|
+
return r;
|
|
84
|
+
};
|
|
85
|
+
for (let i = 0; i < 26; i++) {
|
|
86
|
+
const mid = (lo + hi) / 2;
|
|
87
|
+
const r = consider(mid);
|
|
88
|
+
if (r > ratio) {
|
|
89
|
+
lo = mid;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
hi = mid;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const thr of [0, 1, lo, hi, (lo + hi) / 2]) {
|
|
96
|
+
consider(thr);
|
|
97
|
+
}
|
|
98
|
+
return best;
|
|
99
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface CompressEntityOptions {
|
|
2
|
+
keepSummaries?: boolean | "latest" | "all";
|
|
3
|
+
maxKeyChanges?: number;
|
|
4
|
+
flatFormat?: boolean;
|
|
5
|
+
maxTokens?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function compressEntity(input: string, opts?: CompressEntityOptions): string;
|
|
8
|
+
export declare function compressEntityList(input: string, opts?: CompressEntityOptions): string;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { countTokens } from "../tokens.js";
|
|
2
|
+
const PHASE_ORDER = [
|
|
3
|
+
"plan",
|
|
4
|
+
"triage",
|
|
5
|
+
"review_plan",
|
|
6
|
+
"implementation",
|
|
7
|
+
"code_review",
|
|
8
|
+
"validation",
|
|
9
|
+
"approve",
|
|
10
|
+
];
|
|
11
|
+
export function compressEntity(input, opts) {
|
|
12
|
+
let entity;
|
|
13
|
+
try {
|
|
14
|
+
entity = JSON.parse(input);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return input;
|
|
18
|
+
}
|
|
19
|
+
const keepSummaries = opts?.keepSummaries ?? "latest";
|
|
20
|
+
const maxKeyChanges = opts?.maxKeyChanges ?? 3;
|
|
21
|
+
const result = {};
|
|
22
|
+
const idKey = findIdKey(entity);
|
|
23
|
+
if (idKey)
|
|
24
|
+
result[idKey] = entity[idKey];
|
|
25
|
+
if (entity.title)
|
|
26
|
+
result.title = entity.title;
|
|
27
|
+
if (entity.status)
|
|
28
|
+
result.status = entity.status;
|
|
29
|
+
if (entity.sprintId)
|
|
30
|
+
result.sprintId = entity.sprintId;
|
|
31
|
+
if (entity.feature_id)
|
|
32
|
+
result.feature_id = entity.feature_id;
|
|
33
|
+
if (entity.severity)
|
|
34
|
+
result.severity = entity.severity;
|
|
35
|
+
if (entity.estimate)
|
|
36
|
+
result.estimate = entity.estimate;
|
|
37
|
+
if (entity.executionMode)
|
|
38
|
+
result.executionMode = entity.executionMode;
|
|
39
|
+
if (entity.pipeline)
|
|
40
|
+
result.pipeline = entity.pipeline;
|
|
41
|
+
if (Array.isArray(entity.taskIds)) {
|
|
42
|
+
result.taskIds = entity.taskIds;
|
|
43
|
+
result.taskCount = entity.taskIds.length;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(entity.dependencies) && entity.dependencies.length > 0) {
|
|
46
|
+
result.dependencies = entity.dependencies;
|
|
47
|
+
}
|
|
48
|
+
const summaries = entity.summaries;
|
|
49
|
+
if (summaries && typeof summaries === "object") {
|
|
50
|
+
if (keepSummaries === "all") {
|
|
51
|
+
result.summaries = compressSummaries(summaries, maxKeyChanges);
|
|
52
|
+
}
|
|
53
|
+
else if (keepSummaries === "latest") {
|
|
54
|
+
const latest = findLatestPhase(summaries);
|
|
55
|
+
if (latest) {
|
|
56
|
+
result.latestPhase = latest.phase;
|
|
57
|
+
result.latestSummary = compressOneSummary(latest.summary, maxKeyChanges);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (opts?.flatFormat) {
|
|
62
|
+
return formatFlat(result);
|
|
63
|
+
}
|
|
64
|
+
let output = JSON.stringify(result, null, 2);
|
|
65
|
+
if (opts?.maxTokens && countTokens(output) > opts.maxTokens) {
|
|
66
|
+
output = JSON.stringify(result);
|
|
67
|
+
}
|
|
68
|
+
return output;
|
|
69
|
+
}
|
|
70
|
+
export function compressEntityList(input, opts) {
|
|
71
|
+
let entities;
|
|
72
|
+
try {
|
|
73
|
+
entities = JSON.parse(input);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return input;
|
|
77
|
+
}
|
|
78
|
+
if (!Array.isArray(entities))
|
|
79
|
+
return input;
|
|
80
|
+
const compressed = entities.map((e) => JSON.parse(compressEntity(JSON.stringify(e), { ...opts, flatFormat: false })));
|
|
81
|
+
let output = JSON.stringify(compressed, null, 2);
|
|
82
|
+
if (opts?.maxTokens && countTokens(output) > opts.maxTokens) {
|
|
83
|
+
output = JSON.stringify(compressed);
|
|
84
|
+
}
|
|
85
|
+
return output;
|
|
86
|
+
}
|
|
87
|
+
function findIdKey(entity) {
|
|
88
|
+
for (const key of ["taskId", "sprintId", "bugId", "featureId", "id"]) {
|
|
89
|
+
if (entity[key])
|
|
90
|
+
return key;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
function compressSummaries(summaries, maxKeyChanges) {
|
|
95
|
+
const result = {};
|
|
96
|
+
for (const phase of PHASE_ORDER) {
|
|
97
|
+
if (summaries[phase]) {
|
|
98
|
+
result[phase] = compressOneSummary(summaries[phase], maxKeyChanges);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
function compressOneSummary(summary, maxKeyChanges) {
|
|
104
|
+
const result = {};
|
|
105
|
+
if (summary.objective)
|
|
106
|
+
result.objective = summary.objective;
|
|
107
|
+
if (summary.verdict)
|
|
108
|
+
result.verdict = summary.verdict;
|
|
109
|
+
if (summary.key_changes && summary.key_changes.length > 0) {
|
|
110
|
+
const shown = summary.key_changes.slice(0, maxKeyChanges);
|
|
111
|
+
const extra = summary.key_changes.length - maxKeyChanges;
|
|
112
|
+
result.key_changes = extra > 0 ? [...shown, `+${extra} more`] : shown;
|
|
113
|
+
}
|
|
114
|
+
if (summary.route)
|
|
115
|
+
result.route = summary.route;
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
function findLatestPhase(summaries) {
|
|
119
|
+
for (let i = PHASE_ORDER.length - 1; i >= 0; i--) {
|
|
120
|
+
const phase = PHASE_ORDER[i];
|
|
121
|
+
if (summaries[phase]) {
|
|
122
|
+
return { phase, summary: summaries[phase] };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function formatFlat(obj, prefix = "") {
|
|
128
|
+
const lines = [];
|
|
129
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
130
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
131
|
+
if (value === null || value === undefined)
|
|
132
|
+
continue;
|
|
133
|
+
if (Array.isArray(value)) {
|
|
134
|
+
if (value.length <= 5 && value.every((v) => typeof v === "string")) {
|
|
135
|
+
lines.push(`${fullKey}: ${value.join(", ")}`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
lines.push(`${fullKey}: [${value.length} items]`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
else if (typeof value === "object") {
|
|
142
|
+
lines.push(formatFlat(value, fullKey));
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
lines.push(`${fullKey}: ${value}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return lines.filter(Boolean).join("\n");
|
|
149
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { compressStoreQuery } from "./query.js";
|
|
2
|
+
export type { CompressQueryOptions } from "./query.js";
|
|
3
|
+
export { compressEntity, compressEntityList } from "./entity.js";
|
|
4
|
+
export type { CompressEntityOptions } from "./entity.js";
|
|
5
|
+
export { compressMarkdown } from "./markdown.js";
|
|
6
|
+
export type { CompressMarkdownOptions } from "./markdown.js";
|
|
7
|
+
export { compressValidateStore } from "./validate.js";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { countTokens, truncateToTokenBudget } from "../tokens.js";
|
|
2
|
+
export function compressMarkdown(input, opts) {
|
|
3
|
+
if (!input.trim())
|
|
4
|
+
return "";
|
|
5
|
+
const mode = opts?.mode ?? "map";
|
|
6
|
+
const maxTokens = opts?.maxTokens ?? Infinity;
|
|
7
|
+
switch (mode) {
|
|
8
|
+
case "headings":
|
|
9
|
+
return compressHeadings(input, maxTokens);
|
|
10
|
+
case "truncate":
|
|
11
|
+
return truncateToTokenBudget(input, maxTokens);
|
|
12
|
+
case "map":
|
|
13
|
+
default:
|
|
14
|
+
return compressMap(input, maxTokens);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function compressMap(input, maxTokens) {
|
|
18
|
+
const lines = input.split("\n");
|
|
19
|
+
const picked = [];
|
|
20
|
+
let inFrontmatter = false;
|
|
21
|
+
let frontmatterDone = false;
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
const line = lines[i];
|
|
24
|
+
const trimmed = line.trim();
|
|
25
|
+
if (i === 0 && trimmed === "---") {
|
|
26
|
+
inFrontmatter = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (inFrontmatter) {
|
|
30
|
+
if (trimmed === "---") {
|
|
31
|
+
inFrontmatter = false;
|
|
32
|
+
frontmatterDone = true;
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (trimmed.startsWith("#")) {
|
|
37
|
+
picked.push(line);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (trimmed.startsWith("- **") ||
|
|
41
|
+
trimmed.startsWith("* **") ||
|
|
42
|
+
trimmed.startsWith("| ") ||
|
|
43
|
+
trimmed.startsWith("```")) {
|
|
44
|
+
picked.push(line);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (trimmed.startsWith("- [ ]") ||
|
|
48
|
+
trimmed.startsWith("- [x]") ||
|
|
49
|
+
trimmed.startsWith("- [X]")) {
|
|
50
|
+
picked.push(line);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (i === 0 || (frontmatterDone && picked.length === 0)) {
|
|
54
|
+
picked.push(line);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (picked.length === 0) {
|
|
58
|
+
return truncateToTokenBudget(input, maxTokens);
|
|
59
|
+
}
|
|
60
|
+
const totalLines = lines.length;
|
|
61
|
+
const keptLines = picked.length;
|
|
62
|
+
const omitted = totalLines - keptLines;
|
|
63
|
+
let result = picked.join("\n");
|
|
64
|
+
if (omitted > 0) {
|
|
65
|
+
result += `\n\n[${omitted} body lines omitted — ${totalLines} total]`;
|
|
66
|
+
}
|
|
67
|
+
if (countTokens(result) > maxTokens) {
|
|
68
|
+
result = truncateToTokenBudget(result, maxTokens);
|
|
69
|
+
}
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
function compressHeadings(input, maxTokens) {
|
|
73
|
+
const lines = input.split("\n");
|
|
74
|
+
const headings = [];
|
|
75
|
+
let depth = 0;
|
|
76
|
+
for (const line of lines) {
|
|
77
|
+
const match = line.match(/^(#{1,6})\s+(.+)/);
|
|
78
|
+
if (match) {
|
|
79
|
+
const level = match[1].length;
|
|
80
|
+
const indent = " ".repeat(level - 1);
|
|
81
|
+
headings.push(`${indent}${match[2]}`);
|
|
82
|
+
depth = Math.max(depth, level);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (headings.length === 0) {
|
|
86
|
+
return truncateToTokenBudget(input, maxTokens);
|
|
87
|
+
}
|
|
88
|
+
const result = `${headings.length} sections (depth ${depth}):\n${headings.join("\n")}`;
|
|
89
|
+
return countTokens(result) > maxTokens
|
|
90
|
+
? truncateToTokenBudget(result, maxTokens)
|
|
91
|
+
: result;
|
|
92
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { countTokens } from "../tokens.js";
|
|
2
|
+
export function compressStoreQuery(input, opts) {
|
|
3
|
+
let envelope;
|
|
4
|
+
try {
|
|
5
|
+
envelope = JSON.parse(input);
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return input;
|
|
9
|
+
}
|
|
10
|
+
if (!envelope.results || !Array.isArray(envelope.results))
|
|
11
|
+
return input;
|
|
12
|
+
const keepExcerpts = opts?.keepExcerpts ?? false;
|
|
13
|
+
const maxExcerptLines = opts?.maxExcerptLines ?? 5;
|
|
14
|
+
const maxResults = opts?.maxResults ?? envelope.results.length;
|
|
15
|
+
const results = envelope.results.slice(0, maxResults).map((r) => {
|
|
16
|
+
const entry = {
|
|
17
|
+
id: r.id,
|
|
18
|
+
title: r.title,
|
|
19
|
+
status: r.status,
|
|
20
|
+
type: r.type,
|
|
21
|
+
};
|
|
22
|
+
if (r.relationships && Object.keys(r.relationships).length > 0) {
|
|
23
|
+
const rels = {};
|
|
24
|
+
for (const [k, v] of Object.entries(r.relationships)) {
|
|
25
|
+
if (v !== null && v !== undefined)
|
|
26
|
+
rels[k] = v;
|
|
27
|
+
}
|
|
28
|
+
if (Object.keys(rels).length > 0)
|
|
29
|
+
entry.relationships = rels;
|
|
30
|
+
}
|
|
31
|
+
if (keepExcerpts && r.excerpt) {
|
|
32
|
+
const lines = r.excerpt.split("\n");
|
|
33
|
+
entry.excerpt =
|
|
34
|
+
lines.length > maxExcerptLines
|
|
35
|
+
? lines.slice(0, maxExcerptLines).join("\n") +
|
|
36
|
+
`\n... (${lines.length - maxExcerptLines} more lines)`
|
|
37
|
+
: r.excerpt;
|
|
38
|
+
}
|
|
39
|
+
return entry;
|
|
40
|
+
});
|
|
41
|
+
const compact = { results };
|
|
42
|
+
if (envelope.query)
|
|
43
|
+
compact.query = envelope.query;
|
|
44
|
+
if (envelope.path)
|
|
45
|
+
compact.path = envelope.path;
|
|
46
|
+
if (typeof envelope.count === "number")
|
|
47
|
+
compact.count = envelope.count;
|
|
48
|
+
if (typeof envelope.totalMatched === "number")
|
|
49
|
+
compact.totalMatched = envelope.totalMatched;
|
|
50
|
+
if (typeof envelope.returned === "number")
|
|
51
|
+
compact.returned = envelope.returned;
|
|
52
|
+
if (envelope.results.length > maxResults) {
|
|
53
|
+
compact.truncated = envelope.results.length - maxResults;
|
|
54
|
+
}
|
|
55
|
+
let output = JSON.stringify(compact, null, 2);
|
|
56
|
+
if (opts?.maxTokens && countTokens(output) > opts.maxTokens) {
|
|
57
|
+
output = JSON.stringify(compact);
|
|
58
|
+
}
|
|
59
|
+
return output;
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function compressValidateStore(input: string): string;
|