@astudioplus/compressor 0.1.0

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 (108) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +20 -0
  3. package/README.md +167 -0
  4. package/dist/adapters/agents-md.d.ts +2 -0
  5. package/dist/adapters/agents-md.js +91 -0
  6. package/dist/adapters/apply.d.ts +3 -0
  7. package/dist/adapters/apply.js +83 -0
  8. package/dist/adapters/claude-code.d.ts +2 -0
  9. package/dist/adapters/claude-code.js +403 -0
  10. package/dist/adapters/copilot.d.ts +2 -0
  11. package/dist/adapters/copilot.js +418 -0
  12. package/dist/adapters/cursor.d.ts +2 -0
  13. package/dist/adapters/cursor.js +149 -0
  14. package/dist/adapters/index.d.ts +11 -0
  15. package/dist/adapters/index.js +19 -0
  16. package/dist/adapters/markers.d.ts +7 -0
  17. package/dist/adapters/markers.js +129 -0
  18. package/dist/adapters/types.d.ts +44 -0
  19. package/dist/adapters/types.js +1 -0
  20. package/dist/bench/ablate.d.ts +35 -0
  21. package/dist/bench/ablate.js +163 -0
  22. package/dist/bench/cell.d.ts +33 -0
  23. package/dist/bench/cell.js +437 -0
  24. package/dist/bench/results.d.ts +37 -0
  25. package/dist/bench/results.js +157 -0
  26. package/dist/bench/runner.d.ts +24 -0
  27. package/dist/bench/runner.js +121 -0
  28. package/dist/bench/tasks.d.ts +4 -0
  29. package/dist/bench/tasks.js +147 -0
  30. package/dist/bench/types.d.ts +109 -0
  31. package/dist/bench/types.js +1 -0
  32. package/dist/claude/transcripts.d.ts +30 -0
  33. package/dist/claude/transcripts.js +154 -0
  34. package/dist/cli/commands/benchmark.d.ts +33 -0
  35. package/dist/cli/commands/benchmark.js +203 -0
  36. package/dist/cli/commands/compress.d.ts +8 -0
  37. package/dist/cli/commands/compress.js +45 -0
  38. package/dist/cli/commands/count.d.ts +5 -0
  39. package/dist/cli/commands/count.js +25 -0
  40. package/dist/cli/commands/hook.d.ts +6 -0
  41. package/dist/cli/commands/hook.js +30 -0
  42. package/dist/cli/commands/init.d.ts +16 -0
  43. package/dist/cli/commands/init.js +76 -0
  44. package/dist/cli/commands/report.d.ts +90 -0
  45. package/dist/cli/commands/report.js +464 -0
  46. package/dist/cli/commands/savings.d.ts +38 -0
  47. package/dist/cli/commands/savings.js +196 -0
  48. package/dist/cli/commands/set-mode.d.ts +5 -0
  49. package/dist/cli/commands/set-mode.js +13 -0
  50. package/dist/cli/commands/stats.d.ts +5 -0
  51. package/dist/cli/commands/stats.js +51 -0
  52. package/dist/cli/commands/status.d.ts +1 -0
  53. package/dist/cli/commands/status.js +11 -0
  54. package/dist/cli/commands/uninstall.d.ts +7 -0
  55. package/dist/cli/commands/uninstall.js +22 -0
  56. package/dist/cli/index.d.ts +2 -0
  57. package/dist/cli/index.js +146 -0
  58. package/dist/copilot-hook-entry.d.ts +1 -0
  59. package/dist/copilot-hook-entry.js +36 -0
  60. package/dist/copilot-hook.js +1000 -0
  61. package/dist/engine/detect.d.ts +2 -0
  62. package/dist/engine/detect.js +47 -0
  63. package/dist/engine/index.d.ts +4 -0
  64. package/dist/engine/index.js +90 -0
  65. package/dist/engine/policy.d.ts +2 -0
  66. package/dist/engine/policy.js +48 -0
  67. package/dist/engine/tiers/code.d.ts +7 -0
  68. package/dist/engine/tiers/code.js +206 -0
  69. package/dist/engine/tiers/logs.d.ts +4 -0
  70. package/dist/engine/tiers/logs.js +139 -0
  71. package/dist/engine/tiers/structural.d.ts +28 -0
  72. package/dist/engine/tiers/structural.js +199 -0
  73. package/dist/engine/types.d.ts +71 -0
  74. package/dist/engine/types.js +5 -0
  75. package/dist/hook/copilot.d.ts +5 -0
  76. package/dist/hook/copilot.js +136 -0
  77. package/dist/hook/core.d.ts +36 -0
  78. package/dist/hook/core.js +138 -0
  79. package/dist/hook/exit.d.ts +22 -0
  80. package/dist/hook/exit.js +56 -0
  81. package/dist/hook/post-tool-use.d.ts +5 -0
  82. package/dist/hook/post-tool-use.js +57 -0
  83. package/dist/hook-entry.d.ts +1 -0
  84. package/dist/hook-entry.js +35 -0
  85. package/dist/hook.js +946 -0
  86. package/dist/index.d.ts +15 -0
  87. package/dist/index.js +16 -0
  88. package/dist/ledger/read.d.ts +9 -0
  89. package/dist/ledger/read.js +91 -0
  90. package/dist/ledger/write.d.ts +29 -0
  91. package/dist/ledger/write.js +61 -0
  92. package/dist/packs/atoms.d.ts +3 -0
  93. package/dist/packs/atoms.js +108 -0
  94. package/dist/packs/modes.d.ts +3 -0
  95. package/dist/packs/modes.js +34 -0
  96. package/dist/packs/render.d.ts +24 -0
  97. package/dist/packs/render.js +115 -0
  98. package/dist/packs/types.d.ts +32 -0
  99. package/dist/packs/types.js +1 -0
  100. package/dist/paths.d.ts +29 -0
  101. package/dist/paths.js +87 -0
  102. package/dist/tokens/estimate.d.ts +12 -0
  103. package/dist/tokens/estimate.js +23 -0
  104. package/dist/tokens/exact.d.ts +5 -0
  105. package/dist/tokens/exact.js +16 -0
  106. package/dist/tokens/index.d.ts +2 -0
  107. package/dist/tokens/index.js +2 -0
  108. package/package.json +77 -0
@@ -0,0 +1,2 @@
1
+ import type { ContentKind } from './types.ts';
2
+ export declare function detectKind(content: string, filePath?: string): ContentKind;
@@ -0,0 +1,47 @@
1
+ const CODE_EXTENSIONS = new Set([
2
+ 'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs',
3
+ 'rs', 'py', 'go', 'java', 'c', 'h', 'cpp', 'hpp',
4
+ 'rb', 'php', 'swift', 'kt', 'scala',
5
+ 'sh', 'zsh', 'css', 'scss', 'sql', 'toml', 'yaml', 'yml',
6
+ ]);
7
+ // Checkmark bullets (/^\s*[✓✗√×✘]\s/) alone are NOT a test-log signal: docs,
8
+ // checklists, and CLI summaries use them too. Require an unambiguous runner
9
+ // header/summary before classifying (and thus lossily filtering) as test-log.
10
+ const TEST_LOG_RES = [
11
+ /^(PASS|FAIL)\s/m,
12
+ /^Tests:\s/m,
13
+ /\d+\s+pass(?:ed|ing)\b/,
14
+ /\d+\s+fail(?:ed|ing)\b/,
15
+ /^test result:/m,
16
+ /--- FAIL/,
17
+ /^test\s+\S+\s+\.\.\.\s+(?:ok|FAILED)\s*$/m,
18
+ // node:test spec ('ℹ pass 118') and tap ('# pass 118') summary counters
19
+ /^(?:ℹ|#) (?:tests|suites|pass|fail|cancelled|skipped|todo) \d+\s*$/m,
20
+ ];
21
+ const BUILD_LOG_RES = [
22
+ /error\[E\d+\]/,
23
+ /^\s*(error|warning)(\s+TS\d+)?:/m,
24
+ /\bCompiling\s/,
25
+ /npm ERR!/,
26
+ ];
27
+ const STACK_FRAME_RE = /^\s+at .+:\d+:\d+/m;
28
+ export function detectKind(content, filePath) {
29
+ if (filePath !== undefined) {
30
+ const ext = extensionOf(filePath);
31
+ if (ext !== undefined && CODE_EXTENSIONS.has(ext))
32
+ return 'code';
33
+ }
34
+ if (content.startsWith('#!'))
35
+ return 'code';
36
+ if (TEST_LOG_RES.some((re) => re.test(content)))
37
+ return 'test-log';
38
+ if (BUILD_LOG_RES.some((re) => re.test(content)))
39
+ return 'build-log';
40
+ if (STACK_FRAME_RE.test(content) && /\berror\b/i.test(content))
41
+ return 'build-log';
42
+ return 'generic';
43
+ }
44
+ function extensionOf(filePath) {
45
+ const match = /\.([A-Za-z0-9]+)$/.exec(filePath);
46
+ return match?.[1]?.toLowerCase();
47
+ }
@@ -0,0 +1,4 @@
1
+ import type { CompressMeta, CompressResult, Estimator, Policy } from './types.ts';
2
+ export * from './types.ts';
3
+ export { policyFor } from './policy.ts';
4
+ export declare function compress(content: string, meta: CompressMeta, policy: Policy, estimate: Estimator): CompressResult;
@@ -0,0 +1,90 @@
1
+ import { OMISSION_MARKER } from "./types.js";
2
+ import { detectKind } from "./detect.js";
3
+ import { collapseBlankRuns, dedupeLines, stripAnsi, truncateHeadTail } from "./tiers/structural.js";
4
+ import { langFromPath, skeleton, stripComments } from "./tiers/code.js";
5
+ import { filterBuildLog, filterTestLog } from "./tiers/logs.js";
6
+ export * from "./types.js";
7
+ export { policyFor } from "./policy.js";
8
+ export function compress(content, meta, policy, estimate) {
9
+ const estTokensIn = estimate(content);
10
+ const bytesIn = utf8Bytes(content);
11
+ const passthrough = meta.mode === 'full' ||
12
+ meta.targeted === true ||
13
+ content.includes(OMISSION_MARKER) ||
14
+ estTokensIn < policy.touch;
15
+ if (passthrough) {
16
+ return {
17
+ content,
18
+ stats: {
19
+ bytesIn,
20
+ bytesOut: bytesIn,
21
+ estTokensIn,
22
+ estTokensOut: estTokensIn,
23
+ kind: detectKind(content, meta.filePath),
24
+ transforms: [],
25
+ },
26
+ };
27
+ }
28
+ let current = content;
29
+ const transforms = [];
30
+ const apply = (result) => {
31
+ current = result.content;
32
+ if (result.transform !== undefined)
33
+ transforms.push(result.transform);
34
+ };
35
+ // Decision estimator: threshold checks and truncation boundaries must not
36
+ // depend on marker TEXT, only on content — otherwise the marker-style
37
+ // experiment arms (plain/deterrent/informative phrasings of different
38
+ // lengths) would diverge in WHICH lines they keep, not just in marker
39
+ // wording. Any line containing OMISSION_MARKER mid-pipeline was inserted by
40
+ // an earlier tier in this run (pre-marked input passes through above), and
41
+ // every style inserts the same NUMBER of marker lines, so excluding them
42
+ // keeps decisions identical across styles.
43
+ const decide = (text) => estimate(text.includes(OMISSION_MARKER)
44
+ ? text
45
+ .split('\n')
46
+ .filter((line) => !line.includes(OMISSION_MARKER))
47
+ .join('\n')
48
+ : text);
49
+ if (policy.structural) {
50
+ apply(stripAnsi(current));
51
+ apply(collapseBlankRuns(current));
52
+ apply(dedupeLines(current));
53
+ }
54
+ const kind = detectKind(current, meta.filePath);
55
+ if (policy.codeAware && kind === 'code') {
56
+ const lang = langFromPath(meta.filePath);
57
+ if (decide(current) > policy.skeleton) {
58
+ apply(skeleton(current, lang, meta, estimate, policy.markerStyle));
59
+ }
60
+ else if (decide(current) > policy.commentStrip) {
61
+ apply(stripComments(current, lang, policy.markerStyle));
62
+ }
63
+ }
64
+ if (policy.logAware && decide(current) > policy.logFilter) {
65
+ if (kind === 'test-log')
66
+ apply(filterTestLog(current, policy.markerStyle));
67
+ else if (kind === 'build-log')
68
+ apply(filterBuildLog(current, policy.markerStyle));
69
+ }
70
+ if (decide(current) > policy.truncateBudget) {
71
+ // Earlier tiers (except strip-ansi) delete lines, so array positions no
72
+ // longer correspond to file line numbers.
73
+ const positionsAreFileLines = transforms.every((t) => t.id === 'strip-ansi');
74
+ apply(truncateHeadTail(current, meta, policy, decide, positionsAreFileLines));
75
+ }
76
+ return {
77
+ content: current,
78
+ stats: {
79
+ bytesIn,
80
+ bytesOut: utf8Bytes(current),
81
+ estTokensIn,
82
+ estTokensOut: estimate(current),
83
+ kind,
84
+ transforms,
85
+ },
86
+ };
87
+ }
88
+ function utf8Bytes(text) {
89
+ return new TextEncoder().encode(text).length;
90
+ }
@@ -0,0 +1,2 @@
1
+ import type { Mode, Policy } from './types.ts';
2
+ export declare function policyFor(mode: Mode): Policy;
@@ -0,0 +1,48 @@
1
+ export function policyFor(mode) {
2
+ switch (mode) {
3
+ case 'full':
4
+ return {
5
+ structural: false,
6
+ codeAware: false,
7
+ logAware: false,
8
+ // 'plain' everywhere for now: the marker-style experiment (bench
9
+ // hookArgs --marker-style) varies this per arm and picks the winner.
10
+ markerStyle: 'plain',
11
+ touch: Infinity,
12
+ truncateBudget: Infinity,
13
+ commentStrip: Infinity,
14
+ skeleton: Infinity,
15
+ logFilter: Infinity,
16
+ };
17
+ // PLAN.md: optimized = tier 1 + comment-strip; lossy tier-3 log filtering
18
+ // is reserved for slim.
19
+ case 'optimized':
20
+ return {
21
+ structural: true,
22
+ codeAware: true,
23
+ logAware: false,
24
+ markerStyle: 'plain',
25
+ touch: 600,
26
+ truncateBudget: 5000,
27
+ commentStrip: 2000,
28
+ skeleton: Infinity,
29
+ logFilter: Infinity,
30
+ };
31
+ case 'slim':
32
+ return {
33
+ structural: true,
34
+ codeAware: true,
35
+ logAware: true,
36
+ markerStyle: 'plain',
37
+ touch: 300,
38
+ // measured (bench-20260610-114234/-123102): a 2,500 budget pushed the
39
+ // model into offset/limit pagination — targeted reads pass through, so
40
+ // recovery re-reads nullified all savings (worst cell exceeded the
41
+ // uncompressed baseline). 5,000 stays under the recovery trigger.
42
+ truncateBudget: 5000,
43
+ commentStrip: 1000,
44
+ skeleton: 6000,
45
+ logFilter: 800,
46
+ };
47
+ }
48
+ }
@@ -0,0 +1,7 @@
1
+ import type { CompressMeta, Estimator, MarkerStyle } from '../types.ts';
2
+ import type { TierResult } from './structural.ts';
3
+ export type CodeLang = 'ts-js' | 'rust' | 'python' | 'go' | 'c-like' | 'ruby' | 'shell' | 'css' | 'sql' | 'config';
4
+ export declare function langFromPath(filePath?: string): CodeLang | undefined;
5
+ export declare function hasLineNumbers(content: string): boolean;
6
+ export declare function stripComments(content: string, lang: CodeLang | undefined, style?: MarkerStyle): TierResult;
7
+ export declare function skeleton(content: string, lang: CodeLang | undefined, meta: CompressMeta, estimate: Estimator, style?: MarkerStyle): TierResult;
@@ -0,0 +1,206 @@
1
+ import { formatMatchLines, lineNumberOf, omissionMarker, scanFailureLines, tierResult, } from "./structural.js";
2
+ const LANG_BY_EXT = {
3
+ ts: 'ts-js', tsx: 'ts-js', js: 'ts-js', jsx: 'ts-js', mjs: 'ts-js', cjs: 'ts-js',
4
+ rs: 'rust',
5
+ py: 'python',
6
+ go: 'go',
7
+ java: 'c-like', c: 'c-like', h: 'c-like', cpp: 'c-like', hpp: 'c-like',
8
+ swift: 'c-like', kt: 'c-like', scala: 'c-like', php: 'c-like',
9
+ rb: 'ruby',
10
+ sh: 'shell', zsh: 'shell',
11
+ css: 'css', scss: 'css',
12
+ sql: 'sql',
13
+ toml: 'config', yaml: 'config', yml: 'config',
14
+ };
15
+ export function langFromPath(filePath) {
16
+ if (filePath === undefined)
17
+ return undefined;
18
+ const match = /\.([A-Za-z0-9]+)$/.exec(filePath);
19
+ const ext = match?.[1]?.toLowerCase();
20
+ return ext === undefined ? undefined : LANG_BY_EXT[ext];
21
+ }
22
+ // Claude Code Read prefixes: " 123→content" (U+2192) or tab-separated variants.
23
+ const LINE_NUM_RE = /^ *\d+(?:→|\t)/;
24
+ export function hasLineNumbers(content) {
25
+ const lines = content.split('\n').filter((l) => l.length > 0);
26
+ if (lines.length < 2)
27
+ return false;
28
+ return lines.every((l) => LINE_NUM_RE.test(l));
29
+ }
30
+ function lineText(line) {
31
+ const match = LINE_NUM_RE.exec(line);
32
+ return match === null ? line : line.slice(match[0].length);
33
+ }
34
+ const COMMENT_SYNTAX = {
35
+ 'ts-js': { line: ['//'], block: { open: '/*', close: '*/' } },
36
+ rust: { line: ['//'], block: { open: '/*', close: '*/' } },
37
+ python: { line: ['#'] },
38
+ go: { line: ['//'], block: { open: '/*', close: '*/' } },
39
+ 'c-like': { line: ['//'], block: { open: '/*', close: '*/' } },
40
+ ruby: { line: ['#'] },
41
+ shell: { line: ['#'] },
42
+ css: { line: ['//'], block: { open: '/*', close: '*/' } },
43
+ sql: { line: ['--'], block: { open: '/*', close: '*/' } },
44
+ config: { line: ['#'] },
45
+ };
46
+ /** Advance python triple-quote string state across one line of code. */
47
+ function scanPythonTriples(text, open) {
48
+ let state = open;
49
+ let i = 0;
50
+ while (i < text.length) {
51
+ if (state === null) {
52
+ const dq = text.indexOf('"""', i);
53
+ const sq = text.indexOf("'''", i);
54
+ const next = dq === -1 ? sq : sq === -1 ? dq : Math.min(dq, sq);
55
+ if (next === -1)
56
+ break;
57
+ state = text.startsWith('"""', next) ? '"""' : "'''";
58
+ i = next + 3;
59
+ }
60
+ else {
61
+ const close = text.indexOf(state, i);
62
+ if (close === -1)
63
+ break;
64
+ i = close + 3;
65
+ state = null;
66
+ }
67
+ }
68
+ return state;
69
+ }
70
+ function commentStripMarker(stripped, strippedLines, style) {
71
+ const head = `[compressor: ${stripped} comment/blank lines stripped — line numbers preserved`;
72
+ if (style === 'deterrent') {
73
+ return `${head}; comments are likely irrelevant to the problem you are chasing]`;
74
+ }
75
+ if (style === 'informative') {
76
+ const scan = scanFailureLines(strippedLines);
77
+ return scan.count === 0
78
+ ? `${head}; no error/failure/warning text among them; safe to skip]`
79
+ : `${head}; ${scan.count} stripped lines matching error/fail/warn at lines ${formatMatchLines(scan)}]`;
80
+ }
81
+ return `${head}]`;
82
+ }
83
+ export function stripComments(content, lang, style = 'plain') {
84
+ if (lang === undefined || !hasLineNumbers(content))
85
+ return { content };
86
+ // yaml/toml '#' lines inside block scalars are data, not comments — never strip config.
87
+ if (lang === 'config')
88
+ return { content };
89
+ const syntax = COMMENT_SYNTAX[lang];
90
+ const block = syntax.block;
91
+ const lines = content.split('\n');
92
+ const kept = [];
93
+ const strippedLines = [];
94
+ let stripped = 0;
95
+ let inBlock = false;
96
+ let tripleOpen = null;
97
+ for (const line of lines) {
98
+ if (line.length === 0) {
99
+ kept.push(line);
100
+ continue;
101
+ }
102
+ const text = lineText(line);
103
+ if (lang === 'python' && tripleOpen !== null) {
104
+ // inside a triple-quoted string: '#' and blank lines are literal data
105
+ tripleOpen = scanPythonTriples(text, tripleOpen);
106
+ kept.push(line);
107
+ continue;
108
+ }
109
+ const trimmed = text.trim();
110
+ let drop = false;
111
+ if (inBlock && block !== undefined) {
112
+ const closeIdx = trimmed.indexOf(block.close);
113
+ if (closeIdx >= 0) {
114
+ inBlock = false;
115
+ drop = trimmed.slice(closeIdx + block.close.length).trim() === '';
116
+ }
117
+ else {
118
+ drop = true;
119
+ }
120
+ }
121
+ else if (trimmed === '') {
122
+ drop = true;
123
+ }
124
+ else if (trimmed.startsWith('#!')) {
125
+ drop = false;
126
+ }
127
+ else if (syntax.line.some((p) => trimmed.startsWith(p))) {
128
+ drop = true;
129
+ }
130
+ else if (block !== undefined && trimmed.startsWith(block.open)) {
131
+ const rest = trimmed.slice(block.open.length);
132
+ const closeIdx = rest.indexOf(block.close);
133
+ if (closeIdx >= 0) {
134
+ drop = rest.slice(closeIdx + block.close.length).trim() === '';
135
+ }
136
+ else {
137
+ drop = true;
138
+ inBlock = true;
139
+ }
140
+ }
141
+ if (drop) {
142
+ stripped += 1;
143
+ strippedLines.push(line);
144
+ }
145
+ else {
146
+ if (lang === 'python')
147
+ tripleOpen = scanPythonTriples(text, null);
148
+ kept.push(line);
149
+ }
150
+ }
151
+ if (stripped === 0)
152
+ return { content };
153
+ const marker = commentStripMarker(stripped, strippedLines, style);
154
+ const trailing = kept.length > 0 && kept[kept.length - 1] === '' ? kept.pop() : undefined;
155
+ kept.push(marker);
156
+ if (trailing !== undefined)
157
+ kept.push(trailing);
158
+ return tierResult(content, kept.join('\n'), 'comment-strip');
159
+ }
160
+ const SIGNATURE_TESTS = {
161
+ 'ts-js': (t) => /^(import|export|function|async function|class|interface|type)\b/.test(t),
162
+ rust: (t) => /^\s*(use|pub|fn|struct|enum|trait|impl|mod)\b/.test(t),
163
+ python: (t) => /^\s*(import|from|def|class|async def)\b/.test(t),
164
+ go: (t) => /^(package|import|func|type)\b/.test(t),
165
+ };
166
+ export function skeleton(content, lang, meta, estimate, style = 'plain') {
167
+ if (lang === undefined || !hasLineNumbers(content))
168
+ return { content };
169
+ const isSignature = SIGNATURE_TESTS[lang];
170
+ if (isSignature === undefined)
171
+ return { content };
172
+ const lines = content.split('\n');
173
+ const out = [];
174
+ let gap = [];
175
+ const flushGap = () => {
176
+ if (gap.length === 0)
177
+ return;
178
+ const first = gap[0];
179
+ const last = gap[gap.length - 1];
180
+ const a = first === undefined ? undefined : lineNumberOf(first);
181
+ const b = last === undefined ? undefined : lineNumberOf(last);
182
+ if (gap.length < 2 || a === undefined || b === undefined) {
183
+ out.push(...gap);
184
+ }
185
+ else {
186
+ out.push(omissionMarker(a, b, estimate(gap.join('\n')), meta, style, gap));
187
+ }
188
+ gap = [];
189
+ };
190
+ for (const line of lines) {
191
+ if (line.length === 0) {
192
+ flushGap();
193
+ out.push(line);
194
+ continue;
195
+ }
196
+ if (isSignature(lineText(line))) {
197
+ flushGap();
198
+ out.push(line);
199
+ }
200
+ else {
201
+ gap.push(line);
202
+ }
203
+ }
204
+ flushGap();
205
+ return tierResult(content, out.join('\n'), 'skeleton');
206
+ }
@@ -0,0 +1,4 @@
1
+ import type { MarkerStyle } from '../types.ts';
2
+ import type { TierResult } from './structural.ts';
3
+ export declare function filterTestLog(content: string, style?: MarkerStyle): TierResult;
4
+ export declare function filterBuildLog(content: string, style?: MarkerStyle): TierResult;
@@ -0,0 +1,139 @@
1
+ import { scanFailureLines, tierResult } from "./structural.js";
2
+ /**
3
+ * Style-aware omission marker shared by the test/build log filters. Logs have
4
+ * no file coordinates, so the informative variant reports match counts only.
5
+ */
6
+ function logMarker(noun, omittedLines, style) {
7
+ const head = `[compressor: ${omittedLines.length} ${noun} lines omitted`;
8
+ if (style === 'deterrent') {
9
+ return `${head} — likely irrelevant; re-run with a narrower filter ONLY if the problem you are chasing points into the omitted output]`;
10
+ }
11
+ if (style === 'informative') {
12
+ const matches = scanFailureLines(omittedLines).count;
13
+ return matches === 0
14
+ ? `${head} — no error/failure/warning lines in the omitted output; safe to skip. Re-run with a narrower filter only if needed]`
15
+ : `${head} — ${matches} omitted lines matching error/fail/warn — re-run with a narrower filter to retrieve]`;
16
+ }
17
+ return `${head}]`;
18
+ }
19
+ const TEST_PASS_RES = [
20
+ /^\s*[✓✔√]\s/,
21
+ /^PASS\s/,
22
+ /^\s*--- PASS:/,
23
+ /^test\s+\S+\s+\.\.\.\s+ok\s*$/,
24
+ /\bPASSED\s*$/,
25
+ /^ok\s+\d+\s/,
26
+ ];
27
+ const TEST_FAIL_RES = [
28
+ /^\s*[✗✘✖×]\s/,
29
+ /^FAIL\b/,
30
+ /--- FAIL/,
31
+ /\bFAILED\b/,
32
+ /^not ok\s/,
33
+ /\bpanicked\b/,
34
+ /^\s*●/,
35
+ // error reports only ('Error: boom', 'error: x'), not prose containing 'error'
36
+ /^\s*(?:[A-Za-z]*Error|error)\s*[:(]/,
37
+ ];
38
+ const TEST_SUMMARY_RES = [
39
+ /^(Tests|Test Suites|Snapshots|Time|Duration):/,
40
+ /\d+\s+pass(?:ed|ing)\b/,
41
+ /\d+\s+fail(?:ed|ing)\b/,
42
+ /^test result:/,
43
+ /^Ran all test suites/,
44
+ /^=+ .* =+$/,
45
+ // node:test spec ('ℹ pass 118') and tap ('# pass 118') summary counters
46
+ /^(?:ℹ|#) (?:tests|suites|pass|fail|cancelled|skipped|todo|duration_ms) \d+\s*$/,
47
+ ];
48
+ export function filterTestLog(content, style = 'plain') {
49
+ const lines = content.split('\n');
50
+ const isFail = (l) => TEST_FAIL_RES.some((re) => re.test(l));
51
+ const isSummary = (l) => TEST_SUMMARY_RES.some((re) => re.test(l));
52
+ const isPass = (l) => TEST_PASS_RES.some((re) => re.test(l));
53
+ // unrecognized format: never drop anything
54
+ if (!lines.some(isFail) && !lines.some(isSummary))
55
+ return { content };
56
+ const out = [];
57
+ const omittedLines = [];
58
+ let markerIdx = -1;
59
+ for (const line of lines) {
60
+ if (isPass(line) && !isFail(line) && !isSummary(line)) {
61
+ if (markerIdx < 0)
62
+ markerIdx = out.length;
63
+ omittedLines.push(line);
64
+ }
65
+ else {
66
+ out.push(line);
67
+ }
68
+ }
69
+ if (omittedLines.length === 0)
70
+ return { content };
71
+ out.splice(markerIdx, 0, logMarker('passing-test', omittedLines, style));
72
+ return tierResult(content, out.join('\n'), 'log-filter');
73
+ }
74
+ const BUILD_ERROR_RES = [
75
+ /error\[E\d+\]/,
76
+ /^\s*(error|warning)(\s+[A-Z]+\d+)?:/i,
77
+ /\b(error|warning)\s+TS\d+:/,
78
+ /:\d+(?::\d+)?:\s*(?:fatal\s+)?(?:error|warning)\b/,
79
+ /npm ERR!/,
80
+ /^\s*(Error|TypeError|SyntaxError|ReferenceError|RangeError)\b/,
81
+ ];
82
+ const BUILD_STATUS_RES = [
83
+ /^\s*error: aborting/,
84
+ /^\s*error: could not compile/,
85
+ /Found \d+ errors?/,
86
+ /\d+ errors? generated/,
87
+ /^make: \*\*\*/,
88
+ /^Build (?:FAILED|failed|succeeded)/,
89
+ /^\s*Finished\b/,
90
+ /[✖✗] \d+ problems?/,
91
+ /exited with code \d+/,
92
+ ];
93
+ export function filterBuildLog(content, style = 'plain') {
94
+ const lines = content.split('\n');
95
+ const isError = (l) => BUILD_ERROR_RES.some((re) => re.test(l));
96
+ const isStatus = (l) => BUILD_STATUS_RES.some((re) => re.test(l));
97
+ // unrecognized format: never drop anything
98
+ if (!lines.some(isError) && !lines.some(isStatus))
99
+ return { content };
100
+ const out = [];
101
+ const omittedLines = [];
102
+ let markerIdx = -1;
103
+ let inErrorBlock = false;
104
+ for (const line of lines) {
105
+ let keep;
106
+ if (isError(line)) {
107
+ inErrorBlock = true;
108
+ keep = true;
109
+ }
110
+ else if (isStatus(line)) {
111
+ inErrorBlock = false;
112
+ keep = true;
113
+ }
114
+ else if (inErrorBlock) {
115
+ if (line.trim() === '') {
116
+ inErrorBlock = false;
117
+ keep = false;
118
+ }
119
+ else {
120
+ keep = true;
121
+ }
122
+ }
123
+ else {
124
+ keep = false;
125
+ }
126
+ if (keep) {
127
+ out.push(line);
128
+ }
129
+ else {
130
+ if (markerIdx < 0)
131
+ markerIdx = out.length;
132
+ omittedLines.push(line);
133
+ }
134
+ }
135
+ if (omittedLines.length === 0)
136
+ return { content };
137
+ out.splice(markerIdx, 0, logMarker('build-log', omittedLines, style));
138
+ return tierResult(content, out.join('\n'), 'log-filter');
139
+ }
@@ -0,0 +1,28 @@
1
+ import type { AppliedTransform, CompressMeta, Estimator, MarkerStyle, Policy } from '../types.ts';
2
+ export interface TierResult {
3
+ content: string;
4
+ transform?: AppliedTransform;
5
+ }
6
+ export declare function tierResult(before: string, after: string, id: string): TierResult;
7
+ export declare function stripAnsi(content: string): TierResult;
8
+ export declare function collapseBlankRuns(content: string): TierResult;
9
+ export declare function dedupeLines(content: string): TierResult;
10
+ export declare function lineNumberOf(line: string): number | undefined;
11
+ export interface FailureScan {
12
+ /** matching lines in the scanned region (compressor markers excluded) */
13
+ count: number;
14
+ /** original-coordinate line numbers of the first 3 matches, when resolvable */
15
+ lines: number[];
16
+ }
17
+ /**
18
+ * Scan omitted lines for error/failure/warning content. Original coordinates
19
+ * follow the same rules as the marker math: embedded Read line numbers are
20
+ * authoritative; `firstFileLine + index` is the fallback when the caller
21
+ * vouches that positions are file lines. Marker lines inserted by earlier
22
+ * tiers in this run are never counted as content.
23
+ */
24
+ export declare function scanFailureLines(lines: readonly string[], firstFileLine?: number): FailureScan;
25
+ /** "L1, L2, L3 (first 3)" — the qualifier only when matches were truncated. */
26
+ export declare function formatMatchLines(scan: FailureScan): string;
27
+ export declare function omissionMarker(a: number, b: number, estTokens: number, meta: CompressMeta, style: MarkerStyle, omittedLines: readonly string[]): string;
28
+ export declare function truncateHeadTail(content: string, meta: CompressMeta, policy: Policy, estimate: Estimator, positionsAreFileLines?: boolean): TierResult;