@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.
Files changed (88) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/CHANGELOG-forge-plugin.md +24 -0
  3. package/dist/extensions/forgecli/audience-gate.js +1 -1
  4. package/dist/extensions/forgecli/audience-gate.js.map +1 -1
  5. package/dist/extensions/forgecli/fix-bug.d.ts +1 -2
  6. package/dist/extensions/forgecli/fix-bug.js +678 -609
  7. package/dist/extensions/forgecli/fix-bug.js.map +1 -1
  8. package/dist/extensions/forgecli/forge-artifact-tool.js +15 -3
  9. package/dist/extensions/forgecli/forge-artifact-tool.js.map +1 -1
  10. package/dist/extensions/forgecli/forge-subagent.d.ts +17 -0
  11. package/dist/extensions/forgecli/forge-subagent.js +31 -12
  12. package/dist/extensions/forgecli/forge-subagent.js.map +1 -1
  13. package/dist/extensions/forgecli/forge-tools.d.ts +6 -0
  14. package/dist/extensions/forgecli/forge-tools.js +69 -6
  15. package/dist/extensions/forgecli/forge-tools.js.map +1 -1
  16. package/dist/extensions/forgecli/run-task.js +461 -391
  17. package/dist/extensions/forgecli/run-task.js.map +1 -1
  18. package/dist/extensions/forgecli/session-registry.d.ts +12 -0
  19. package/dist/extensions/forgecli/session-registry.js +23 -0
  20. package/dist/extensions/forgecli/session-registry.js.map +1 -1
  21. package/dist/extensions/forgecli/subagent/caller-context.d.ts +35 -11
  22. package/dist/extensions/forgecli/subagent/caller-context.js +49 -21
  23. package/dist/extensions/forgecli/subagent/caller-context.js.map +1 -1
  24. package/dist/extensions/forgecli/subagent/orchestrator-transcript.d.ts +66 -0
  25. package/dist/extensions/forgecli/subagent/orchestrator-transcript.js +66 -0
  26. package/dist/extensions/forgecli/subagent/orchestrator-transcript.js.map +1 -0
  27. package/dist/extensions/forgecli/subagent/phase-guard.d.ts +34 -0
  28. package/dist/extensions/forgecli/subagent/phase-guard.js +139 -0
  29. package/dist/extensions/forgecli/subagent/phase-guard.js.map +1 -0
  30. package/dist/extensions/forgecli/subagent/phase-summary-map.d.ts +1 -0
  31. package/dist/extensions/forgecli/subagent/phase-summary-map.js +22 -0
  32. package/dist/extensions/forgecli/subagent/phase-summary-map.js.map +1 -0
  33. package/dist/extensions/forgecli/thread-switcher.js +2 -2
  34. package/dist/extensions/forgecli/thread-switcher.js.map +1 -1
  35. package/dist/extensions/forgecli/viewport-events.d.ts +4 -0
  36. package/dist/extensions/forgecli/viewport-events.js +18 -1
  37. package/dist/extensions/forgecli/viewport-events.js.map +1 -1
  38. package/dist/extensions/forgecli/viewport-renderer.d.ts +12 -2
  39. package/dist/extensions/forgecli/viewport-renderer.js +8 -6
  40. package/dist/extensions/forgecli/viewport-renderer.js.map +1 -1
  41. package/dist/forge-payload/.base-pack/workflows/fix_bug.md +10 -28
  42. package/dist/forge-payload/.base-pack/workflows/triage.md +190 -0
  43. package/dist/forge-payload/.claude-plugin/plugin.json +1 -1
  44. package/dist/forge-payload/.schemas/enum-catalog.json +1 -1
  45. package/dist/forge-payload/.schemas/migrations.json +9 -0
  46. package/dist/forge-payload/integrity.json +3 -3
  47. package/dist/forge-payload/meta/fragments/tool-discipline.md +21 -2
  48. package/dist/forge-payload/meta/workflows/meta-bug-triage.md +210 -0
  49. package/dist/forge-payload/meta/workflows/meta-fix-bug.md +10 -28
  50. package/dist/forge-payload/schemas/enum-catalog.json +1 -1
  51. package/dist/forge-payload/schemas/structure-manifest.json +20 -1
  52. package/dist/forge-payload/tools/artifact.cjs +34 -5
  53. package/node_modules/@entelligentsia/forge-compress/dist/compressor.d.ts +6 -0
  54. package/node_modules/@entelligentsia/forge-compress/dist/compressor.js +137 -0
  55. package/node_modules/@entelligentsia/forge-compress/dist/entropy.d.ts +3 -0
  56. package/node_modules/@entelligentsia/forge-compress/dist/entropy.js +99 -0
  57. package/node_modules/@entelligentsia/forge-compress/dist/forge/entity.d.ts +8 -0
  58. package/node_modules/@entelligentsia/forge-compress/dist/forge/entity.js +149 -0
  59. package/node_modules/@entelligentsia/forge-compress/dist/forge/index.d.ts +7 -0
  60. package/node_modules/@entelligentsia/forge-compress/dist/forge/index.js +4 -0
  61. package/node_modules/@entelligentsia/forge-compress/dist/forge/markdown.d.ts +5 -0
  62. package/node_modules/@entelligentsia/forge-compress/dist/forge/markdown.js +92 -0
  63. package/node_modules/@entelligentsia/forge-compress/dist/forge/query.d.ts +7 -0
  64. package/node_modules/@entelligentsia/forge-compress/dist/forge/query.js +60 -0
  65. package/node_modules/@entelligentsia/forge-compress/dist/forge/validate.d.ts +1 -0
  66. package/node_modules/@entelligentsia/forge-compress/dist/forge/validate.js +82 -0
  67. package/node_modules/@entelligentsia/forge-compress/dist/index.d.ts +6 -0
  68. package/node_modules/@entelligentsia/forge-compress/dist/index.js +5 -0
  69. package/node_modules/@entelligentsia/forge-compress/dist/progressive.d.ts +1 -0
  70. package/node_modules/@entelligentsia/forge-compress/dist/progressive.js +108 -0
  71. package/node_modules/@entelligentsia/forge-compress/dist/strip.d.ts +4 -0
  72. package/node_modules/@entelligentsia/forge-compress/dist/strip.js +55 -0
  73. package/node_modules/@entelligentsia/forge-compress/dist/tokens.d.ts +2 -0
  74. package/node_modules/@entelligentsia/forge-compress/dist/tokens.js +17 -0
  75. package/node_modules/@entelligentsia/forge-compress/package.json +45 -0
  76. package/node_modules/@entelligentsia/forge-compress/src/__tests__/compress.test.ts +409 -0
  77. package/node_modules/@entelligentsia/forge-compress/src/compressor.ts +147 -0
  78. package/node_modules/@entelligentsia/forge-compress/src/entropy.ts +105 -0
  79. package/node_modules/@entelligentsia/forge-compress/src/forge/entity.ts +184 -0
  80. package/node_modules/@entelligentsia/forge-compress/src/forge/index.ts +10 -0
  81. package/node_modules/@entelligentsia/forge-compress/src/forge/markdown.ts +122 -0
  82. package/node_modules/@entelligentsia/forge-compress/src/forge/query.ts +105 -0
  83. package/node_modules/@entelligentsia/forge-compress/src/forge/validate.ts +86 -0
  84. package/node_modules/@entelligentsia/forge-compress/src/index.ts +22 -0
  85. package/node_modules/@entelligentsia/forge-compress/src/progressive.ts +123 -0
  86. package/node_modules/@entelligentsia/forge-compress/src/strip.ts +58 -0
  87. package/node_modules/@entelligentsia/forge-compress/src/tokens.ts +19 -0
  88. 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
+ }