@wooojin/forgen 0.4.9 → 0.4.11
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +46 -0
- package/README.md +33 -1
- package/assets/claude/agents/forgen-verify.md +65 -0
- package/assets/claude/workflows/compound-extract.js +136 -0
- package/assets/claude/workflows/evidence-gate-audit.js +107 -0
- package/assets/shared/hook-registry.json +1 -0
- package/dist/checks/_shared/meta-guard-dispatch.d.ts +38 -0
- package/dist/checks/_shared/meta-guard-dispatch.js +80 -0
- package/dist/checks/_shared/text-sanitizer.js +15 -0
- package/dist/cli.js +65 -2
- package/dist/core/changelog-cli.d.ts +7 -0
- package/dist/core/changelog-cli.js +100 -0
- package/dist/core/doctor.d.ts +3 -0
- package/dist/core/doctor.js +75 -1
- package/dist/core/effort-advisory.d.ts +23 -0
- package/dist/core/effort-advisory.js +29 -0
- package/dist/core/explain-cli.d.ts +6 -0
- package/dist/core/explain-cli.js +99 -0
- package/dist/core/git-stats.d.ts +23 -0
- package/dist/core/git-stats.js +50 -0
- package/dist/core/health-cli.d.ts +23 -0
- package/dist/core/health-cli.js +86 -0
- package/dist/core/inspect-cli.js +60 -1
- package/dist/core/probe-workflow-cli.d.ts +72 -0
- package/dist/core/probe-workflow-cli.js +282 -0
- package/dist/core/regress-map-cli.d.ts +7 -0
- package/dist/core/regress-map-cli.js +59 -0
- package/dist/core/stats-cli.d.ts +22 -9
- package/dist/core/stats-cli.js +149 -0
- package/dist/core/watch-cli.d.ts +7 -0
- package/dist/core/watch-cli.js +185 -0
- package/dist/core/workflows-cli.d.ts +26 -0
- package/dist/core/workflows-cli.js +120 -0
- package/dist/engine/compound-export.d.ts +12 -0
- package/dist/engine/compound-export.js +136 -14
- package/dist/engine/compound-extractor.d.ts +12 -43
- package/dist/engine/compound-extractor.js +27 -756
- package/dist/engine/extraction-diff.d.ts +11 -0
- package/dist/engine/extraction-diff.js +105 -0
- package/dist/engine/extraction-gates.d.ts +37 -0
- package/dist/engine/extraction-gates.js +100 -0
- package/dist/engine/extraction-git.d.ts +20 -0
- package/dist/engine/extraction-git.js +75 -0
- package/dist/engine/extraction-persistence.d.ts +27 -0
- package/dist/engine/extraction-persistence.js +140 -0
- package/dist/engine/extraction-session.d.ts +26 -0
- package/dist/engine/extraction-session.js +230 -0
- package/dist/engine/lifecycle/types.d.ts +1 -1
- package/dist/engine/meta-learning/matcher-weight-loader.d.ts +16 -0
- package/dist/engine/meta-learning/matcher-weight-loader.js +45 -0
- package/dist/engine/precision-guards.d.ts +14 -0
- package/dist/engine/precision-guards.js +39 -0
- package/dist/engine/ranking-pipeline.d.ts +45 -0
- package/dist/engine/ranking-pipeline.js +66 -0
- package/dist/engine/relevance-scorer.d.ts +43 -0
- package/dist/engine/relevance-scorer.js +81 -0
- package/dist/engine/scoring-algorithms.d.ts +31 -0
- package/dist/engine/scoring-algorithms.js +109 -0
- package/dist/engine/solution-matcher-eval.d.ts +97 -0
- package/dist/engine/solution-matcher-eval.js +122 -0
- package/dist/engine/solution-matcher.d.ts +21 -380
- package/dist/engine/solution-matcher.js +27 -828
- package/dist/fgx.d.ts +4 -1
- package/dist/fgx.js +46 -10
- package/dist/hooks/notepad-injector.js +7 -0
- package/dist/hooks/post-tool-use.js +8 -1
- package/dist/hooks/shared/preflight-check.d.ts +15 -0
- package/dist/hooks/shared/preflight-check.js +51 -0
- package/dist/hooks/stop-guard.js +19 -60
- package/dist/hooks/subagent-stop-guard.d.ts +23 -0
- package/dist/hooks/subagent-stop-guard.js +158 -0
- package/dist/hooks/subagent-tracker.d.ts +36 -3
- package/dist/hooks/subagent-tracker.js +86 -39
- package/dist/store/evidence-store.js +9 -2
- package/hooks/hooks.json +6 -1
- package/package.json +7 -7
- package/plugin.json +1 -1
- package/scripts/postinstall.js +10 -7
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern extraction from git diff.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from compound-extractor.ts — heuristic extraction of reusable
|
|
5
|
+
* patterns from git diff content without LLM involvement.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtractedSolution } from './extraction-gates.js';
|
|
8
|
+
/** Find common prefix among an array of strings */
|
|
9
|
+
export declare function findCommonPrefix(strings: string[]): string;
|
|
10
|
+
/** Simple local extraction from git diff (no LLM needed) */
|
|
11
|
+
export declare function extractFromDiff(gitLog: string, gitDiff: string): ExtractedSolution[];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern extraction from git diff.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from compound-extractor.ts — heuristic extraction of reusable
|
|
5
|
+
* patterns from git diff content without LLM involvement.
|
|
6
|
+
*/
|
|
7
|
+
import * as path from 'node:path';
|
|
8
|
+
import { extractTags } from './solution-format.js';
|
|
9
|
+
/** Find common prefix among an array of strings */
|
|
10
|
+
export function findCommonPrefix(strings) {
|
|
11
|
+
if (strings.length === 0)
|
|
12
|
+
return '';
|
|
13
|
+
let prefix = strings[0];
|
|
14
|
+
for (const s of strings.slice(1)) {
|
|
15
|
+
while (!s.startsWith(prefix) && prefix.length > 0) {
|
|
16
|
+
prefix = prefix.slice(0, -1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return prefix.replace(/-$/, '');
|
|
20
|
+
}
|
|
21
|
+
/** Simple local extraction from git diff (no LLM needed) */
|
|
22
|
+
export function extractFromDiff(gitLog, gitDiff) {
|
|
23
|
+
const solutions = [];
|
|
24
|
+
// 1. Detect new files/modules created
|
|
25
|
+
const newFiles = gitDiff.match(/^\+\+\+ b\/(.+)$/gm);
|
|
26
|
+
if (newFiles && newFiles.length >= 2) {
|
|
27
|
+
const fileNames = newFiles.map(f => f.replace('+++ b/', ''));
|
|
28
|
+
const ext = path.extname(fileNames[0]);
|
|
29
|
+
const dir = path.dirname(fileNames[0]).split('/').pop() ?? '';
|
|
30
|
+
if (ext && dir) {
|
|
31
|
+
const basenames = fileNames.map(f => path.basename(f, ext));
|
|
32
|
+
const commonPrefix = findCommonPrefix(basenames);
|
|
33
|
+
if (commonPrefix.length >= 3) {
|
|
34
|
+
solutions.push({
|
|
35
|
+
name: `module-${commonPrefix}-pattern`,
|
|
36
|
+
type: 'pattern',
|
|
37
|
+
tags: extractTags(`${fileNames.join(' ')} ${dir}`),
|
|
38
|
+
identifiers: basenames.filter(b => b.length >= 4).slice(0, 5),
|
|
39
|
+
context: `File organization pattern in ${dir}/`,
|
|
40
|
+
content: `Files follow the naming pattern: ${commonPrefix}*${ext} in ${dir}/`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// 2. Detect error handling patterns from diff
|
|
46
|
+
const errorPatterns = gitDiff.match(/^\+.*(?:try\s*\{|catch\s*[({]|\.catch\(|throw new|Error\()/gm);
|
|
47
|
+
if (errorPatterns && errorPatterns.length >= 3) {
|
|
48
|
+
const sample = errorPatterns.slice(0, 3).map(l => l.replace(/^\+\s*/, '').trim());
|
|
49
|
+
solutions.push({
|
|
50
|
+
name: 'error-handling-pattern',
|
|
51
|
+
type: 'pattern',
|
|
52
|
+
tags: ['error', 'handling', 'try-catch', 'pattern'],
|
|
53
|
+
identifiers: sample.filter(s => s.length >= 4).slice(0, 3),
|
|
54
|
+
context: 'Error handling approach used in this codebase',
|
|
55
|
+
content: `Consistent error handling: ${sample.join('; ')}`.slice(0, 500),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// 3. Detect import/dependency patterns
|
|
59
|
+
const imports = gitDiff.match(/^\+\s*import\s+.+from\s+['"]([^'"]+)['"]/gm);
|
|
60
|
+
if (imports && imports.length >= 3) {
|
|
61
|
+
const packages = imports
|
|
62
|
+
.map(i => i.match(/from\s+['"]([^'"]+)['"]/)?.[1])
|
|
63
|
+
.filter((p) => !!p && !p.startsWith('.'))
|
|
64
|
+
.filter((v, i, a) => a.indexOf(v) === i);
|
|
65
|
+
if (packages.length >= 2) {
|
|
66
|
+
solutions.push({
|
|
67
|
+
name: 'dependency-stack',
|
|
68
|
+
type: 'decision',
|
|
69
|
+
tags: ['dependency', 'stack', ...packages.slice(0, 3)],
|
|
70
|
+
identifiers: packages.filter(p => p.length >= 4).slice(0, 5),
|
|
71
|
+
context: 'Technology stack and dependency choices',
|
|
72
|
+
content: `Project uses: ${packages.join(', ')}`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// 4. Detect from commit messages
|
|
77
|
+
const commitKeywords = {
|
|
78
|
+
'fix': { type: 'troubleshoot', tags: ['bugfix', 'troubleshoot'] },
|
|
79
|
+
'refactor': { type: 'pattern', tags: ['refactor', 'cleanup'] },
|
|
80
|
+
'test': { type: 'pattern', tags: ['testing', 'tdd'] },
|
|
81
|
+
'security': { type: 'pattern', tags: ['security', 'hardening'] },
|
|
82
|
+
};
|
|
83
|
+
for (const [keyword, meta] of Object.entries(commitKeywords)) {
|
|
84
|
+
const re = new RegExp(`^[a-f0-9]+\\s+${keyword}[:\\s](.+)$`, 'gim');
|
|
85
|
+
const matches = [...gitLog.matchAll(re)];
|
|
86
|
+
if (matches.length >= 2) {
|
|
87
|
+
const descriptions = matches.map(m => m[1].trim()).slice(0, 3);
|
|
88
|
+
const commitIdentifiers = descriptions
|
|
89
|
+
.join(' ')
|
|
90
|
+
.match(/\b[a-zA-Z][a-zA-Z0-9]*(?:[A-Z][a-z]+)+\b|\b[a-z]+(?:_[a-z]+)+\b/g)
|
|
91
|
+
?.filter(id => id.length >= 6)
|
|
92
|
+
?.filter((v, i, a) => a.indexOf(v) === i)
|
|
93
|
+
?.slice(0, 5) ?? [];
|
|
94
|
+
solutions.push({
|
|
95
|
+
name: `${keyword}-pattern`,
|
|
96
|
+
type: meta.type,
|
|
97
|
+
tags: [...meta.tags, keyword],
|
|
98
|
+
identifiers: commitIdentifiers,
|
|
99
|
+
context: `Recurring ${keyword} pattern from commit history`,
|
|
100
|
+
content: descriptions.join('. ').slice(0, 500),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return solutions.slice(0, 3);
|
|
105
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality gates for compound extraction.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from compound-extractor.ts — validation and dedup logic that
|
|
5
|
+
* determines whether an extracted solution is worth persisting.
|
|
6
|
+
*/
|
|
7
|
+
import type { SolutionType } from './solution-format.js';
|
|
8
|
+
/** Shape of a solution extracted from git diff or session context */
|
|
9
|
+
export interface ExtractedSolution {
|
|
10
|
+
name: string;
|
|
11
|
+
type: SolutionType;
|
|
12
|
+
tags: string[];
|
|
13
|
+
identifiers: string[];
|
|
14
|
+
context: string;
|
|
15
|
+
content: string;
|
|
16
|
+
}
|
|
17
|
+
/** Gate 0: Is this extraction worth doing? */
|
|
18
|
+
export declare function gate0(stats: {
|
|
19
|
+
files: number;
|
|
20
|
+
lines: number;
|
|
21
|
+
hasCodeFiles: boolean;
|
|
22
|
+
}): boolean;
|
|
23
|
+
/** Gate 1: Structural validation (pure — does not mutate input) */
|
|
24
|
+
export declare function gate1(sol: ExtractedSolution): boolean;
|
|
25
|
+
/** Gate 2: Toxicity filter */
|
|
26
|
+
export declare function gate2(sol: ExtractedSolution): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Gate 2.5: Trivial pattern rejection — 자명한 패턴은 축적할 가치 없음.
|
|
29
|
+
*/
|
|
30
|
+
export declare function gateTrivial(sol: ExtractedSolution): boolean;
|
|
31
|
+
/** Gate 3: Dedup check against existing solutions */
|
|
32
|
+
export declare function gate3(sol: ExtractedSolution): 'new' | 're-extract' | 'duplicate';
|
|
33
|
+
/** Evaluate an extracted solution against all quality gates */
|
|
34
|
+
export declare function evaluateExtractedSolution(sol: ExtractedSolution): {
|
|
35
|
+
action: 'accept' | 'skip' | 'duplicate' | 're-extract';
|
|
36
|
+
message?: string;
|
|
37
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality gates for compound extraction.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from compound-extractor.ts — validation and dedup logic that
|
|
5
|
+
* determines whether an extracted solution is worth persisting.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { ME_SOLUTIONS } from '../core/paths.js';
|
|
10
|
+
import { createLogger } from '../core/logger.js';
|
|
11
|
+
const log = createLogger('extraction-gates');
|
|
12
|
+
const TOXICITY_PATTERNS = [
|
|
13
|
+
/@ts-ignore/i, /@ts-nocheck/i, /as\s+any\b/i,
|
|
14
|
+
/--force\b/i, /--no-verify\b/i, /--skip-ci\b/i,
|
|
15
|
+
/eslint-disable/i, /prettier-ignore/i, /noqa/i,
|
|
16
|
+
/\bTODO:/i, /\bFIXME:/i, /\bHACK:/i, /\bXXX:/i,
|
|
17
|
+
/\/Users\//i, /\/home\//i, /C:\\\\Users/i,
|
|
18
|
+
];
|
|
19
|
+
/** Gate 0: Is this extraction worth doing? */
|
|
20
|
+
export function gate0(stats) {
|
|
21
|
+
if (stats.files < 1)
|
|
22
|
+
return false;
|
|
23
|
+
if (stats.lines < 30)
|
|
24
|
+
return false;
|
|
25
|
+
if (!stats.hasCodeFiles)
|
|
26
|
+
return false;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
/** Gate 1: Structural validation (pure — does not mutate input) */
|
|
30
|
+
export function gate1(sol) {
|
|
31
|
+
if (!sol.name || sol.name.length < 3)
|
|
32
|
+
return false;
|
|
33
|
+
if (!sol.tags || sol.tags.length === 0)
|
|
34
|
+
return false;
|
|
35
|
+
if (!sol.content || sol.content.length < 50)
|
|
36
|
+
return false;
|
|
37
|
+
if (!sol.context)
|
|
38
|
+
return false;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
/** Gate 2: Toxicity filter */
|
|
42
|
+
export function gate2(sol) {
|
|
43
|
+
const text = `${sol.context} ${sol.content}`;
|
|
44
|
+
return !TOXICITY_PATTERNS.some(p => p.test(text));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Gate 2.5: Trivial pattern rejection — 자명한 패턴은 축적할 가치 없음.
|
|
48
|
+
*/
|
|
49
|
+
export function gateTrivial(sol) {
|
|
50
|
+
const content = sol.content.trim();
|
|
51
|
+
if (content.length < 80)
|
|
52
|
+
return false;
|
|
53
|
+
if (/^주로\s/.test(content) && content.split('\n').length < 3)
|
|
54
|
+
return false;
|
|
55
|
+
if (sol.identifiers.length === 0 && sol.tags.length < 3)
|
|
56
|
+
return false;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
/** Gate 3: Dedup check against existing solutions */
|
|
60
|
+
export function gate3(sol) {
|
|
61
|
+
if (!fs.existsSync(ME_SOLUTIONS))
|
|
62
|
+
return 'new';
|
|
63
|
+
try {
|
|
64
|
+
const files = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md'));
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
const content = fs.readFileSync(path.join(ME_SOLUTIONS, file), 'utf-8');
|
|
67
|
+
const tagMatch = content.match(/tags:\s*\[([^\]]*)\]/);
|
|
68
|
+
if (!tagMatch)
|
|
69
|
+
continue;
|
|
70
|
+
const existingTags = tagMatch[1].split(',').map(t => t.trim().replace(/"/g, ''));
|
|
71
|
+
const overlap = sol.tags.filter(t => existingTags.includes(t));
|
|
72
|
+
const overlapRatio = overlap.length / Math.max(sol.tags.length, existingTags.length, 1);
|
|
73
|
+
if (overlapRatio >= 0.7) {
|
|
74
|
+
if (content.includes('status: "experiment"') || content.includes("status: 'experiment'") || content.includes('status: experiment')) {
|
|
75
|
+
return 're-extract';
|
|
76
|
+
}
|
|
77
|
+
return 'duplicate';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
log.debug('gate3 기존 솔루션 파일 읽기 실패 — new로 간주', e);
|
|
83
|
+
}
|
|
84
|
+
return 'new';
|
|
85
|
+
}
|
|
86
|
+
/** Evaluate an extracted solution against all quality gates */
|
|
87
|
+
export function evaluateExtractedSolution(sol) {
|
|
88
|
+
if (!gate1(sol))
|
|
89
|
+
return { action: 'skip', message: `${sol.name ?? 'unnamed'}: Gate 1 실패 (구조 검증)` };
|
|
90
|
+
if (!gate2(sol))
|
|
91
|
+
return { action: 'skip', message: `${sol.name}: Gate 2 실패 (독성 필터)` };
|
|
92
|
+
if (!gateTrivial(sol))
|
|
93
|
+
return { action: 'skip', message: `${sol.name}: Gate 2.5 실패 (자명한 패턴)` };
|
|
94
|
+
const dupResult = gate3(sol);
|
|
95
|
+
if (dupResult === 'duplicate')
|
|
96
|
+
return { action: 'duplicate', message: `${sol.name}: Gate 3 중복` };
|
|
97
|
+
if (dupResult === 're-extract')
|
|
98
|
+
return { action: 're-extract', message: `${sol.name}: 재추출 (기존 솔루션 강화)` };
|
|
99
|
+
return { action: 'accept' };
|
|
100
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git helpers for compound extraction.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from compound-extractor.ts — pure functions that shell out to git
|
|
5
|
+
* for commit/diff data. All use execFileSync to prevent shell injection.
|
|
6
|
+
*/
|
|
7
|
+
/** Validate that a string is a valid git SHA (7-64 hex chars) */
|
|
8
|
+
export declare function isValidSha(sha: string): boolean;
|
|
9
|
+
/** Get new commits since last extraction */
|
|
10
|
+
export declare function getNewCommits(cwd: string, lastSha: string): string;
|
|
11
|
+
/** Get commit messages for "why" context enrichment */
|
|
12
|
+
export declare function getCommitMessages(cwd: string, lastSha: string): string;
|
|
13
|
+
/** Get git diff for extraction */
|
|
14
|
+
export declare function getGitDiff(cwd: string, lastSha: string): string;
|
|
15
|
+
/** Get diff stats for Gate 0 */
|
|
16
|
+
export declare function getDiffStats(cwd: string, lastSha: string): {
|
|
17
|
+
files: number;
|
|
18
|
+
lines: number;
|
|
19
|
+
hasCodeFiles: boolean;
|
|
20
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git helpers for compound extraction.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from compound-extractor.ts — pure functions that shell out to git
|
|
5
|
+
* for commit/diff data. All use execFileSync to prevent shell injection.
|
|
6
|
+
*/
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
const MAX_DIFF_LENGTH = 3000;
|
|
9
|
+
/** Validate that a string is a valid git SHA (7-64 hex chars) */
|
|
10
|
+
export function isValidSha(sha) {
|
|
11
|
+
return /^[a-f0-9]{7,64}$/.test(sha);
|
|
12
|
+
}
|
|
13
|
+
/** Get new commits since last extraction */
|
|
14
|
+
export function getNewCommits(cwd, lastSha) {
|
|
15
|
+
try {
|
|
16
|
+
if (!lastSha || !isValidSha(lastSha)) {
|
|
17
|
+
return execFileSync('git', ['log', '--oneline', '-5'], { cwd, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
18
|
+
}
|
|
19
|
+
return execFileSync('git', ['log', '--oneline', `${lastSha}..HEAD`], { cwd, encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return '';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Get commit messages for "why" context enrichment */
|
|
26
|
+
export function getCommitMessages(cwd, lastSha) {
|
|
27
|
+
try {
|
|
28
|
+
const args = lastSha && isValidSha(lastSha)
|
|
29
|
+
? ['log', '--format=%B', `${lastSha}..HEAD`]
|
|
30
|
+
: ['log', '--format=%B', '-5'];
|
|
31
|
+
const msgs = execFileSync('git', args, { cwd, encoding: 'utf-8', timeout: 5000 });
|
|
32
|
+
return msgs.slice(0, 1000).trim();
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Get git diff for extraction */
|
|
39
|
+
export function getGitDiff(cwd, lastSha) {
|
|
40
|
+
try {
|
|
41
|
+
const args = lastSha && isValidSha(lastSha)
|
|
42
|
+
? ['diff', `${lastSha}..HEAD`]
|
|
43
|
+
: ['diff', 'HEAD~1'];
|
|
44
|
+
const diff = execFileSync('git', args, { cwd, encoding: 'utf-8', timeout: 10000 });
|
|
45
|
+
return diff.slice(0, MAX_DIFF_LENGTH);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Get diff stats for Gate 0 */
|
|
52
|
+
export function getDiffStats(cwd, lastSha) {
|
|
53
|
+
try {
|
|
54
|
+
const args = lastSha && isValidSha(lastSha)
|
|
55
|
+
? ['diff', '--stat', `${lastSha}..HEAD`]
|
|
56
|
+
: ['diff', '--stat', 'HEAD~1'];
|
|
57
|
+
const stat = execFileSync('git', args, { cwd, encoding: 'utf-8', timeout: 5000 });
|
|
58
|
+
const lines = stat.split('\n').filter(l => l.trim());
|
|
59
|
+
const codeExts = /\.(ts|tsx|js|jsx|py|rs|go|java|rb|c|cpp|h|swift|kt)$/;
|
|
60
|
+
const hasCodeFiles = lines.some(line => {
|
|
61
|
+
const filePath = line.split('|')[0]?.trim() ?? '';
|
|
62
|
+
return codeExts.test(filePath);
|
|
63
|
+
});
|
|
64
|
+
const lastLine = lines[lines.length - 1] ?? '';
|
|
65
|
+
const changedMatch = lastLine.match(/(\d+)\s+files?\s+changed/);
|
|
66
|
+
const insertMatch = lastLine.match(/(\d+)\s+insertion/);
|
|
67
|
+
const deleteMatch = lastLine.match(/(\d+)\s+deletion/);
|
|
68
|
+
const fileCount = parseInt(changedMatch?.[1] ?? '0', 10);
|
|
69
|
+
const lineCount = parseInt(insertMatch?.[1] ?? '0', 10) + parseInt(deleteMatch?.[1] ?? '0', 10);
|
|
70
|
+
return { files: fileCount, lines: lineCount, hasCodeFiles };
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { files: 0, lines: 0, hasCodeFiles: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solution persistence for compound extraction.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from compound-extractor.ts — saving extracted solutions,
|
|
5
|
+
* updating re-extraction counters, and managing extraction state.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtractedSolution } from './extraction-gates.js';
|
|
8
|
+
export interface LastExtraction {
|
|
9
|
+
lastCommitSha: string;
|
|
10
|
+
lastExtractedAt: string;
|
|
11
|
+
extractionsToday: number;
|
|
12
|
+
todayDate: string;
|
|
13
|
+
}
|
|
14
|
+
/** Load last extraction state */
|
|
15
|
+
export declare function loadLastExtraction(): LastExtraction;
|
|
16
|
+
/** Save last extraction state */
|
|
17
|
+
export declare function saveLastExtraction(state: LastExtraction): void;
|
|
18
|
+
/** Save an extracted solution as experiment */
|
|
19
|
+
export declare function saveExtractedSolution(sol: ExtractedSolution, _sessionId: string): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Increment reExtracted counter on existing solution that matches given tags.
|
|
22
|
+
*/
|
|
23
|
+
export declare function updateReExtractedCounter(tags: string[]): void;
|
|
24
|
+
/**
|
|
25
|
+
* Observability: detect references to existing solutions in new content → emit acted_on.
|
|
26
|
+
*/
|
|
27
|
+
export declare function emitCompoundExtractActedOn(sessionId: string, newSolutionName: string, newContent: string, newSupersedes: string | null): void;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solution persistence for compound extraction.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from compound-extractor.ts — saving extracted solutions,
|
|
5
|
+
* updating re-extraction counters, and managing extraction state.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import { ME_SOLUTIONS, STATE_DIR } from '../core/paths.js';
|
|
10
|
+
import { createLogger } from '../core/logger.js';
|
|
11
|
+
import { emitSolutionEvent } from '../core/observability-store.js';
|
|
12
|
+
import { serializeSolutionV3, DEFAULT_EVIDENCE } from './solution-format.js';
|
|
13
|
+
import { atomicWriteJSON, atomicWriteText } from '../hooks/shared/atomic-write.js';
|
|
14
|
+
import { mutateSolutionFile } from './solution-writer.js';
|
|
15
|
+
const log = createLogger('extraction-persistence');
|
|
16
|
+
const LAST_EXTRACTION_PATH = path.join(STATE_DIR, 'last-extraction.json');
|
|
17
|
+
/** Load last extraction state */
|
|
18
|
+
export function loadLastExtraction() {
|
|
19
|
+
try {
|
|
20
|
+
if (fs.existsSync(LAST_EXTRACTION_PATH)) {
|
|
21
|
+
return JSON.parse(fs.readFileSync(LAST_EXTRACTION_PATH, 'utf-8'));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
log.debug('last extraction state read failed — may cause duplicate extractions', e);
|
|
26
|
+
}
|
|
27
|
+
return { lastCommitSha: '', lastExtractedAt: '', extractionsToday: 0, todayDate: '' };
|
|
28
|
+
}
|
|
29
|
+
/** Save last extraction state */
|
|
30
|
+
export function saveLastExtraction(state) {
|
|
31
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
32
|
+
atomicWriteJSON(LAST_EXTRACTION_PATH, state);
|
|
33
|
+
}
|
|
34
|
+
/** Save an extracted solution as experiment */
|
|
35
|
+
export function saveExtractedSolution(sol, _sessionId) {
|
|
36
|
+
const today = new Date().toISOString().split('T')[0];
|
|
37
|
+
const slugName = sol.name.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9가-힣\s-]/g, '')
|
|
39
|
+
.replace(/\s+/g, '-')
|
|
40
|
+
.replace(/-+/g, '-')
|
|
41
|
+
.replace(/^-|-$/g, '')
|
|
42
|
+
.slice(0, 60) || `untitled-${Date.now()}`;
|
|
43
|
+
const solution = {
|
|
44
|
+
frontmatter: {
|
|
45
|
+
name: slugName,
|
|
46
|
+
version: 1,
|
|
47
|
+
status: 'experiment',
|
|
48
|
+
confidence: 0.3,
|
|
49
|
+
type: sol.type,
|
|
50
|
+
scope: 'me',
|
|
51
|
+
tags: sol.tags.slice(0, 5),
|
|
52
|
+
identifiers: sol.identifiers.filter(id => id.length >= 4),
|
|
53
|
+
evidence: { ...DEFAULT_EVIDENCE },
|
|
54
|
+
created: today,
|
|
55
|
+
updated: today,
|
|
56
|
+
supersedes: null,
|
|
57
|
+
extractedBy: 'auto',
|
|
58
|
+
},
|
|
59
|
+
context: sol.context,
|
|
60
|
+
content: sol.content,
|
|
61
|
+
};
|
|
62
|
+
const filePath = path.join(ME_SOLUTIONS, `${slugName}.md`);
|
|
63
|
+
if (fs.existsSync(filePath))
|
|
64
|
+
return null;
|
|
65
|
+
fs.mkdirSync(ME_SOLUTIONS, { recursive: true });
|
|
66
|
+
atomicWriteText(filePath, serializeSolutionV3(solution));
|
|
67
|
+
return slugName;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Increment reExtracted counter on existing solution that matches given tags.
|
|
71
|
+
*/
|
|
72
|
+
export function updateReExtractedCounter(tags) {
|
|
73
|
+
if (!fs.existsSync(ME_SOLUTIONS))
|
|
74
|
+
return;
|
|
75
|
+
const files = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md'));
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const filePath = path.join(ME_SOLUTIONS, file);
|
|
78
|
+
try {
|
|
79
|
+
if (fs.lstatSync(filePath).isSymbolicLink())
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
let preview;
|
|
86
|
+
try {
|
|
87
|
+
preview = fs.readFileSync(filePath, 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
const tagMatch = preview.match(/tags:\s*\[([^\]]*)\]/);
|
|
93
|
+
if (!tagMatch)
|
|
94
|
+
continue;
|
|
95
|
+
const existingTags = tagMatch[1].split(',').map(t => t.trim().replace(/"/g, ''));
|
|
96
|
+
const overlap = tags.filter(t => existingTags.includes(t));
|
|
97
|
+
if (overlap.length / Math.max(tags.length, existingTags.length, 1) < 0.7)
|
|
98
|
+
continue;
|
|
99
|
+
mutateSolutionFile(filePath, sol => {
|
|
100
|
+
sol.frontmatter.evidence.reExtracted = (sol.frontmatter.evidence.reExtracted ?? 0) + 1;
|
|
101
|
+
return true;
|
|
102
|
+
});
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Observability: detect references to existing solutions in new content → emit acted_on.
|
|
108
|
+
*/
|
|
109
|
+
export function emitCompoundExtractActedOn(sessionId, newSolutionName, newContent, newSupersedes) {
|
|
110
|
+
try {
|
|
111
|
+
if (!fs.existsSync(ME_SOLUTIONS))
|
|
112
|
+
return;
|
|
113
|
+
const bodyLower = newContent.toLowerCase();
|
|
114
|
+
const supersedes = newSupersedes ?? '';
|
|
115
|
+
const files = fs.readdirSync(ME_SOLUTIONS).filter(f => f.endsWith('.md'));
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
const existingName = path.basename(file, '.md');
|
|
118
|
+
if (existingName === newSolutionName)
|
|
119
|
+
continue;
|
|
120
|
+
const referenced = (supersedes && existingName === supersedes)
|
|
121
|
+
|| bodyLower.includes(existingName.toLowerCase());
|
|
122
|
+
if (!referenced)
|
|
123
|
+
continue;
|
|
124
|
+
emitSolutionEvent({
|
|
125
|
+
sessionId,
|
|
126
|
+
solutionId: existingName,
|
|
127
|
+
eventType: 'acted_on',
|
|
128
|
+
signalSource: 'compound-extract',
|
|
129
|
+
signalScore: 0.20,
|
|
130
|
+
meta: {
|
|
131
|
+
new_solution: newSolutionName,
|
|
132
|
+
via: (supersedes && existingName === supersedes) ? 'supersedes' : 'body-mention',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
log.debug('emitCompoundExtractActedOn 실패', e);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session context extraction for compound knowledge.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from compound-extractor.ts — reads Claude session JSONL files
|
|
5
|
+
* to extract prompts and write history correlated to a specific project.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtractedSolution } from './extraction-gates.js';
|
|
8
|
+
interface WriteContextEntry {
|
|
9
|
+
filePath: string;
|
|
10
|
+
contentSnippet: string;
|
|
11
|
+
fileExtension: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function loadPromptHistoryFallback(): string[];
|
|
14
|
+
/**
|
|
15
|
+
* Load Claude session prompts + writes correlated to `cwd`.
|
|
16
|
+
*
|
|
17
|
+
* Exported primarily for test assertions (the `claude-session-context`
|
|
18
|
+
* tests verify correlation logic directly).
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadClaudeProjectSessionContext(cwd: string, lastExtractedAt: string): {
|
|
21
|
+
prompts: string[];
|
|
22
|
+
writes: WriteContextEntry[];
|
|
23
|
+
};
|
|
24
|
+
/** Extract patterns from accumulated session context (prompts + writes + diff) */
|
|
25
|
+
export declare function extractFromSessionContext(gitDiff: string, cwd: string, lastExtractedAt: string): ExtractedSolution[];
|
|
26
|
+
export {};
|