@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
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
compressStoreQuery,
|
|
4
|
+
compressEntity,
|
|
5
|
+
compressEntityList,
|
|
6
|
+
compressMarkdown,
|
|
7
|
+
compressValidateStore,
|
|
8
|
+
countTokens,
|
|
9
|
+
compressIb,
|
|
10
|
+
compressProgressive,
|
|
11
|
+
truncateToTokenBudget,
|
|
12
|
+
lightweightCleanup,
|
|
13
|
+
verbatimCompact,
|
|
14
|
+
stripAnsi,
|
|
15
|
+
} from "../index.js";
|
|
16
|
+
|
|
17
|
+
// ── store-query compressor ──────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const SAMPLE_QUERY = JSON.stringify(
|
|
20
|
+
{
|
|
21
|
+
query: "open tasks in S12",
|
|
22
|
+
path: "intent-nlp",
|
|
23
|
+
traversalTrace: [
|
|
24
|
+
"parsed intent: entity=task, status=open, sprint=S12",
|
|
25
|
+
"resolved S12 → PROJ-S12",
|
|
26
|
+
"loaded 8 task records",
|
|
27
|
+
"filtered by status: 5 matched",
|
|
28
|
+
],
|
|
29
|
+
results: [
|
|
30
|
+
{
|
|
31
|
+
id: "PROJ-S12-T01",
|
|
32
|
+
title: "Add auth middleware",
|
|
33
|
+
status: "implementing",
|
|
34
|
+
type: "task",
|
|
35
|
+
relationships: { sprintId: "PROJ-S12", featureId: "F-AUTH", blockedBy: null },
|
|
36
|
+
fileRefs: { json: ".forge/store/tasks/PROJ-S12-T01.json", md: ".forge/sprints/S12/tasks/T01/INDEX.md" },
|
|
37
|
+
storeRef: ".forge/store/tasks/PROJ-S12-T01.json",
|
|
38
|
+
indexRef: ".forge/sprints/S12/tasks/T01/INDEX.md",
|
|
39
|
+
excerpt: "# T01: Add auth middleware\n\n## Status: implementing\n\n## Plan\nImplement JWT-based auth middleware...\n\n## Key Changes\n- Added middleware handler\n- Updated route config\n- Added token validation\n\n## Notes\nBlocked by upstream API changes.\nNeed to coordinate with team B.\nAlso requires DB migration.\nSee PR #42 for details.\nReviewed by @alice.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "PROJ-S12-T02",
|
|
43
|
+
title: "Fix rate limiter",
|
|
44
|
+
status: "planned",
|
|
45
|
+
type: "task",
|
|
46
|
+
relationships: { sprintId: "PROJ-S12" },
|
|
47
|
+
fileRefs: { json: ".forge/store/tasks/PROJ-S12-T02.json", md: ".forge/sprints/S12/tasks/T02/INDEX.md" },
|
|
48
|
+
storeRef: ".forge/store/tasks/PROJ-S12-T02.json",
|
|
49
|
+
indexRef: ".forge/sprints/S12/tasks/T02/INDEX.md",
|
|
50
|
+
excerpt: "# T02: Fix rate limiter\n\n## Status: planned\n\nRate limiter is dropping legitimate requests under load.",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
relatedFileRefs: [".forge/sprints/S12/INDEX.md", ".forge/features/F-AUTH.md"],
|
|
54
|
+
config: { store: ".forge/store", engineering: ".forge/engineering" },
|
|
55
|
+
meta: { mode: "intent-nlp", engineVersion: "1.2.0", totalTimeMs: 42 },
|
|
56
|
+
},
|
|
57
|
+
null,
|
|
58
|
+
2,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
describe("compressStoreQuery", () => {
|
|
62
|
+
it("strips envelope fields", () => {
|
|
63
|
+
const result = compressStoreQuery(SAMPLE_QUERY);
|
|
64
|
+
const parsed = JSON.parse(result);
|
|
65
|
+
expect(parsed.traversalTrace).toBeUndefined();
|
|
66
|
+
expect(parsed.config).toBeUndefined();
|
|
67
|
+
expect(parsed.meta).toBeUndefined();
|
|
68
|
+
expect(parsed.relatedFileRefs).toBeUndefined();
|
|
69
|
+
expect(parsed.results).toHaveLength(2);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("strips fileRefs and storeRef from results", () => {
|
|
73
|
+
const result = compressStoreQuery(SAMPLE_QUERY);
|
|
74
|
+
const parsed = JSON.parse(result);
|
|
75
|
+
expect(parsed.results[0].fileRefs).toBeUndefined();
|
|
76
|
+
expect(parsed.results[0].storeRef).toBeUndefined();
|
|
77
|
+
expect(parsed.results[0].indexRef).toBeUndefined();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("strips excerpts by default", () => {
|
|
81
|
+
const result = compressStoreQuery(SAMPLE_QUERY);
|
|
82
|
+
const parsed = JSON.parse(result);
|
|
83
|
+
expect(parsed.results[0].excerpt).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("keeps truncated excerpts when requested", () => {
|
|
87
|
+
const result = compressStoreQuery(SAMPLE_QUERY, {
|
|
88
|
+
keepExcerpts: true,
|
|
89
|
+
maxExcerptLines: 3,
|
|
90
|
+
});
|
|
91
|
+
const parsed = JSON.parse(result);
|
|
92
|
+
expect(parsed.results[0].excerpt).toContain("Add auth middleware");
|
|
93
|
+
expect(parsed.results[0].excerpt).toContain("more lines");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("preserves query and path", () => {
|
|
97
|
+
const result = compressStoreQuery(SAMPLE_QUERY);
|
|
98
|
+
const parsed = JSON.parse(result);
|
|
99
|
+
expect(parsed.query).toBe("open tasks in S12");
|
|
100
|
+
expect(parsed.path).toBe("intent-nlp");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("limits results count", () => {
|
|
104
|
+
const result = compressStoreQuery(SAMPLE_QUERY, { maxResults: 1 });
|
|
105
|
+
const parsed = JSON.parse(result);
|
|
106
|
+
expect(parsed.results).toHaveLength(1);
|
|
107
|
+
expect(parsed.truncated).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("strips null relationships", () => {
|
|
111
|
+
const result = compressStoreQuery(SAMPLE_QUERY);
|
|
112
|
+
const parsed = JSON.parse(result);
|
|
113
|
+
expect(parsed.results[0].relationships.blockedBy).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("is significantly smaller than input", () => {
|
|
117
|
+
const result = compressStoreQuery(SAMPLE_QUERY);
|
|
118
|
+
expect(countTokens(result)).toBeLessThan(countTokens(SAMPLE_QUERY) * 0.5);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("passes through non-JSON input", () => {
|
|
122
|
+
expect(compressStoreQuery("not json")).toBe("not json");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ── entity compressor ───────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
const SAMPLE_TASK = JSON.stringify(
|
|
129
|
+
{
|
|
130
|
+
taskId: "PROJ-S12-T01",
|
|
131
|
+
sprintId: "PROJ-S12",
|
|
132
|
+
title: "Add auth middleware",
|
|
133
|
+
status: "implementing",
|
|
134
|
+
path: "sprints/S12/tasks/T01",
|
|
135
|
+
feature_id: "F-AUTH",
|
|
136
|
+
description: "Implement JWT-based authentication middleware for all API routes.",
|
|
137
|
+
estimate: "M",
|
|
138
|
+
dependencies: ["PROJ-S12-T03"],
|
|
139
|
+
knowledgeUpdates: [{ file: "auth.md", delta: "Added JWT flow" }],
|
|
140
|
+
planIterations: 2,
|
|
141
|
+
codeReviewIterations: 1,
|
|
142
|
+
assignedModel: "claude-opus-4-7",
|
|
143
|
+
pipeline: "standard",
|
|
144
|
+
summaries: {
|
|
145
|
+
plan: {
|
|
146
|
+
objective: "Design JWT middleware architecture",
|
|
147
|
+
written_at: "2026-05-20T10:00:00Z",
|
|
148
|
+
key_changes: ["Created middleware handler", "Updated route config", "Added token validation", "Set up refresh flow"],
|
|
149
|
+
findings: ["Need to handle expired tokens gracefully"],
|
|
150
|
+
verdict: "approved",
|
|
151
|
+
artifact_ref: "PLAN.md",
|
|
152
|
+
},
|
|
153
|
+
review_plan: {
|
|
154
|
+
objective: "Review plan for auth middleware",
|
|
155
|
+
written_at: "2026-05-20T14:00:00Z",
|
|
156
|
+
key_changes: ["Approved with minor suggestions"],
|
|
157
|
+
verdict: "approved",
|
|
158
|
+
},
|
|
159
|
+
implementation: {
|
|
160
|
+
objective: "Implement auth middleware per plan",
|
|
161
|
+
written_at: "2026-05-21T09:00:00Z",
|
|
162
|
+
key_changes: ["Implemented JWT parsing", "Added route guards", "Unit tests passing", "Integration tests added", "Error handling complete"],
|
|
163
|
+
verdict: "approved",
|
|
164
|
+
artifact_ref: "PROGRESS.md",
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
null,
|
|
169
|
+
2,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
describe("compressEntity", () => {
|
|
173
|
+
it("keeps essential fields", () => {
|
|
174
|
+
const result = compressEntity(SAMPLE_TASK);
|
|
175
|
+
const parsed = JSON.parse(result);
|
|
176
|
+
expect(parsed.taskId).toBe("PROJ-S12-T01");
|
|
177
|
+
expect(parsed.title).toBe("Add auth middleware");
|
|
178
|
+
expect(parsed.status).toBe("implementing");
|
|
179
|
+
expect(parsed.sprintId).toBe("PROJ-S12");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("drops verbose fields by default", () => {
|
|
183
|
+
const result = compressEntity(SAMPLE_TASK);
|
|
184
|
+
const parsed = JSON.parse(result);
|
|
185
|
+
expect(parsed.description).toBeUndefined();
|
|
186
|
+
expect(parsed.knowledgeUpdates).toBeUndefined();
|
|
187
|
+
expect(parsed.assignedModel).toBeUndefined();
|
|
188
|
+
expect(parsed.path).toBeUndefined();
|
|
189
|
+
expect(parsed.planIterations).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("keeps only latest summary by default", () => {
|
|
193
|
+
const result = compressEntity(SAMPLE_TASK);
|
|
194
|
+
const parsed = JSON.parse(result);
|
|
195
|
+
expect(parsed.summaries).toBeUndefined();
|
|
196
|
+
expect(parsed.latestPhase).toBe("implementation");
|
|
197
|
+
expect(parsed.latestSummary.objective).toBe("Implement auth middleware per plan");
|
|
198
|
+
expect(parsed.latestSummary.verdict).toBe("approved");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("truncates key_changes in summary", () => {
|
|
202
|
+
const result = compressEntity(SAMPLE_TASK, { maxKeyChanges: 2 });
|
|
203
|
+
const parsed = JSON.parse(result);
|
|
204
|
+
expect(parsed.latestSummary.key_changes).toHaveLength(3);
|
|
205
|
+
expect(parsed.latestSummary.key_changes[2]).toContain("+3 more");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("keeps all summaries when requested", () => {
|
|
209
|
+
const result = compressEntity(SAMPLE_TASK, { keepSummaries: "all" });
|
|
210
|
+
const parsed = JSON.parse(result);
|
|
211
|
+
expect(parsed.summaries).toBeDefined();
|
|
212
|
+
expect(parsed.summaries.plan).toBeDefined();
|
|
213
|
+
expect(parsed.summaries.implementation).toBeDefined();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("produces flat format", () => {
|
|
217
|
+
const result = compressEntity(SAMPLE_TASK, { flatFormat: true });
|
|
218
|
+
expect(result).toContain("taskId: PROJ-S12-T01");
|
|
219
|
+
expect(result).toContain("status: implementing");
|
|
220
|
+
expect(result).not.toContain("{");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("is smaller than input", () => {
|
|
224
|
+
const result = compressEntity(SAMPLE_TASK);
|
|
225
|
+
expect(countTokens(result)).toBeLessThan(countTokens(SAMPLE_TASK) * 0.5);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("compressEntityList", () => {
|
|
230
|
+
it("compresses array of entities", () => {
|
|
231
|
+
const list = JSON.stringify([JSON.parse(SAMPLE_TASK), JSON.parse(SAMPLE_TASK)], null, 2);
|
|
232
|
+
const result = compressEntityList(list);
|
|
233
|
+
const parsed = JSON.parse(result);
|
|
234
|
+
expect(parsed).toHaveLength(2);
|
|
235
|
+
expect(parsed[0].taskId).toBe("PROJ-S12-T01");
|
|
236
|
+
expect(parsed[0].description).toBeUndefined();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ── markdown compressor ─────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
const SAMPLE_MD = `---
|
|
243
|
+
title: Sprint S12 Progress
|
|
244
|
+
date: 2026-05-25
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
# Sprint S12: Auth & Rate Limiting
|
|
248
|
+
|
|
249
|
+
## Overview
|
|
250
|
+
This sprint covers the implementation of JWT authentication
|
|
251
|
+
middleware and fixing the rate limiter under high load.
|
|
252
|
+
There are multiple sub-tasks involved.
|
|
253
|
+
|
|
254
|
+
## Tasks
|
|
255
|
+
|
|
256
|
+
### T01: Add auth middleware
|
|
257
|
+
- **Status**: implementing
|
|
258
|
+
- **Estimate**: M
|
|
259
|
+
- Started implementation on May 20th
|
|
260
|
+
- Plan was approved after 2 iterations
|
|
261
|
+
- Currently blocked on upstream API
|
|
262
|
+
|
|
263
|
+
### T02: Fix rate limiter
|
|
264
|
+
- **Status**: planned
|
|
265
|
+
- **Estimate**: S
|
|
266
|
+
- Root cause identified as connection pool exhaustion
|
|
267
|
+
|
|
268
|
+
## Risks
|
|
269
|
+
- Upstream API team may not deliver on time
|
|
270
|
+
- Rate limiter fix requires load testing infrastructure
|
|
271
|
+
- JWT library has a known CVE that needs patching
|
|
272
|
+
|
|
273
|
+
## Timeline
|
|
274
|
+
| Milestone | Date | Status |
|
|
275
|
+
|-----------|------|--------|
|
|
276
|
+
| Plan approval | May 20 | Done |
|
|
277
|
+
| Auth impl | May 25 | In progress |
|
|
278
|
+
| Rate limiter | May 28 | Not started |
|
|
279
|
+
`;
|
|
280
|
+
|
|
281
|
+
describe("compressMarkdown", () => {
|
|
282
|
+
it("map mode keeps headings and key lines", () => {
|
|
283
|
+
const result = compressMarkdown(SAMPLE_MD, { mode: "map" });
|
|
284
|
+
expect(result).toContain("# Sprint S12");
|
|
285
|
+
expect(result).toContain("### T01");
|
|
286
|
+
expect(result).toContain("### T02");
|
|
287
|
+
expect(result).toContain("**Status**");
|
|
288
|
+
expect(result).toContain("body lines omitted");
|
|
289
|
+
expect(countTokens(result)).toBeLessThan(countTokens(SAMPLE_MD) * 0.7);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it("headings mode extracts structure only", () => {
|
|
293
|
+
const result = compressMarkdown(SAMPLE_MD, { mode: "headings" });
|
|
294
|
+
expect(result).toContain("Sprint S12");
|
|
295
|
+
expect(result).toContain("T01: Add auth middleware");
|
|
296
|
+
expect(result).toContain("sections");
|
|
297
|
+
expect(result).not.toContain("implementing");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("truncate mode respects token budget", () => {
|
|
301
|
+
const result = compressMarkdown(SAMPLE_MD, { mode: "truncate", maxTokens: 50 });
|
|
302
|
+
expect(countTokens(result)).toBeLessThanOrEqual(60);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("skips frontmatter", () => {
|
|
306
|
+
const result = compressMarkdown(SAMPLE_MD, { mode: "map" });
|
|
307
|
+
expect(result).not.toContain("title: Sprint S12 Progress");
|
|
308
|
+
expect(result).not.toContain("date: 2026");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("handles empty input", () => {
|
|
312
|
+
expect(compressMarkdown("")).toBe("");
|
|
313
|
+
expect(compressMarkdown(" ")).toBe("");
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ── validate-store compressor ───────────────────────────────
|
|
318
|
+
|
|
319
|
+
describe("compressValidateStore", () => {
|
|
320
|
+
it("compresses JSON report", () => {
|
|
321
|
+
const report = JSON.stringify({
|
|
322
|
+
ok: false,
|
|
323
|
+
errors: [
|
|
324
|
+
{ entity: "task", id: "PROJ-S12-T01", category: "missing-required", field: "title", message: "title is required" },
|
|
325
|
+
{ entity: "task", id: "PROJ-S12-T02", category: "missing-required", field: "status", message: "status is required" },
|
|
326
|
+
{ entity: "bug", id: "PROJ-B01", category: "orphaned-fk", field: "sprintId", message: "references non-existent sprint" },
|
|
327
|
+
],
|
|
328
|
+
warnings: [
|
|
329
|
+
{ entity: "task", id: "PROJ-S12-T03", category: "stale-path", message: "path does not match filesystem" },
|
|
330
|
+
],
|
|
331
|
+
fixes: [
|
|
332
|
+
{ entity: "task", id: "PROJ-S12-T01", category: "backfill", message: "backfilled path", applied: true },
|
|
333
|
+
],
|
|
334
|
+
summary: { sprints: 2, tasks: 5, bugs: 1, features: 0, errors: 3, warnings: 1, fixes: 1 },
|
|
335
|
+
});
|
|
336
|
+
const result = compressValidateStore(report);
|
|
337
|
+
expect(result).toContain("missing-required (2)");
|
|
338
|
+
expect(result).toContain("orphaned-fk (1)");
|
|
339
|
+
expect(result).toContain("1 warnings");
|
|
340
|
+
expect(result).toContain("1 fixes (1 applied)");
|
|
341
|
+
expect(countTokens(result)).toBeLessThan(countTokens(report) * 0.6);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("returns ok for valid store", () => {
|
|
345
|
+
const report = JSON.stringify({
|
|
346
|
+
ok: true,
|
|
347
|
+
errors: [],
|
|
348
|
+
warnings: [],
|
|
349
|
+
fixes: [],
|
|
350
|
+
summary: { sprints: 3, tasks: 12, bugs: 2, features: 1, errors: 0, warnings: 0, fixes: 0 },
|
|
351
|
+
});
|
|
352
|
+
const result = compressValidateStore(report);
|
|
353
|
+
expect(result).toContain("3 sprints");
|
|
354
|
+
expect(result).toContain("12 tasks");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
it("compresses plain text validation output", () => {
|
|
358
|
+
const lines = Array.from({ length: 20 }, (_, i) =>
|
|
359
|
+
`ERROR PROJ-S12-T${String(i).padStart(2, "0")}: missing required field`
|
|
360
|
+
).join("\n");
|
|
361
|
+
const result = compressValidateStore(lines);
|
|
362
|
+
expect(result).toContain("20 errors");
|
|
363
|
+
expect(result).toContain("... +15 more");
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// ── core utilities ──────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
describe("core utilities", () => {
|
|
370
|
+
it("countTokens", () => {
|
|
371
|
+
expect(countTokens("")).toBe(0);
|
|
372
|
+
expect(countTokens("hello world")).toBeGreaterThan(0);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("stripAnsi", () => {
|
|
376
|
+
expect(stripAnsi("\x1b[31mERROR\x1b[0m")).toBe("ERROR");
|
|
377
|
+
expect(stripAnsi("clean")).toBe("clean");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("truncateToTokenBudget", () => {
|
|
381
|
+
const long = "word ".repeat(1000);
|
|
382
|
+
const result = truncateToTokenBudget(long, 50);
|
|
383
|
+
expect(countTokens(result)).toBeLessThanOrEqual(60);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("compressIb preserves high-entropy lines", () => {
|
|
387
|
+
let boring = "aaa bbb aaa bbb\n".repeat(30);
|
|
388
|
+
boring += "unique_identifier_xyz_quartz\n";
|
|
389
|
+
const result = compressIb(boring, 0.15);
|
|
390
|
+
expect(result).toContain("unique_identifier_xyz_quartz");
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it("compressProgressive respects budget", () => {
|
|
394
|
+
const segs = Array.from({ length: 4 }, (_, i) => `segment ${i} content\n`);
|
|
395
|
+
const out = compressProgressive(segs, 80);
|
|
396
|
+
const total = out.reduce((acc, s) => acc + countTokens(s), 0);
|
|
397
|
+
expect(total).toBeLessThanOrEqual(80);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("lightweightCleanup collapses blanks", () => {
|
|
401
|
+
const result = lightweightCleanup("a\n\n\n\n\nb");
|
|
402
|
+
expect(result.match(/\n/g)?.length ?? 0).toBeLessThanOrEqual(3);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("verbatimCompact deduplicates", () => {
|
|
406
|
+
const result = verbatimCompact("same\n".repeat(20));
|
|
407
|
+
expect(result).toContain("[20x]");
|
|
408
|
+
});
|
|
409
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { countTokens } from "./tokens.js";
|
|
2
|
+
import {
|
|
3
|
+
stripAnsi,
|
|
4
|
+
stripTimestampsAndHashes,
|
|
5
|
+
normalizeWhitespace,
|
|
6
|
+
isBoilerplate,
|
|
7
|
+
} from "./strip.js";
|
|
8
|
+
|
|
9
|
+
export function lightweightCleanup(content: string): string {
|
|
10
|
+
const lines = content.split("\n");
|
|
11
|
+
const total = lines.length;
|
|
12
|
+
const result: string[] = [];
|
|
13
|
+
let blankCount = 0;
|
|
14
|
+
let braceRun: string[] = [];
|
|
15
|
+
|
|
16
|
+
const flushBraceRun = () => {
|
|
17
|
+
if (total <= 200 || braceRun.length <= 5) {
|
|
18
|
+
result.push(...braceRun);
|
|
19
|
+
} else {
|
|
20
|
+
result.push(braceRun[0], braceRun[1]);
|
|
21
|
+
result.push(`[${braceRun.length - 2} brace-only lines collapsed]`);
|
|
22
|
+
}
|
|
23
|
+
braceRun = [];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
for (const line of lines) {
|
|
27
|
+
const trimmed = line.trim();
|
|
28
|
+
if (!trimmed) {
|
|
29
|
+
flushBraceRun();
|
|
30
|
+
blankCount++;
|
|
31
|
+
if (blankCount <= 1) result.push("");
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
blankCount = 0;
|
|
35
|
+
if (/^[}\]);]+$/.test(trimmed)) {
|
|
36
|
+
braceRun.push(trimmed);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
flushBraceRun();
|
|
40
|
+
result.push(line);
|
|
41
|
+
}
|
|
42
|
+
flushBraceRun();
|
|
43
|
+
return result.join("\n");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function verbatimCompact(text: string): string {
|
|
47
|
+
const lines: string[] = [];
|
|
48
|
+
let blankCount = 0;
|
|
49
|
+
let prevLine: string | null = null;
|
|
50
|
+
let repeatCount = 0;
|
|
51
|
+
|
|
52
|
+
const flushRepeats = () => {
|
|
53
|
+
if (repeatCount > 1 && prevLine !== null) {
|
|
54
|
+
const lastIdx = lines.length - 1;
|
|
55
|
+
if (lastIdx >= 0) {
|
|
56
|
+
lines[lastIdx] = `[${repeatCount}x] ${prevLine}`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
repeatCount = 0;
|
|
60
|
+
prevLine = null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
for (const line of text.split("\n")) {
|
|
64
|
+
const trimmed = line.trim();
|
|
65
|
+
if (!trimmed) {
|
|
66
|
+
blankCount++;
|
|
67
|
+
if (blankCount <= 1) {
|
|
68
|
+
flushRepeats();
|
|
69
|
+
lines.push("");
|
|
70
|
+
}
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
blankCount = 0;
|
|
74
|
+
if (isBoilerplate(trimmed)) continue;
|
|
75
|
+
|
|
76
|
+
const normalized = normalizeWhitespace(trimmed);
|
|
77
|
+
const stripped = stripTimestampsAndHashes(normalized);
|
|
78
|
+
|
|
79
|
+
if (prevLine !== null && prevLine === stripped) {
|
|
80
|
+
repeatCount++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
flushRepeats();
|
|
84
|
+
prevLine = stripped;
|
|
85
|
+
repeatCount = 1;
|
|
86
|
+
lines.push(stripped);
|
|
87
|
+
}
|
|
88
|
+
flushRepeats();
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function aggressiveCompress(
|
|
93
|
+
content: string,
|
|
94
|
+
ext?: string,
|
|
95
|
+
): string {
|
|
96
|
+
const result: string[] = [];
|
|
97
|
+
const isPython = ext === "py";
|
|
98
|
+
const isHtml = ext === "html" || ext === "htm" || ext === "xml" || ext === "svg";
|
|
99
|
+
const isSql = ext === "sql";
|
|
100
|
+
const isShell = ext === "sh" || ext === "bash" || ext === "zsh" || ext === "fish";
|
|
101
|
+
let inBlockComment = false;
|
|
102
|
+
|
|
103
|
+
for (const line of content.split("\n")) {
|
|
104
|
+
const trimmed = line.trim();
|
|
105
|
+
if (!trimmed) continue;
|
|
106
|
+
|
|
107
|
+
if (inBlockComment) {
|
|
108
|
+
if (trimmed.includes("*/") || (isHtml && trimmed.includes("-->"))) {
|
|
109
|
+
inBlockComment = false;
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (trimmed.startsWith("/*") || (isHtml && trimmed.startsWith("<!--"))) {
|
|
114
|
+
if (!(trimmed.includes("*/") || trimmed.includes("-->"))) {
|
|
115
|
+
inBlockComment = true;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (trimmed.startsWith("//") && !trimmed.startsWith("///")) continue;
|
|
120
|
+
if (trimmed.startsWith("*") || trimmed.startsWith("*/")) continue;
|
|
121
|
+
if (isPython && trimmed.startsWith("#")) continue;
|
|
122
|
+
if (isSql && trimmed.startsWith("--")) continue;
|
|
123
|
+
if (isShell && trimmed.startsWith("#") && !trimmed.startsWith("#!")) continue;
|
|
124
|
+
|
|
125
|
+
if (/^[}\]);]+$/.test(trimmed)) {
|
|
126
|
+
const last = result[result.length - 1];
|
|
127
|
+
if (last && /^[}\]);]+$/.test(last.trim())) {
|
|
128
|
+
result[result.length - 1] = last + trimmed;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
result.push(trimmed);
|
|
133
|
+
}
|
|
134
|
+
return result.join("\n");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function safeguardRatio(original: string, compressed: string): string {
|
|
138
|
+
const origTokens = countTokens(original);
|
|
139
|
+
const compTokens = countTokens(compressed);
|
|
140
|
+
if (origTokens === 0) return compressed;
|
|
141
|
+
if (compTokens > origTokens) return original;
|
|
142
|
+
const ratio = compTokens / origTokens;
|
|
143
|
+
if (ratio < 0.05 && origTokens < 2000) return original;
|
|
144
|
+
return compressed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export { stripAnsi };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { countTokens } from "./tokens.js";
|
|
2
|
+
|
|
3
|
+
export function shannonEntropy(text: string): number {
|
|
4
|
+
if (!text) return 0;
|
|
5
|
+
const freq = new Map<string, number>();
|
|
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) entropy -= p * Math.log2(p);
|
|
14
|
+
}
|
|
15
|
+
return entropy;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizedTokenEntropy(text: string): number {
|
|
19
|
+
if (!text || text.trim().length === 0) return 0;
|
|
20
|
+
const words = text.trim().split(/\s+/);
|
|
21
|
+
|
|
22
|
+
if (words.length <= 1) {
|
|
23
|
+
const charEntropy = shannonEntropy(text.trim());
|
|
24
|
+
return Math.min(1, charEntropy / 4.5);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const freq = new Map<string, number>();
|
|
28
|
+
for (const w of words) {
|
|
29
|
+
freq.set(w, (freq.get(w) ?? 0) + 1);
|
|
30
|
+
}
|
|
31
|
+
const n = words.length;
|
|
32
|
+
const uniqueRatio = freq.size / n;
|
|
33
|
+
let entropy = 0;
|
|
34
|
+
for (const count of freq.values()) {
|
|
35
|
+
const p = count / n;
|
|
36
|
+
if (p > 0) 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
|
+
|
|
43
|
+
function renderIb(
|
|
44
|
+
lines: string[],
|
|
45
|
+
scores: number[],
|
|
46
|
+
threshold: number,
|
|
47
|
+
): string {
|
|
48
|
+
const out: string[] = [];
|
|
49
|
+
let omitRun = 0;
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
if (scores[i] >= threshold) {
|
|
52
|
+
if (omitRun > 0) {
|
|
53
|
+
out.push(`// ... ${omitRun} low-info lines omitted`);
|
|
54
|
+
omitRun = 0;
|
|
55
|
+
}
|
|
56
|
+
out.push(lines[i]);
|
|
57
|
+
} else {
|
|
58
|
+
omitRun++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (omitRun > 0) {
|
|
62
|
+
out.push(`// ... ${omitRun} low-info lines omitted`);
|
|
63
|
+
}
|
|
64
|
+
return out.join("\n");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function compressIb(text: string, targetRatio: number): string {
|
|
68
|
+
if (!text) return "";
|
|
69
|
+
const inputTokens = countTokens(text);
|
|
70
|
+
if (inputTokens === 0) return text;
|
|
71
|
+
|
|
72
|
+
const ratio = Math.max(0.02, Math.min(1.0, targetRatio));
|
|
73
|
+
const lines = text.split("\n");
|
|
74
|
+
const scores = lines.map((ln) => normalizedTokenEntropy(ln));
|
|
75
|
+
|
|
76
|
+
let lo = 0;
|
|
77
|
+
let hi = 1;
|
|
78
|
+
let best = renderIb(lines, scores, 0);
|
|
79
|
+
let bestDiff = Infinity;
|
|
80
|
+
|
|
81
|
+
const consider = (thr: number) => {
|
|
82
|
+
const cand = renderIb(lines, scores, thr);
|
|
83
|
+
const r = countTokens(cand) / inputTokens;
|
|
84
|
+
const diff = Math.abs(r - ratio);
|
|
85
|
+
if (diff < bestDiff) {
|
|
86
|
+
bestDiff = diff;
|
|
87
|
+
best = cand;
|
|
88
|
+
}
|
|
89
|
+
return r;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for (let i = 0; i < 26; i++) {
|
|
93
|
+
const mid = (lo + hi) / 2;
|
|
94
|
+
const r = consider(mid);
|
|
95
|
+
if (r > ratio) {
|
|
96
|
+
lo = mid;
|
|
97
|
+
} else {
|
|
98
|
+
hi = mid;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const thr of [0, 1, lo, hi, (lo + hi) / 2]) {
|
|
102
|
+
consider(thr);
|
|
103
|
+
}
|
|
104
|
+
return best;
|
|
105
|
+
}
|