@codeledger/cli 0.6.9 → 0.7.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/dist/commands/activate.d.ts.map +1 -1
- package/dist/commands/activate.js +79 -26
- package/dist/commands/activate.js.map +1 -1
- package/dist/commands/ci.d.ts +16 -0
- package/dist/commands/ci.d.ts.map +1 -0
- package/dist/commands/ci.js +323 -0
- package/dist/commands/ci.js.map +1 -0
- package/dist/commands/compare.d.ts.map +1 -1
- package/dist/commands/compare.js +8 -0
- package/dist/commands/compare.js.map +1 -1
- package/dist/commands/context.d.ts +17 -0
- package/dist/commands/context.d.ts.map +1 -0
- package/dist/commands/context.js +687 -0
- package/dist/commands/context.js.map +1 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +65 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/enable.d.ts +7 -0
- package/dist/commands/enable.d.ts.map +1 -0
- package/dist/commands/enable.js +75 -0
- package/dist/commands/enable.js.map +1 -0
- package/dist/commands/features.d.ts +2 -0
- package/dist/commands/features.d.ts.map +1 -0
- package/dist/commands/features.js +182 -0
- package/dist/commands/features.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/intent.d.ts +4 -4
- package/dist/commands/intent.d.ts.map +1 -1
- package/dist/commands/intent.js +70 -5
- package/dist/commands/intent.js.map +1 -1
- package/dist/commands/ledger.d.ts +14 -0
- package/dist/commands/ledger.d.ts.map +1 -0
- package/dist/commands/ledger.js +128 -0
- package/dist/commands/ledger.js.map +1 -0
- package/dist/commands/license.d.ts +45 -0
- package/dist/commands/license.d.ts.map +1 -0
- package/dist/commands/license.js +240 -0
- package/dist/commands/license.js.map +1 -0
- package/dist/commands/orchestrate.d.ts +12 -0
- package/dist/commands/orchestrate.d.ts.map +1 -0
- package/dist/commands/orchestrate.js +119 -0
- package/dist/commands/orchestrate.js.map +1 -0
- package/dist/commands/policy-simulate.d.ts +9 -0
- package/dist/commands/policy-simulate.d.ts.map +1 -0
- package/dist/commands/policy-simulate.js +46 -0
- package/dist/commands/policy-simulate.js.map +1 -0
- package/dist/commands/provenance.d.ts +10 -0
- package/dist/commands/provenance.d.ts.map +1 -0
- package/dist/commands/provenance.js +87 -0
- package/dist/commands/provenance.js.map +1 -0
- package/dist/commands/replay.d.ts +2 -0
- package/dist/commands/replay.d.ts.map +1 -0
- package/dist/commands/replay.js +181 -0
- package/dist/commands/replay.js.map +1 -0
- package/dist/commands/serve.d.ts +3 -0
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +73 -1
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/session-summary.d.ts.map +1 -1
- package/dist/commands/session-summary.js +47 -2
- package/dist/commands/session-summary.js.map +1 -1
- package/dist/commands/setup-ci.d.ts +17 -1
- package/dist/commands/setup-ci.d.ts.map +1 -1
- package/dist/commands/setup-ci.js +190 -51
- package/dist/commands/setup-ci.js.map +1 -1
- package/dist/commands/share.js +1 -1
- package/dist/commands/share.js.map +1 -1
- package/dist/commands/stats.d.ts +8 -0
- package/dist/commands/stats.d.ts.map +1 -0
- package/dist/commands/stats.js +164 -0
- package/dist/commands/stats.js.map +1 -0
- package/dist/commands/team-ledger.d.ts +11 -0
- package/dist/commands/team-ledger.d.ts.map +1 -0
- package/dist/commands/team-ledger.js +74 -0
- package/dist/commands/team-ledger.js.map +1 -0
- package/dist/commands/team-metrics.d.ts +8 -0
- package/dist/commands/team-metrics.d.ts.map +1 -0
- package/dist/commands/team-metrics.js +57 -0
- package/dist/commands/team-metrics.js.map +1 -0
- package/dist/commands/upgrade.d.ts +6 -0
- package/dist/commands/upgrade.d.ts.map +1 -0
- package/dist/commands/upgrade.js +39 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +241 -65
- package/dist/index.js.map +1 -1
- package/dist/templates/claude-md.d.ts.map +1 -1
- package/dist/templates/claude-md.js +14 -4
- package/dist/templates/claude-md.js.map +1 -1
- package/dist/templates/config.js +4 -4
- package/dist/templates/config.js.map +1 -1
- package/dist/templates/hooks.d.ts +3 -1
- package/dist/templates/hooks.d.ts.map +1 -1
- package/dist/templates/hooks.js +62 -2
- package/dist/templates/hooks.js.map +1 -1
- package/package.json +11 -10
- package/LICENSE +0 -27
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context — DCOL v2 context introspection subcommand
|
|
3
|
+
*
|
|
4
|
+
* Subcommands:
|
|
5
|
+
* explain Why each file/symbol was selected
|
|
6
|
+
* diff Show context change between two bundle iterations
|
|
7
|
+
* graph Output Mermaid dependency diagram
|
|
8
|
+
* validate Run execution-aware context validation
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* codeledger context explain [--bundle <path>] [--json]
|
|
12
|
+
* codeledger context diff [--from <bundle-id>] [--to <bundle-id>] [--json]
|
|
13
|
+
* codeledger context graph [--bundle <path>] [--output <path>]
|
|
14
|
+
* codeledger context validate [--bundle <path>] [--json]
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync, existsSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { computeCcs, loadEntries, historicalSuccessContext, checkIntentSufficiency } from '@codeledger/engine';
|
|
19
|
+
// ─── Entry Point ─────────────────────────────────────────────────────────────
|
|
20
|
+
export async function runContext(cwd, flags) {
|
|
21
|
+
const sub = flags['_sub'] ?? 'explain';
|
|
22
|
+
switch (sub) {
|
|
23
|
+
case 'explain':
|
|
24
|
+
return runContextExplain(cwd, flags);
|
|
25
|
+
case 'diff':
|
|
26
|
+
return runContextDiff(cwd, flags);
|
|
27
|
+
case 'graph':
|
|
28
|
+
return runContextGraph(cwd, flags);
|
|
29
|
+
case 'validate':
|
|
30
|
+
return runContextValidate(cwd, flags);
|
|
31
|
+
default:
|
|
32
|
+
console.error(`❌ Unknown context subcommand: ${sub}`);
|
|
33
|
+
console.error(` Valid: explain | diff | graph | validate`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ─── context explain ─────────────────────────────────────────────────────────
|
|
38
|
+
async function runContextExplain(cwd, flags) {
|
|
39
|
+
const bundle = loadBundle(cwd, flags['bundle']);
|
|
40
|
+
if (!bundle)
|
|
41
|
+
return;
|
|
42
|
+
const jsonMode = flags['json'] === 'true';
|
|
43
|
+
if (jsonMode) {
|
|
44
|
+
console.log(JSON.stringify(buildExplainPayload(bundle), null, 2));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
console.log(`\n📋 Context Explanation — Bundle ${bundle.bundle_id}\n`);
|
|
48
|
+
console.log(` Task: ${bundle.task}`);
|
|
49
|
+
console.log(` Files: ${bundle.files.length} Tokens: ${bundle.total_tokens.toLocaleString()}`);
|
|
50
|
+
if (bundle.confidence) {
|
|
51
|
+
const c = bundle.confidence;
|
|
52
|
+
console.log(` Confidence: ${c.level.toUpperCase()} (${(c.score * 100).toFixed(0)}%)`);
|
|
53
|
+
}
|
|
54
|
+
console.log('');
|
|
55
|
+
for (const file of bundle.files) {
|
|
56
|
+
const reasons = file.reasons.join(', ');
|
|
57
|
+
const score = file.score.toFixed(3);
|
|
58
|
+
const tokens = file.token_estimate?.toLocaleString() ?? '—';
|
|
59
|
+
const stub = file.is_stub ? ' [stub]' : '';
|
|
60
|
+
const exemplar = file.is_exemplar ? ' [exemplar]' : '';
|
|
61
|
+
const shadow = file.shadow_reason ? ` [shadow affinity=${file.shadow_reason.affinity.toFixed(2)}]` : '';
|
|
62
|
+
console.log(` ${score} ${file.path}${stub}${exemplar}${shadow}`);
|
|
63
|
+
console.log(` reasons: ${reasons} tokens: ${tokens}`);
|
|
64
|
+
// Feature breakdown (if explain data present)
|
|
65
|
+
if (bundle.explain?.[file.path]) {
|
|
66
|
+
const f = bundle.explain[file.path];
|
|
67
|
+
const parts = [];
|
|
68
|
+
if (f.keyword > 0)
|
|
69
|
+
parts.push(`keyword=${f.keyword.toFixed(2)}`);
|
|
70
|
+
if (f.centrality > 0)
|
|
71
|
+
parts.push(`centrality=${f.centrality.toFixed(2)}`);
|
|
72
|
+
if (f.churn > 0)
|
|
73
|
+
parts.push(`churn=${f.churn.toFixed(2)}`);
|
|
74
|
+
if (f.recent_touch > 0)
|
|
75
|
+
parts.push(`recent_touch=${f.recent_touch.toFixed(2)}`);
|
|
76
|
+
if (f.test_relevance > 0)
|
|
77
|
+
parts.push(`test=${f.test_relevance.toFixed(2)}`);
|
|
78
|
+
if (f.error_infrastructure > 0)
|
|
79
|
+
parts.push(`error_infra=${f.error_infrastructure.toFixed(2)}`);
|
|
80
|
+
if (f.branch_changed > 0)
|
|
81
|
+
parts.push(`branch=${f.branch_changed.toFixed(2)}`);
|
|
82
|
+
if (f.security_surface > 0)
|
|
83
|
+
parts.push(`security=${f.security_surface.toFixed(2)}`);
|
|
84
|
+
if (parts.length > 0) {
|
|
85
|
+
console.log(` features: ${parts.join(' ')}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
console.log('');
|
|
89
|
+
}
|
|
90
|
+
if (bundle.near_misses && bundle.near_misses.length > 0) {
|
|
91
|
+
console.log(` Near-misses (files that almost made the cut):`);
|
|
92
|
+
for (const nm of bundle.near_misses.slice(0, 5)) {
|
|
93
|
+
console.log(` ${nm.score.toFixed(3)} ${nm.path} [gap: ${nm.budget_gap_tokens} tokens]`);
|
|
94
|
+
if (nm.suggestion)
|
|
95
|
+
console.log(` → ${nm.suggestion}`);
|
|
96
|
+
}
|
|
97
|
+
console.log('');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function buildExplainPayload(bundle) {
|
|
101
|
+
return {
|
|
102
|
+
bundle_id: bundle.bundle_id,
|
|
103
|
+
task: bundle.task,
|
|
104
|
+
total_files: bundle.files.length,
|
|
105
|
+
total_tokens: bundle.total_tokens,
|
|
106
|
+
confidence: bundle.confidence,
|
|
107
|
+
files: bundle.files.map((f) => ({
|
|
108
|
+
path: f.path,
|
|
109
|
+
score: f.score,
|
|
110
|
+
reasons: f.reasons,
|
|
111
|
+
token_estimate: f.token_estimate,
|
|
112
|
+
is_stub: f.is_stub,
|
|
113
|
+
is_exemplar: f.is_exemplar,
|
|
114
|
+
features: bundle.explain?.[f.path],
|
|
115
|
+
shadow_reason: f.shadow_reason,
|
|
116
|
+
})),
|
|
117
|
+
near_misses: bundle.near_misses,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// ─── context diff ─────────────────────────────────────────────────────────────
|
|
121
|
+
async function runContextDiff(cwd, flags) {
|
|
122
|
+
const jsonMode = flags['json'] === 'true';
|
|
123
|
+
// Load two bundles for comparison
|
|
124
|
+
const fromId = flags['from'];
|
|
125
|
+
const toId = flags['to'];
|
|
126
|
+
const artifactsDir = join(cwd, '.codeledger', 'artifacts');
|
|
127
|
+
const bundlesDir = join(artifactsDir, 'bundles');
|
|
128
|
+
let fromBundle = null;
|
|
129
|
+
let toBundle = null;
|
|
130
|
+
if (fromId) {
|
|
131
|
+
// Try bundles/ subdirectory first, then artifacts/ root
|
|
132
|
+
const p1 = join(bundlesDir, `${fromId}.json`);
|
|
133
|
+
const p2 = join(artifactsDir, `${fromId}.json`);
|
|
134
|
+
const p = existsSync(p1) ? p1 : p2;
|
|
135
|
+
fromBundle = existsSync(p)
|
|
136
|
+
? JSON.parse(readFileSync(p, 'utf-8'))
|
|
137
|
+
: null;
|
|
138
|
+
}
|
|
139
|
+
// Default: compare previous vs current active bundle
|
|
140
|
+
const activeBundlePath = join(cwd, '.codeledger', 'active-bundle.md');
|
|
141
|
+
// Look in bundles/ subdirectory first, fall back to artifacts/ root
|
|
142
|
+
const bundlesDirResult = loadLatestBundles(bundlesDir, 2);
|
|
143
|
+
const bundleArtifacts = bundlesDirResult.length >= 2
|
|
144
|
+
? bundlesDirResult
|
|
145
|
+
: loadLatestBundles(artifactsDir, 2);
|
|
146
|
+
if (!fromBundle && bundleArtifacts.length >= 2) {
|
|
147
|
+
fromBundle = bundleArtifacts[bundleArtifacts.length - 2] ?? null;
|
|
148
|
+
}
|
|
149
|
+
if (!toBundle) {
|
|
150
|
+
if (toId) {
|
|
151
|
+
const p1 = join(bundlesDir, `${toId}.json`);
|
|
152
|
+
const p2 = join(artifactsDir, `${toId}.json`);
|
|
153
|
+
const p = existsSync(p1) ? p1 : p2;
|
|
154
|
+
toBundle = existsSync(p)
|
|
155
|
+
? JSON.parse(readFileSync(p, 'utf-8'))
|
|
156
|
+
: null;
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
toBundle = bundleArtifacts[bundleArtifacts.length - 1] ?? null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (!fromBundle || !toBundle) {
|
|
163
|
+
console.error('❌ Need two bundles to diff. Use --from <id> --to <id> or generate 2+ bundles first.');
|
|
164
|
+
if (!existsSync(activeBundlePath)) {
|
|
165
|
+
console.error(' No active bundle found. Run: codeledger activate --task "..."');
|
|
166
|
+
}
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
const diff = computeContextDiff(fromBundle, toBundle);
|
|
170
|
+
if (jsonMode) {
|
|
171
|
+
console.log(JSON.stringify(diff, null, 2));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
console.log(`\n📊 Context Diff\n`);
|
|
175
|
+
console.log(` From: ${diff.from_bundle_id} → To: ${diff.to_bundle_id}`);
|
|
176
|
+
const sign = diff.token_delta >= 0 ? '+' : '';
|
|
177
|
+
console.log(` Token delta: ${sign}${diff.token_delta.toLocaleString()}\n`);
|
|
178
|
+
if (diff.added.length > 0) {
|
|
179
|
+
console.log(` ✅ Added to context (${diff.added.length}):`);
|
|
180
|
+
for (const f of diff.added)
|
|
181
|
+
console.log(` + ${f}`);
|
|
182
|
+
console.log('');
|
|
183
|
+
}
|
|
184
|
+
if (diff.removed.length > 0) {
|
|
185
|
+
console.log(` ❌ Removed from context (${diff.removed.length}):`);
|
|
186
|
+
for (const f of diff.removed)
|
|
187
|
+
console.log(` - ${f}`);
|
|
188
|
+
console.log('');
|
|
189
|
+
}
|
|
190
|
+
if (diff.score_changed.length > 0) {
|
|
191
|
+
console.log(` 📈 Score changed (${diff.score_changed.length}):`);
|
|
192
|
+
for (const sc of diff.score_changed) {
|
|
193
|
+
const delta = sc.to_score - sc.from_score;
|
|
194
|
+
const sign2 = delta >= 0 ? '+' : '';
|
|
195
|
+
console.log(` ~ ${sc.path} ${sc.from_score.toFixed(3)} → ${sc.to_score.toFixed(3)} (${sign2}${delta.toFixed(3)})`);
|
|
196
|
+
}
|
|
197
|
+
console.log('');
|
|
198
|
+
}
|
|
199
|
+
if (diff.added.length === 0 && diff.removed.length === 0 && diff.score_changed.length === 0) {
|
|
200
|
+
console.log(' ✓ No changes between bundles.');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function computeContextDiff(from, to) {
|
|
204
|
+
const fromPaths = new Set(from.files.map((f) => f.path));
|
|
205
|
+
const toPaths = new Set(to.files.map((f) => f.path));
|
|
206
|
+
const fromScores = new Map(from.files.map((f) => [f.path, f.score]));
|
|
207
|
+
const toScores = new Map(to.files.map((f) => [f.path, f.score]));
|
|
208
|
+
const added = to.files.filter((f) => !fromPaths.has(f.path)).map((f) => f.path);
|
|
209
|
+
const removed = from.files.filter((f) => !toPaths.has(f.path)).map((f) => f.path);
|
|
210
|
+
const scoreChanged = [];
|
|
211
|
+
for (const path of fromPaths) {
|
|
212
|
+
if (toPaths.has(path)) {
|
|
213
|
+
const fromScore = fromScores.get(path) ?? 0;
|
|
214
|
+
const toScore = toScores.get(path) ?? 0;
|
|
215
|
+
if (Math.abs(toScore - fromScore) > 0.005) {
|
|
216
|
+
scoreChanged.push({ path, from_score: fromScore, to_score: toScore });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
scoreChanged.sort((a, b) => Math.abs(b.to_score - b.from_score) - Math.abs(a.to_score - a.from_score));
|
|
221
|
+
return {
|
|
222
|
+
from_bundle_id: from.bundle_id,
|
|
223
|
+
to_bundle_id: to.bundle_id,
|
|
224
|
+
added,
|
|
225
|
+
removed,
|
|
226
|
+
score_changed: scoreChanged,
|
|
227
|
+
token_delta: to.total_tokens - from.total_tokens,
|
|
228
|
+
generated_at: new Date().toISOString(),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
// ─── context graph ────────────────────────────────────────────────────────────
|
|
232
|
+
async function runContextGraph(cwd, flags) {
|
|
233
|
+
const bundle = loadBundle(cwd, flags['bundle']);
|
|
234
|
+
if (!bundle)
|
|
235
|
+
return;
|
|
236
|
+
const outputPath = flags['output'];
|
|
237
|
+
const graph = buildMermaidGraph(bundle, cwd);
|
|
238
|
+
if (outputPath) {
|
|
239
|
+
writeFileSync(outputPath, graph.markup, 'utf-8');
|
|
240
|
+
console.log(`✅ Mermaid diagram written to: ${outputPath}`);
|
|
241
|
+
console.log(` Nodes: ${graph.included_files.length} files`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
console.log(`\n🗂 Context Dependency Graph — Bundle ${bundle.bundle_id}\n`);
|
|
245
|
+
console.log('```mermaid');
|
|
246
|
+
console.log(graph.markup);
|
|
247
|
+
console.log('```');
|
|
248
|
+
console.log(`\n Files: ${graph.included_files.length}`);
|
|
249
|
+
console.log(' Paste the above into https://mermaid.live to visualize.\n');
|
|
250
|
+
}
|
|
251
|
+
function buildMermaidGraph(bundle, _cwd) {
|
|
252
|
+
const files = bundle.files.map((f) => f.path);
|
|
253
|
+
const relationships = bundle.file_relationships ?? [];
|
|
254
|
+
// Shorten paths for readability
|
|
255
|
+
const shortName = (p) => {
|
|
256
|
+
const parts = p.split('/');
|
|
257
|
+
return parts.slice(-2).join('/').replace(/\.[tj]sx?$/, '');
|
|
258
|
+
};
|
|
259
|
+
// Assign layers
|
|
260
|
+
const layerOf = (p) => {
|
|
261
|
+
if (/\.(test|spec)\.[tj]sx?$/.test(p))
|
|
262
|
+
return 'tests';
|
|
263
|
+
if (/\/types?(\/|$)/.test(p) || p.endsWith('.d.ts'))
|
|
264
|
+
return 'types';
|
|
265
|
+
if (/\/models?(\/|$)/.test(p) || /\.(schema|entity|model)\.[tj]sx?$/.test(p))
|
|
266
|
+
return 'models';
|
|
267
|
+
if (/\/routes?(\/|$)/.test(p) || /\.(route|handler|controller)\.[tj]sx?$/.test(p))
|
|
268
|
+
return 'routes';
|
|
269
|
+
if (/\/services?(\/|$)/.test(p) || /\.service\.[tj]sx?$/.test(p))
|
|
270
|
+
return 'services';
|
|
271
|
+
if (/\/config(\/|$)/.test(p) || /\.(config|env)\.[tj]sx?$/.test(p))
|
|
272
|
+
return 'config';
|
|
273
|
+
return 'unknown';
|
|
274
|
+
};
|
|
275
|
+
// Group files by layer for subgraph
|
|
276
|
+
const byLayer = new Map();
|
|
277
|
+
for (const f of files) {
|
|
278
|
+
const layer = layerOf(f);
|
|
279
|
+
const existing = byLayer.get(layer) ?? [];
|
|
280
|
+
existing.push(f);
|
|
281
|
+
byLayer.set(layer, existing);
|
|
282
|
+
}
|
|
283
|
+
// Build node ID map (sanitize for Mermaid)
|
|
284
|
+
const nodeId = (p) => p.replace(/[^a-zA-Z0-9]/g, '_').replace(/__+/g, '_');
|
|
285
|
+
const lines = ['flowchart TD'];
|
|
286
|
+
// Subgraphs per layer
|
|
287
|
+
const layerOrder = ['types', 'models', 'services', 'routes', 'tests', 'config', 'unknown'];
|
|
288
|
+
for (const layer of layerOrder) {
|
|
289
|
+
const layerFiles = byLayer.get(layer);
|
|
290
|
+
if (!layerFiles || layerFiles.length === 0)
|
|
291
|
+
continue;
|
|
292
|
+
lines.push(` subgraph ${layer}`);
|
|
293
|
+
for (const f of layerFiles) {
|
|
294
|
+
const id = nodeId(f);
|
|
295
|
+
const label = shortName(f);
|
|
296
|
+
// High-score files get a highlighted style
|
|
297
|
+
const file = bundle.files.find((bf) => bf.path === f);
|
|
298
|
+
const style = (file?.score ?? 0) >= 0.7 ? `["🔥 ${label}"]` : `["${label}"]`;
|
|
299
|
+
lines.push(` ${id}${style}`);
|
|
300
|
+
}
|
|
301
|
+
lines.push(' end');
|
|
302
|
+
}
|
|
303
|
+
// Edges from file_relationships
|
|
304
|
+
const fileSet = new Set(files);
|
|
305
|
+
for (const rel of relationships) {
|
|
306
|
+
if (fileSet.has(rel.from) && fileSet.has(rel.to)) {
|
|
307
|
+
lines.push(` ${nodeId(rel.from)} --> ${nodeId(rel.to)}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
diagram_type: 'flowchart',
|
|
312
|
+
markup: lines.join('\n'),
|
|
313
|
+
included_files: files,
|
|
314
|
+
generated_at: new Date().toISOString(),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
// ─── context validate ─────────────────────────────────────────────────────────
|
|
318
|
+
async function runContextValidate(cwd, flags) {
|
|
319
|
+
const bundle = loadBundle(cwd, flags['bundle']);
|
|
320
|
+
if (!bundle)
|
|
321
|
+
return;
|
|
322
|
+
const jsonMode = flags['json'] === 'true';
|
|
323
|
+
const forceMode = flags['force'] === 'true';
|
|
324
|
+
// ── ISC — Intent Sufficiency Check (PRE-CONTEXT gate) ─────────────────────
|
|
325
|
+
const taskText = flags['task'] ?? bundle.task ?? '';
|
|
326
|
+
const isc = checkIntentSufficiency(taskText);
|
|
327
|
+
const iscBlocked = isc.decision === 'INSUFFICIENT' && !forceMode;
|
|
328
|
+
if (jsonMode) {
|
|
329
|
+
if (iscBlocked) {
|
|
330
|
+
console.log(JSON.stringify({ isc, ccs: null, validation: null, forced: false }, null, 2));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
printIscSummary(isc, forceMode);
|
|
337
|
+
if (iscBlocked) {
|
|
338
|
+
process.exit(1);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// ── Structural validation ─────────────────────────────────────────────────
|
|
343
|
+
const result = validateContextBundle(bundle, cwd);
|
|
344
|
+
// ── CCS — Context Confidence Score ────────────────────────────────────────
|
|
345
|
+
const taskKeywords = extractTaskKeywords(bundle, taskText);
|
|
346
|
+
const expansionLevel = parseInt(flags['expansion-level'] ?? '1', 10);
|
|
347
|
+
const ledgerEntries = loadEntries(cwd);
|
|
348
|
+
const intentSig = bundle.bundle_intent_hash ?? '';
|
|
349
|
+
const { rate: histRate, sampleSize } = historicalSuccessContext(ledgerEntries, intentSig);
|
|
350
|
+
// WEAK ISC applies a -0.05 CCS penalty via intentConfidence
|
|
351
|
+
const iscIntentConfidence = isc.decision === 'WEAK' ? Math.max(0, isc.score - 0.05) : isc.score;
|
|
352
|
+
const ccs = computeCcs({
|
|
353
|
+
bundle,
|
|
354
|
+
taskKeywords,
|
|
355
|
+
expansionLevel: isNaN(expansionLevel) ? 1 : expansionLevel,
|
|
356
|
+
historicalSuccessRate: histRate,
|
|
357
|
+
historySampleSize: sampleSize,
|
|
358
|
+
intentConfidence: iscIntentConfidence,
|
|
359
|
+
});
|
|
360
|
+
// Determine final gate decision (--force overrides BLOCK)
|
|
361
|
+
const blocked = !result.passed || ccs.recommendation === 'block';
|
|
362
|
+
const shouldExit = blocked && !forceMode;
|
|
363
|
+
if (jsonMode) {
|
|
364
|
+
console.log(JSON.stringify({ isc, validation: result, ccs, forced: forceMode && blocked }, null, 2));
|
|
365
|
+
if (shouldExit)
|
|
366
|
+
process.exit(1);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const validIcon = result.passed ? '✅' : '❌';
|
|
370
|
+
console.log(`\n${validIcon} Context Validation — Bundle ${bundle.bundle_id}\n`);
|
|
371
|
+
// ── CCS Summary ────────────────────────────────────────────────────────────
|
|
372
|
+
printCcsSummary(ccs, forceMode);
|
|
373
|
+
// ── Validation Issues ──────────────────────────────────────────────────────
|
|
374
|
+
if (result.issues.length === 0) {
|
|
375
|
+
console.log(' No structural issues found.\n');
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
const errors = result.issues.filter((i) => i.severity === 'error');
|
|
379
|
+
const warnings = result.issues.filter((i) => i.severity === 'warning');
|
|
380
|
+
const infos = result.issues.filter((i) => i.severity === 'info');
|
|
381
|
+
if (errors.length > 0) {
|
|
382
|
+
console.log(` Errors (${errors.length}):`);
|
|
383
|
+
for (const e of errors)
|
|
384
|
+
printIssue(e, ' ❌');
|
|
385
|
+
}
|
|
386
|
+
if (warnings.length > 0) {
|
|
387
|
+
console.log(` Warnings (${warnings.length}):`);
|
|
388
|
+
for (const w of warnings)
|
|
389
|
+
printIssue(w, ' ⚠️');
|
|
390
|
+
}
|
|
391
|
+
if (infos.length > 0) {
|
|
392
|
+
console.log(` Info (${infos.length}):`);
|
|
393
|
+
for (const i of infos)
|
|
394
|
+
printIssue(i, ' ℹ️');
|
|
395
|
+
}
|
|
396
|
+
console.log('');
|
|
397
|
+
}
|
|
398
|
+
if (result.missing_dependencies.length > 0) {
|
|
399
|
+
console.log(` Missing dependencies (files not in context):`);
|
|
400
|
+
for (const d of result.missing_dependencies)
|
|
401
|
+
console.log(` • ${d}`);
|
|
402
|
+
console.log('');
|
|
403
|
+
}
|
|
404
|
+
if (result.uncovered_files.length > 0) {
|
|
405
|
+
console.log(` Files without test coverage:`);
|
|
406
|
+
for (const f of result.uncovered_files)
|
|
407
|
+
console.log(` • ${f}`);
|
|
408
|
+
console.log('');
|
|
409
|
+
}
|
|
410
|
+
if (result.boundary_violations.length > 0) {
|
|
411
|
+
console.log(` Architecture boundary violations:`);
|
|
412
|
+
for (const v of result.boundary_violations) {
|
|
413
|
+
console.log(` ${v.from} → ${v.to}: ${v.reason}`);
|
|
414
|
+
}
|
|
415
|
+
console.log('');
|
|
416
|
+
}
|
|
417
|
+
if (shouldExit)
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
function printIscSummary(isc, forced = false) {
|
|
421
|
+
const icon = isc.decision === 'SUFFICIENT'
|
|
422
|
+
? '🟢'
|
|
423
|
+
: isc.decision === 'WEAK'
|
|
424
|
+
? '🟡'
|
|
425
|
+
: forced
|
|
426
|
+
? '🔴'
|
|
427
|
+
: '🔴';
|
|
428
|
+
const pct = (isc.score * 100).toFixed(0);
|
|
429
|
+
const decisionLabel = isc.decision === 'INSUFFICIENT' && forced
|
|
430
|
+
? 'INSUFFICIENT (overridden by --force)'
|
|
431
|
+
: isc.decision;
|
|
432
|
+
console.log(` ── Intent Sufficiency Check (ISC) ───────────────────────`);
|
|
433
|
+
console.log(` ${icon} Score: ${pct}% Decision: ${decisionLabel}`);
|
|
434
|
+
const f = isc.factors;
|
|
435
|
+
console.log(` token signal : ${(f.tokenSignalScore * 100).toFixed(0)}%` +
|
|
436
|
+
(isc.detectedOperation ? ` op: ${isc.detectedOperation}` : ''));
|
|
437
|
+
console.log(` operation clarity: ${(f.operationClarityScore * 100).toFixed(0)}%` +
|
|
438
|
+
(isc.detectedDomain ? ` domain: ${isc.detectedDomain}` : ''));
|
|
439
|
+
console.log(` domain clarity : ${(f.domainClarityScore * 100).toFixed(0)}%`);
|
|
440
|
+
console.log(` target specificity: ${(f.targetSpecificityScore * 100).toFixed(0)}%` +
|
|
441
|
+
(isc.detectedTargets && isc.detectedTargets.length > 0
|
|
442
|
+
? ` targets: ${isc.detectedTargets.slice(0, 3).join(', ')}`
|
|
443
|
+
: ''));
|
|
444
|
+
console.log(` constraint present: ${(f.constraintPresenceScore * 100).toFixed(0)}%`);
|
|
445
|
+
if (isc.issues.length > 0) {
|
|
446
|
+
console.log(`\n ISC issues:`);
|
|
447
|
+
for (const issue of isc.issues)
|
|
448
|
+
console.log(` • ${issue}`);
|
|
449
|
+
}
|
|
450
|
+
if (isc.decision === 'INSUFFICIENT' && !forced) {
|
|
451
|
+
console.log(`\n Context construction blocked. Improve your task prompt:`);
|
|
452
|
+
for (const rec of isc.recommendations)
|
|
453
|
+
console.log(` → ${rec}`);
|
|
454
|
+
console.log(`\n Use --force to bypass this gate.\n`);
|
|
455
|
+
}
|
|
456
|
+
else if (isc.decision === 'WEAK') {
|
|
457
|
+
console.log(`\n Weak intent: CCS receives a -0.05 penalty. Consider refining your prompt:`);
|
|
458
|
+
for (const rec of isc.recommendations)
|
|
459
|
+
console.log(` → ${rec}`);
|
|
460
|
+
}
|
|
461
|
+
console.log('');
|
|
462
|
+
}
|
|
463
|
+
function printCcsSummary(ccs, forced = false) {
|
|
464
|
+
const pct = (ccs.score * 100).toFixed(0);
|
|
465
|
+
const levelIcon = ccs.level === 'high' ? '🟢' : ccs.level === 'medium' ? '🟡' : '🔴';
|
|
466
|
+
const decisionLabel = {
|
|
467
|
+
proceed: 'PROCEED',
|
|
468
|
+
expand: `EXPAND → suggest Level ${ccs.suggestedExpansionLevel ?? '?'}`,
|
|
469
|
+
block: forced ? 'BLOCK (overridden by --force)' : 'BLOCK',
|
|
470
|
+
};
|
|
471
|
+
console.log(` ── Context Confidence Score (CCS) ──────────────────────`);
|
|
472
|
+
console.log(` ${levelIcon} Score: ${pct}% Decision: ${decisionLabel[ccs.recommendation] ?? ccs.recommendation.toUpperCase()}`);
|
|
473
|
+
console.log(` dep coverage : ${(ccs.factors.dependencyCoverage * 100).toFixed(0)}%`);
|
|
474
|
+
console.log(` test coverage : ${(ccs.factors.testCoverageMapping * 100).toFixed(0)}%`);
|
|
475
|
+
// Historical confidence: show sample size + effective weight
|
|
476
|
+
const histPct = (ccs.factors.historicalSuccessRate * 100).toFixed(0);
|
|
477
|
+
const weightPct = (ccs.factors.adjustedHistoricalWeight * 100 / 0.20).toFixed(0); // as % of max
|
|
478
|
+
const sampleLabel = ccs.factors.historySampleSize === 0
|
|
479
|
+
? 'no data'
|
|
480
|
+
: `${ccs.factors.historySampleSize} entries, weight ${weightPct}% of full`;
|
|
481
|
+
console.log(` hist success : ${histPct}% (${sampleLabel})`);
|
|
482
|
+
console.log(` symbol comp. : ${(ccs.factors.symbolCompleteness * 100).toFixed(0)}%`);
|
|
483
|
+
console.log(` expansion eff.: ${(100 - ccs.factors.expansionLevelPenalty * 100 / 0.3).toFixed(0)}%`);
|
|
484
|
+
if (ccs.issues.length > 0) {
|
|
485
|
+
console.log(`\n CCS issues:`);
|
|
486
|
+
for (const issue of ccs.issues)
|
|
487
|
+
console.log(` • ${issue}`);
|
|
488
|
+
}
|
|
489
|
+
console.log('');
|
|
490
|
+
}
|
|
491
|
+
/** Extract task keywords from bundle task description or explicit --task flag. */
|
|
492
|
+
function extractTaskKeywords(bundle, taskOverride) {
|
|
493
|
+
const raw = taskOverride || bundle.task || '';
|
|
494
|
+
if (!raw)
|
|
495
|
+
return [];
|
|
496
|
+
// Simple tokenization: split on whitespace, strip punctuation, keep ≥3 chars
|
|
497
|
+
return raw
|
|
498
|
+
.toLowerCase()
|
|
499
|
+
.split(/\s+/)
|
|
500
|
+
.map((t) => t.replace(/[^a-z0-9_-]/g, ''))
|
|
501
|
+
.filter((t) => t.length >= 3);
|
|
502
|
+
}
|
|
503
|
+
function printIssue(issue, prefix) {
|
|
504
|
+
const location = issue.file ? ` [${issue.file}${issue.symbol ? `::${issue.symbol}` : ''}]` : '';
|
|
505
|
+
console.log(` ${prefix} [${issue.code}]${location}: ${issue.message}`);
|
|
506
|
+
}
|
|
507
|
+
function validateContextBundle(bundle, cwd) {
|
|
508
|
+
const issues = [];
|
|
509
|
+
const missingDependencies = [];
|
|
510
|
+
const uncoveredFiles = [];
|
|
511
|
+
const boundaryViolations = [];
|
|
512
|
+
const bundlePaths = new Set(bundle.files.map((f) => f.path));
|
|
513
|
+
// 1. Check for missing dependencies (imported files not in bundle)
|
|
514
|
+
if (bundle.file_relationships) {
|
|
515
|
+
for (const rel of bundle.file_relationships) {
|
|
516
|
+
if (!bundlePaths.has(rel.to)) {
|
|
517
|
+
missingDependencies.push(rel.to);
|
|
518
|
+
issues.push({
|
|
519
|
+
code: 'MISSING_DEP',
|
|
520
|
+
severity: 'warning',
|
|
521
|
+
message: `Dependency ${rel.to} is imported but not in context`,
|
|
522
|
+
file: rel.from,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
// 2. Check for files without test coverage
|
|
528
|
+
const testMap = bundle.files.filter((f) => f.path.match(/\.(test|spec)\.[tj]sx?$/));
|
|
529
|
+
const testedSources = new Set();
|
|
530
|
+
for (const t of testMap) {
|
|
531
|
+
// Infer source from test file name
|
|
532
|
+
const source = t.path.replace(/\.(test|spec)\./, '.').replace(/\/__tests__\//, '/');
|
|
533
|
+
testedSources.add(source);
|
|
534
|
+
}
|
|
535
|
+
for (const f of bundle.files) {
|
|
536
|
+
if (!f.path.match(/\.(test|spec)\.[tj]sx?$/) && !f.is_stub && !f.is_exemplar) {
|
|
537
|
+
if (!testedSources.has(f.path) && !f.reasons.includes('test_relevant')) {
|
|
538
|
+
uncoveredFiles.push(f.path);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (uncoveredFiles.length > bundle.files.length * 0.5) {
|
|
543
|
+
issues.push({
|
|
544
|
+
code: 'LOW_TEST_COVERAGE',
|
|
545
|
+
severity: 'warning',
|
|
546
|
+
message: `${uncoveredFiles.length}/${bundle.files.length} context files have no linked tests`,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
// 3. Check token budget
|
|
550
|
+
const maxTokens = 16000;
|
|
551
|
+
if (bundle.total_tokens > maxTokens) {
|
|
552
|
+
issues.push({
|
|
553
|
+
code: 'BUDGET_EXCEEDED',
|
|
554
|
+
severity: 'warning',
|
|
555
|
+
message: `Context is ${bundle.total_tokens.toLocaleString()} tokens (recommend ≤ ${maxTokens.toLocaleString()})`,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
// 4. Check confidence (if available)
|
|
559
|
+
if (bundle.confidence && bundle.confidence.level === 'low') {
|
|
560
|
+
issues.push({
|
|
561
|
+
code: 'LOW_CONFIDENCE',
|
|
562
|
+
severity: 'warning',
|
|
563
|
+
message: `Bundle confidence is LOW (${(bundle.confidence.score * 100).toFixed(0)}%). Consider running with --expand.`,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
// 5. Check for architecture boundary violations
|
|
567
|
+
// (test files importing from non-test files at the wrong layer)
|
|
568
|
+
const layerOf = (p) => {
|
|
569
|
+
if (/\.(test|spec)\.[tj]sx?$/.test(p))
|
|
570
|
+
return 'tests';
|
|
571
|
+
if (/\/types?(\/|$)/.test(p))
|
|
572
|
+
return 'types';
|
|
573
|
+
if (/\/routes?(\/|$)/.test(p))
|
|
574
|
+
return 'routes';
|
|
575
|
+
if (/\/services?(\/|$)/.test(p))
|
|
576
|
+
return 'services';
|
|
577
|
+
return 'impl';
|
|
578
|
+
};
|
|
579
|
+
if (bundle.file_relationships) {
|
|
580
|
+
for (const rel of bundle.file_relationships) {
|
|
581
|
+
const fromLayer = layerOf(rel.from);
|
|
582
|
+
const toLayer = layerOf(rel.to);
|
|
583
|
+
// Routes should not import from tests
|
|
584
|
+
if (fromLayer === 'routes' && toLayer === 'tests') {
|
|
585
|
+
boundaryViolations.push({
|
|
586
|
+
from: rel.from,
|
|
587
|
+
to: rel.to,
|
|
588
|
+
reason: 'Route file imports from test file',
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// 6. Check for stale/addressed files
|
|
594
|
+
if (bundle.addressed_files && bundle.addressed_files.length > 0) {
|
|
595
|
+
const ratio = bundle.addressed_files.length / bundle.files.length;
|
|
596
|
+
if (ratio > 0.5) {
|
|
597
|
+
issues.push({
|
|
598
|
+
code: 'STALE_BUNDLE',
|
|
599
|
+
severity: 'info',
|
|
600
|
+
message: `${bundle.addressed_files.length} of ${bundle.files.length} bundle files have been committed. Consider refreshing: codeledger activate --task "..."`,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Read existence checks only if cwd is provided
|
|
605
|
+
if (cwd) {
|
|
606
|
+
for (const f of bundle.files) {
|
|
607
|
+
const abs = join(cwd, f.path);
|
|
608
|
+
if (!existsSync(abs)) {
|
|
609
|
+
issues.push({
|
|
610
|
+
code: 'FILE_NOT_FOUND',
|
|
611
|
+
severity: 'error',
|
|
612
|
+
message: `Bundle file no longer exists on disk`,
|
|
613
|
+
file: f.path,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const hasErrors = issues.some((i) => i.severity === 'error');
|
|
619
|
+
return {
|
|
620
|
+
passed: !hasErrors,
|
|
621
|
+
issues,
|
|
622
|
+
missing_dependencies: [...new Set(missingDependencies)],
|
|
623
|
+
uncovered_files: uncoveredFiles,
|
|
624
|
+
boundary_violations: boundaryViolations,
|
|
625
|
+
validated_at: new Date().toISOString(),
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
// ─── Shared Utilities ─────────────────────────────────────────────────────────
|
|
629
|
+
function loadBundle(cwd, bundlePath) {
|
|
630
|
+
// If explicit path provided
|
|
631
|
+
if (bundlePath) {
|
|
632
|
+
if (!existsSync(bundlePath)) {
|
|
633
|
+
console.error(`❌ Bundle file not found: ${bundlePath}`);
|
|
634
|
+
process.exit(1);
|
|
635
|
+
}
|
|
636
|
+
try {
|
|
637
|
+
return JSON.parse(readFileSync(bundlePath, 'utf-8'));
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
console.error(`❌ Failed to parse bundle: ${bundlePath}`);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
// Try latest artifact in .codeledger/artifacts/bundles/ then artifacts/
|
|
645
|
+
const artifactsDir = join(cwd, '.codeledger', 'artifacts');
|
|
646
|
+
const bundlesDir = join(artifactsDir, 'bundles');
|
|
647
|
+
const bundlesDirResult = loadLatestBundles(bundlesDir, 1);
|
|
648
|
+
const bundles = bundlesDirResult.length > 0
|
|
649
|
+
? bundlesDirResult
|
|
650
|
+
: loadLatestBundles(artifactsDir, 1);
|
|
651
|
+
if (bundles.length > 0)
|
|
652
|
+
return bundles[0];
|
|
653
|
+
// Try active-bundle.md (extract JSON if present)
|
|
654
|
+
const activePath = join(cwd, '.codeledger', 'active-bundle.md');
|
|
655
|
+
if (!existsSync(activePath)) {
|
|
656
|
+
console.error('❌ No active bundle found. Run: codeledger activate --task "..."');
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
console.error('⚠️ Active bundle is in Markdown format. Use --bundle <json-path> for full data.');
|
|
660
|
+
console.error(' JSON bundles are written to .codeledger/artifacts/bundles/ by default.');
|
|
661
|
+
process.exit(1);
|
|
662
|
+
}
|
|
663
|
+
function loadLatestBundles(artifactsDir, limit) {
|
|
664
|
+
if (!existsSync(artifactsDir))
|
|
665
|
+
return [];
|
|
666
|
+
try {
|
|
667
|
+
const files = readdirSync(artifactsDir)
|
|
668
|
+
.filter((f) => f.endsWith('.json') && !f.includes('policy') && !f.includes('manifest'))
|
|
669
|
+
.map((f) => ({ name: f, mtime: statSync(join(artifactsDir, f)).mtime.getTime() }))
|
|
670
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
671
|
+
.slice(0, limit)
|
|
672
|
+
.map((f) => {
|
|
673
|
+
try {
|
|
674
|
+
return JSON.parse(readFileSync(join(artifactsDir, f.name), 'utf-8'));
|
|
675
|
+
}
|
|
676
|
+
catch {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
})
|
|
680
|
+
.filter((b) => b !== null);
|
|
681
|
+
return files;
|
|
682
|
+
}
|
|
683
|
+
catch {
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
//# sourceMappingURL=context.js.map
|