@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.
Files changed (79) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +33 -1
  4. package/assets/claude/agents/forgen-verify.md +65 -0
  5. package/assets/claude/workflows/compound-extract.js +136 -0
  6. package/assets/claude/workflows/evidence-gate-audit.js +107 -0
  7. package/assets/shared/hook-registry.json +1 -0
  8. package/dist/checks/_shared/meta-guard-dispatch.d.ts +38 -0
  9. package/dist/checks/_shared/meta-guard-dispatch.js +80 -0
  10. package/dist/checks/_shared/text-sanitizer.js +15 -0
  11. package/dist/cli.js +65 -2
  12. package/dist/core/changelog-cli.d.ts +7 -0
  13. package/dist/core/changelog-cli.js +100 -0
  14. package/dist/core/doctor.d.ts +3 -0
  15. package/dist/core/doctor.js +75 -1
  16. package/dist/core/effort-advisory.d.ts +23 -0
  17. package/dist/core/effort-advisory.js +29 -0
  18. package/dist/core/explain-cli.d.ts +6 -0
  19. package/dist/core/explain-cli.js +99 -0
  20. package/dist/core/git-stats.d.ts +23 -0
  21. package/dist/core/git-stats.js +50 -0
  22. package/dist/core/health-cli.d.ts +23 -0
  23. package/dist/core/health-cli.js +86 -0
  24. package/dist/core/inspect-cli.js +60 -1
  25. package/dist/core/probe-workflow-cli.d.ts +72 -0
  26. package/dist/core/probe-workflow-cli.js +282 -0
  27. package/dist/core/regress-map-cli.d.ts +7 -0
  28. package/dist/core/regress-map-cli.js +59 -0
  29. package/dist/core/stats-cli.d.ts +22 -9
  30. package/dist/core/stats-cli.js +149 -0
  31. package/dist/core/watch-cli.d.ts +7 -0
  32. package/dist/core/watch-cli.js +185 -0
  33. package/dist/core/workflows-cli.d.ts +26 -0
  34. package/dist/core/workflows-cli.js +120 -0
  35. package/dist/engine/compound-export.d.ts +12 -0
  36. package/dist/engine/compound-export.js +136 -14
  37. package/dist/engine/compound-extractor.d.ts +12 -43
  38. package/dist/engine/compound-extractor.js +27 -756
  39. package/dist/engine/extraction-diff.d.ts +11 -0
  40. package/dist/engine/extraction-diff.js +105 -0
  41. package/dist/engine/extraction-gates.d.ts +37 -0
  42. package/dist/engine/extraction-gates.js +100 -0
  43. package/dist/engine/extraction-git.d.ts +20 -0
  44. package/dist/engine/extraction-git.js +75 -0
  45. package/dist/engine/extraction-persistence.d.ts +27 -0
  46. package/dist/engine/extraction-persistence.js +140 -0
  47. package/dist/engine/extraction-session.d.ts +26 -0
  48. package/dist/engine/extraction-session.js +230 -0
  49. package/dist/engine/lifecycle/types.d.ts +1 -1
  50. package/dist/engine/meta-learning/matcher-weight-loader.d.ts +16 -0
  51. package/dist/engine/meta-learning/matcher-weight-loader.js +45 -0
  52. package/dist/engine/precision-guards.d.ts +14 -0
  53. package/dist/engine/precision-guards.js +39 -0
  54. package/dist/engine/ranking-pipeline.d.ts +45 -0
  55. package/dist/engine/ranking-pipeline.js +66 -0
  56. package/dist/engine/relevance-scorer.d.ts +43 -0
  57. package/dist/engine/relevance-scorer.js +81 -0
  58. package/dist/engine/scoring-algorithms.d.ts +31 -0
  59. package/dist/engine/scoring-algorithms.js +109 -0
  60. package/dist/engine/solution-matcher-eval.d.ts +97 -0
  61. package/dist/engine/solution-matcher-eval.js +122 -0
  62. package/dist/engine/solution-matcher.d.ts +21 -380
  63. package/dist/engine/solution-matcher.js +27 -828
  64. package/dist/fgx.d.ts +4 -1
  65. package/dist/fgx.js +46 -10
  66. package/dist/hooks/notepad-injector.js +7 -0
  67. package/dist/hooks/post-tool-use.js +8 -1
  68. package/dist/hooks/shared/preflight-check.d.ts +15 -0
  69. package/dist/hooks/shared/preflight-check.js +51 -0
  70. package/dist/hooks/stop-guard.js +19 -60
  71. package/dist/hooks/subagent-stop-guard.d.ts +23 -0
  72. package/dist/hooks/subagent-stop-guard.js +158 -0
  73. package/dist/hooks/subagent-tracker.d.ts +36 -3
  74. package/dist/hooks/subagent-tracker.js +86 -39
  75. package/dist/store/evidence-store.js +9 -2
  76. package/hooks/hooks.json +6 -1
  77. package/package.json +7 -7
  78. package/plugin.json +1 -1
  79. package/scripts/postinstall.js +10 -7
@@ -152,14 +152,116 @@ export function importKnowledge(archivePath) {
152
152
  fs.rmSync(tmpDir, { recursive: true, force: true });
153
153
  }
154
154
  }
155
+ /**
156
+ * Selective export — filter to specific categories.
157
+ * `--only solutions,rules` exports only those two directories.
158
+ */
159
+ export function exportKnowledgeSelective(categories, outputPath) {
160
+ const date = new Date().toISOString().split('T')[0];
161
+ const resolved = outputPath ?? path.join(process.cwd(), `forgen-knowledge-${date}.tar.gz`);
162
+ const validCategories = categories.filter(c => KNOWLEDGE_DIRS.includes(c));
163
+ if (validCategories.length === 0) {
164
+ throw new Error(`No valid categories. Choose from: ${KNOWLEDGE_DIRS.join(', ')}`);
165
+ }
166
+ const counts = {};
167
+ const existingDirs = [];
168
+ for (const name of validCategories) {
169
+ const dir = path.join(ME_DIR, name);
170
+ const count = countFiles(dir);
171
+ counts[name] = count;
172
+ if (fs.existsSync(dir)) {
173
+ existingDirs.push(name);
174
+ }
175
+ }
176
+ const totalFiles = Object.values(counts).reduce((a, b) => a + b, 0);
177
+ if (existingDirs.length === 0) {
178
+ throw new Error('No knowledge directories found to export.');
179
+ }
180
+ const outDir = path.dirname(resolved);
181
+ fs.mkdirSync(outDir, { recursive: true });
182
+ execFileSync('tar', ['czf', resolved, ...existingDirs], {
183
+ cwd: ME_DIR,
184
+ timeout: 30000,
185
+ stdio: ['pipe', 'pipe', 'pipe'],
186
+ });
187
+ return { outputPath: resolved, counts, totalFiles };
188
+ }
189
+ /**
190
+ * Import with merge mode — updates existing files instead of skipping them.
191
+ * Newer files in the archive overwrite older local files.
192
+ */
193
+ export function importKnowledgeMerge(archivePath) {
194
+ if (!fs.existsSync(archivePath)) {
195
+ throw new Error(`Archive not found: ${archivePath}`);
196
+ }
197
+ const listOutput = execFileSync('tar', ['tzf', archivePath], {
198
+ timeout: 30000,
199
+ encoding: 'utf-8',
200
+ stdio: ['pipe', 'pipe', 'pipe'],
201
+ });
202
+ const archiveFiles = listOutput
203
+ .split('\n')
204
+ .map(f => f.trim())
205
+ .filter(f => f && !f.endsWith('/'));
206
+ const tmpDir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'forgen-merge-'));
207
+ try {
208
+ execFileSync('tar', ['xzf', archivePath, '-C', tmpDir], {
209
+ timeout: 30000,
210
+ stdio: ['pipe', 'pipe', 'pipe'],
211
+ });
212
+ const result = { imported: 0, skipped: 0, merged: 0, details: [] };
213
+ const meDirCanon = path.resolve(ME_DIR) + path.sep;
214
+ const tmpDirCanon = path.resolve(tmpDir) + path.sep;
215
+ for (const relFile of archiveFiles) {
216
+ const srcPath = path.resolve(path.join(tmpDir, relFile));
217
+ const destPath = path.resolve(path.join(ME_DIR, relFile));
218
+ if (!isPathInside(meDirCanon, destPath) || !isPathInside(tmpDirCanon, srcPath)) {
219
+ result.skipped++;
220
+ result.details.push({ file: relFile, action: 'skipped' });
221
+ continue;
222
+ }
223
+ if (fs.existsSync(destPath)) {
224
+ const srcMtime = fs.statSync(srcPath).mtimeMs;
225
+ const destMtime = fs.statSync(destPath).mtimeMs;
226
+ if (srcMtime > destMtime) {
227
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
228
+ fs.copyFileSync(srcPath, destPath);
229
+ result.merged++;
230
+ result.details.push({ file: relFile, action: 'imported' });
231
+ }
232
+ else {
233
+ result.skipped++;
234
+ result.details.push({ file: relFile, action: 'skipped' });
235
+ }
236
+ }
237
+ else {
238
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
239
+ fs.copyFileSync(srcPath, destPath);
240
+ result.imported++;
241
+ result.details.push({ file: relFile, action: 'imported' });
242
+ }
243
+ }
244
+ return result;
245
+ }
246
+ finally {
247
+ fs.rmSync(tmpDir, { recursive: true, force: true });
248
+ }
249
+ }
155
250
  /** CLI handler: forgen compound export */
156
251
  export async function handleExport(args) {
157
252
  const outputIdx = args.indexOf('--output');
158
253
  const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : undefined;
254
+ const onlyIdx = args.indexOf('--only');
255
+ const onlyCategories = onlyIdx !== -1 ? (args[onlyIdx + 1] ?? '').split(',').filter(Boolean) : [];
159
256
  try {
160
- const result = exportKnowledge(outputPath);
257
+ const result = onlyCategories.length > 0
258
+ ? exportKnowledgeSelective(onlyCategories, outputPath)
259
+ : exportKnowledge(outputPath);
161
260
  console.log('\n Compound Knowledge Export\n');
162
261
  console.log(` Output: ${result.outputPath}`);
262
+ if (onlyCategories.length > 0) {
263
+ console.log(` Filter: ${onlyCategories.join(', ')}`);
264
+ }
163
265
  console.log();
164
266
  for (const [category, count] of Object.entries(result.counts)) {
165
267
  console.log(` ${category}: ${count} files`);
@@ -173,23 +275,43 @@ export async function handleExport(args) {
173
275
  }
174
276
  /** CLI handler: forgen compound import */
175
277
  export async function handleImport(args) {
176
- const archivePath = args[0];
177
- if (!archivePath || archivePath.startsWith('--')) {
178
- console.log(' Usage: forgen compound import <path-to-archive>\n');
278
+ const filteredArgs = args.filter(a => !a.startsWith('--'));
279
+ const archivePath = filteredArgs[0];
280
+ const mergeMode = args.includes('--merge');
281
+ if (!archivePath) {
282
+ console.log(' Usage: forgen compound import <path-to-archive> [--merge]\n');
283
+ console.log(' --merge Update existing files if archive version is newer');
179
284
  return;
180
285
  }
181
286
  try {
182
287
  const resolved = path.resolve(archivePath);
183
- const result = importKnowledge(resolved);
184
- console.log('\n Compound Knowledge Import\n');
185
- console.log(` Archive: ${resolved}`);
186
- console.log(` Imported: ${result.imported} new files`);
187
- console.log(` Skipped: ${result.skipped} existing files`);
188
- if (result.details.length > 0 && result.details.length <= 20) {
189
- console.log();
190
- for (const d of result.details) {
191
- const icon = d.action === 'imported' ? '+' : '-';
192
- console.log(` ${icon} ${d.file}`);
288
+ if (mergeMode) {
289
+ const result = importKnowledgeMerge(resolved);
290
+ console.log('\n Compound Knowledge Import (merge mode)\n');
291
+ console.log(` Archive: ${resolved}`);
292
+ console.log(` New: ${result.imported} files`);
293
+ console.log(` Merged: ${result.merged} files (newer version overwrote local)`);
294
+ console.log(` Skipped: ${result.skipped} files (local is same or newer)`);
295
+ if (result.details.length > 0 && result.details.length <= 20) {
296
+ console.log();
297
+ for (const d of result.details) {
298
+ const icon = d.action === 'imported' ? '+' : '-';
299
+ console.log(` ${icon} ${d.file}`);
300
+ }
301
+ }
302
+ }
303
+ else {
304
+ const result = importKnowledge(resolved);
305
+ console.log('\n Compound Knowledge Import\n');
306
+ console.log(` Archive: ${resolved}`);
307
+ console.log(` Imported: ${result.imported} new files`);
308
+ console.log(` Skipped: ${result.skipped} existing files`);
309
+ if (result.details.length > 0 && result.details.length <= 20) {
310
+ console.log();
311
+ for (const d of result.details) {
312
+ const icon = d.action === 'imported' ? '+' : '-';
313
+ console.log(` ${icon} ${d.file}`);
314
+ }
193
315
  }
194
316
  }
195
317
  console.log();
@@ -1,54 +1,24 @@
1
1
  /**
2
- * Forgen — Compound Knowledge Extractor
2
+ * Forgen — Compound Knowledge Extractor (facade)
3
3
  *
4
- * Extracts reusable patterns and decisions from git history and session context.
5
- * Runs quality gates (structure, toxicity, trivial, dedup) before persisting solutions.
4
+ * Orchestrates extraction pipeline: git analysis → quality gates →
5
+ * pattern extraction → persistence. Re-exports from decomposed modules.
6
6
  *
7
- * Module Structure:
8
- * - Lines 1-50: Imports, constants, SHA validation, LastExtraction/ExtractedSolution interfaces
9
- * - Lines 50-115: Git helpers — getNewCommits, getCommitMessages, getGitDiff, getDiffStats
10
- * - Lines 115-190: Quality Gates — gate0 (worth extracting), gate1 (structure), gate2 (toxicity),
11
- * gateTrivial (trivial rejection), gate3 (dedup)
12
- * - Lines 190-275: extractFromDiff — pattern extraction from git diff (modules, errors, imports, commits)
13
- * - Lines 275-395: extractFromSessionContext — prompt/write history analysis (actions, hotspots, tech)
14
- * - Lines 396-475: saveExtractedSolution, updateReExtractedCounter — solution persistence
15
- * - Lines 477-555: runExtraction — main entry point orchestrating gates + extraction + state
16
- * - Lines 557-634: processExtractionResults, isExtractionPaused, pauseExtraction, resumeExtraction
7
+ * Module layout (post-decomposition):
8
+ * extraction-git.ts — getNewCommits, getCommitMessages, getGitDiff, getDiffStats
9
+ * extraction-gates.ts — gate0-gate3, gateTrivial, evaluateExtractedSolution, ExtractedSolution
10
+ * extraction-diff.ts — extractFromDiff, findCommonPrefix
11
+ * extraction-session.ts — extractFromSessionContext, loadClaudeProjectSessionContext
12
+ * extraction-persistence.ts — saveExtractedSolution, updateReExtractedCounter, LastExtraction
17
13
  */
18
- import type { SolutionType } from './solution-format.js';
19
- interface ExtractedSolution {
20
- name: string;
21
- type: SolutionType;
22
- tags: string[];
23
- identifiers: string[];
24
- context: string;
25
- content: string;
26
- }
27
- interface WriteContextEntry {
28
- filePath: string;
29
- contentSnippet: string;
30
- fileExtension: string;
31
- }
32
- /**
33
- * Load Claude session prompts + writes correlated to `cwd`.
34
- *
35
- * Exported primarily for test assertions (the `claude-session-context`
36
- * tests need to verify that correlation picks the right project's
37
- * sessions and ignores unrelated ones). Before C4 the tests could
38
- * observe this indirectly via the now-removed `recurring-task-pattern`
39
- * extractor; now they check this loader directly. Not intended for
40
- * production callers outside the extractor pipeline.
41
- */
42
- export declare function loadClaudeProjectSessionContext(cwd: string, lastExtractedAt: string): {
43
- prompts: string[];
44
- writes: WriteContextEntry[];
45
- };
14
+ import type { ExtractedSolution } from './extraction-gates.js';
15
+ export { loadClaudeProjectSessionContext } from './extraction-session.js';
16
+ export type { ExtractedSolution } from './extraction-gates.js';
46
17
  export declare function previewExtraction(cwd: string): Promise<{
47
18
  preview: ExtractedSolution[];
48
19
  skipped: string[];
49
20
  reason?: string;
50
21
  }>;
51
- /** Main extraction function — called from SessionStart or CLI */
52
22
  export declare function runExtraction(cwd: string, sessionId: string): Promise<{
53
23
  extracted: string[];
54
24
  skipped: string[];
@@ -65,4 +35,3 @@ export declare function isExtractionPaused(): boolean;
65
35
  export declare function pauseExtraction(): void;
66
36
  /** Resume auto-extraction */
67
37
  export declare function resumeExtraction(): void;
68
- export {};