agentic-compaction 0.0.1

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.
@@ -0,0 +1,181 @@
1
+ const PYTHON_EXTENSIONS = ['.py'];
2
+
3
+ export const isPythonParseable = (path) => {
4
+ return PYTHON_EXTENSIONS.some(ext => path.endsWith(ext));
5
+ };
6
+
7
+ /**
8
+ * Extract skeleton from Python source code using regex-based line-by-line parsing.
9
+ * Only parses top-level statements (no indentation).
10
+ * @param {string} code - Source code to parse
11
+ * @param {string} filePath - File path for error reporting
12
+ * @returns {Object|null} Skeleton data
13
+ */
14
+ export const extractSkeleton = (code, filePath = '') => {
15
+ const lines = code.split('\n');
16
+ const skeleton = {
17
+ imports: [],
18
+ functions: [],
19
+ classes: [],
20
+ constants: 0,
21
+ };
22
+
23
+ // Collect decorators as we scan
24
+ let pendingDecorators = [];
25
+
26
+ for (let i = 0; i < lines.length; i++) {
27
+ const line = lines[i];
28
+ const lineNum = i + 1;
29
+
30
+ // Skip empty lines and comments
31
+ if (/^\s*$/.test(line) || /^\s*#/.test(line)) continue;
32
+
33
+ // Only process top-level (no indentation)
34
+ if (/^\s/.test(line) && !/^@/.test(line)) {
35
+ pendingDecorators = [];
36
+ continue;
37
+ }
38
+
39
+ // Decorators
40
+ const decoratorMatch = line.match(/^@(\w[\w.]*)/);
41
+ if (decoratorMatch) {
42
+ pendingDecorators.push(decoratorMatch[1]);
43
+ continue;
44
+ }
45
+
46
+ // import x / import x, y
47
+ const importMatch = line.match(/^import\s+(.+)/);
48
+ if (importMatch) {
49
+ const modules = importMatch[1].split(',').map(s => s.trim().split(/\s+as\s+/)[0]);
50
+ for (const mod of modules) {
51
+ skeleton.imports.push({ module: mod, names: [] });
52
+ }
53
+ pendingDecorators = [];
54
+ continue;
55
+ }
56
+
57
+ // from x import y, z
58
+ const fromImportMatch = line.match(/^from\s+([\w.]+)\s+import\s+(.+)/);
59
+ if (fromImportMatch) {
60
+ const module = fromImportMatch[1];
61
+ let namesStr = fromImportMatch[2].trim();
62
+
63
+ // Handle multi-line imports with parentheses
64
+ if (namesStr.startsWith('(')) {
65
+ namesStr = namesStr.slice(1);
66
+ while (i + 1 < lines.length && !namesStr.includes(')')) {
67
+ i++;
68
+ namesStr += ' ' + lines[i].trim();
69
+ }
70
+ namesStr = namesStr.replace(')', '');
71
+ }
72
+
73
+ const names = namesStr
74
+ .split(',')
75
+ .map(s => s.trim().split(/\s+as\s+/)[0])
76
+ .filter(Boolean);
77
+
78
+ skeleton.imports.push({ module, names });
79
+ pendingDecorators = [];
80
+ continue;
81
+ }
82
+
83
+ // async def / def
84
+ const funcMatch = line.match(/^(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)/);
85
+ if (funcMatch) {
86
+ // For multi-line params, grab until closing paren
87
+ let params = funcMatch[2];
88
+ if (!line.includes(')')) {
89
+ let j = i + 1;
90
+ while (j < lines.length && !lines[j].includes(')')) {
91
+ params += ' ' + lines[j].trim();
92
+ j++;
93
+ }
94
+ if (j < lines.length) {
95
+ params += ' ' + lines[j].split(')')[0].trim();
96
+ }
97
+ }
98
+ // Clean up params: remove type annotations and defaults for brevity
99
+ params = params.replace(/\s+/g, ' ').trim();
100
+
101
+ skeleton.functions.push({
102
+ name: funcMatch[1],
103
+ line: lineNum,
104
+ decorators: [...pendingDecorators],
105
+ params,
106
+ });
107
+ pendingDecorators = [];
108
+ continue;
109
+ }
110
+
111
+ // class Name(bases):
112
+ const classMatch = line.match(/^class\s+(\w+)\s*(?:\(([^)]*)\))?\s*:/);
113
+ if (classMatch) {
114
+ const bases = classMatch[2]
115
+ ? classMatch[2].split(',').map(s => s.trim()).filter(Boolean)
116
+ : [];
117
+
118
+ skeleton.classes.push({
119
+ name: classMatch[1],
120
+ line: lineNum,
121
+ decorators: [...pendingDecorators],
122
+ bases,
123
+ });
124
+ pendingDecorators = [];
125
+ continue;
126
+ }
127
+
128
+ // Top-level assignments (constants)
129
+ const assignMatch = line.match(/^[A-Za-z_]\w*\s*[=:]/) || line.match(/^[A-Za-z_]\w*\s*:/);
130
+ if (assignMatch) {
131
+ skeleton.constants++;
132
+ pendingDecorators = [];
133
+ continue;
134
+ }
135
+
136
+ // Reset decorators for any other top-level statement
137
+ pendingDecorators = [];
138
+ }
139
+
140
+ return skeleton;
141
+ };
142
+
143
+ /**
144
+ * Format Python skeleton for prompt output
145
+ * @param {Object} skeleton - Skeleton data object
146
+ * @returns {string}
147
+ */
148
+ export const formatSkeletonForPrompt = (skeleton) => {
149
+ if (!skeleton) return '';
150
+
151
+ const lines = [];
152
+
153
+ if (skeleton.imports.length > 0) {
154
+ const local = skeleton.imports.filter(i => i.module.startsWith('.'));
155
+ const extCount = skeleton.imports.length - local.length;
156
+ const parts = [];
157
+ if (extCount > 0) parts.push(`${extCount} ext`);
158
+ parts.push(...local.map(i => i.module));
159
+ lines.push(`imports: ${parts.join(', ')}`);
160
+ }
161
+
162
+ if (skeleton.classes.length > 0) {
163
+ const classList = skeleton.classes.map(c => {
164
+ const parts = [c.name];
165
+ if (c.decorators.length > 0) parts.push(`@${c.decorators[0]}`);
166
+ if (c.bases.length > 0) parts.push(`(${c.bases.join(',')})`);
167
+ return `${parts.join(' ')}:${c.line}`;
168
+ }).join(', ');
169
+ lines.push(`classes: ${classList}`);
170
+ }
171
+
172
+ if (skeleton.functions.length > 0) {
173
+ const funcList = skeleton.functions.map(f => {
174
+ const deco = f.decorators.length > 0 ? `@${f.decorators[0]} ` : '';
175
+ return `${deco}${f.name}:${f.line}`;
176
+ }).join(', ');
177
+ lines.push(`fn: ${funcList}`);
178
+ }
179
+
180
+ return lines.join('\n');
181
+ };
package/src/walker.js ADDED
@@ -0,0 +1,54 @@
1
+ import { readdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { isBabelParseable } from './parsers/babel.js';
4
+ import { isPythonParseable } from './parsers/python.js';
5
+
6
+ const SKIP_DIRECTORIES = new Set([
7
+ 'node_modules',
8
+ 'dist',
9
+ '.git',
10
+ 'target',
11
+ 'build',
12
+ '.next',
13
+ '.turbo',
14
+ 'out',
15
+ 'coverage',
16
+ '.cache',
17
+ '__pycache__',
18
+ '.venv',
19
+ 'venv',
20
+ '.idea',
21
+ '.vscode',
22
+ ]);
23
+
24
+ export function collectFiles(dir, rootDir = dir, files = []) {
25
+ let entries;
26
+ try {
27
+ entries = readdirSync(dir, { withFileTypes: true });
28
+ } catch {
29
+ return files;
30
+ }
31
+
32
+ for (const entry of entries) {
33
+ const fullPath = join(dir, entry.name);
34
+
35
+ if (entry.isDirectory()) {
36
+ if (!SKIP_DIRS_OR_DOT(entry.name)) {
37
+ collectFiles(fullPath, rootDir, files);
38
+ }
39
+ } else if (entry.isFile() && isParseable(fullPath)) {
40
+ const relativePath = fullPath.slice(rootDir.length + 1);
41
+ files.push({ path: fullPath, relativePath });
42
+ }
43
+ }
44
+
45
+ return files;
46
+ }
47
+
48
+ function SKIP_DIRS_OR_DOT(name) {
49
+ return SKIP_DIRECTORIES.has(name) || name.startsWith('.');
50
+ }
51
+
52
+ function isParseable(path) {
53
+ return isBabelParseable(path) || isPythonParseable(path);
54
+ }
@@ -0,0 +1,18 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { fetchData } from './api';
3
+
4
+ const API_URL = 'https://example.com';
5
+
6
+ export function MyComponent({ id }) {
7
+ const [data, setData] = useState(null);
8
+
9
+ useEffect(() => {
10
+ fetchData(id).then(setData);
11
+ }, [id]);
12
+
13
+ return <div>{data}</div>;
14
+ }
15
+
16
+ const helper = (x) => x * 2;
17
+
18
+ export default MyComponent;
@@ -0,0 +1,25 @@
1
+ import os
2
+ import sys
3
+ from pathlib import Path
4
+ from typing import List, Optional
5
+
6
+ MAX_RETRIES = 3
7
+ DEFAULT_TIMEOUT = 30
8
+
9
+ class BaseProcessor:
10
+ pass
11
+
12
+ @dataclass
13
+ class Config(BaseProcessor):
14
+ name: str
15
+ value: int
16
+
17
+ def process_data(items, timeout=30):
18
+ pass
19
+
20
+ async def fetch_remote(url, **kwargs):
21
+ pass
22
+
23
+ @app.route('/api')
24
+ def api_handler(request):
25
+ pass
package/test/test.js ADDED
@@ -0,0 +1,102 @@
1
+ import { describe, it } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { readFileSync } from 'fs';
4
+ import { resolve, dirname } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ import { compactFile, compactProject } from '../src/index.js';
8
+ import { extractSkeleton as extractBabelSkeleton } from '../src/parsers/babel.js';
9
+ import { extractSkeleton as extractPythonSkeleton, formatSkeletonForPrompt as formatPythonSkeleton } from '../src/parsers/python.js';
10
+ import { collectFiles } from '../src/walker.js';
11
+
12
+ const __dirname = dirname(fileURLToPath(import.meta.url));
13
+ const fixturesDir = resolve(__dirname, 'fixtures');
14
+
15
+ describe('babel parser', () => {
16
+ it('extracts skeleton from JS file', () => {
17
+ const code = readFileSync(resolve(fixturesDir, 'sample.js'), 'utf-8');
18
+ const skeleton = extractBabelSkeleton(code, 'sample.js');
19
+
20
+ assert.ok(skeleton);
21
+ assert.ok(skeleton.imports.length > 0);
22
+ assert.ok(skeleton.components.length > 0);
23
+ assert.ok(skeleton.functions.length > 0);
24
+ assert.strictEqual(skeleton.hooks.useState, 1);
25
+ assert.strictEqual(skeleton.hooks.useEffect.length, 1);
26
+ });
27
+ });
28
+
29
+ describe('python parser', () => {
30
+ it('extracts skeleton from Python file', () => {
31
+ const code = readFileSync(resolve(fixturesDir, 'sample.py'), 'utf-8');
32
+ const skeleton = extractPythonSkeleton(code, 'sample.py');
33
+
34
+ assert.ok(skeleton);
35
+ assert.strictEqual(skeleton.imports.length, 4);
36
+ assert.strictEqual(skeleton.functions.length, 3);
37
+ assert.strictEqual(skeleton.classes.length, 2);
38
+ assert.ok(skeleton.constants >= 2);
39
+ });
40
+
41
+ it('parses decorators', () => {
42
+ const code = readFileSync(resolve(fixturesDir, 'sample.py'), 'utf-8');
43
+ const skeleton = extractPythonSkeleton(code, 'sample.py');
44
+
45
+ const config = skeleton.classes.find(c => c.name === 'Config');
46
+ assert.ok(config);
47
+ assert.deepStrictEqual(config.decorators, ['dataclass']);
48
+ assert.deepStrictEqual(config.bases, ['BaseProcessor']);
49
+
50
+ const apiHandler = skeleton.functions.find(f => f.name === 'api_handler');
51
+ assert.ok(apiHandler);
52
+ assert.deepStrictEqual(apiHandler.decorators, ['app.route']);
53
+ });
54
+
55
+ it('formats skeleton', () => {
56
+ const code = readFileSync(resolve(fixturesDir, 'sample.py'), 'utf-8');
57
+ const skeleton = extractPythonSkeleton(code, 'sample.py');
58
+ const output = formatPythonSkeleton(skeleton);
59
+
60
+ assert.ok(output.includes('imports:'));
61
+ assert.ok(output.includes('classes:'));
62
+ assert.ok(output.includes('fn:'));
63
+ });
64
+ });
65
+
66
+ describe('walker', () => {
67
+ it('collects files from fixtures', () => {
68
+ const files = collectFiles(fixturesDir);
69
+ assert.ok(files.length >= 2);
70
+ assert.ok(files.some(f => f.relativePath.endsWith('.js')));
71
+ assert.ok(files.some(f => f.relativePath.endsWith('.py')));
72
+ });
73
+ });
74
+
75
+ describe('compactFile', () => {
76
+ it('compacts a JS file', () => {
77
+ const code = readFileSync(resolve(fixturesDir, 'sample.js'), 'utf-8');
78
+ const result = compactFile('sample.js', code);
79
+ assert.ok(result.skeleton);
80
+ assert.ok(result.formatted.length > 0);
81
+ });
82
+
83
+ it('compacts a Python file', () => {
84
+ const code = readFileSync(resolve(fixturesDir, 'sample.py'), 'utf-8');
85
+ const result = compactFile('sample.py', code);
86
+ assert.ok(result.skeleton);
87
+ assert.ok(result.formatted.length > 0);
88
+ });
89
+ });
90
+
91
+ describe('compactProject', () => {
92
+ it('compacts the fixtures directory', () => {
93
+ const result = compactProject(fixturesDir);
94
+ assert.ok(result.output.length > 0);
95
+ assert.ok(result.output.includes('## sample.js'));
96
+ assert.ok(result.output.includes('## sample.py'));
97
+ assert.ok(result.stats.files >= 2);
98
+ assert.ok(result.stats.rawTokens > 0);
99
+ assert.ok(result.stats.compactedTokens > 0);
100
+ assert.ok(result.stats.compactedTokens < result.stats.rawTokens);
101
+ });
102
+ });