coban 0.0.0-stage → 0.2.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,174 @@
1
+ // Deterministic context packing: a cheap repo map for the orchestrator and code snippets inlined into worker tasks,
2
+ // so workers don't spend requests (each re-reading ~45k of baseline context) searching for code they were pointed at.
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { execFileSync } from 'node:child_process';
6
+ const CODE = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|swift|kt|java|rb|php|cs)$/;
7
+ const SKIP = /(^|\/)(node_modules|dist|build|\.git|\.next|vendor|Pods)\//;
8
+ const MAX_SNIPPET_LINES = 150;
9
+ const MAX_TOTAL_LINES = 400;
10
+ function listFiles(root) {
11
+ try {
12
+ return execFileSync('git', ['ls-files'], { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
13
+ .split('\n').filter(Boolean);
14
+ }
15
+ catch {
16
+ const out = [];
17
+ const walk = (d) => {
18
+ for (const e of fs.readdirSync(path.join(root, d), { withFileTypes: true })) {
19
+ const rel = d ? `${d}/${e.name}` : e.name;
20
+ if (e.name.startsWith('.') || SKIP.test(rel + '/'))
21
+ continue;
22
+ if (e.isDirectory())
23
+ walk(rel);
24
+ else
25
+ out.push(rel);
26
+ }
27
+ };
28
+ walk('');
29
+ return out;
30
+ }
31
+ }
32
+ // Top-level declarations and class members, with 1-based line numbers.
33
+ function symbols(src, file) {
34
+ const out = [];
35
+ const py = file.endsWith('.py');
36
+ src.split('\n').forEach((l, i) => {
37
+ let m = null;
38
+ if (py)
39
+ m = l.match(/^(?: {4})?(?:async\s+)?(?:def|class)\s+(\w+)/);
40
+ else {
41
+ m = l.match(/^(?:export\s+)?(?:default\s+)?(?:async\s+)?(?:function\*?|class|interface|type|enum|const|let)\s+(\w+)/) ??
42
+ l.match(/^ {2}(?:(?:public|private|protected|static|async|get|set)\s+)*(\w+)\s*(?:<[^>]*>)?\([^)]*\)?.*\{\s*$/) ??
43
+ l.match(/^(?:func|fn|pub fn|def)\s+(?:\([^)]*\)\s*)?(\w+)/);
44
+ }
45
+ if (m && !/^(if|for|while|switch|return|catch|constructor)$/.test(m[1]))
46
+ out.push(`${m[1]}:${i + 1}`);
47
+ });
48
+ return out;
49
+ }
50
+ export function repoMap(root, sub = '') {
51
+ const files = listFiles(root).filter((f) => (!sub || f.startsWith(sub.replace(/^\.\//, ''))) && !SKIP.test(f));
52
+ const lines = [];
53
+ let budget = 160;
54
+ for (const f of files) {
55
+ const abs = path.join(root, f);
56
+ let src = '';
57
+ try {
58
+ if (fs.statSync(abs).size > 400_000)
59
+ continue;
60
+ src = fs.readFileSync(abs, 'utf8');
61
+ }
62
+ catch {
63
+ continue;
64
+ }
65
+ const n = src.split('\n').length;
66
+ if (CODE.test(f) && budget > 0) {
67
+ const syms = symbols(src, f);
68
+ lines.push(`${f} (${n})${syms.length ? ' ' + syms.join(' ') : ''}`);
69
+ budget--;
70
+ }
71
+ else if (!/\.(lock|png|jpe?g|gif|ico|svg|woff2?)$|package-lock\.json$/.test(f)) {
72
+ lines.push(`${f} (${n})`);
73
+ }
74
+ }
75
+ return lines.join('\n');
76
+ }
77
+ // End line (0-based, inclusive) of the block starting at `start`.
78
+ function blockEnd(ls, start, py) {
79
+ if (py) {
80
+ const ind = ls[start].match(/^\s*/)[0].length;
81
+ let end = start;
82
+ for (let i = start + 1; i < ls.length; i++) {
83
+ if (!ls[i].trim())
84
+ continue;
85
+ if (ls[i].match(/^\s*/)[0].length <= ind)
86
+ break;
87
+ end = i;
88
+ }
89
+ return end;
90
+ }
91
+ let depth = 0, seen = false;
92
+ for (let i = start; i < ls.length; i++) {
93
+ for (const ch of ls[i].replace(/(["'`])(?:\\.|(?!\1).)*\1/g, '""')) {
94
+ if (ch === '{') {
95
+ depth++;
96
+ seen = true;
97
+ }
98
+ else if (ch === '}')
99
+ depth--;
100
+ }
101
+ if (seen && depth <= 0)
102
+ return i;
103
+ if (!seen && /;\s*$/.test(ls[i]))
104
+ return i; // one-line declaration
105
+ }
106
+ return ls.length - 1;
107
+ }
108
+ function findSymbol(ls, name, py) {
109
+ const n = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
110
+ const pats = py
111
+ ? [new RegExp(`^\\s*(async\\s+)?(def|class)\\s+${n}\\b`)]
112
+ : [
113
+ new RegExp(`^\\s*(export\\s+)?(default\\s+)?(async\\s+)?(function\\*?|class|interface|type|enum|const|let|var)\\s+${n}\\b`),
114
+ new RegExp(`^\\s*((public|private|protected|static|async|get|set)\\s+)*${n}\\s*(<[^>]*>)?\\(`),
115
+ new RegExp(`^\\s*(func|fn|pub fn)\\s+(\\([^)]*\\)\\s*)?${n}\\b`),
116
+ ];
117
+ for (const re of pats) {
118
+ const i = ls.findIndex((l) => re.test(l));
119
+ if (i >= 0)
120
+ return i;
121
+ }
122
+ return -1;
123
+ }
124
+ const numbered = (ls, a, b) => ls.slice(a, b + 1).map((l, k) => `${String(a + k + 1).padStart(4)}│ ${l}`).join('\n');
125
+ // spec: "path:10-80" | "path:42" | "path#sym1,sym2" | "path" (whole file, capped)
126
+ export function snippets(specs, cwd) {
127
+ const parts = [];
128
+ let total = 0;
129
+ for (const spec of specs.flatMap((s) => s.split(';')).map((s) => s.trim()).filter(Boolean)) {
130
+ const m = spec.match(/^(.*?)(?:#(.+)|:(\d+)(?:-(\d+))?)?$/);
131
+ const file = m[1];
132
+ let ls;
133
+ try {
134
+ ls = fs.readFileSync(path.resolve(cwd, file), 'utf8').split('\n');
135
+ }
136
+ catch {
137
+ parts.push(`### ${spec}\n(dosya okunamadı)`);
138
+ continue;
139
+ }
140
+ const py = file.endsWith('.py');
141
+ const ranges = []; // a < 0 marks a missing symbol, kept in order
142
+ if (m[2]) {
143
+ for (const sym of m[2].split(',').map((s) => s.trim()).filter(Boolean)) {
144
+ const s = findSymbol(ls, sym, py);
145
+ if (s < 0) {
146
+ ranges.push([-1, -1, `#${sym}`]);
147
+ continue;
148
+ }
149
+ ranges.push([s, Math.min(blockEnd(ls, s, py), s + MAX_SNIPPET_LINES - 1), `#${sym}`]);
150
+ }
151
+ }
152
+ else if (m[3]) {
153
+ const a = Math.max(1, Number(m[3])), b = m[4] ? Number(m[4]) : a + 20;
154
+ ranges.push([a - 1, Math.min(ls.length - 1, b - 1, a - 1 + MAX_SNIPPET_LINES - 1), `:${a}-${b}`]);
155
+ }
156
+ else
157
+ ranges.push([0, Math.min(ls.length - 1, MAX_SNIPPET_LINES - 1), '']);
158
+ for (const [a, b, tag] of ranges) {
159
+ if (a < 0) {
160
+ parts.push(`### ${file}${tag}\n(sembol bulunamadı)`);
161
+ continue;
162
+ }
163
+ if (total >= MAX_TOTAL_LINES) {
164
+ parts.push(`### ${file}${tag}\n(atlandı: bağlam sınırı ${MAX_TOTAL_LINES} satır)`);
165
+ continue;
166
+ }
167
+ const end = Math.min(b, a + (MAX_TOTAL_LINES - total) - 1);
168
+ total += end - a + 1;
169
+ const cut = end < blockEnd(ls, a, py) && tag.startsWith('#') ? `\n … (kısaltıldı, devamı ${end + 2}. satırdan)` : '';
170
+ parts.push(`### ${file}${tag} (satır ${a + 1}-${end + 1})\n${numbered(ls, a, end)}${cut}`);
171
+ }
172
+ }
173
+ return parts.length ? `\n\nİLGİLİ KOD (coban'ın görevi verdiği an itibarıyla güncel; bu kısımları yeniden okuma/arama):\n${parts.join('\n\n')}` : '';
174
+ }