@wooojin/forgen 0.2.0 → 0.3.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/CHANGELOG.md +72 -0
- package/README.ja.md +79 -14
- package/README.ko.md +100 -14
- package/README.md +124 -17
- package/README.zh.md +79 -14
- package/agents/analyst.md +48 -4
- package/agents/architect.md +39 -4
- package/agents/code-reviewer.md +107 -77
- package/agents/critic.md +47 -4
- package/agents/debugger.md +46 -4
- package/agents/designer.md +40 -4
- package/agents/executor.md +112 -30
- package/agents/explore.md +45 -5
- package/agents/git-master.md +48 -4
- package/agents/planner.md +121 -18
- package/agents/test-engineer.md +58 -4
- package/agents/verifier.md +92 -77
- package/commands/architecture-decision.md +127 -258
- package/commands/calibrate.md +225 -0
- package/commands/code-review.md +163 -178
- package/commands/compound.md +127 -68
- package/commands/deep-interview.md +273 -0
- package/commands/docker.md +68 -178
- package/commands/forge-loop.md +215 -0
- package/commands/learn.md +231 -0
- package/commands/retro.md +215 -0
- package/commands/ship.md +277 -0
- package/dist/cli.js +26 -9
- package/dist/core/auto-compound-runner.js +14 -0
- package/dist/core/config-injector.d.ts +2 -1
- package/dist/core/config-injector.js +2 -1
- package/dist/core/dashboard.d.ts +108 -0
- package/dist/core/dashboard.js +495 -0
- package/dist/core/doctor.js +151 -21
- package/dist/core/drift-score.d.ts +49 -0
- package/dist/core/drift-score.js +87 -0
- package/dist/core/harness.d.ts +6 -1
- package/dist/core/harness.js +75 -19
- package/dist/core/mcp-config.d.ts +2 -0
- package/dist/core/mcp-config.js +6 -1
- package/dist/core/paths.d.ts +6 -1
- package/dist/core/paths.js +18 -2
- package/dist/core/spawn.d.ts +3 -2
- package/dist/core/spawn.js +27 -8
- package/dist/core/types.d.ts +34 -0
- package/dist/engine/compound-export.d.ts +41 -0
- package/dist/engine/compound-export.js +169 -0
- package/dist/engine/compound-lifecycle.d.ts +4 -3
- package/dist/engine/compound-lifecycle.js +91 -46
- package/dist/engine/compound-loop.js +18 -0
- package/dist/engine/meta-learning/adaptive-thresholds.d.ts +20 -0
- package/dist/engine/meta-learning/adaptive-thresholds.js +126 -0
- package/dist/engine/meta-learning/extraction-tuner.d.ts +15 -0
- package/dist/engine/meta-learning/extraction-tuner.js +99 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.d.ts +21 -0
- package/dist/engine/meta-learning/matcher-weight-tuner.js +151 -0
- package/dist/engine/meta-learning/runner.d.ts +14 -0
- package/dist/engine/meta-learning/runner.js +90 -0
- package/dist/engine/meta-learning/scope-promoter.d.ts +21 -0
- package/dist/engine/meta-learning/scope-promoter.js +84 -0
- package/dist/engine/meta-learning/session-quality-scorer.d.ts +61 -0
- package/dist/engine/meta-learning/session-quality-scorer.js +166 -0
- package/dist/engine/meta-learning/types.d.ts +114 -0
- package/dist/engine/meta-learning/types.js +43 -0
- package/dist/engine/solution-format.d.ts +2 -2
- package/dist/engine/solution-format.js +249 -34
- package/dist/engine/solution-index.d.ts +1 -1
- package/dist/engine/solution-matcher.d.ts +30 -1
- package/dist/engine/solution-matcher.js +235 -45
- package/dist/fgx.js +12 -8
- package/dist/hooks/context-guard.d.ts +15 -0
- package/dist/hooks/context-guard.js +218 -56
- package/dist/hooks/db-guard.js +2 -2
- package/dist/hooks/hook-config.d.ts +27 -1
- package/dist/hooks/hook-config.js +72 -12
- package/dist/hooks/hooks-generator.d.ts +3 -0
- package/dist/hooks/hooks-generator.js +23 -6
- package/dist/hooks/intent-classifier.d.ts +0 -2
- package/dist/hooks/intent-classifier.js +32 -18
- package/dist/hooks/keyword-detector.js +126 -204
- package/dist/hooks/notepad-injector.js +2 -2
- package/dist/hooks/permission-handler.js +2 -2
- package/dist/hooks/post-tool-failure.js +12 -6
- package/dist/hooks/post-tool-handlers.d.ts +1 -1
- package/dist/hooks/post-tool-handlers.js +14 -11
- package/dist/hooks/post-tool-use.d.ts +11 -0
- package/dist/hooks/post-tool-use.js +184 -71
- package/dist/hooks/pre-compact.d.ts +11 -1
- package/dist/hooks/pre-compact.js +112 -37
- package/dist/hooks/pre-tool-use.js +86 -56
- package/dist/hooks/rate-limiter.js +3 -3
- package/dist/hooks/secret-filter.js +2 -2
- package/dist/hooks/session-recovery.js +256 -236
- package/dist/hooks/shared/hook-response.d.ts +4 -4
- package/dist/hooks/shared/hook-response.js +13 -24
- package/dist/hooks/shared/hook-timing.d.ts +15 -0
- package/dist/hooks/shared/hook-timing.js +64 -0
- package/dist/hooks/skill-injector.d.ts +4 -3
- package/dist/hooks/skill-injector.js +47 -16
- package/dist/hooks/slop-detector.js +3 -3
- package/dist/hooks/solution-injector.js +224 -197
- package/dist/hooks/subagent-tracker.js +2 -2
- package/dist/host/codex-adapter.d.ts +10 -0
- package/dist/host/codex-adapter.js +154 -0
- package/dist/mcp/solution-reader.d.ts +5 -5
- package/dist/mcp/solution-reader.js +34 -24
- package/dist/renderer/rule-renderer.js +9 -11
- package/dist/services/session.d.ts +19 -0
- package/dist/services/session.js +62 -0
- package/hooks/hooks.json +2 -2
- package/package.json +2 -1
- package/skills/architecture-decision/SKILL.md +113 -257
- package/skills/calibrate/SKILL.md +207 -0
- package/skills/code-review/SKILL.md +151 -178
- package/skills/compound/SKILL.md +126 -68
- package/skills/deep-interview/SKILL.md +266 -0
- package/skills/docker/SKILL.md +57 -179
- package/skills/forge-loop/SKILL.md +198 -0
- package/skills/learn/SKILL.md +216 -0
- package/skills/retro/SKILL.md +199 -0
- package/skills/ship/SKILL.md +259 -0
- package/agents/code-simplifier.md +0 -197
- package/agents/performance-reviewer.md +0 -172
- package/agents/qa-tester.md +0 -158
- package/agents/refactoring-expert.md +0 -168
- package/agents/scientist.md +0 -144
- package/agents/security-reviewer.md +0 -137
- package/agents/writer.md +0 -184
- package/commands/api-design.md +0 -268
- package/commands/ci-cd.md +0 -270
- package/commands/database.md +0 -263
- package/commands/debug-detective.md +0 -99
- package/commands/documentation.md +0 -276
- package/commands/ecomode.md +0 -51
- package/commands/frontend.md +0 -271
- package/commands/git-master.md +0 -90
- package/commands/incident-response.md +0 -292
- package/commands/migrate.md +0 -101
- package/commands/performance.md +0 -288
- package/commands/refactor.md +0 -105
- package/commands/security-review.md +0 -288
- package/commands/tdd.md +0 -183
- package/commands/testing-strategy.md +0 -265
- package/skills/api-design/SKILL.md +0 -262
- package/skills/ci-cd/SKILL.md +0 -264
- package/skills/database/SKILL.md +0 -257
- package/skills/debug-detective/SKILL.md +0 -95
- package/skills/documentation/SKILL.md +0 -270
- package/skills/ecomode/SKILL.md +0 -46
- package/skills/frontend/SKILL.md +0 -265
- package/skills/git-master/SKILL.md +0 -86
- package/skills/incident-response/SKILL.md +0 -286
- package/skills/migrate/SKILL.md +0 -96
- package/skills/performance/SKILL.md +0 -282
- package/skills/refactor/SKILL.md +0 -100
- package/skills/security-review/SKILL.md +0 -282
- package/skills/tdd/SKILL.md +0 -178
- package/skills/testing-strategy/SKILL.md +0 -260
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen Meta-Learning — Adaptive Thresholds (Feature 4)
|
|
3
|
+
*
|
|
4
|
+
* Computes learning velocity and adapts promotion thresholds
|
|
5
|
+
* based on the user's solution accumulation rate.
|
|
6
|
+
*
|
|
7
|
+
* Learning velocity = solutions created in last 30 days / 4.3 weeks
|
|
8
|
+
*
|
|
9
|
+
* > 3/week (high volume) → thresholds +1 (more evidence needed)
|
|
10
|
+
* 0.5~3/week (normal) → no change
|
|
11
|
+
* < 0.5/week (slow pace) → thresholds -1 (lower the bar)
|
|
12
|
+
*
|
|
13
|
+
* Guardrails: [thresholdFloor, thresholdCeiling], max ±1 per cycle.
|
|
14
|
+
*/
|
|
15
|
+
import type { AdaptiveLifecycleThresholds, MetaLearningConfig } from './types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Compute adaptive promotion thresholds based on learning velocity.
|
|
18
|
+
* Returns null if cold-start conditions are not met.
|
|
19
|
+
*/
|
|
20
|
+
export declare function computeAdaptiveThresholds(config: MetaLearningConfig): AdaptiveLifecycleThresholds | null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen Meta-Learning — Adaptive Thresholds (Feature 4)
|
|
3
|
+
*
|
|
4
|
+
* Computes learning velocity and adapts promotion thresholds
|
|
5
|
+
* based on the user's solution accumulation rate.
|
|
6
|
+
*
|
|
7
|
+
* Learning velocity = solutions created in last 30 days / 4.3 weeks
|
|
8
|
+
*
|
|
9
|
+
* > 3/week (high volume) → thresholds +1 (more evidence needed)
|
|
10
|
+
* 0.5~3/week (normal) → no change
|
|
11
|
+
* < 0.5/week (slow pace) → thresholds -1 (lower the bar)
|
|
12
|
+
*
|
|
13
|
+
* Guardrails: [thresholdFloor, thresholdCeiling], max ±1 per cycle.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'node:fs';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { ME_SOLUTIONS, META_LEARNING_DIR } from '../../core/paths.js';
|
|
18
|
+
import { atomicWriteJSON, safeReadJSON } from '../../hooks/shared/atomic-write.js';
|
|
19
|
+
import { parseFrontmatterOnly } from '../solution-format.js';
|
|
20
|
+
import { DEFAULT_PROMOTION_THRESHOLDS } from './types.js';
|
|
21
|
+
const THRESHOLDS_PATH = path.join(META_LEARNING_DIR, 'lifecycle-thresholds.json');
|
|
22
|
+
const VELOCITY_WINDOW_DAYS = 30;
|
|
23
|
+
const WEEKS_IN_WINDOW = VELOCITY_WINDOW_DAYS / 7;
|
|
24
|
+
function loadCurrentThresholds() {
|
|
25
|
+
return safeReadJSON(THRESHOLDS_PATH, null);
|
|
26
|
+
}
|
|
27
|
+
function saveThresholds(t) {
|
|
28
|
+
atomicWriteJSON(THRESHOLDS_PATH, t, { pretty: true });
|
|
29
|
+
}
|
|
30
|
+
function clampThreshold(value, floor, ceiling) {
|
|
31
|
+
return Math.max(floor, Math.min(ceiling, Math.round(value)));
|
|
32
|
+
}
|
|
33
|
+
function computeLearningVelocity() {
|
|
34
|
+
try {
|
|
35
|
+
if (!fs.existsSync(ME_SOLUTIONS))
|
|
36
|
+
return { velocity: 0, totalSolutions: 0 };
|
|
37
|
+
const files = fs.readdirSync(ME_SOLUTIONS).filter((f) => f.endsWith('.md'));
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
const windowMs = VELOCITY_WINDOW_DAYS * 24 * 60 * 60 * 1000;
|
|
40
|
+
let recentCount = 0;
|
|
41
|
+
let totalSolutions = 0;
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(path.join(ME_SOLUTIONS, file), 'utf-8');
|
|
45
|
+
const fm = parseFrontmatterOnly(content);
|
|
46
|
+
if (!fm || fm.status === 'retired')
|
|
47
|
+
continue;
|
|
48
|
+
totalSolutions++;
|
|
49
|
+
const created = fm.created ? new Date(fm.created).getTime() : 0;
|
|
50
|
+
if (created > 0 && now - created <= windowMs) {
|
|
51
|
+
recentCount++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
velocity: recentCount / WEEKS_IN_WINDOW,
|
|
58
|
+
totalSolutions,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { velocity: 0, totalSolutions: 0 };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Compute adaptive promotion thresholds based on learning velocity.
|
|
67
|
+
* Returns null if cold-start conditions are not met.
|
|
68
|
+
*/
|
|
69
|
+
export function computeAdaptiveThresholds(config) {
|
|
70
|
+
const { velocity, totalSolutions } = computeLearningVelocity();
|
|
71
|
+
// Cold-start check
|
|
72
|
+
if (totalSolutions < config.coldStart.minSolutionsForThresholds) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const current = loadCurrentThresholds();
|
|
76
|
+
const base = current ?? {
|
|
77
|
+
experiment: { ...DEFAULT_PROMOTION_THRESHOLDS.experiment },
|
|
78
|
+
candidate: { ...DEFAULT_PROMOTION_THRESHOLDS.candidate },
|
|
79
|
+
verified: { ...DEFAULT_PROMOTION_THRESHOLDS.verified },
|
|
80
|
+
learningVelocity: velocity,
|
|
81
|
+
updatedAt: new Date().toISOString(),
|
|
82
|
+
sampleSize: totalSolutions,
|
|
83
|
+
defaults: { ...DEFAULT_PROMOTION_THRESHOLDS },
|
|
84
|
+
};
|
|
85
|
+
// Determine adjustment direction
|
|
86
|
+
const { maxThresholdDelta, thresholdFloor, thresholdCeiling } = config.guardrails;
|
|
87
|
+
let delta = 0;
|
|
88
|
+
if (velocity > 3) {
|
|
89
|
+
delta = maxThresholdDelta; // high volume → stricter
|
|
90
|
+
}
|
|
91
|
+
else if (velocity < 0.5) {
|
|
92
|
+
delta = -maxThresholdDelta; // slow pace → more lenient
|
|
93
|
+
}
|
|
94
|
+
if (delta === 0 && current) {
|
|
95
|
+
// No change needed, but update metadata
|
|
96
|
+
current.learningVelocity = velocity;
|
|
97
|
+
current.sampleSize = totalSolutions;
|
|
98
|
+
current.updatedAt = new Date().toISOString();
|
|
99
|
+
saveThresholds(current);
|
|
100
|
+
return current;
|
|
101
|
+
}
|
|
102
|
+
const result = {
|
|
103
|
+
experiment: {
|
|
104
|
+
reflected: clampThreshold(base.experiment.reflected + delta, thresholdFloor, thresholdCeiling),
|
|
105
|
+
sessions: clampThreshold(base.experiment.sessions + delta, thresholdFloor, thresholdCeiling),
|
|
106
|
+
reExtracted: clampThreshold(base.experiment.reExtracted + delta, thresholdFloor, thresholdCeiling),
|
|
107
|
+
},
|
|
108
|
+
candidate: {
|
|
109
|
+
reflected: clampThreshold(base.candidate.reflected + delta, thresholdFloor, thresholdCeiling),
|
|
110
|
+
sessions: clampThreshold(base.candidate.sessions + delta, thresholdFloor, thresholdCeiling),
|
|
111
|
+
reExtracted: clampThreshold(base.candidate.reExtracted + delta, thresholdFloor, thresholdCeiling),
|
|
112
|
+
},
|
|
113
|
+
verified: {
|
|
114
|
+
reflected: clampThreshold(base.verified.reflected + delta, thresholdFloor, thresholdCeiling),
|
|
115
|
+
sessions: clampThreshold(base.verified.sessions + delta, thresholdFloor, thresholdCeiling),
|
|
116
|
+
reExtracted: clampThreshold(base.verified.reExtracted + delta, thresholdFloor, thresholdCeiling),
|
|
117
|
+
negative: base.verified.negative, // negative threshold does not adapt
|
|
118
|
+
},
|
|
119
|
+
learningVelocity: velocity,
|
|
120
|
+
updatedAt: new Date().toISOString(),
|
|
121
|
+
sampleSize: totalSolutions,
|
|
122
|
+
defaults: { ...DEFAULT_PROMOTION_THRESHOLDS },
|
|
123
|
+
};
|
|
124
|
+
saveThresholds(result);
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen Meta-Learning — Extraction Tuner (Feature 5)
|
|
3
|
+
*
|
|
4
|
+
* Tracks which solution types (pattern, solution, decision, etc.) have the
|
|
5
|
+
* best reflected/injected ratio and biases future extraction toward those types.
|
|
6
|
+
*
|
|
7
|
+
* Uses Laplace smoothing (pseudo-count +1) to prevent zero weights
|
|
8
|
+
* for underrepresented types.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtractionBias, MetaLearningConfig } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Compute extraction bias based on type effectiveness.
|
|
13
|
+
* Returns null if cold-start conditions are not met.
|
|
14
|
+
*/
|
|
15
|
+
export declare function computeExtractionBias(config: MetaLearningConfig): ExtractionBias | null;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen Meta-Learning — Extraction Tuner (Feature 5)
|
|
3
|
+
*
|
|
4
|
+
* Tracks which solution types (pattern, solution, decision, etc.) have the
|
|
5
|
+
* best reflected/injected ratio and biases future extraction toward those types.
|
|
6
|
+
*
|
|
7
|
+
* Uses Laplace smoothing (pseudo-count +1) to prevent zero weights
|
|
8
|
+
* for underrepresented types.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { ME_SOLUTIONS, META_LEARNING_DIR } from '../../core/paths.js';
|
|
13
|
+
import { atomicWriteJSON } from '../../hooks/shared/atomic-write.js';
|
|
14
|
+
import { parseFrontmatterOnly } from '../solution-format.js';
|
|
15
|
+
const BIAS_PATH = path.join(META_LEARNING_DIR, 'extraction-bias.json');
|
|
16
|
+
const ALL_TYPES = [
|
|
17
|
+
'pattern',
|
|
18
|
+
'solution',
|
|
19
|
+
'decision',
|
|
20
|
+
'troubleshoot',
|
|
21
|
+
'anti-pattern',
|
|
22
|
+
'convention',
|
|
23
|
+
];
|
|
24
|
+
function computeTypeEffectiveness() {
|
|
25
|
+
const stats = {};
|
|
26
|
+
for (const t of ALL_TYPES) {
|
|
27
|
+
stats[t] = { injected: 0, reflected: 0, ratio: 0 };
|
|
28
|
+
}
|
|
29
|
+
let totalSolutions = 0;
|
|
30
|
+
const typesWithData = new Set();
|
|
31
|
+
try {
|
|
32
|
+
if (!fs.existsSync(ME_SOLUTIONS))
|
|
33
|
+
return { stats, totalSolutions: 0, typeCount: 0 };
|
|
34
|
+
const files = fs.readdirSync(ME_SOLUTIONS).filter((f) => f.endsWith('.md'));
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
try {
|
|
37
|
+
const content = fs.readFileSync(path.join(ME_SOLUTIONS, file), 'utf-8');
|
|
38
|
+
const fm = parseFrontmatterOnly(content);
|
|
39
|
+
if (!fm || fm.status === 'retired')
|
|
40
|
+
continue;
|
|
41
|
+
totalSolutions++;
|
|
42
|
+
const type = fm.type;
|
|
43
|
+
if (!stats[type])
|
|
44
|
+
stats[type] = { injected: 0, reflected: 0, ratio: 0 };
|
|
45
|
+
stats[type].injected += fm.evidence.injected;
|
|
46
|
+
stats[type].reflected += fm.evidence.reflected;
|
|
47
|
+
if (fm.evidence.injected > 0)
|
|
48
|
+
typesWithData.add(type);
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
/* empty */
|
|
55
|
+
}
|
|
56
|
+
// Compute ratios
|
|
57
|
+
for (const type of Object.keys(stats)) {
|
|
58
|
+
stats[type].ratio = stats[type].injected > 0 ? stats[type].reflected / stats[type].injected : 0;
|
|
59
|
+
}
|
|
60
|
+
return { stats, totalSolutions, typeCount: typesWithData.size };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Compute extraction bias based on type effectiveness.
|
|
64
|
+
* Returns null if cold-start conditions are not met.
|
|
65
|
+
*/
|
|
66
|
+
export function computeExtractionBias(config) {
|
|
67
|
+
const { stats, totalSolutions, typeCount } = computeTypeEffectiveness();
|
|
68
|
+
// Cold-start check
|
|
69
|
+
if (totalSolutions < config.coldStart.minSolutionsForExtraction || typeCount < 3) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// Laplace-smoothed weights: ratio + 1 pseudo-count per type
|
|
73
|
+
const rawWeights = {};
|
|
74
|
+
let sum = 0;
|
|
75
|
+
for (const type of ALL_TYPES) {
|
|
76
|
+
const w = stats[type].ratio + 1 / ALL_TYPES.length; // Laplace smoothing
|
|
77
|
+
rawWeights[type] = w;
|
|
78
|
+
sum += w;
|
|
79
|
+
}
|
|
80
|
+
// Normalize to sum = 1.0, cap individual type at 0.5
|
|
81
|
+
const typeWeights = {};
|
|
82
|
+
for (const type of ALL_TYPES) {
|
|
83
|
+
typeWeights[type] = Math.min(0.5, Math.round((rawWeights[type] / sum) * 1000) / 1000);
|
|
84
|
+
}
|
|
85
|
+
// Re-normalize after capping
|
|
86
|
+
const cappedSum = Object.values(typeWeights).reduce((s, v) => s + v, 0);
|
|
87
|
+
if (cappedSum > 0) {
|
|
88
|
+
for (const type of ALL_TYPES) {
|
|
89
|
+
typeWeights[type] = Math.round((typeWeights[type] / cappedSum) * 1000) / 1000;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const result = {
|
|
93
|
+
typeWeights,
|
|
94
|
+
updatedAt: new Date().toISOString(),
|
|
95
|
+
sampleSize: totalSolutions,
|
|
96
|
+
};
|
|
97
|
+
atomicWriteJSON(BIAS_PATH, result, { pretty: true });
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen Meta-Learning — Matcher Weight Tuner (Feature 2)
|
|
3
|
+
*
|
|
4
|
+
* Analyzes which scoring component (TF-IDF, BM25, Bigram) best discriminates
|
|
5
|
+
* reflected vs. non-reflected solutions and adjusts ensemble weights.
|
|
6
|
+
*
|
|
7
|
+
* Algorithm:
|
|
8
|
+
* 1. Load all non-retired solutions with evidence.injected > 0
|
|
9
|
+
* 2. Partition into "effective" (reflected/injected > median) vs "ineffective"
|
|
10
|
+
* 3. For each component: compute discrimination ratio (effective_mean / ineffective_mean)
|
|
11
|
+
* 4. Shift weights toward the component with highest discrimination
|
|
12
|
+
* 5. Apply guardrails: clamp [floor, ceiling], max delta per cycle, normalize to 1.0
|
|
13
|
+
*
|
|
14
|
+
* Cold-start: requires 10+ solutions with injected > 0, 3+ with reflected > 0.
|
|
15
|
+
*/
|
|
16
|
+
import type { MatcherWeights, MetaLearningConfig } from './types.js';
|
|
17
|
+
/**
|
|
18
|
+
* Tune matcher ensemble weights based on solution effectiveness data.
|
|
19
|
+
* Returns null if cold-start conditions are not met.
|
|
20
|
+
*/
|
|
21
|
+
export declare function tuneMatcherWeights(config: MetaLearningConfig): MatcherWeights | null;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen Meta-Learning — Matcher Weight Tuner (Feature 2)
|
|
3
|
+
*
|
|
4
|
+
* Analyzes which scoring component (TF-IDF, BM25, Bigram) best discriminates
|
|
5
|
+
* reflected vs. non-reflected solutions and adjusts ensemble weights.
|
|
6
|
+
*
|
|
7
|
+
* Algorithm:
|
|
8
|
+
* 1. Load all non-retired solutions with evidence.injected > 0
|
|
9
|
+
* 2. Partition into "effective" (reflected/injected > median) vs "ineffective"
|
|
10
|
+
* 3. For each component: compute discrimination ratio (effective_mean / ineffective_mean)
|
|
11
|
+
* 4. Shift weights toward the component with highest discrimination
|
|
12
|
+
* 5. Apply guardrails: clamp [floor, ceiling], max delta per cycle, normalize to 1.0
|
|
13
|
+
*
|
|
14
|
+
* Cold-start: requires 10+ solutions with injected > 0, 3+ with reflected > 0.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as path from 'node:path';
|
|
18
|
+
import { ME_SOLUTIONS, META_LEARNING_DIR } from '../../core/paths.js';
|
|
19
|
+
import { atomicWriteJSON, safeReadJSON } from '../../hooks/shared/atomic-write.js';
|
|
20
|
+
import { parseFrontmatterOnly } from '../solution-format.js';
|
|
21
|
+
import { DEFAULT_MATCHER_WEIGHTS } from './types.js';
|
|
22
|
+
const WEIGHTS_PATH = path.join(META_LEARNING_DIR, 'matcher-weights.json');
|
|
23
|
+
function loadSolutionEffectivenessData() {
|
|
24
|
+
try {
|
|
25
|
+
if (!fs.existsSync(ME_SOLUTIONS))
|
|
26
|
+
return [];
|
|
27
|
+
const files = fs.readdirSync(ME_SOLUTIONS).filter((f) => f.endsWith('.md'));
|
|
28
|
+
const data = [];
|
|
29
|
+
for (const file of files) {
|
|
30
|
+
try {
|
|
31
|
+
const content = fs.readFileSync(path.join(ME_SOLUTIONS, file), 'utf-8');
|
|
32
|
+
const fm = parseFrontmatterOnly(content);
|
|
33
|
+
if (!fm || fm.status === 'retired')
|
|
34
|
+
continue;
|
|
35
|
+
if (!fm.evidence || fm.evidence.injected <= 0)
|
|
36
|
+
continue;
|
|
37
|
+
data.push({
|
|
38
|
+
name: fm.name,
|
|
39
|
+
injected: fm.evidence.injected,
|
|
40
|
+
reflected: fm.evidence.reflected,
|
|
41
|
+
ratio: fm.evidence.reflected / fm.evidence.injected,
|
|
42
|
+
tags: fm.tags ?? [],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch { }
|
|
46
|
+
}
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function loadCurrentWeights() {
|
|
54
|
+
return safeReadJSON(WEIGHTS_PATH, null);
|
|
55
|
+
}
|
|
56
|
+
function saveWeights(weights) {
|
|
57
|
+
atomicWriteJSON(WEIGHTS_PATH, weights, { pretty: true });
|
|
58
|
+
}
|
|
59
|
+
function median(values) {
|
|
60
|
+
if (values.length === 0)
|
|
61
|
+
return 0;
|
|
62
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
63
|
+
const mid = Math.floor(sorted.length / 2);
|
|
64
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
65
|
+
}
|
|
66
|
+
function clampWeight(value, floor, ceiling) {
|
|
67
|
+
return Math.max(floor, Math.min(ceiling, value));
|
|
68
|
+
}
|
|
69
|
+
function normalizeWeights(w) {
|
|
70
|
+
const sum = w.tfidf + w.bm25 + w.bigram;
|
|
71
|
+
if (sum <= 0)
|
|
72
|
+
return { ...DEFAULT_MATCHER_WEIGHTS };
|
|
73
|
+
return {
|
|
74
|
+
tfidf: Math.round((w.tfidf / sum) * 1000) / 1000,
|
|
75
|
+
bm25: Math.round((w.bm25 / sum) * 1000) / 1000,
|
|
76
|
+
bigram: Math.round((w.bigram / sum) * 1000) / 1000,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Tune matcher ensemble weights based on solution effectiveness data.
|
|
81
|
+
* Returns null if cold-start conditions are not met.
|
|
82
|
+
*/
|
|
83
|
+
export function tuneMatcherWeights(config) {
|
|
84
|
+
const data = loadSolutionEffectivenessData();
|
|
85
|
+
// Cold-start check
|
|
86
|
+
const injectedCount = data.length;
|
|
87
|
+
const reflectedCount = data.filter((d) => d.reflected > 0).length;
|
|
88
|
+
if (injectedCount < config.coldStart.minSolutionsForMatcher || reflectedCount < 3) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
// Partition by median effectiveness ratio
|
|
92
|
+
const ratios = data.map((d) => d.ratio);
|
|
93
|
+
const medianRatio = median(ratios);
|
|
94
|
+
const effective = data.filter((d) => d.ratio > medianRatio);
|
|
95
|
+
const ineffective = data.filter((d) => d.ratio <= medianRatio);
|
|
96
|
+
if (effective.length === 0 || ineffective.length === 0)
|
|
97
|
+
return null;
|
|
98
|
+
// Compute discrimination signals per component.
|
|
99
|
+
// We use tag count as a proxy for component contribution:
|
|
100
|
+
// - TF-IDF benefits from more exact tag matches
|
|
101
|
+
// - BM25 benefits from longer documents (more tags)
|
|
102
|
+
// - Bigram benefits from partial/fuzzy matches (shorter tags)
|
|
103
|
+
//
|
|
104
|
+
// Since we can't replay the exact scoring without the original queries,
|
|
105
|
+
// we use statistical proxies from the solution characteristics.
|
|
106
|
+
const avgTagsEffective = effective.reduce((s, d) => s + d.tags.length, 0) / effective.length;
|
|
107
|
+
const avgTagsIneffective = ineffective.reduce((s, d) => s + d.tags.length, 0) / ineffective.length;
|
|
108
|
+
// Discrimination signals:
|
|
109
|
+
// - If effective solutions have more tags → BM25 (length normalization) discriminates well
|
|
110
|
+
// - If effective solutions have fewer tags → TF-IDF (exact match) discriminates well
|
|
111
|
+
// - Bigram gets a boost proportional to how many effective solutions have short tags
|
|
112
|
+
const tagRatio = avgTagsEffective / Math.max(avgTagsIneffective, 1);
|
|
113
|
+
const shortTagEffective = effective.filter((d) => d.tags.some((t) => t.length <= 5)).length / effective.length;
|
|
114
|
+
// Raw discrimination scores (higher = more discriminating)
|
|
115
|
+
const tfidfSignal = tagRatio < 1 ? 1.2 : 1.0; // exact match helps when effective have fewer tags
|
|
116
|
+
const bm25Signal = tagRatio > 1 ? 1.2 : 1.0; // length normalization helps when effective have more tags
|
|
117
|
+
const bigramSignal = shortTagEffective > 0.5 ? 1.15 : 0.95; // fuzzy match helps with short tags
|
|
118
|
+
// Load current weights or defaults
|
|
119
|
+
const current = loadCurrentWeights();
|
|
120
|
+
const currentW = current
|
|
121
|
+
? { tfidf: current.tfidf, bm25: current.bm25, bigram: current.bigram }
|
|
122
|
+
: { ...DEFAULT_MATCHER_WEIGHTS };
|
|
123
|
+
// Compute target weights from discrimination signals
|
|
124
|
+
const signalSum = tfidfSignal + bm25Signal + bigramSignal;
|
|
125
|
+
const targetW = {
|
|
126
|
+
tfidf: tfidfSignal / signalSum,
|
|
127
|
+
bm25: bm25Signal / signalSum,
|
|
128
|
+
bigram: bigramSignal / signalSum,
|
|
129
|
+
};
|
|
130
|
+
// Apply max delta per cycle guardrail
|
|
131
|
+
const { maxWeightDelta, weightFloor, weightCeiling } = config.guardrails;
|
|
132
|
+
const newW = {
|
|
133
|
+
tfidf: clampWeight(currentW.tfidf +
|
|
134
|
+
Math.max(-maxWeightDelta, Math.min(maxWeightDelta, targetW.tfidf - currentW.tfidf)), weightFloor, weightCeiling),
|
|
135
|
+
bm25: clampWeight(currentW.bm25 +
|
|
136
|
+
Math.max(-maxWeightDelta, Math.min(maxWeightDelta, targetW.bm25 - currentW.bm25)), weightFloor, weightCeiling),
|
|
137
|
+
bigram: clampWeight(currentW.bigram +
|
|
138
|
+
Math.max(-maxWeightDelta, Math.min(maxWeightDelta, targetW.bigram - currentW.bigram)), weightFloor, weightCeiling),
|
|
139
|
+
};
|
|
140
|
+
// Normalize to sum = 1.0
|
|
141
|
+
const normalized = normalizeWeights(newW);
|
|
142
|
+
const result = {
|
|
143
|
+
...normalized,
|
|
144
|
+
updatedAt: new Date().toISOString(),
|
|
145
|
+
sampleSize: data.length,
|
|
146
|
+
version: (current?.version ?? 0) + 1,
|
|
147
|
+
defaults: { ...DEFAULT_MATCHER_WEIGHTS },
|
|
148
|
+
};
|
|
149
|
+
saveWeights(result);
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen Meta-Learning — Runner (Orchestrator)
|
|
3
|
+
*
|
|
4
|
+
* Coordinates execution of all meta-learning features at session end.
|
|
5
|
+
* Called from auto-compound-runner.ts as Step 5.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Each feature is independently gated by config + cold-start check
|
|
9
|
+
* - Failures in one feature do not block others (fail-open per feature)
|
|
10
|
+
* - Session quality scoring always runs first (feeds other features)
|
|
11
|
+
*/
|
|
12
|
+
import { type MetaLearningConfig, type MetaLearningResult } from './types.js';
|
|
13
|
+
export declare function loadMetaLearningConfig(): MetaLearningConfig;
|
|
14
|
+
export declare function runMetaLearning(sessionId: string, cwd: string): MetaLearningResult;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen Meta-Learning — Runner (Orchestrator)
|
|
3
|
+
*
|
|
4
|
+
* Coordinates execution of all meta-learning features at session end.
|
|
5
|
+
* Called from auto-compound-runner.ts as Step 5.
|
|
6
|
+
*
|
|
7
|
+
* Design:
|
|
8
|
+
* - Each feature is independently gated by config + cold-start check
|
|
9
|
+
* - Failures in one feature do not block others (fail-open per feature)
|
|
10
|
+
* - Session quality scoring always runs first (feeds other features)
|
|
11
|
+
*/
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { safeReadJSON } from '../../hooks/shared/atomic-write.js';
|
|
14
|
+
import { computeAdaptiveThresholds } from './adaptive-thresholds.js';
|
|
15
|
+
import { computeExtractionBias } from './extraction-tuner.js';
|
|
16
|
+
import { tuneMatcherWeights } from './matcher-weight-tuner.js';
|
|
17
|
+
import { checkScopePromotions, updateProjectUsageMap } from './scope-promoter.js';
|
|
18
|
+
import { saveSessionQuality, scoreSession } from './session-quality-scorer.js';
|
|
19
|
+
import { DEFAULT_CONFIG } from './types.js';
|
|
20
|
+
export function loadMetaLearningConfig() {
|
|
21
|
+
const hookConfigPath = path.join(process.env.HOME ?? process.env.USERPROFILE ?? '', '.forgen', 'hook-config.json');
|
|
22
|
+
const hookConfig = safeReadJSON(hookConfigPath, {});
|
|
23
|
+
const metaSection = hookConfig?.['meta-learning'];
|
|
24
|
+
if (!metaSection)
|
|
25
|
+
return DEFAULT_CONFIG;
|
|
26
|
+
return {
|
|
27
|
+
enabled: metaSection.enabled ?? DEFAULT_CONFIG.enabled,
|
|
28
|
+
features: { ...DEFAULT_CONFIG.features, ...metaSection.features },
|
|
29
|
+
coldStart: { ...DEFAULT_CONFIG.coldStart, ...metaSection.coldStart },
|
|
30
|
+
guardrails: { ...DEFAULT_CONFIG.guardrails, ...metaSection.guardrails },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function runMetaLearning(sessionId, cwd) {
|
|
34
|
+
const config = loadMetaLearningConfig();
|
|
35
|
+
if (!config.enabled) {
|
|
36
|
+
return { skipped: true, reason: 'meta-learning disabled in config' };
|
|
37
|
+
}
|
|
38
|
+
const result = {};
|
|
39
|
+
// Feature 1: Session Quality Scorer (always first — feeds other features)
|
|
40
|
+
if (config.features.sessionQualityScorer) {
|
|
41
|
+
try {
|
|
42
|
+
const score = scoreSession(sessionId);
|
|
43
|
+
if (score) {
|
|
44
|
+
saveSessionQuality(score);
|
|
45
|
+
result.qualityScore = score;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
process.stderr.write(`[forgen-meta] quality scorer: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Feature 2: Matcher Weight Tuning
|
|
53
|
+
if (config.features.matcherWeightTuning) {
|
|
54
|
+
try {
|
|
55
|
+
result.matcherWeights = tuneMatcherWeights(config);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
process.stderr.write(`[forgen-meta] matcher tuning: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Feature 3: Scope Auto-Promotion
|
|
62
|
+
if (config.features.scopeAutoPromotion) {
|
|
63
|
+
try {
|
|
64
|
+
updateProjectUsageMap(sessionId, cwd, config);
|
|
65
|
+
result.scopePromotions = checkScopePromotions(config);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
process.stderr.write(`[forgen-meta] scope promotion: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Feature 4: Adaptive Thresholds
|
|
72
|
+
if (config.features.adaptiveThresholds) {
|
|
73
|
+
try {
|
|
74
|
+
result.thresholds = computeAdaptiveThresholds(config);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
process.stderr.write(`[forgen-meta] adaptive thresholds: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Feature 5: Extraction Tuning
|
|
81
|
+
if (config.features.extractionTuning) {
|
|
82
|
+
try {
|
|
83
|
+
result.extractionBias = computeExtractionBias(config);
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
process.stderr.write(`[forgen-meta] extraction tuning: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen Meta-Learning — Scope Promoter (Feature 3)
|
|
3
|
+
*
|
|
4
|
+
* Tracks cross-project solution usage and auto-promotes solutions
|
|
5
|
+
* from scope:'me' to scope:'universal' when used in 3+ distinct projects.
|
|
6
|
+
*
|
|
7
|
+
* Data flow:
|
|
8
|
+
* 1. At session end: read injection-cache for injected solutions + session cwd
|
|
9
|
+
* 2. Record (solution, project) pair in project-usage-map.json
|
|
10
|
+
* 3. Check if any solution has 3+ distinct projects → mutate frontmatter
|
|
11
|
+
*/
|
|
12
|
+
import type { MetaLearningConfig } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Record which project (cwd) each injected solution was used in.
|
|
15
|
+
*/
|
|
16
|
+
export declare function updateProjectUsageMap(sessionId: string, cwd: string, _config: MetaLearningConfig): void;
|
|
17
|
+
/**
|
|
18
|
+
* Check for solutions that should be promoted to universal scope.
|
|
19
|
+
* Returns names of promoted solutions.
|
|
20
|
+
*/
|
|
21
|
+
export declare function checkScopePromotions(config: MetaLearningConfig): string[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Forgen Meta-Learning — Scope Promoter (Feature 3)
|
|
3
|
+
*
|
|
4
|
+
* Tracks cross-project solution usage and auto-promotes solutions
|
|
5
|
+
* from scope:'me' to scope:'universal' when used in 3+ distinct projects.
|
|
6
|
+
*
|
|
7
|
+
* Data flow:
|
|
8
|
+
* 1. At session end: read injection-cache for injected solutions + session cwd
|
|
9
|
+
* 2. Record (solution, project) pair in project-usage-map.json
|
|
10
|
+
* 3. Check if any solution has 3+ distinct projects → mutate frontmatter
|
|
11
|
+
*/
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import { META_LEARNING_DIR, STATE_DIR } from '../../core/paths.js';
|
|
14
|
+
import { atomicWriteJSON, safeReadJSON } from '../../hooks/shared/atomic-write.js';
|
|
15
|
+
import { mutateSolutionByName } from '../solution-writer.js';
|
|
16
|
+
const USAGE_MAP_PATH = path.join(META_LEARNING_DIR, 'project-usage-map.json');
|
|
17
|
+
function sanitizeId(id) {
|
|
18
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
19
|
+
}
|
|
20
|
+
function loadUsageMap() {
|
|
21
|
+
return safeReadJSON(USAGE_MAP_PATH, { solutions: {} });
|
|
22
|
+
}
|
|
23
|
+
function saveUsageMap(map) {
|
|
24
|
+
atomicWriteJSON(USAGE_MAP_PATH, map, { pretty: true });
|
|
25
|
+
}
|
|
26
|
+
function loadInjectedSolutions(sessionId) {
|
|
27
|
+
// Try solution-cache (primary) and injection-cache (fallback)
|
|
28
|
+
for (const prefix of ['solution-cache', 'injection-cache']) {
|
|
29
|
+
const cachePath = path.join(STATE_DIR, `${prefix}-${sanitizeId(sessionId)}.json`);
|
|
30
|
+
const data = safeReadJSON(cachePath, {});
|
|
31
|
+
if (data.injected && data.injected.length > 0)
|
|
32
|
+
return data.injected;
|
|
33
|
+
}
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Record which project (cwd) each injected solution was used in.
|
|
38
|
+
*/
|
|
39
|
+
export function updateProjectUsageMap(sessionId, cwd, _config) {
|
|
40
|
+
const injected = loadInjectedSolutions(sessionId);
|
|
41
|
+
if (injected.length === 0)
|
|
42
|
+
return;
|
|
43
|
+
// Normalize cwd to project root name for privacy
|
|
44
|
+
const projectKey = path.basename(cwd);
|
|
45
|
+
const map = loadUsageMap();
|
|
46
|
+
let changed = false;
|
|
47
|
+
for (const name of injected) {
|
|
48
|
+
if (!map.solutions[name]) {
|
|
49
|
+
map.solutions[name] = { projects: [projectKey], updatedAt: new Date().toISOString() };
|
|
50
|
+
changed = true;
|
|
51
|
+
}
|
|
52
|
+
else if (!map.solutions[name].projects.includes(projectKey)) {
|
|
53
|
+
map.solutions[name].projects.push(projectKey);
|
|
54
|
+
map.solutions[name].updatedAt = new Date().toISOString();
|
|
55
|
+
changed = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (changed)
|
|
59
|
+
saveUsageMap(map);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Check for solutions that should be promoted to universal scope.
|
|
63
|
+
* Returns names of promoted solutions.
|
|
64
|
+
*/
|
|
65
|
+
export function checkScopePromotions(config) {
|
|
66
|
+
const map = loadUsageMap();
|
|
67
|
+
const minProjects = config.coldStart.minProjectsForScope;
|
|
68
|
+
const promoted = [];
|
|
69
|
+
for (const [name, entry] of Object.entries(map.solutions)) {
|
|
70
|
+
if (entry.projects.length < minProjects)
|
|
71
|
+
continue;
|
|
72
|
+
const success = mutateSolutionByName(name, (sol) => {
|
|
73
|
+
if (sol.frontmatter.scope === 'universal')
|
|
74
|
+
return false; // already promoted
|
|
75
|
+
if (sol.frontmatter.scope !== 'me')
|
|
76
|
+
return false; // only promote from 'me'
|
|
77
|
+
sol.frontmatter.scope = 'universal';
|
|
78
|
+
return true;
|
|
79
|
+
});
|
|
80
|
+
if (success)
|
|
81
|
+
promoted.push(name);
|
|
82
|
+
}
|
|
83
|
+
return promoted;
|
|
84
|
+
}
|