Skip to content

Commit 85e9135

Browse files
committed
build: sync bundle + bump version to 1.4.0
- gen-context.js: bundled factories updated for handlers, tools, server, scorer, defaults; VERSION and SERVER_INFO.version bumped to 1.4.0; --health display now shows strategy and cold freshness lines. - Health scorer fix: untracked projects (totalRuns=0) no longer penalised for 0% reduction. - package.json: version 1.4.0
1 parent 325d8da commit 85e9135

2 files changed

Lines changed: 238 additions & 12 deletions

File tree

gen-context.js

Lines changed: 237 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ __factories["./src/config/defaults"] = function(module, exports) {
6262
// Sort recently git-committed files higher in output
6363
diffPriority: true,
6464

65+
// Debounce delay (ms) between file-system events and regeneration in watch mode
66+
watchDebounce: 300,
67+
6568
// Append model routing hints section to the context output
6669
// Routes files to fast/balanced/powerful model tiers based on complexity
6770
routing: false,
@@ -1581,6 +1584,9 @@ __factories["./src/health/scorer"] = function(module, exports) {
15811584
* 2. Average token reduction percentage (low-reduction penalty 20 pts)
15821585
* 3. Over-budget run rate (budget penalty 20 pts)
15831586
*
1587+
* Strategy-aware: thresholds adjust based on the active strategy so that
1588+
* hot-cold (90% reduction intentional) is not penalized as 'low reduction'.
1589+
*
15841590
* Grade scale: A ≥ 90 | B ≥ 75 | C ≥ 60 | D < 60
15851591
*
15861592
* Never throws — returns graceful result with nulls for unavailable metrics.
@@ -1589,8 +1595,10 @@ __factories["./src/health/scorer"] = function(module, exports) {
15891595
* @returns {{
15901596
* score: number,
15911597
* grade: 'A'|'B'|'C'|'D',
1598+
* strategy: string,
15921599
* tokenReductionPct: number|null,
15931600
* daysSinceRegen: number|null,
1601+
* strategyFreshnessDays: number|null,
15941602
* totalRuns: number,
15951603
* overBudgetRuns: number,
15961604
* }}
@@ -1601,30 +1609,52 @@ __factories["./src/health/scorer"] = function(module, exports) {
16011609

16021610
let tokenReductionPct = null;
16031611
let daysSinceRegen = null;
1612+
let strategyFreshnessDays = null;
16041613
let overBudgetRuns = 0;
16051614
let totalRuns = 0;
16061615

1616+
// ── Detect active strategy ──────────────────────────────────────────────
1617+
let strategy = 'full';
1618+
try {
1619+
const cfgPath = path.join(cwd, 'gen-context.config.json');
1620+
if (fs.existsSync(cfgPath)) {
1621+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
1622+
strategy = cfg.strategy || 'full';
1623+
}
1624+
} catch (_) {}
1625+
16071626
// ── Read usage log via tracking logger ──────────────────────────────────
16081627
try {
16091628
const { readLog, summarize } = __require('./src/tracking/logger');
16101629
const entries = readLog(cwd);
16111630
const s = summarize(entries);
1612-
tokenReductionPct = s.avgReductionPct;
1631+
// Only set tokenReductionPct when there is actual history; a brand-new/
1632+
// untracked project should not be penalised for "0% reduction".
1633+
if (s.totalRuns > 0) tokenReductionPct = s.avgReductionPct;
16131634
overBudgetRuns = s.overBudgetRuns;
16141635
totalRuns = s.totalRuns;
16151636
} catch (_) {
16161637
// No usage log yet — proceed with nulls
16171638
}
16181639

1619-
// ── Days since context file was last regenerated ─────────────────────────
1640+
// ── Days since primary context file was last regenerated ─────────────────
16201641
try {
16211642
const ctxFile = path.join(cwd, '.github', 'copilot-instructions.md');
16221643
if (fs.existsSync(ctxFile)) {
16231644
const mtime = fs.statSync(ctxFile).mtimeMs;
16241645
daysSinceRegen = parseFloat(((Date.now() - mtime) / (1000 * 60 * 60 * 24)).toFixed(1));
16251646
}
1626-
} catch (_) {
1627-
// File not found or stat failed — leave as null
1647+
} catch (_) {}
1648+
1649+
// ── Strategy freshness: context-cold.md age (hot-cold only) ─────────────
1650+
if (strategy === 'hot-cold') {
1651+
try {
1652+
const coldFile = path.join(cwd, '.github', 'context-cold.md');
1653+
if (fs.existsSync(coldFile)) {
1654+
const mtime = fs.statSync(coldFile).mtimeMs;
1655+
strategyFreshnessDays = parseFloat(((Date.now() - mtime) / (1000 * 60 * 60 * 24)).toFixed(1));
1656+
}
1657+
} catch (_) {}
16281658
}
16291659

16301660
// ── Compute composite score ───────────────────────────────────────────────
@@ -1635,11 +1665,20 @@ __factories["./src/health/scorer"] = function(module, exports) {
16351665
points -= Math.min(30, Math.floor((daysSinceRegen - 7) * 4));
16361666
}
16371667

1638-
// Low-reduction penalty: context is barely smaller than the raw source (-20)
1639-
if (tokenReductionPct !== null && tokenReductionPct < 60) {
1668+
// Low-reduction penalty — threshold depends on strategy:
1669+
// - hot-cold: primary output tiny by design; use cold freshness instead
1670+
// - per-module: per-file budgets; global < 60% is expected, no penalty
1671+
// - full: standard 60% threshold
1672+
const reductionThreshold = (strategy === 'full') ? 60 : 0;
1673+
if (tokenReductionPct !== null && tokenReductionPct < reductionThreshold) {
16401674
points -= 20;
16411675
}
16421676

1677+
// hot-cold strategy freshness penalty: context-cold.md older than 1 day (-10 pts)
1678+
if (strategy === 'hot-cold' && strategyFreshnessDays !== null && strategyFreshnessDays > 1) {
1679+
points -= Math.min(10, Math.floor(strategyFreshnessDays - 1) * 3);
1680+
}
1681+
16431682
// Over-budget penalty: more than 20% of runs exceeded the token budget (-20)
16441683
if (overBudgetRuns > 0 && totalRuns > 0) {
16451684
const overBudgetRate = (overBudgetRuns / totalRuns) * 100;
@@ -1654,7 +1693,7 @@ __factories["./src/health/scorer"] = function(module, exports) {
16541693
else if (points >= 60) grade = 'C';
16551694
else grade = 'D';
16561695

1657-
return { score: points, grade, tokenReductionPct, daysSinceRegen, totalRuns, overBudgetRuns };
1696+
return { score: points, grade, strategy, tokenReductionPct, daysSinceRegen, strategyFreshnessDays, totalRuns, overBudgetRuns };
16581697
}
16591698

16601699
module.exports = { score };
@@ -2327,7 +2366,156 @@ __factories["./src/mcp/handlers"] = function(module, exports) {
23272366
}
23282367
}
23292368

2330-
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting };
2369+
function explainFile(args, cwd) {
2370+
if (!args || !args.path) return 'Missing required argument: path';
2371+
2372+
const targetRel = args.path.replace(/\\/g, '/').replace(/^\//, '');
2373+
const targetAbs = path.resolve(cwd, targetRel);
2374+
const contextPath = path.join(cwd, CONTEXT_FILE);
2375+
2376+
const lines = ['# explain_file: ' + targetRel, ''];
2377+
2378+
lines.push('## Signatures');
2379+
let indexedFiles = [];
2380+
2381+
if (fs.existsSync(contextPath)) {
2382+
const ctxContent = fs.readFileSync(contextPath, 'utf8');
2383+
const ctxLines = ctxContent.split('\n');
2384+
let capturing = false;
2385+
const sigLines = [];
2386+
2387+
for (const line of ctxLines) {
2388+
if (line.startsWith('### ')) {
2389+
if (capturing) break;
2390+
const rel = line.slice(4).trim().replace(/\\/g, '/');
2391+
capturing = rel === targetRel || rel.endsWith('/' + targetRel) || targetRel.endsWith('/' + rel);
2392+
if (capturing) continue;
2393+
} else if (capturing) {
2394+
sigLines.push(line);
2395+
}
2396+
}
2397+
2398+
const sigs = sigLines.filter((l) => l !== '```' && l.trim() !== '');
2399+
if (sigs.length > 0) {
2400+
lines.push(...sigs);
2401+
} else {
2402+
lines.push('_No signatures indexed for this file. Run: node gen-context.js_');
2403+
}
2404+
2405+
indexedFiles = ctxContent
2406+
.split('\n')
2407+
.filter((l) => l.startsWith('### '))
2408+
.map((l) => path.resolve(cwd, l.slice(4).trim()));
2409+
} else {
2410+
lines.push('_No context file found. Run: node gen-context.js_');
2411+
}
2412+
2413+
if (!fs.existsSync(targetAbs)) {
2414+
lines.push('');
2415+
lines.push('> File not found on disk: ' + targetRel);
2416+
return lines.join('\n');
2417+
}
2418+
2419+
lines.push('');
2420+
2421+
lines.push('## Imports (direct dependencies)');
2422+
try {
2423+
const { extractImports } = __require('./src/map/import-graph');
2424+
const fileContent = fs.readFileSync(targetAbs, 'utf8');
2425+
const fileSet = new Set(indexedFiles);
2426+
fileSet.add(targetAbs);
2427+
const imports = extractImports(targetAbs, fileContent, fileSet);
2428+
if (imports.length > 0) {
2429+
for (const imp of imports) lines.push('- ' + path.relative(cwd, imp).replace(/\\/g, '/'));
2430+
} else {
2431+
lines.push('_No resolvable relative imports found._');
2432+
}
2433+
} catch (err) {
2434+
lines.push('_Could not analyze imports: ' + err.message + '_');
2435+
}
2436+
2437+
lines.push('');
2438+
2439+
lines.push('## Callers (files that import this file)');
2440+
try {
2441+
const { extractImports } = __require('./src/map/import-graph');
2442+
const fileSet = new Set(indexedFiles);
2443+
fileSet.add(targetAbs);
2444+
const callers = [];
2445+
for (const f of indexedFiles) {
2446+
if (f === targetAbs || !fs.existsSync(f)) continue;
2447+
try {
2448+
const fc = fs.readFileSync(f, 'utf8');
2449+
const imps = extractImports(f, fc, fileSet);
2450+
if (imps.includes(targetAbs)) callers.push(path.relative(cwd, f).replace(/\\/g, '/'));
2451+
} catch (_) {}
2452+
}
2453+
if (callers.length > 0) {
2454+
for (const c of callers) lines.push('- ' + c);
2455+
} else {
2456+
lines.push('_No indexed files import this file._');
2457+
}
2458+
} catch (err) {
2459+
lines.push('_Could not analyze callers: ' + err.message + '_');
2460+
}
2461+
2462+
return lines.join('\n');
2463+
}
2464+
2465+
function listModules(args, cwd) {
2466+
const contextPath = path.join(cwd, CONTEXT_FILE);
2467+
if (!fs.existsSync(contextPath)) {
2468+
return 'No context file found. Run: node gen-context.js';
2469+
}
2470+
2471+
const content = fs.readFileSync(contextPath, 'utf8');
2472+
const ctxLines = content.split('\n');
2473+
const groups = {};
2474+
let currentGroup = null;
2475+
let blockBuf = [];
2476+
2477+
function flushBlock() {
2478+
if (currentGroup === null || blockBuf.length === 0) return;
2479+
if (!groups[currentGroup]) groups[currentGroup] = { fileCount: 0, tokenCount: 0 };
2480+
groups[currentGroup].fileCount++;
2481+
groups[currentGroup].tokenCount += Math.ceil(blockBuf.join('\n').length / 4);
2482+
blockBuf = [];
2483+
}
2484+
2485+
for (const line of ctxLines) {
2486+
if (line.startsWith('### ')) {
2487+
flushBlock();
2488+
const rel = line.slice(4).trim().replace(/\\/g, '/');
2489+
const parts = rel.split('/');
2490+
currentGroup = parts.length > 1 ? parts[0] : '.';
2491+
} else if (currentGroup !== null) {
2492+
blockBuf.push(line);
2493+
}
2494+
}
2495+
flushBlock();
2496+
2497+
const sorted = Object.entries(groups)
2498+
.map(([mod, data]) => ({ module: mod, fileCount: data.fileCount, tokenCount: data.tokenCount }))
2499+
.sort((a, b) => b.tokenCount - a.tokenCount);
2500+
2501+
if (sorted.length === 0) return 'No modules found in context file.';
2502+
2503+
const total = sorted.reduce((s, m) => s + m.tokenCount, 0);
2504+
2505+
return [
2506+
'# Modules',
2507+
'',
2508+
'| Module | Files | Tokens |',
2509+
'|--------|-------|--------|',
2510+
...sorted.map((m) => `| ${m.module} | ${m.fileCount} | ~${m.tokenCount} |`),
2511+
'',
2512+
`**Total context tokens: ~${total}**`,
2513+
'',
2514+
'_Use `read_context({ module: "name" })` to get signatures for a specific module._',
2515+
].join('\n');
2516+
}
2517+
2518+
module.exports = { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules };
23312519
};
23322520

23332521
// ── ./src/mcp/server ──
@@ -2347,11 +2535,11 @@ __factories["./src/mcp/server"] = function(module, exports) {
23472535

23482536
const readline = require('readline');
23492537
const { TOOLS } = __require('./src/mcp/tools');
2350-
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting } = __require('./src/mcp/handlers');
2538+
const { readContext, searchSignatures, getMap, createCheckpoint, getRouting, explainFile, listModules } = __require('./src/mcp/handlers');
23512539

23522540
const SERVER_INFO = {
23532541
name: 'context-forge',
2354-
version: '1.3.0',
2542+
version: '1.4.0',
23552543
description: 'ContextForge MCP server — code signatures on demand',
23562544
};
23572545

@@ -2404,6 +2592,8 @@ __factories["./src/mcp/server"] = function(module, exports) {
24042592
else if (name === 'get_map') text = getMap(args, cwd);
24052593
else if (name === 'create_checkpoint') text = createCheckpoint(args, cwd);
24062594
else if (name === 'get_routing') text = getRouting(args, cwd);
2595+
else if (name === 'explain_file') text = explainFile(args, cwd);
2596+
else if (name === 'list_modules') text = listModules(args, cwd);
24072597
else {
24082598
respondError(id, -32601, `Unknown tool: ${name}`);
24092599
return;
@@ -2551,6 +2741,38 @@ __factories["./src/mcp/tools"] = function(module, exports) {
25512741
required: [],
25522742
},
25532743
},
2744+
{
2745+
name: 'explain_file',
2746+
description:
2747+
'Explain a specific file: returns its extracted signatures, direct imports ' +
2748+
'(files it depends on), and callers (files that import it). ' +
2749+
'Ideal for understanding a file in isolation without reading raw source. ' +
2750+
'Requires the context file to have been generated first.',
2751+
inputSchema: {
2752+
type: 'object',
2753+
properties: {
2754+
path: {
2755+
type: 'string',
2756+
description:
2757+
'Relative path from the project root (e.g. "src/services/auth.ts"). ' +
2758+
'Use the paths shown in read_context output.',
2759+
},
2760+
},
2761+
required: ['path'],
2762+
},
2763+
},
2764+
{
2765+
name: 'list_modules',
2766+
description:
2767+
'List all top-level modules (srcDirs) present in the context file, ' +
2768+
'sorted by token count descending. Use this to decide which module to ' +
2769+
'pass to read_context before querying a specific area of the codebase.',
2770+
inputSchema: {
2771+
type: 'object',
2772+
properties: {},
2773+
required: [],
2774+
},
2775+
},
25542776
];
25552777

25562778
module.exports = { TOOLS };
@@ -2997,7 +3219,7 @@ const path = require('path');
29973219
const os = require('os');
29983220
const { execSync } = require('child_process');
29993221

3000-
const VERSION = '1.3.0';
3222+
const VERSION = '1.4.0';
30013223
const MARKER = '\n\n## Auto-generated signatures\n<!-- Updated by gen-context.js -->\n';
30023224

30033225
// ---------------------------------------------------------------------------
@@ -4004,8 +4226,12 @@ function main() {
40044226
} else {
40054227
console.log('[context-forge] health:');
40064228
console.log(` score : ${result.score}/100 (grade ${result.grade})`);
4229+
console.log(` strategy : ${result.strategy}`);
40074230
console.log(` token reduction : ${result.tokenReductionPct !== null ? result.tokenReductionPct + '%' : 'no history'}`);
40084231
console.log(` days since regen: ${result.daysSinceRegen !== null ? result.daysSinceRegen : 'context file not found'}`);
4232+
if (result.strategyFreshnessDays !== null) {
4233+
console.log(` cold freshness : ${result.strategyFreshnessDays} day(s)`);
4234+
}
40094235
console.log(` total runs : ${result.totalRuns}`);
40104236
console.log(` over-budget runs: ${result.overBudgetRuns}`);
40114237
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "context-forge",
3-
"version": "1.2.0",
3+
"version": "1.4.0",
44
"description": "Zero-dependency AI context engine — 97% token reduction. No npm install. Runs on Node 18+.",
55
"main": "gen-context.js",
66
"bin": {

0 commit comments

Comments
 (0)