@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,184 @@
|
|
|
1
|
+
import { countTokens } from "../tokens.js";
|
|
2
|
+
|
|
3
|
+
interface PhaseSummary {
|
|
4
|
+
objective?: string;
|
|
5
|
+
written_at?: string;
|
|
6
|
+
key_changes?: string[];
|
|
7
|
+
findings?: string[];
|
|
8
|
+
verdict?: string;
|
|
9
|
+
artifact_ref?: string;
|
|
10
|
+
route?: string;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const PHASE_ORDER = [
|
|
15
|
+
"plan",
|
|
16
|
+
"triage",
|
|
17
|
+
"review_plan",
|
|
18
|
+
"implementation",
|
|
19
|
+
"code_review",
|
|
20
|
+
"validation",
|
|
21
|
+
"approve",
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
export interface CompressEntityOptions {
|
|
25
|
+
keepSummaries?: boolean | "latest" | "all";
|
|
26
|
+
maxKeyChanges?: number;
|
|
27
|
+
flatFormat?: boolean;
|
|
28
|
+
maxTokens?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function compressEntity(
|
|
32
|
+
input: string,
|
|
33
|
+
opts?: CompressEntityOptions,
|
|
34
|
+
): string {
|
|
35
|
+
let entity: Record<string, unknown>;
|
|
36
|
+
try {
|
|
37
|
+
entity = JSON.parse(input);
|
|
38
|
+
} catch {
|
|
39
|
+
return input;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const keepSummaries = opts?.keepSummaries ?? "latest";
|
|
43
|
+
const maxKeyChanges = opts?.maxKeyChanges ?? 3;
|
|
44
|
+
|
|
45
|
+
const result: Record<string, unknown> = {};
|
|
46
|
+
|
|
47
|
+
const idKey = findIdKey(entity);
|
|
48
|
+
if (idKey) result[idKey] = entity[idKey];
|
|
49
|
+
if (entity.title) result.title = entity.title;
|
|
50
|
+
if (entity.status) result.status = entity.status;
|
|
51
|
+
|
|
52
|
+
if (entity.sprintId) result.sprintId = entity.sprintId;
|
|
53
|
+
if (entity.feature_id) result.feature_id = entity.feature_id;
|
|
54
|
+
if (entity.severity) result.severity = entity.severity;
|
|
55
|
+
if (entity.estimate) result.estimate = entity.estimate;
|
|
56
|
+
if (entity.executionMode) result.executionMode = entity.executionMode;
|
|
57
|
+
if (entity.pipeline) result.pipeline = entity.pipeline;
|
|
58
|
+
|
|
59
|
+
if (Array.isArray(entity.taskIds)) {
|
|
60
|
+
result.taskIds = entity.taskIds;
|
|
61
|
+
result.taskCount = entity.taskIds.length;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (Array.isArray(entity.dependencies) && entity.dependencies.length > 0) {
|
|
65
|
+
result.dependencies = entity.dependencies;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const summaries = entity.summaries as Record<string, PhaseSummary> | undefined;
|
|
69
|
+
if (summaries && typeof summaries === "object") {
|
|
70
|
+
if (keepSummaries === "all") {
|
|
71
|
+
result.summaries = compressSummaries(summaries, maxKeyChanges);
|
|
72
|
+
} else if (keepSummaries === "latest") {
|
|
73
|
+
const latest = findLatestPhase(summaries);
|
|
74
|
+
if (latest) {
|
|
75
|
+
result.latestPhase = latest.phase;
|
|
76
|
+
result.latestSummary = compressOneSummary(latest.summary, maxKeyChanges);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (opts?.flatFormat) {
|
|
82
|
+
return formatFlat(result);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let output = JSON.stringify(result, null, 2);
|
|
86
|
+
if (opts?.maxTokens && countTokens(output) > opts.maxTokens) {
|
|
87
|
+
output = JSON.stringify(result);
|
|
88
|
+
}
|
|
89
|
+
return output;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function compressEntityList(
|
|
93
|
+
input: string,
|
|
94
|
+
opts?: CompressEntityOptions,
|
|
95
|
+
): string {
|
|
96
|
+
let entities: Record<string, unknown>[];
|
|
97
|
+
try {
|
|
98
|
+
entities = JSON.parse(input);
|
|
99
|
+
} catch {
|
|
100
|
+
return input;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!Array.isArray(entities)) return input;
|
|
104
|
+
|
|
105
|
+
const compressed = entities.map((e) =>
|
|
106
|
+
JSON.parse(compressEntity(JSON.stringify(e), { ...opts, flatFormat: false })),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
let output = JSON.stringify(compressed, null, 2);
|
|
110
|
+
if (opts?.maxTokens && countTokens(output) > opts.maxTokens) {
|
|
111
|
+
output = JSON.stringify(compressed);
|
|
112
|
+
}
|
|
113
|
+
return output;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function findIdKey(entity: Record<string, unknown>): string | undefined {
|
|
117
|
+
for (const key of ["taskId", "sprintId", "bugId", "featureId", "id"]) {
|
|
118
|
+
if (entity[key]) return key;
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function compressSummaries(
|
|
124
|
+
summaries: Record<string, PhaseSummary>,
|
|
125
|
+
maxKeyChanges: number,
|
|
126
|
+
): Record<string, unknown> {
|
|
127
|
+
const result: Record<string, unknown> = {};
|
|
128
|
+
for (const phase of PHASE_ORDER) {
|
|
129
|
+
if (summaries[phase]) {
|
|
130
|
+
result[phase] = compressOneSummary(summaries[phase], maxKeyChanges);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function compressOneSummary(
|
|
137
|
+
summary: PhaseSummary,
|
|
138
|
+
maxKeyChanges: number,
|
|
139
|
+
): Record<string, unknown> {
|
|
140
|
+
const result: Record<string, unknown> = {};
|
|
141
|
+
if (summary.objective) result.objective = summary.objective;
|
|
142
|
+
if (summary.verdict) result.verdict = summary.verdict;
|
|
143
|
+
|
|
144
|
+
if (summary.key_changes && summary.key_changes.length > 0) {
|
|
145
|
+
const shown = summary.key_changes.slice(0, maxKeyChanges);
|
|
146
|
+
const extra = summary.key_changes.length - maxKeyChanges;
|
|
147
|
+
result.key_changes = extra > 0 ? [...shown, `+${extra} more`] : shown;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (summary.route) result.route = summary.route;
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function findLatestPhase(
|
|
155
|
+
summaries: Record<string, PhaseSummary>,
|
|
156
|
+
): { phase: string; summary: PhaseSummary } | null {
|
|
157
|
+
for (let i = PHASE_ORDER.length - 1; i >= 0; i--) {
|
|
158
|
+
const phase = PHASE_ORDER[i];
|
|
159
|
+
if (summaries[phase]) {
|
|
160
|
+
return { phase, summary: summaries[phase] };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function formatFlat(obj: Record<string, unknown>, prefix = ""): string {
|
|
167
|
+
const lines: string[] = [];
|
|
168
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
169
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
170
|
+
if (value === null || value === undefined) continue;
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
if (value.length <= 5 && value.every((v) => typeof v === "string")) {
|
|
173
|
+
lines.push(`${fullKey}: ${value.join(", ")}`);
|
|
174
|
+
} else {
|
|
175
|
+
lines.push(`${fullKey}: [${value.length} items]`);
|
|
176
|
+
}
|
|
177
|
+
} else if (typeof value === "object") {
|
|
178
|
+
lines.push(formatFlat(value as Record<string, unknown>, fullKey));
|
|
179
|
+
} else {
|
|
180
|
+
lines.push(`${fullKey}: ${value}`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return lines.filter(Boolean).join("\n");
|
|
184
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { compressStoreQuery } from "./query.js";
|
|
2
|
+
export type { CompressQueryOptions } from "./query.js";
|
|
3
|
+
|
|
4
|
+
export { compressEntity, compressEntityList } from "./entity.js";
|
|
5
|
+
export type { CompressEntityOptions } from "./entity.js";
|
|
6
|
+
|
|
7
|
+
export { compressMarkdown } from "./markdown.js";
|
|
8
|
+
export type { CompressMarkdownOptions } from "./markdown.js";
|
|
9
|
+
|
|
10
|
+
export { compressValidateStore } from "./validate.js";
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { countTokens, truncateToTokenBudget } from "../tokens.js";
|
|
2
|
+
|
|
3
|
+
export interface CompressMarkdownOptions {
|
|
4
|
+
maxTokens?: number;
|
|
5
|
+
mode?: "map" | "headings" | "truncate";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function compressMarkdown(
|
|
9
|
+
input: string,
|
|
10
|
+
opts?: CompressMarkdownOptions,
|
|
11
|
+
): string {
|
|
12
|
+
if (!input.trim()) return "";
|
|
13
|
+
|
|
14
|
+
const mode = opts?.mode ?? "map";
|
|
15
|
+
const maxTokens = opts?.maxTokens ?? Infinity;
|
|
16
|
+
|
|
17
|
+
switch (mode) {
|
|
18
|
+
case "headings":
|
|
19
|
+
return compressHeadings(input, maxTokens);
|
|
20
|
+
case "truncate":
|
|
21
|
+
return truncateToTokenBudget(input, maxTokens);
|
|
22
|
+
case "map":
|
|
23
|
+
default:
|
|
24
|
+
return compressMap(input, maxTokens);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function compressMap(input: string, maxTokens: number): string {
|
|
29
|
+
const lines = input.split("\n");
|
|
30
|
+
const picked: string[] = [];
|
|
31
|
+
let inFrontmatter = false;
|
|
32
|
+
let frontmatterDone = false;
|
|
33
|
+
|
|
34
|
+
for (let i = 0; i < lines.length; i++) {
|
|
35
|
+
const line = lines[i];
|
|
36
|
+
const trimmed = line.trim();
|
|
37
|
+
|
|
38
|
+
if (i === 0 && trimmed === "---") {
|
|
39
|
+
inFrontmatter = true;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (inFrontmatter) {
|
|
43
|
+
if (trimmed === "---") {
|
|
44
|
+
inFrontmatter = false;
|
|
45
|
+
frontmatterDone = true;
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (trimmed.startsWith("#")) {
|
|
51
|
+
picked.push(line);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
trimmed.startsWith("- **") ||
|
|
57
|
+
trimmed.startsWith("* **") ||
|
|
58
|
+
trimmed.startsWith("| ") ||
|
|
59
|
+
trimmed.startsWith("```")
|
|
60
|
+
) {
|
|
61
|
+
picked.push(line);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (
|
|
66
|
+
trimmed.startsWith("- [ ]") ||
|
|
67
|
+
trimmed.startsWith("- [x]") ||
|
|
68
|
+
trimmed.startsWith("- [X]")
|
|
69
|
+
) {
|
|
70
|
+
picked.push(line);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (i === 0 || (frontmatterDone && picked.length === 0)) {
|
|
75
|
+
picked.push(line);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (picked.length === 0) {
|
|
80
|
+
return truncateToTokenBudget(input, maxTokens);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const totalLines = lines.length;
|
|
84
|
+
const keptLines = picked.length;
|
|
85
|
+
const omitted = totalLines - keptLines;
|
|
86
|
+
|
|
87
|
+
let result = picked.join("\n");
|
|
88
|
+
if (omitted > 0) {
|
|
89
|
+
result += `\n\n[${omitted} body lines omitted — ${totalLines} total]`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (countTokens(result) > maxTokens) {
|
|
93
|
+
result = truncateToTokenBudget(result, maxTokens);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function compressHeadings(input: string, maxTokens: number): string {
|
|
100
|
+
const lines = input.split("\n");
|
|
101
|
+
const headings: string[] = [];
|
|
102
|
+
let depth = 0;
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const match = line.match(/^(#{1,6})\s+(.+)/);
|
|
106
|
+
if (match) {
|
|
107
|
+
const level = match[1].length;
|
|
108
|
+
const indent = " ".repeat(level - 1);
|
|
109
|
+
headings.push(`${indent}${match[2]}`);
|
|
110
|
+
depth = Math.max(depth, level);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (headings.length === 0) {
|
|
115
|
+
return truncateToTokenBudget(input, maxTokens);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const result = `${headings.length} sections (depth ${depth}):\n${headings.join("\n")}`;
|
|
119
|
+
return countTokens(result) > maxTokens
|
|
120
|
+
? truncateToTokenBudget(result, maxTokens)
|
|
121
|
+
: result;
|
|
122
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { countTokens } from "../tokens.js";
|
|
2
|
+
|
|
3
|
+
interface QueryResultEntry {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
status: string;
|
|
7
|
+
type: string;
|
|
8
|
+
relationships?: Record<string, unknown>;
|
|
9
|
+
fileRefs?: { json: string; md: string };
|
|
10
|
+
storeRef?: string;
|
|
11
|
+
indexRef?: string;
|
|
12
|
+
excerpt?: string | null;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface QueryEnvelope {
|
|
17
|
+
query?: string;
|
|
18
|
+
path?: string;
|
|
19
|
+
traversalTrace?: string[];
|
|
20
|
+
results?: QueryResultEntry[];
|
|
21
|
+
relatedFileRefs?: string[];
|
|
22
|
+
count?: number;
|
|
23
|
+
totalMatched?: number;
|
|
24
|
+
returned?: number;
|
|
25
|
+
limit?: number | null;
|
|
26
|
+
sort?: string | null;
|
|
27
|
+
config?: Record<string, unknown>;
|
|
28
|
+
meta?: Record<string, unknown>;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CompressQueryOptions {
|
|
33
|
+
keepExcerpts?: boolean;
|
|
34
|
+
maxExcerptLines?: number;
|
|
35
|
+
maxResults?: number;
|
|
36
|
+
maxTokens?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function compressStoreQuery(
|
|
40
|
+
input: string,
|
|
41
|
+
opts?: CompressQueryOptions,
|
|
42
|
+
): string {
|
|
43
|
+
let envelope: QueryEnvelope;
|
|
44
|
+
try {
|
|
45
|
+
envelope = JSON.parse(input);
|
|
46
|
+
} catch {
|
|
47
|
+
return input;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!envelope.results || !Array.isArray(envelope.results)) return input;
|
|
51
|
+
|
|
52
|
+
const keepExcerpts = opts?.keepExcerpts ?? false;
|
|
53
|
+
const maxExcerptLines = opts?.maxExcerptLines ?? 5;
|
|
54
|
+
const maxResults = opts?.maxResults ?? envelope.results.length;
|
|
55
|
+
|
|
56
|
+
const results = envelope.results.slice(0, maxResults).map((r) => {
|
|
57
|
+
const entry: Record<string, unknown> = {
|
|
58
|
+
id: r.id,
|
|
59
|
+
title: r.title,
|
|
60
|
+
status: r.status,
|
|
61
|
+
type: r.type,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (r.relationships && Object.keys(r.relationships).length > 0) {
|
|
65
|
+
const rels: Record<string, unknown> = {};
|
|
66
|
+
for (const [k, v] of Object.entries(r.relationships)) {
|
|
67
|
+
if (v !== null && v !== undefined) rels[k] = v;
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(rels).length > 0) entry.relationships = rels;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (keepExcerpts && r.excerpt) {
|
|
73
|
+
const lines = r.excerpt.split("\n");
|
|
74
|
+
entry.excerpt =
|
|
75
|
+
lines.length > maxExcerptLines
|
|
76
|
+
? lines.slice(0, maxExcerptLines).join("\n") +
|
|
77
|
+
`\n... (${lines.length - maxExcerptLines} more lines)`
|
|
78
|
+
: r.excerpt;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return entry;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const compact: Record<string, unknown> = { results };
|
|
85
|
+
|
|
86
|
+
if (envelope.query) compact.query = envelope.query;
|
|
87
|
+
if (envelope.path) compact.path = envelope.path;
|
|
88
|
+
if (typeof envelope.count === "number") compact.count = envelope.count;
|
|
89
|
+
if (typeof envelope.totalMatched === "number")
|
|
90
|
+
compact.totalMatched = envelope.totalMatched;
|
|
91
|
+
if (typeof envelope.returned === "number")
|
|
92
|
+
compact.returned = envelope.returned;
|
|
93
|
+
|
|
94
|
+
if (envelope.results.length > maxResults) {
|
|
95
|
+
compact.truncated = envelope.results.length - maxResults;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let output = JSON.stringify(compact, null, 2);
|
|
99
|
+
|
|
100
|
+
if (opts?.maxTokens && countTokens(output) > opts.maxTokens) {
|
|
101
|
+
output = JSON.stringify(compact);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return output;
|
|
105
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export function compressValidateStore(input: string): string {
|
|
2
|
+
let report: Record<string, unknown>;
|
|
3
|
+
try {
|
|
4
|
+
report = JSON.parse(input);
|
|
5
|
+
} catch {
|
|
6
|
+
return compressPlainValidation(input);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const errors = (report.errors ?? []) as { id: string; category: string; message: string }[];
|
|
10
|
+
const warnings = (report.warnings ?? []) as { id: string; category: string; message: string }[];
|
|
11
|
+
const fixes = (report.fixes ?? []) as { id: string; message: string; applied: boolean }[];
|
|
12
|
+
const summary = report.summary as Record<string, number> | undefined;
|
|
13
|
+
|
|
14
|
+
const parts: string[] = [];
|
|
15
|
+
|
|
16
|
+
if (summary) {
|
|
17
|
+
const counts = Object.entries(summary)
|
|
18
|
+
.filter(([, v]) => v > 0)
|
|
19
|
+
.map(([k, v]) => `${v} ${k}`)
|
|
20
|
+
.join(", ");
|
|
21
|
+
if (counts) parts.push(counts);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (errors.length > 0) {
|
|
25
|
+
const grouped = groupBy(errors, "category");
|
|
26
|
+
for (const [cat, items] of Object.entries(grouped)) {
|
|
27
|
+
parts.push(`ERR ${cat} (${items.length}):`);
|
|
28
|
+
for (const item of items.slice(0, 5)) {
|
|
29
|
+
parts.push(` ${item.id}: ${item.message}`);
|
|
30
|
+
}
|
|
31
|
+
if (items.length > 5) parts.push(` ... +${items.length - 5} more`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (warnings.length > 0) {
|
|
36
|
+
const grouped = groupBy(warnings, "category");
|
|
37
|
+
for (const [cat, items] of Object.entries(grouped)) {
|
|
38
|
+
parts.push(`WARN ${cat} (${items.length}):`);
|
|
39
|
+
for (const item of items.slice(0, 3)) {
|
|
40
|
+
parts.push(` ${item.id}: ${item.message}`);
|
|
41
|
+
}
|
|
42
|
+
if (items.length > 3) parts.push(` ... +${items.length - 3} more`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (fixes.length > 0) {
|
|
47
|
+
const applied = fixes.filter((f) => f.applied).length;
|
|
48
|
+
parts.push(`${fixes.length} fixes (${applied} applied)`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (parts.length === 0) return "ok (store valid)";
|
|
52
|
+
return parts.join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function compressPlainValidation(input: string): string {
|
|
56
|
+
const lines = input.split("\n").filter((l) => l.trim());
|
|
57
|
+
if (lines.length <= 10) return input;
|
|
58
|
+
|
|
59
|
+
const errors = lines.filter((l) => l.includes("ERROR"));
|
|
60
|
+
const warns = lines.filter((l) => l.includes("WARN"));
|
|
61
|
+
const fixed = lines.filter((l) => l.includes("FIXED"));
|
|
62
|
+
|
|
63
|
+
const parts: string[] = [];
|
|
64
|
+
if (errors.length > 0) {
|
|
65
|
+
parts.push(`${errors.length} errors:`);
|
|
66
|
+
for (const e of errors.slice(0, 5)) parts.push(` ${e.trim()}`);
|
|
67
|
+
if (errors.length > 5) parts.push(` ... +${errors.length - 5} more`);
|
|
68
|
+
}
|
|
69
|
+
if (warns.length > 0) parts.push(`${warns.length} warnings`);
|
|
70
|
+
if (fixed.length > 0) parts.push(`${fixed.length} fixed`);
|
|
71
|
+
|
|
72
|
+
return parts.length > 0 ? parts.join("\n") : input;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function groupBy<T extends Record<string, unknown>>(
|
|
76
|
+
items: T[],
|
|
77
|
+
key: string,
|
|
78
|
+
): Record<string, T[]> {
|
|
79
|
+
const result: Record<string, T[]> = {};
|
|
80
|
+
for (const item of items) {
|
|
81
|
+
const k = String(item[key] ?? "unknown");
|
|
82
|
+
if (!result[k]) result[k] = [];
|
|
83
|
+
result[k].push(item);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
compressStoreQuery,
|
|
3
|
+
compressEntity,
|
|
4
|
+
compressEntityList,
|
|
5
|
+
compressMarkdown,
|
|
6
|
+
compressValidateStore,
|
|
7
|
+
} from "./forge/index.js";
|
|
8
|
+
|
|
9
|
+
export type {
|
|
10
|
+
CompressQueryOptions,
|
|
11
|
+
CompressEntityOptions,
|
|
12
|
+
CompressMarkdownOptions,
|
|
13
|
+
} from "./forge/index.js";
|
|
14
|
+
|
|
15
|
+
export { countTokens, truncateToTokenBudget } from "./tokens.js";
|
|
16
|
+
export { compressIb } from "./entropy.js";
|
|
17
|
+
export { compressProgressive } from "./progressive.js";
|
|
18
|
+
export {
|
|
19
|
+
lightweightCleanup,
|
|
20
|
+
verbatimCompact,
|
|
21
|
+
stripAnsi,
|
|
22
|
+
} from "./compressor.js";
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { countTokens, truncateToTokenBudget } from "./tokens.js";
|
|
2
|
+
|
|
3
|
+
const LAMBDA = 1.35;
|
|
4
|
+
|
|
5
|
+
function expWeights(n: number): number[] {
|
|
6
|
+
return Array.from({ length: n }, (_, i) => Math.exp(LAMBDA * i));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function allocateBudget(budgetTokens: number, weights: number[]): number[] {
|
|
10
|
+
const n = weights.length;
|
|
11
|
+
if (n === 0 || budgetTokens === 0) return new Array(n).fill(0);
|
|
12
|
+
|
|
13
|
+
const sumW = Math.max(weights.reduce((a, b) => a + b, 0), Number.EPSILON);
|
|
14
|
+
const base = new Array(n).fill(0);
|
|
15
|
+
const frac = new Array(n).fill(0);
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < n; i++) {
|
|
18
|
+
const exact = (budgetTokens * weights[i]) / sumW;
|
|
19
|
+
base[i] = Math.floor(exact);
|
|
20
|
+
frac[i] = exact - base[i];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const given = base.reduce((a: number, b: number) => a + b, 0);
|
|
24
|
+
const order = Array.from({ length: n }, (_, i) => i).sort(
|
|
25
|
+
(a, b) => frac[b] - frac[a],
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
let extra = budgetTokens - given;
|
|
29
|
+
for (const i of order) {
|
|
30
|
+
if (extra <= 0) break;
|
|
31
|
+
base[i]++;
|
|
32
|
+
extra--;
|
|
33
|
+
}
|
|
34
|
+
return base;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function tierForIndex(i: number, n: number): number {
|
|
38
|
+
if (n <= 1) return 2;
|
|
39
|
+
const r = i / (n - 1);
|
|
40
|
+
if (r < 1 / 3) return 0;
|
|
41
|
+
if (r < 2 / 3) return 1;
|
|
42
|
+
return 2;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const STRUCTURE_KEYWORDS = [
|
|
46
|
+
"fn ",
|
|
47
|
+
"pub ",
|
|
48
|
+
"struct ",
|
|
49
|
+
"enum ",
|
|
50
|
+
"trait ",
|
|
51
|
+
"impl ",
|
|
52
|
+
"mod ",
|
|
53
|
+
"use ",
|
|
54
|
+
"def ",
|
|
55
|
+
"class ",
|
|
56
|
+
"function ",
|
|
57
|
+
"export ",
|
|
58
|
+
"import ",
|
|
59
|
+
"const ",
|
|
60
|
+
"interface ",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
function mapLike(s: string, maxTokens: number): string {
|
|
64
|
+
const picked: string[] = [];
|
|
65
|
+
const lines = s.split("\n");
|
|
66
|
+
for (let i = 0; i < lines.length; i++) {
|
|
67
|
+
if (i === 0 || STRUCTURE_KEYWORDS.some((k) => lines[i].includes(k))) {
|
|
68
|
+
picked.push(lines[i]);
|
|
69
|
+
}
|
|
70
|
+
if (picked.length >= 48) break;
|
|
71
|
+
}
|
|
72
|
+
if (picked.length === 0 && lines.length > 0) {
|
|
73
|
+
picked.push(lines[0]);
|
|
74
|
+
}
|
|
75
|
+
return truncateToTokenBudget(picked.join("\n"), Math.max(4, maxTokens));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function oneLineSummary(
|
|
79
|
+
segIdx: number,
|
|
80
|
+
s: string,
|
|
81
|
+
maxTokens: number,
|
|
82
|
+
): string {
|
|
83
|
+
const preview = s.split("\n")[0]?.slice(0, 120) ?? "";
|
|
84
|
+
const lineCount = s.split("\n").length;
|
|
85
|
+
const draft = `// seg[${segIdx}] ${lineCount} lines, ${s.length} chars | ${preview}`;
|
|
86
|
+
return truncateToTokenBudget(draft, Math.max(8, maxTokens));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function compressProgressive(
|
|
90
|
+
segments: string[],
|
|
91
|
+
budgetTokens: number,
|
|
92
|
+
): string[] {
|
|
93
|
+
const n = segments.length;
|
|
94
|
+
if (n === 0) return [];
|
|
95
|
+
if (budgetTokens === 0) return segments.map(() => "");
|
|
96
|
+
|
|
97
|
+
const weights = expWeights(n);
|
|
98
|
+
const allocs = allocateBudget(budgetTokens, weights);
|
|
99
|
+
const out: string[] = [];
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < n; i++) {
|
|
102
|
+
const alloc = allocs[i];
|
|
103
|
+
const tier = tierForIndex(i, n);
|
|
104
|
+
if (alloc === 0) {
|
|
105
|
+
out.push("");
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
let compressed: string;
|
|
109
|
+
switch (tier) {
|
|
110
|
+
case 2:
|
|
111
|
+
compressed = truncateToTokenBudget(segments[i], alloc);
|
|
112
|
+
break;
|
|
113
|
+
case 1:
|
|
114
|
+
compressed = mapLike(segments[i], alloc);
|
|
115
|
+
break;
|
|
116
|
+
default:
|
|
117
|
+
compressed = oneLineSummary(i, segments[i], alloc);
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
out.push(truncateToTokenBudget(compressed, alloc));
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|