@wooojin/forgen 0.1.0
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 +20 -0
- package/CHANGELOG.md +353 -0
- package/CONTRIBUTING.md +98 -0
- package/LICENSE +21 -0
- package/README.ja.md +469 -0
- package/README.ko.md +469 -0
- package/README.md +483 -0
- package/README.zh.md +469 -0
- package/agents/analyst.md +98 -0
- package/agents/architect.md +62 -0
- package/agents/code-reviewer.md +120 -0
- package/agents/code-simplifier.md +197 -0
- package/agents/critic.md +70 -0
- package/agents/debugger.md +117 -0
- package/agents/designer.md +131 -0
- package/agents/executor.md +54 -0
- package/agents/explore.md +145 -0
- package/agents/git-master.md +212 -0
- package/agents/performance-reviewer.md +172 -0
- package/agents/planner.md +29 -0
- package/agents/qa-tester.md +158 -0
- package/agents/refactoring-expert.md +168 -0
- package/agents/scientist.md +144 -0
- package/agents/security-reviewer.md +137 -0
- package/agents/test-engineer.md +153 -0
- package/agents/verifier.md +133 -0
- package/agents/writer.md +184 -0
- package/commands/api-design.md +268 -0
- package/commands/architecture-decision.md +314 -0
- package/commands/ci-cd.md +270 -0
- package/commands/code-review.md +233 -0
- package/commands/compound.md +117 -0
- package/commands/database.md +263 -0
- package/commands/debug-detective.md +99 -0
- package/commands/docker.md +274 -0
- package/commands/documentation.md +276 -0
- package/commands/ecomode.md +51 -0
- package/commands/frontend.md +271 -0
- package/commands/git-master.md +90 -0
- package/commands/incident-response.md +292 -0
- package/commands/migrate.md +101 -0
- package/commands/performance.md +288 -0
- package/commands/refactor.md +105 -0
- package/commands/security-review.md +288 -0
- package/commands/tdd.md +183 -0
- package/commands/testing-strategy.md +265 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +295 -0
- package/dist/core/auto-compound-runner.d.ts +12 -0
- package/dist/core/auto-compound-runner.js +460 -0
- package/dist/core/config-hooks.d.ts +10 -0
- package/dist/core/config-hooks.js +112 -0
- package/dist/core/config-injector.d.ts +50 -0
- package/dist/core/config-injector.js +455 -0
- package/dist/core/doctor.d.ts +1 -0
- package/dist/core/doctor.js +163 -0
- package/dist/core/errors.d.ts +81 -0
- package/dist/core/errors.js +133 -0
- package/dist/core/global-config.d.ts +43 -0
- package/dist/core/global-config.js +25 -0
- package/dist/core/harness.d.ts +24 -0
- package/dist/core/harness.js +621 -0
- package/dist/core/init.d.ts +7 -0
- package/dist/core/init.js +37 -0
- package/dist/core/inspect-cli.d.ts +7 -0
- package/dist/core/inspect-cli.js +47 -0
- package/dist/core/legacy-detector.d.ts +33 -0
- package/dist/core/legacy-detector.js +66 -0
- package/dist/core/logger.d.ts +34 -0
- package/dist/core/logger.js +121 -0
- package/dist/core/mcp-config.d.ts +44 -0
- package/dist/core/mcp-config.js +177 -0
- package/dist/core/notepad.d.ts +31 -0
- package/dist/core/notepad.js +88 -0
- package/dist/core/paths.d.ts +85 -0
- package/dist/core/paths.js +101 -0
- package/dist/core/plugin-detector.d.ts +44 -0
- package/dist/core/plugin-detector.js +226 -0
- package/dist/core/runtime-detector.d.ts +8 -0
- package/dist/core/runtime-detector.js +49 -0
- package/dist/core/scope-resolver.d.ts +8 -0
- package/dist/core/scope-resolver.js +45 -0
- package/dist/core/session-logger.d.ts +6 -0
- package/dist/core/session-logger.js +111 -0
- package/dist/core/session-store.d.ts +28 -0
- package/dist/core/session-store.js +218 -0
- package/dist/core/settings-lock.d.ts +18 -0
- package/dist/core/settings-lock.js +125 -0
- package/dist/core/spawn.d.ts +3 -0
- package/dist/core/spawn.js +135 -0
- package/dist/core/types.d.ts +108 -0
- package/dist/core/types.js +1 -0
- package/dist/core/uninstall.d.ts +4 -0
- package/dist/core/uninstall.js +307 -0
- package/dist/core/v1-bootstrap.d.ts +26 -0
- package/dist/core/v1-bootstrap.js +155 -0
- package/dist/engine/compound-cli.d.ts +24 -0
- package/dist/engine/compound-cli.js +250 -0
- package/dist/engine/compound-extractor.d.ts +68 -0
- package/dist/engine/compound-extractor.js +860 -0
- package/dist/engine/compound-lifecycle.d.ts +32 -0
- package/dist/engine/compound-lifecycle.js +305 -0
- package/dist/engine/compound-loop.d.ts +32 -0
- package/dist/engine/compound-loop.js +511 -0
- package/dist/engine/match-eval-log.d.ts +139 -0
- package/dist/engine/match-eval-log.js +270 -0
- package/dist/engine/phrase-blocklist.d.ts +119 -0
- package/dist/engine/phrase-blocklist.js +208 -0
- package/dist/engine/skill-promoter.d.ts +20 -0
- package/dist/engine/skill-promoter.js +115 -0
- package/dist/engine/solution-format.d.ts +160 -0
- package/dist/engine/solution-format.js +432 -0
- package/dist/engine/solution-index.d.ts +13 -0
- package/dist/engine/solution-index.js +252 -0
- package/dist/engine/solution-matcher.d.ts +364 -0
- package/dist/engine/solution-matcher.js +656 -0
- package/dist/engine/solution-writer.d.ts +76 -0
- package/dist/engine/solution-writer.js +157 -0
- package/dist/engine/term-matcher.d.ts +81 -0
- package/dist/engine/term-matcher.js +268 -0
- package/dist/engine/term-normalizer.d.ts +116 -0
- package/dist/engine/term-normalizer.js +171 -0
- package/dist/fgx.d.ts +6 -0
- package/dist/fgx.js +42 -0
- package/dist/forge/cli.d.ts +11 -0
- package/dist/forge/cli.js +100 -0
- package/dist/forge/evidence-processor.d.ts +21 -0
- package/dist/forge/evidence-processor.js +87 -0
- package/dist/forge/mismatch-detector.d.ts +44 -0
- package/dist/forge/mismatch-detector.js +83 -0
- package/dist/forge/onboarding-cli.d.ts +6 -0
- package/dist/forge/onboarding-cli.js +89 -0
- package/dist/forge/onboarding.d.ts +25 -0
- package/dist/forge/onboarding.js +122 -0
- package/dist/hooks/compound-reflection.d.ts +45 -0
- package/dist/hooks/compound-reflection.js +82 -0
- package/dist/hooks/context-guard.d.ts +24 -0
- package/dist/hooks/context-guard.js +156 -0
- package/dist/hooks/dangerous-patterns.json +18 -0
- package/dist/hooks/db-guard.d.ts +17 -0
- package/dist/hooks/db-guard.js +105 -0
- package/dist/hooks/hook-config.d.ts +29 -0
- package/dist/hooks/hook-config.js +92 -0
- package/dist/hooks/hook-registry.d.ts +43 -0
- package/dist/hooks/hook-registry.js +31 -0
- package/dist/hooks/hooks-generator.d.ts +49 -0
- package/dist/hooks/hooks-generator.js +99 -0
- package/dist/hooks/intent-classifier.d.ts +12 -0
- package/dist/hooks/intent-classifier.js +62 -0
- package/dist/hooks/keyword-detector.d.ts +25 -0
- package/dist/hooks/keyword-detector.js +389 -0
- package/dist/hooks/notepad-injector.d.ts +18 -0
- package/dist/hooks/notepad-injector.js +51 -0
- package/dist/hooks/permission-handler.d.ts +14 -0
- package/dist/hooks/permission-handler.js +114 -0
- package/dist/hooks/post-tool-failure.d.ts +11 -0
- package/dist/hooks/post-tool-failure.js +118 -0
- package/dist/hooks/post-tool-handlers.d.ts +17 -0
- package/dist/hooks/post-tool-handlers.js +115 -0
- package/dist/hooks/post-tool-use.d.ts +29 -0
- package/dist/hooks/post-tool-use.js +151 -0
- package/dist/hooks/pre-compact.d.ts +10 -0
- package/dist/hooks/pre-compact.js +165 -0
- package/dist/hooks/pre-tool-use.d.ts +31 -0
- package/dist/hooks/pre-tool-use.js +325 -0
- package/dist/hooks/prompt-injection-filter.d.ts +56 -0
- package/dist/hooks/prompt-injection-filter.js +287 -0
- package/dist/hooks/rate-limiter.d.ts +21 -0
- package/dist/hooks/rate-limiter.js +86 -0
- package/dist/hooks/secret-filter.d.ts +14 -0
- package/dist/hooks/secret-filter.js +65 -0
- package/dist/hooks/session-recovery.d.ts +27 -0
- package/dist/hooks/session-recovery.js +406 -0
- package/dist/hooks/shared/atomic-write.d.ts +41 -0
- package/dist/hooks/shared/atomic-write.js +148 -0
- package/dist/hooks/shared/context-budget.d.ts +37 -0
- package/dist/hooks/shared/context-budget.js +45 -0
- package/dist/hooks/shared/file-lock.d.ts +56 -0
- package/dist/hooks/shared/file-lock.js +253 -0
- package/dist/hooks/shared/hook-response.d.ts +33 -0
- package/dist/hooks/shared/hook-response.js +62 -0
- package/dist/hooks/shared/injection-caps.d.ts +39 -0
- package/dist/hooks/shared/injection-caps.js +52 -0
- package/dist/hooks/shared/plugin-signal.d.ts +23 -0
- package/dist/hooks/shared/plugin-signal.js +104 -0
- package/dist/hooks/shared/read-stdin.d.ts +8 -0
- package/dist/hooks/shared/read-stdin.js +63 -0
- package/dist/hooks/shared/sanitize-id.d.ts +7 -0
- package/dist/hooks/shared/sanitize-id.js +9 -0
- package/dist/hooks/shared/sanitize.d.ts +7 -0
- package/dist/hooks/shared/sanitize.js +22 -0
- package/dist/hooks/skill-injector.d.ts +38 -0
- package/dist/hooks/skill-injector.js +285 -0
- package/dist/hooks/slop-detector.d.ts +18 -0
- package/dist/hooks/slop-detector.js +93 -0
- package/dist/hooks/solution-injector.d.ts +58 -0
- package/dist/hooks/solution-injector.js +436 -0
- package/dist/hooks/subagent-tracker.d.ts +10 -0
- package/dist/hooks/subagent-tracker.js +90 -0
- package/dist/i18n/index.d.ts +43 -0
- package/dist/i18n/index.js +224 -0
- package/dist/lib.d.ts +14 -0
- package/dist/lib.js +14 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +40 -0
- package/dist/mcp/solution-reader.d.ts +90 -0
- package/dist/mcp/solution-reader.js +273 -0
- package/dist/mcp/tools.d.ts +16 -0
- package/dist/mcp/tools.js +302 -0
- package/dist/preset/facet-catalog.d.ts +17 -0
- package/dist/preset/facet-catalog.js +46 -0
- package/dist/preset/preset-manager.d.ts +31 -0
- package/dist/preset/preset-manager.js +111 -0
- package/dist/renderer/inspect-renderer.d.ts +11 -0
- package/dist/renderer/inspect-renderer.js +123 -0
- package/dist/renderer/rule-renderer.d.ts +18 -0
- package/dist/renderer/rule-renderer.js +159 -0
- package/dist/store/evidence-store.d.ts +23 -0
- package/dist/store/evidence-store.js +58 -0
- package/dist/store/profile-store.d.ts +12 -0
- package/dist/store/profile-store.js +53 -0
- package/dist/store/recommendation-store.d.ts +22 -0
- package/dist/store/recommendation-store.js +64 -0
- package/dist/store/rule-store.d.ts +22 -0
- package/dist/store/rule-store.js +62 -0
- package/dist/store/session-state-store.d.ts +11 -0
- package/dist/store/session-state-store.js +44 -0
- package/dist/store/types.d.ts +159 -0
- package/dist/store/types.js +7 -0
- package/hooks/hook-registry.json +21 -0
- package/hooks/hooks.json +185 -0
- package/package.json +89 -0
- package/plugin.json +20 -0
- package/scripts/postinstall.js +826 -0
- package/skills/api-design/SKILL.md +262 -0
- package/skills/architecture-decision/SKILL.md +309 -0
- package/skills/ci-cd/SKILL.md +264 -0
- package/skills/code-review/SKILL.md +228 -0
- package/skills/compound/SKILL.md +101 -0
- package/skills/database/SKILL.md +257 -0
- package/skills/debug-detective/SKILL.md +95 -0
- package/skills/docker/SKILL.md +268 -0
- package/skills/documentation/SKILL.md +270 -0
- package/skills/ecomode/SKILL.md +46 -0
- package/skills/frontend/SKILL.md +265 -0
- package/skills/git-master/SKILL.md +86 -0
- package/skills/incident-response/SKILL.md +286 -0
- package/skills/migrate/SKILL.md +96 -0
- package/skills/performance/SKILL.md +282 -0
- package/skills/refactor/SKILL.md +100 -0
- package/skills/security-review/SKILL.md +282 -0
- package/skills/tdd/SKILL.md +178 -0
- package/skills/testing-strategy/SKILL.md +260 -0
- package/starter-pack/solutions/starter-api-error-responses.md +37 -0
- package/starter-pack/solutions/starter-async-patterns.md +40 -0
- package/starter-pack/solutions/starter-caching-strategy.md +40 -0
- package/starter-pack/solutions/starter-code-review-checklist.md +39 -0
- package/starter-pack/solutions/starter-debugging-systematic.md +40 -0
- package/starter-pack/solutions/starter-dependency-injection.md +40 -0
- package/starter-pack/solutions/starter-error-handling-patterns.md +38 -0
- package/starter-pack/solutions/starter-git-atomic-commits.md +36 -0
- package/starter-pack/solutions/starter-input-validation.md +40 -0
- package/starter-pack/solutions/starter-n-plus-one-queries.md +37 -0
- package/starter-pack/solutions/starter-refactor-safely.md +38 -0
- package/starter-pack/solutions/starter-secret-management.md +37 -0
- package/starter-pack/solutions/starter-separation-of-concerns.md +36 -0
- package/starter-pack/solutions/starter-tdd-red-green-refactor.md +40 -0
- package/starter-pack/solutions/starter-typescript-strict-types.md +39 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SolutionFrontmatter, SolutionStatus } from './solution-format.js';
|
|
2
|
+
export interface LifecycleResult {
|
|
3
|
+
promoted: string[];
|
|
4
|
+
demoted: string[];
|
|
5
|
+
retired: string[];
|
|
6
|
+
contradictions: string[];
|
|
7
|
+
}
|
|
8
|
+
/** Get the next promotion status */
|
|
9
|
+
export declare function nextStatus(current: SolutionStatus): SolutionStatus | null;
|
|
10
|
+
/** Get confidence for a status level.
|
|
11
|
+
* Spacing: 0.25 between levels for meaningful differentiation in matching scores.
|
|
12
|
+
* Previous: 0.3/0.6/0.8/0.85 had only 0.05 gap between verified and mature. */
|
|
13
|
+
export declare function statusConfidence(status: SolutionStatus): number;
|
|
14
|
+
/** Check promotion eligibility */
|
|
15
|
+
export declare function checkPromotion(fm: SolutionFrontmatter): boolean;
|
|
16
|
+
/** Check if solution should be demoted due to confidence-status mismatch */
|
|
17
|
+
export declare function checkConfidenceDemotion(fm: SolutionFrontmatter): SolutionStatus | null;
|
|
18
|
+
/** Check if solution identifiers still exist in codebase (staleness detection) */
|
|
19
|
+
export declare function checkIdentifierStaleness(fm: SolutionFrontmatter, cwd: string): boolean;
|
|
20
|
+
/** Check if solution is stale (status-specific inactivity threshold) */
|
|
21
|
+
export declare function isStale(fm: SolutionFrontmatter): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Update a solution file with new frontmatter.
|
|
24
|
+
* PR2b: solution-writer.mutateSolutionFile로 통합. lock + fresh re-read + atomic write.
|
|
25
|
+
*/
|
|
26
|
+
export declare function updateSolutionFile(filePath: string, updates: Partial<SolutionFrontmatter>): boolean;
|
|
27
|
+
/** Run lifecycle check on all solutions */
|
|
28
|
+
export declare function runLifecycleCheck(sessionId?: string): LifecycleResult;
|
|
29
|
+
/** Detect contradictions between solutions */
|
|
30
|
+
export declare function detectContradictions(dirs: string[]): string[];
|
|
31
|
+
/** Manual verify command: immediately promote to verified */
|
|
32
|
+
export declare function verifySolution(solutionName: string): boolean;
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { parseFrontmatterOnly } from './solution-format.js';
|
|
5
|
+
import { mutateSolutionFile } from './solution-writer.js';
|
|
6
|
+
import { createLogger } from '../core/logger.js';
|
|
7
|
+
const log = createLogger('compound-lifecycle');
|
|
8
|
+
import { ME_SOLUTIONS, ME_RULES } from '../core/paths.js';
|
|
9
|
+
/** Circuit breaker negative thresholds by status */
|
|
10
|
+
const CIRCUIT_BREAKER_THRESHOLDS = {
|
|
11
|
+
experiment: 2,
|
|
12
|
+
candidate: 3,
|
|
13
|
+
verified: 4,
|
|
14
|
+
};
|
|
15
|
+
/** Minimum age (ms) before promotion is allowed */
|
|
16
|
+
const MIN_AGE_FOR_PROMOTION = {
|
|
17
|
+
experiment: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
18
|
+
candidate: 14 * 24 * 60 * 60 * 1000, // 14 days
|
|
19
|
+
verified: 7 * 24 * 60 * 60 * 1000, // 7 days (prevents instant mature)
|
|
20
|
+
};
|
|
21
|
+
/** Confidence-status consistency thresholds */
|
|
22
|
+
const STATUS_CONFIDENCE_MIN = {
|
|
23
|
+
mature: 0.75,
|
|
24
|
+
verified: 0.5,
|
|
25
|
+
candidate: 0.2,
|
|
26
|
+
experiment: 0.05,
|
|
27
|
+
retired: 0,
|
|
28
|
+
};
|
|
29
|
+
/** Get the next promotion status */
|
|
30
|
+
export function nextStatus(current) {
|
|
31
|
+
switch (current) {
|
|
32
|
+
case 'experiment': return 'candidate';
|
|
33
|
+
case 'candidate': return 'verified';
|
|
34
|
+
case 'verified': return 'mature';
|
|
35
|
+
default: return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Get confidence for a status level.
|
|
39
|
+
* Spacing: 0.25 between levels for meaningful differentiation in matching scores.
|
|
40
|
+
* Previous: 0.3/0.6/0.8/0.85 had only 0.05 gap between verified and mature. */
|
|
41
|
+
export function statusConfidence(status) {
|
|
42
|
+
switch (status) {
|
|
43
|
+
case 'experiment': return 0.3;
|
|
44
|
+
case 'candidate': return 0.55;
|
|
45
|
+
case 'verified': return 0.75;
|
|
46
|
+
case 'mature': return 0.90;
|
|
47
|
+
case 'retired': return 0;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Check promotion eligibility */
|
|
51
|
+
export function checkPromotion(fm) {
|
|
52
|
+
const ev = fm.evidence;
|
|
53
|
+
switch (fm.status) {
|
|
54
|
+
case 'experiment':
|
|
55
|
+
// A: reflected >= 3 AND negative == 0 AND sessions >= 3 (Beta(4,1) → P(rate>0.5)=0.94)
|
|
56
|
+
// B: reExtracted >= 2 AND negative == 0 AND reflected >= 1 (prevents trivial re-extraction)
|
|
57
|
+
return (ev.negative === 0) && ((ev.reflected >= 3 && ev.sessions >= 3) ||
|
|
58
|
+
(ev.reExtracted >= 2 && ev.reflected >= 1));
|
|
59
|
+
case 'candidate':
|
|
60
|
+
// A: reflected >= 4 AND negative == 0 AND sessions >= 3
|
|
61
|
+
// B: reExtracted >= 2 AND negative == 0
|
|
62
|
+
return (ev.negative === 0) && ((ev.reflected >= 4 && ev.sessions >= 3) ||
|
|
63
|
+
(ev.reExtracted >= 2));
|
|
64
|
+
case 'verified':
|
|
65
|
+
// reflected >= 8, negative <= 1, sessions >= 5
|
|
66
|
+
return ev.reflected >= 8 && ev.negative <= 1 && ev.sessions >= 5;
|
|
67
|
+
default:
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Check if solution should be demoted due to confidence-status mismatch */
|
|
72
|
+
export function checkConfidenceDemotion(fm) {
|
|
73
|
+
if (fm.status === 'retired')
|
|
74
|
+
return null;
|
|
75
|
+
// Check from highest to lowest
|
|
76
|
+
if (fm.status === 'mature' && fm.confidence < STATUS_CONFIDENCE_MIN.mature)
|
|
77
|
+
return 'verified';
|
|
78
|
+
if (fm.status === 'verified' && fm.confidence < STATUS_CONFIDENCE_MIN.verified)
|
|
79
|
+
return 'candidate';
|
|
80
|
+
if (fm.status === 'candidate' && fm.confidence < STATUS_CONFIDENCE_MIN.candidate)
|
|
81
|
+
return 'experiment';
|
|
82
|
+
if (fm.status === 'experiment' && fm.confidence < STATUS_CONFIDENCE_MIN.experiment)
|
|
83
|
+
return 'retired';
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/** Check if solution identifiers still exist in codebase (staleness detection) */
|
|
87
|
+
export function checkIdentifierStaleness(fm, cwd) {
|
|
88
|
+
if (fm.identifiers.length === 0)
|
|
89
|
+
return false; // no identifiers to check
|
|
90
|
+
try {
|
|
91
|
+
const validIds = fm.identifiers.slice(0, 5).filter(id => id.length >= 6);
|
|
92
|
+
// All identifiers were too short — nothing to grep, treat as stale (matches original behavior)
|
|
93
|
+
if (validIds.length === 0)
|
|
94
|
+
return true;
|
|
95
|
+
// Escape regex metacharacters and join with OR for a single grep call
|
|
96
|
+
// (previously: one execFileSync per identifier — up to 15s worst case)
|
|
97
|
+
const pattern = validIds.map(id => id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
|
|
98
|
+
execFileSync('grep', [
|
|
99
|
+
'-r', '-E',
|
|
100
|
+
'--include=*.ts', '--include=*.tsx',
|
|
101
|
+
'--include=*.js', '--include=*.jsx',
|
|
102
|
+
'--exclude-dir=node_modules',
|
|
103
|
+
'--exclude-dir=dist',
|
|
104
|
+
'--exclude-dir=.git',
|
|
105
|
+
'-l', '-m', '1', pattern, '.',
|
|
106
|
+
], { cwd, encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
107
|
+
return false; // grep exit 0 = at least one identifier found = not stale
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
// grep exit 1 = no matches found = stale
|
|
111
|
+
// Other errors (timeout, ENOENT) = don't penalize — same as original outer catch behavior
|
|
112
|
+
const status = e.status;
|
|
113
|
+
return status === 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Status-specific staleness thresholds (days).
|
|
117
|
+
* experiment decays faster, mature gets longer grace period. */
|
|
118
|
+
const STALENESS_DAYS = {
|
|
119
|
+
experiment: 60,
|
|
120
|
+
candidate: 90,
|
|
121
|
+
verified: 120,
|
|
122
|
+
mature: 120,
|
|
123
|
+
};
|
|
124
|
+
/** Check if solution is stale (status-specific inactivity threshold) */
|
|
125
|
+
export function isStale(fm) {
|
|
126
|
+
if (fm.status === 'retired')
|
|
127
|
+
return false;
|
|
128
|
+
const staleDays = STALENESS_DAYS[fm.status] ?? 90;
|
|
129
|
+
const ninetyDaysMs = staleDays * 24 * 60 * 60 * 1000;
|
|
130
|
+
if (fm.evidence.injected === 0) {
|
|
131
|
+
// Never injected — check age
|
|
132
|
+
const created = new Date(fm.created).getTime();
|
|
133
|
+
const age = Date.now() - created;
|
|
134
|
+
return age > ninetyDaysMs;
|
|
135
|
+
}
|
|
136
|
+
// Has been injected — check last update
|
|
137
|
+
const updated = new Date(fm.updated).getTime();
|
|
138
|
+
const age = Date.now() - updated;
|
|
139
|
+
return age > ninetyDaysMs;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Update a solution file with new frontmatter.
|
|
143
|
+
* PR2b: solution-writer.mutateSolutionFile로 통합. lock + fresh re-read + atomic write.
|
|
144
|
+
*/
|
|
145
|
+
export function updateSolutionFile(filePath, updates) {
|
|
146
|
+
return mutateSolutionFile(filePath, sol => {
|
|
147
|
+
sol.frontmatter = {
|
|
148
|
+
...sol.frontmatter,
|
|
149
|
+
...updates,
|
|
150
|
+
};
|
|
151
|
+
return true;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/** Run lifecycle check on all solutions */
|
|
155
|
+
export function runLifecycleCheck(sessionId = 'system') {
|
|
156
|
+
const result = { promoted: [], demoted: [], retired: [], contradictions: [] };
|
|
157
|
+
const dirs = [ME_SOLUTIONS, ME_RULES];
|
|
158
|
+
for (const dir of dirs) {
|
|
159
|
+
if (!fs.existsSync(dir))
|
|
160
|
+
continue;
|
|
161
|
+
let files;
|
|
162
|
+
try {
|
|
163
|
+
files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
for (const file of files) {
|
|
169
|
+
const filePath = path.join(dir, file);
|
|
170
|
+
try {
|
|
171
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
172
|
+
const fm = parseFrontmatterOnly(content);
|
|
173
|
+
if (!fm || fm.status === 'retired')
|
|
174
|
+
continue;
|
|
175
|
+
// 1. Check stale
|
|
176
|
+
if (isStale(fm)) {
|
|
177
|
+
if (updateSolutionFile(filePath, { status: 'retired', confidence: 0 })) {
|
|
178
|
+
result.retired.push(fm.name);
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
// 2. Check confidence-status consistency
|
|
183
|
+
const demoteTo = checkConfidenceDemotion(fm);
|
|
184
|
+
if (demoteTo) {
|
|
185
|
+
if (updateSolutionFile(filePath, { status: demoteTo, confidence: statusConfidence(demoteTo) })) {
|
|
186
|
+
result.demoted.push(`${fm.name}: ${fm.status} → ${demoteTo}`);
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
// 3. Circuit breaker BEFORE promotion — negative evidence takes priority
|
|
191
|
+
const cbThreshold = CIRCUIT_BREAKER_THRESHOLDS[fm.status];
|
|
192
|
+
if (cbThreshold !== undefined && fm.evidence.negative >= cbThreshold) {
|
|
193
|
+
if (updateSolutionFile(filePath, { status: 'retired', confidence: 0 })) {
|
|
194
|
+
result.retired.push(`${fm.name} (circuit-breaker:${fm.status})`);
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// 4. Check promotion FIRST (with minimum age gate based on updated timestamp)
|
|
199
|
+
// Promotion must run before identifier staleness to give solutions a chance
|
|
200
|
+
// to be promoted before being penalized for stale identifiers.
|
|
201
|
+
if (checkPromotion(fm)) {
|
|
202
|
+
const minAgeMs = MIN_AGE_FOR_PROMOTION[fm.status] ?? 0;
|
|
203
|
+
const ageMs = Date.now() - new Date(fm.updated || fm.created).getTime();
|
|
204
|
+
if (ageMs >= minAgeMs) {
|
|
205
|
+
const next = nextStatus(fm.status);
|
|
206
|
+
if (next) {
|
|
207
|
+
if (updateSolutionFile(filePath, { status: next, confidence: statusConfidence(next) })) {
|
|
208
|
+
result.promoted.push(`${fm.name}: ${fm.status} → ${next}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
// 5. Identifier staleness — code references no longer exist
|
|
215
|
+
if (fm.identifiers.length > 0) {
|
|
216
|
+
const effectiveCwd = process.env.FORGEN_CWD ?? process.env.COMPOUND_CWD ?? process.cwd();
|
|
217
|
+
if (checkIdentifierStaleness(fm, effectiveCwd)) {
|
|
218
|
+
const newConf = Math.max(0, fm.confidence - 0.20);
|
|
219
|
+
if (updateSolutionFile(filePath, { confidence: newConf })) {
|
|
220
|
+
result.demoted.push(`${fm.name}: identifier-stale (confidence → ${newConf})`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch (e) {
|
|
226
|
+
log.debug(`lifecycle check failed: ${file}`, e);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// 5. Contradiction detection
|
|
231
|
+
result.contradictions = detectContradictions(dirs);
|
|
232
|
+
return result;
|
|
233
|
+
}
|
|
234
|
+
/** Detect contradictions between solutions */
|
|
235
|
+
export function detectContradictions(dirs) {
|
|
236
|
+
const contradictions = [];
|
|
237
|
+
const solutions = [];
|
|
238
|
+
for (const dir of dirs) {
|
|
239
|
+
if (!fs.existsSync(dir))
|
|
240
|
+
continue;
|
|
241
|
+
try {
|
|
242
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
243
|
+
for (const file of files) {
|
|
244
|
+
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
245
|
+
const fm = parseFrontmatterOnly(content);
|
|
246
|
+
if (!fm || fm.status === 'retired')
|
|
247
|
+
continue;
|
|
248
|
+
solutions.push({ name: fm.name, tags: fm.tags, identifiers: fm.identifiers });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch { /* 솔루션 파일 파싱 실패 무시 — 중복 감지는 best-effort */ }
|
|
252
|
+
}
|
|
253
|
+
// Pre-build tag Sets for O(1) lookup — avoids O(m²) per pair
|
|
254
|
+
const tagSets = solutions.map(s => new Set(s.tags));
|
|
255
|
+
// Pairwise comparison
|
|
256
|
+
for (let i = 0; i < solutions.length; i++) {
|
|
257
|
+
for (let j = i + 1; j < solutions.length; j++) {
|
|
258
|
+
const a = solutions[i];
|
|
259
|
+
const b = solutions[j];
|
|
260
|
+
// Tags overlap > 70%
|
|
261
|
+
const overlap = a.tags.filter(t => tagSets[j].has(t));
|
|
262
|
+
const overlapRatio = overlap.length / Math.max(a.tags.length, b.tags.length, 1);
|
|
263
|
+
if (overlapRatio < 0.7)
|
|
264
|
+
continue;
|
|
265
|
+
// Identifiers completely different
|
|
266
|
+
const idOverlap = a.identifiers.filter(id => b.identifiers.includes(id));
|
|
267
|
+
if (idOverlap.length === 0 && a.identifiers.length > 0 && b.identifiers.length > 0) {
|
|
268
|
+
contradictions.push(`${a.name} vs ${b.name} (tags ${(overlapRatio * 100).toFixed(0)}% overlap, identifiers disjoint)`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return contradictions;
|
|
273
|
+
}
|
|
274
|
+
/** Manual verify command: immediately promote to verified */
|
|
275
|
+
export function verifySolution(solutionName) {
|
|
276
|
+
const dirs = [ME_SOLUTIONS, ME_RULES];
|
|
277
|
+
for (const dir of dirs) {
|
|
278
|
+
if (!fs.existsSync(dir))
|
|
279
|
+
continue;
|
|
280
|
+
try {
|
|
281
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
282
|
+
for (const file of files) {
|
|
283
|
+
const filePath = path.join(dir, file);
|
|
284
|
+
// PR2c-4 (security L-1): symlink을 통한 임의 파일 read 차단.
|
|
285
|
+
// mutateSolutionByName과 일관성 유지.
|
|
286
|
+
try {
|
|
287
|
+
if (fs.lstatSync(filePath).isSymbolicLink())
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
294
|
+
const fm = parseFrontmatterOnly(content);
|
|
295
|
+
if (!fm || fm.name !== solutionName)
|
|
296
|
+
continue;
|
|
297
|
+
if (fm.status === 'verified' || fm.status === 'mature')
|
|
298
|
+
return true; // already verified
|
|
299
|
+
return updateSolutionFile(filePath, { status: 'verified', confidence: 0.8 });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch { /* 솔루션 파일 읽기/업데이트 실패 무시 — false 반환으로 재시도 가능 */ }
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface CompoundInsight {
|
|
2
|
+
id: string;
|
|
3
|
+
type: 'solution' | 'rule' | 'convention' | 'pattern';
|
|
4
|
+
title: string;
|
|
5
|
+
content: string;
|
|
6
|
+
scope: 'me' | 'team';
|
|
7
|
+
/** 자동 분류: personal (개인 스타일) vs team (공통 패턴) */
|
|
8
|
+
classification: 'personal' | 'team';
|
|
9
|
+
/** 분류 근거 */
|
|
10
|
+
reason: string;
|
|
11
|
+
source: 'session' | 'manual';
|
|
12
|
+
}
|
|
13
|
+
/** 키워드 기반으로 인사이트를 개인/팀으로 자동 분류 */
|
|
14
|
+
export declare function classifyInsight(title: string, content: string): {
|
|
15
|
+
classification: 'personal' | 'team';
|
|
16
|
+
reason: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Compound Loop — 이미 추출된 인사이트를 저장
|
|
20
|
+
*/
|
|
21
|
+
export declare function runCompoundLoop(cwd: string, insights: CompoundInsight[]): Promise<{
|
|
22
|
+
saved: string[];
|
|
23
|
+
skipped: string[];
|
|
24
|
+
}>;
|
|
25
|
+
/** 팀 제안으로 저장 (.compound/proposals/) */
|
|
26
|
+
export declare function saveTeamProposals(insights: CompoundInsight[], cwd: string): void;
|
|
27
|
+
/** .compound/proposals/ 에서 제안 파일 로드 */
|
|
28
|
+
export declare function loadProposals(proposalsDir: string): CompoundInsight[];
|
|
29
|
+
/** 제안 파일 정리 */
|
|
30
|
+
export declare function cleanProposals(proposalsDir: string): void;
|
|
31
|
+
/** CLI 핸들러: forgen compound */
|
|
32
|
+
export declare function handleCompound(args: string[]): Promise<void>;
|