sentix 2.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,203 @@
1
+ /**
2
+ * sentix evolve — 자기 개선 루프
3
+ *
4
+ * sentix가 자기 자신의 코드를 분석하고 개선점을 찾아 티켓을 생성한다.
5
+ * --auto 플래그를 붙이면 sentix run으로 직접 수정까지 실행한다.
6
+ *
7
+ * 사용법:
8
+ * sentix evolve # 분석 + 티켓 생성만
9
+ * sentix evolve --auto # 분석 + 티켓 생성 + sentix run으로 자동 수정
10
+ */
11
+
12
+ import { spawnSync } from 'node:child_process';
13
+ import { registerCommand } from '../registry.js';
14
+ import { runGates } from '../lib/verify-gates.js';
15
+
16
+ registerCommand('evolve', {
17
+ description: 'Self-analyze and improve (Layer 5)',
18
+ usage: 'sentix evolve [--auto]',
19
+
20
+ async run(args, ctx) {
21
+ const autoFix = args.includes('--auto');
22
+ ctx.log('=== Sentix Self-Evolution ===\n');
23
+
24
+ const issues = [];
25
+
26
+ // ── 1. 테스트 상태 ──────────────────────────────
27
+ ctx.log('--- Test Health ---\n');
28
+ const testResult = spawnSync('npm', ['test'], {
29
+ cwd: ctx.cwd,
30
+ encoding: 'utf-8',
31
+ stdio: 'pipe',
32
+ timeout: 60000,
33
+ });
34
+
35
+ if (testResult.status === 0) {
36
+ const passMatch = testResult.stdout.match(/# pass (\d+)/);
37
+ const failMatch = testResult.stdout.match(/# fail (\d+)/);
38
+ ctx.success(`Tests: ${passMatch?.[1] || '?'} passed, ${failMatch?.[1] || '0'} failed`);
39
+ } else {
40
+ ctx.error('Tests: FAILING');
41
+ issues.push({
42
+ type: 'bug',
43
+ severity: 'critical',
44
+ title: 'Tests are failing',
45
+ detail: testResult.stderr?.slice(-300) || 'Unknown error',
46
+ });
47
+ }
48
+
49
+ // ── 2. 검증 게이트 ──────────────────────────────
50
+ ctx.log('\n--- Verification Gates ---\n');
51
+ const gates = runGates(ctx.cwd);
52
+
53
+ if (gates.passed) {
54
+ ctx.success(`Gates: ${gates.checks.length}/${gates.checks.length} passed`);
55
+ } else {
56
+ for (const v of gates.violations) {
57
+ ctx.warn(`Gate violation: [${v.rule}] ${v.message}`);
58
+ issues.push({
59
+ type: 'bug',
60
+ severity: 'warning',
61
+ title: `Gate violation: ${v.rule}`,
62
+ detail: v.message,
63
+ });
64
+ }
65
+ }
66
+
67
+ // ── 3. Doctor 체크 ──────────────────────────────
68
+ ctx.log('\n--- Doctor ---\n');
69
+
70
+ const requiredFiles = [
71
+ 'CLAUDE.md', 'FRAMEWORK.md', '.sentix/config.toml',
72
+ '.sentix/rules/hard-rules.md', 'tasks/lessons.md',
73
+ 'docs/governor-sop.md', 'docs/architecture.md',
74
+ ];
75
+
76
+ for (const file of requiredFiles) {
77
+ if (!ctx.exists(file)) {
78
+ ctx.warn(`Missing: ${file}`);
79
+ issues.push({
80
+ type: 'bug',
81
+ severity: 'suggestion',
82
+ title: `Missing file: ${file}`,
83
+ detail: `Required file ${file} does not exist`,
84
+ });
85
+ }
86
+ }
87
+
88
+ if (requiredFiles.every(f => ctx.exists(f))) {
89
+ ctx.success('All required files present');
90
+ }
91
+
92
+ // ── 4. 코드 품질 (간이 분석) ────────────────────
93
+ ctx.log('\n--- Code Quality ---\n');
94
+
95
+ // 큰 파일 탐지
96
+ const { readdirSync, statSync } = await import('node:fs');
97
+ const { resolve, join } = await import('node:path');
98
+
99
+ const bigFiles = [];
100
+ function scanDir(dir, prefix = '') {
101
+ try {
102
+ const entries = readdirSync(resolve(ctx.cwd, dir), { withFileTypes: true });
103
+ for (const entry of entries) {
104
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
105
+ const relPath = join(prefix, entry.name);
106
+ if (entry.isDirectory()) {
107
+ scanDir(join(dir, entry.name), relPath);
108
+ } else if (entry.name.endsWith('.js')) {
109
+ const stat = statSync(resolve(ctx.cwd, dir, entry.name));
110
+ const lines = Math.round(stat.size / 40); // 대략적 줄 수
111
+ if (lines > 300) {
112
+ bigFiles.push({ path: relPath, lines });
113
+ }
114
+ }
115
+ }
116
+ } catch { /* 접근 불가 디렉토리 무시 */ }
117
+ }
118
+
119
+ scanDir('src');
120
+ scanDir('bin');
121
+
122
+ if (bigFiles.length > 0) {
123
+ for (const f of bigFiles) {
124
+ ctx.warn(`Large file: ${f.path} (~${f.lines} lines) — consider splitting`);
125
+ issues.push({
126
+ type: 'feature',
127
+ severity: 'suggestion',
128
+ title: `Refactor: ${f.path} is too large (~${f.lines} lines)`,
129
+ detail: `Consider splitting into smaller modules`,
130
+ });
131
+ }
132
+ } else {
133
+ ctx.success('No oversized files');
134
+ }
135
+
136
+ // ── 5. 학습 파일 상태 ───────────────────────────
137
+ ctx.log('\n--- Learning Health ---\n');
138
+
139
+ if (ctx.exists('tasks/lessons.md')) {
140
+ const lessons = await ctx.readFile('tasks/lessons.md');
141
+ const lessonCount = (lessons.match(/^##/gm) || []).length;
142
+ ctx.log(` Lessons: ${lessonCount} entries`);
143
+ }
144
+
145
+ if (ctx.exists('tasks/agent-metrics.jsonl')) {
146
+ const metrics = await ctx.readFile('tasks/agent-metrics.jsonl');
147
+ const lineCount = metrics.trim().split('\n').filter(Boolean).length;
148
+ ctx.log(` Metrics: ${lineCount} records`);
149
+ } else {
150
+ ctx.log(' Metrics: (empty — run sentix run to collect)');
151
+ }
152
+
153
+ // ── 결과 요약 ───────────────────────────────────
154
+ ctx.log('\n=== Evolution Summary ===\n');
155
+
156
+ if (issues.length === 0) {
157
+ ctx.success('No issues found. Sentix is in good shape.');
158
+ return;
159
+ }
160
+
161
+ ctx.log(`Found ${issues.length} issue(s):\n`);
162
+
163
+ const critical = issues.filter(i => i.severity === 'critical');
164
+ const warnings = issues.filter(i => i.severity === 'warning');
165
+ const suggestions = issues.filter(i => i.severity === 'suggestion');
166
+
167
+ if (critical.length > 0) {
168
+ ctx.error(` Critical: ${critical.length}`);
169
+ for (const i of critical) ctx.log(` - ${i.title}`);
170
+ }
171
+ if (warnings.length > 0) {
172
+ ctx.warn(` Warning: ${warnings.length}`);
173
+ for (const i of warnings) ctx.log(` - ${i.title}`);
174
+ }
175
+ if (suggestions.length > 0) {
176
+ ctx.log(` Suggestion: ${suggestions.length}`);
177
+ for (const i of suggestions) ctx.log(` - ${i.title}`);
178
+ }
179
+
180
+ // ── 티켓 생성 ───────────────────────────────────
181
+ if (critical.length > 0 || warnings.length > 0) {
182
+ ctx.log('');
183
+ for (const issue of [...critical, ...warnings]) {
184
+ const cmdType = issue.type === 'feature' ? 'feature add' : 'ticket create';
185
+ const severityFlag = issue.severity === 'critical' ? ' --severity critical' : '';
186
+ ctx.log(` → sentix ${cmdType} "${issue.title}"${severityFlag}`);
187
+ }
188
+
189
+ // --auto: 가장 심각한 이슈를 sentix run으로 자동 수정
190
+ if (autoFix && critical.length > 0) {
191
+ ctx.log('\n--- Auto-fix (critical issues) ---\n');
192
+ const firstIssue = critical[0];
193
+
194
+ ctx.log(`Running: sentix run "${firstIssue.title}"\n`);
195
+ spawnSync('node', ['bin/sentix.js', 'run', firstIssue.title], {
196
+ cwd: ctx.cwd,
197
+ stdio: 'inherit',
198
+ timeout: 600_000,
199
+ });
200
+ }
201
+ }
202
+ },
203
+ });
@@ -0,0 +1,327 @@
1
+ /**
2
+ * sentix feature — 기능 추가 워크플로우
3
+ *
4
+ * sentix feature add "설명" — 기능 티켓 생성 + Governor 파이프라인 실행
5
+ * sentix feature list [--status open] — 기능 목록
6
+ * sentix feature impact <id|"설명"> — 영향 분석
7
+ */
8
+
9
+ import { registerCommand } from '../registry.js';
10
+ import {
11
+ loadIndex, addTicket, nextTicketId, createTicketEntry, findTicket,
12
+ } from '../lib/ticket-index.js';
13
+
14
+ registerCommand('feature', {
15
+ description: 'Manage feature development (add | list | impact)',
16
+ usage: 'sentix feature <add|list|impact> [args...]',
17
+
18
+ async run(args, ctx) {
19
+ const subcommand = args[0];
20
+
21
+ if (subcommand === 'add') {
22
+ await addFeature(args.slice(1), ctx);
23
+ } else if (!subcommand || subcommand === 'list') {
24
+ await listFeatures(args.slice(1), ctx);
25
+ } else if (subcommand === 'impact') {
26
+ await analyzeImpact(args.slice(1), ctx);
27
+ } else {
28
+ ctx.error(`Unknown subcommand: ${subcommand}`);
29
+ ctx.log('Usage: sentix feature <add|list|impact> [args...]');
30
+ }
31
+ },
32
+ });
33
+
34
+ // ── Complexity keywords ───────────────────────────────
35
+
36
+ const COMPLEXITY_KEYWORDS = {
37
+ high: ['api', 'database', 'auth', 'migration', 'multi-tenant', 'real-time', 'websocket', 'oauth', '인증', '데이터베이스', '마이그레이션'],
38
+ medium: ['form', 'validation', 'upload', 'notification', 'cache', 'filter', 'search', '폼', '알림', '캐시'],
39
+ };
40
+
41
+ function assessComplexity(description) {
42
+ const lower = description.toLowerCase();
43
+ let score = 0;
44
+
45
+ for (const keyword of COMPLEXITY_KEYWORDS.high) {
46
+ if (lower.includes(keyword)) score += 2;
47
+ }
48
+ for (const keyword of COMPLEXITY_KEYWORDS.medium) {
49
+ if (lower.includes(keyword)) score += 1;
50
+ }
51
+
52
+ // Multiple systems mentioned
53
+ if ((lower.match(/\band\b|,\s*\w+/g) || []).length >= 2) score += 2;
54
+
55
+ if (score >= 4) return 'high';
56
+ if (score >= 2) return 'medium';
57
+ return 'low';
58
+ }
59
+
60
+ // ── sentix feature add ────────────────────────────────
61
+
62
+ async function addFeature(args, ctx) {
63
+ const description = args.join(' ').trim();
64
+ if (!description) {
65
+ ctx.error('Usage: sentix feature add "feature description"');
66
+ return;
67
+ }
68
+
69
+ const complexity = assessComplexity(description);
70
+ const id = await nextTicketId(ctx, 'feat');
71
+ const title = description.length > 80 ? description.slice(0, 77) + '...' : description;
72
+
73
+ ctx.log(`Feature: ${title}`);
74
+ ctx.log(`Complexity: ${complexity}\n`);
75
+
76
+ // Impact analysis
77
+ const impact = await getImpactData(description, ctx);
78
+
79
+ // Create ticket entry
80
+ const entry = createTicketEntry({
81
+ id,
82
+ type: 'feature',
83
+ title,
84
+ severity: null,
85
+ description,
86
+ });
87
+
88
+ // Generate markdown
89
+ const md = `# ${id}: ${title}
90
+
91
+ - **Status:** open
92
+ - **Complexity:** ${complexity}
93
+ - **Deploy flag:** ${impact.deployFlag}
94
+ - **Security flag:** ${impact.securityFlag}
95
+ - **Created:** ${entry.created_at}
96
+
97
+ ## Description
98
+
99
+ ${description}
100
+
101
+ ## Impact Analysis
102
+
103
+ ${impact.summary}
104
+
105
+ ## Decomposition
106
+
107
+ ${complexity === 'high' ? generateDecomposition(description) : '<!-- N/A — low/medium complexity -->'}
108
+
109
+ ## Acceptance Criteria
110
+
111
+ <!-- Populated by planner agent -->
112
+ `;
113
+
114
+ await ctx.writeFile(entry.file_path, md);
115
+ await addTicket(ctx, entry);
116
+
117
+ // Log event
118
+ await ctx.appendJSONL('tasks/pattern-log.jsonl', {
119
+ ts: new Date().toISOString(),
120
+ event: 'feature:add',
121
+ id,
122
+ complexity,
123
+ title,
124
+ });
125
+
126
+ ctx.success(`Created ${id}: ${title}`);
127
+ ctx.log(` Complexity: ${complexity}`);
128
+ ctx.log(` Deploy flag: ${impact.deployFlag}`);
129
+ ctx.log(` Security flag: ${impact.securityFlag}`);
130
+ ctx.log(` File: ${entry.file_path}`);
131
+ ctx.log('');
132
+ ctx.log('Governor: proceed with FEATURE pipeline using this ticket.');
133
+ }
134
+
135
+ // ── sentix feature list ───────────────────────────────
136
+
137
+ async function listFeatures(args, ctx) {
138
+ ctx.log('=== Features ===\n');
139
+
140
+ let entries = await loadIndex(ctx);
141
+ entries = entries.filter(e => e.type === 'feature');
142
+
143
+ // Parse --status filter
144
+ const statusIdx = args.indexOf('--status');
145
+ if (statusIdx !== -1 && args[statusIdx + 1]) {
146
+ const status = args[statusIdx + 1];
147
+ entries = entries.filter(e => e.status === status);
148
+ }
149
+
150
+ if (entries.length === 0) {
151
+ ctx.log(' (no features)');
152
+ ctx.log('\n Create one: sentix feature add "description"');
153
+ return;
154
+ }
155
+
156
+ // Read complexity from ticket files
157
+ ctx.log(` ${'ID'.padEnd(12)} ${'COMPLEXITY'.padEnd(12)} ${'STATUS'.padEnd(14)} TITLE`);
158
+ ctx.log(` ${'─'.repeat(12)} ${'─'.repeat(12)} ${'─'.repeat(14)} ${'─'.repeat(30)}`);
159
+
160
+ for (const e of entries) {
161
+ let complexity = '-';
162
+ if (ctx.exists(e.file_path)) {
163
+ try {
164
+ const content = await ctx.readFile(e.file_path);
165
+ const match = content.match(/\*\*Complexity:\*\*\s*(\w+)/);
166
+ if (match) complexity = match[1];
167
+ } catch { /* use default */ }
168
+ }
169
+ ctx.log(` ${e.id.padEnd(12)} ${complexity.padEnd(12)} ${e.status.padEnd(14)} ${e.title}`);
170
+ }
171
+
172
+ ctx.log(`\n Total: ${entries.length} feature(s)`);
173
+ }
174
+
175
+ // ── sentix feature impact ─────────────────────────────
176
+
177
+ async function analyzeImpact(args, ctx) {
178
+ const input = args.join(' ').trim();
179
+ if (!input) {
180
+ ctx.error('Usage: sentix feature impact <feature-id | "description">');
181
+ return;
182
+ }
183
+
184
+ // Resolve description: check if it's a ticket ID first
185
+ let description = input;
186
+ const ticket = await findTicket(ctx, input);
187
+ if (ticket && ctx.exists(ticket.file_path)) {
188
+ const content = await ctx.readFile(ticket.file_path);
189
+ const descMatch = content.match(/## Description\n\n([\s\S]*?)(?=\n## |\n$)/);
190
+ if (descMatch) description = descMatch[1].trim();
191
+ }
192
+
193
+ ctx.log('=== Impact Analysis ===\n');
194
+
195
+ const impact = await getImpactData(description, ctx);
196
+
197
+ ctx.log(impact.summary);
198
+ ctx.log('');
199
+ ctx.log('--- Flags ---');
200
+ ctx.log(` DEPLOY_FLAG: ${impact.deployFlag}`);
201
+ ctx.log(` SECURITY_FLAG: ${impact.securityFlag}`);
202
+ ctx.log('');
203
+
204
+ const complexity = assessComplexity(description);
205
+ ctx.log(` Recommendation: COMPLEXITY ${complexity}`);
206
+ if (impact.securityFlag) {
207
+ ctx.log(' → Run with security pre-analysis');
208
+ }
209
+
210
+ // Log event
211
+ await ctx.appendJSONL('tasks/pattern-log.jsonl', {
212
+ ts: new Date().toISOString(),
213
+ event: 'feature:impact',
214
+ input,
215
+ deploy_flag: impact.deployFlag,
216
+ security_flag: impact.securityFlag,
217
+ });
218
+ }
219
+
220
+ // ── Impact analysis engine ────────────────────────────
221
+
222
+ async function getImpactData(description, ctx) {
223
+ const lower = description.toLowerCase();
224
+ const lines = [];
225
+ let deployFlag = false;
226
+ let securityFlag = false;
227
+
228
+ // Check INTERFACE.md for affected APIs
229
+ if (ctx.exists('INTERFACE.md')) {
230
+ try {
231
+ const iface = await ctx.readFile('INTERFACE.md');
232
+
233
+ // Extract exported API entries
234
+ const apiSection = iface.match(/## Exported APIs[\s\S]*?(?=\n## |$)/);
235
+ if (apiSection) {
236
+ const apis = apiSection[0].match(/(?:GET|POST|PUT|DELETE|PATCH)\s+\S+/g) || [];
237
+ const affected = [];
238
+ for (const api of apis) {
239
+ const endpoint = api.split(/\s+/)[1];
240
+ const parts = endpoint.split('/').filter(Boolean);
241
+ if (parts.some(p => lower.includes(p.toLowerCase()))) {
242
+ affected.push(api);
243
+ }
244
+ }
245
+ if (affected.length > 0) {
246
+ lines.push('Affected APIs:');
247
+ for (const api of affected) {
248
+ lines.push(` - ${api} (INTERFACE.md)`);
249
+ }
250
+ deployFlag = true;
251
+ }
252
+ }
253
+ } catch { /* non-critical */ }
254
+ }
255
+
256
+ // Check registry.md for downstream projects
257
+ if (ctx.exists('registry.md')) {
258
+ try {
259
+ const registry = await ctx.readFile('registry.md');
260
+ // Parse markdown table rows (skip header and separator)
261
+ const rows = registry.split('\n').filter(l => l.startsWith('|') && !l.includes('---'));
262
+ const downstream = [];
263
+
264
+ for (const row of rows) {
265
+ const cells = row.split('|').map(c => c.trim()).filter(Boolean);
266
+ const name = cells[0];
267
+ if (name && name !== '프로젝트' && name !== 'Project') {
268
+ downstream.push(name);
269
+ }
270
+ }
271
+
272
+ if (downstream.length > 0) {
273
+ lines.push('');
274
+ lines.push('Downstream projects in registry:');
275
+ for (const proj of downstream) {
276
+ lines.push(` - ${proj}`);
277
+ }
278
+ }
279
+ } catch { /* non-critical */ }
280
+ }
281
+
282
+ // Keyword-based flag detection
283
+ const securityKeywords = ['auth', 'login', 'session', 'token', 'password', 'permission', 'role', 'oauth', 'jwt', '인증', '세션', '권한'];
284
+ const deployKeywords = ['api', 'endpoint', 'route', 'schema', 'migration', 'database', 'config'];
285
+
286
+ if (securityKeywords.some(k => lower.includes(k))) {
287
+ securityFlag = true;
288
+ lines.push('');
289
+ lines.push('Security-related keywords detected → security pre-analysis recommended');
290
+ }
291
+
292
+ if (deployKeywords.some(k => lower.includes(k))) {
293
+ deployFlag = true;
294
+ }
295
+
296
+ if (lines.length === 0) {
297
+ lines.push('No direct API or downstream impact detected.');
298
+ lines.push('Standard development pipeline recommended.');
299
+ }
300
+
301
+ return {
302
+ summary: lines.join('\n'),
303
+ deployFlag,
304
+ securityFlag,
305
+ };
306
+ }
307
+
308
+ // ── Feature decomposition for high complexity ─────────
309
+
310
+ function generateDecomposition(description) {
311
+ // Split at logical boundaries
312
+ const parts = description
313
+ .split(/(?:,\s*(?:and\s+)?|;\s*|\band\b\s+)/i)
314
+ .map(p => p.trim())
315
+ .filter(p => p.length > 3);
316
+
317
+ if (parts.length <= 1) {
318
+ return '<!-- Governor planner will decompose this feature -->';
319
+ }
320
+
321
+ const lines = ['PARALLEL_HINT (preliminary — planner will refine):', ''];
322
+ parts.forEach((part, i) => {
323
+ lines.push(`- Sub-task ${i + 1}: ${part}`);
324
+ });
325
+
326
+ return lines.join('\n');
327
+ }