fluffy-context 0.7.6 → 0.8.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/README.md +115 -36
- package/dist/src/agent/api.d.ts +49 -4
- package/dist/src/agent/api.js +183 -131
- package/dist/src/agent/index.d.ts +7 -1
- package/dist/src/agent/index.js +1 -0
- package/dist/src/cli/main.js +301 -15
- package/dist/src/cognition/evidence-graph.d.ts +4 -0
- package/dist/src/cognition/evidence-graph.js +187 -0
- package/dist/src/cognition/evolution.d.ts +36 -0
- package/dist/src/cognition/evolution.js +221 -0
- package/dist/src/cognition/journal.js +143 -2
- package/dist/src/cognition/projections.js +124 -2
- package/dist/src/cognition/recipes.d.ts +25 -0
- package/dist/src/cognition/recipes.js +149 -0
- package/dist/src/cognition/skills.d.ts +45 -0
- package/dist/src/cognition/skills.js +184 -0
- package/dist/src/cognition/types.d.ts +196 -2
- package/dist/src/cognition/usage-report.d.ts +40 -0
- package/dist/src/cognition/usage-report.js +232 -0
- package/dist/src/cognition/usage.d.ts +5 -0
- package/dist/src/cognition/usage.js +48 -0
- package/dist/src/compiler/compile.js +73 -4
- package/dist/src/compiler/graph.d.ts +7 -0
- package/dist/src/compiler/graph.js +117 -0
- package/dist/src/compiler/identity.d.ts +1 -1
- package/dist/src/compiler/identity.js +3 -1
- package/dist/src/compiler/sources.d.ts +3 -1
- package/dist/src/compiler/sources.js +11 -2
- package/dist/src/compiler/types.d.ts +23 -2
- package/dist/src/mcp/server.js +228 -6
- package/dist/src/storage/layout.d.ts +5 -0
- package/dist/src/storage/layout.js +15 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
- package/skills/fluffy-context/SKILL.md +53 -5
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { resolveProjectRoot } from '../project/project-resolver.js';
|
|
3
|
+
import { readJson, isRecord } from '../storage/json-store.js';
|
|
4
|
+
import { contextsRoot, contextMetadataPath, snapshotsDirectory } from '../storage/layout.js';
|
|
5
|
+
import { readJournal } from './journal.js';
|
|
6
|
+
import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
|
|
7
|
+
import { listNotes } from '../runtime/notes.js';
|
|
8
|
+
function metric(value, quality, unit, method) {
|
|
9
|
+
return { value, quality, unit, ...(method ? { method } : {}) };
|
|
10
|
+
}
|
|
11
|
+
function iso(value) {
|
|
12
|
+
const time = Date.parse(value);
|
|
13
|
+
if (!Number.isFinite(time))
|
|
14
|
+
throw new Error(`invalid ISO date: ${value}`);
|
|
15
|
+
return new Date(time).toISOString();
|
|
16
|
+
}
|
|
17
|
+
function windowOf(options, now = new Date()) {
|
|
18
|
+
if (options.month && !/^\d{4}-\d{2}$/.test(options.month))
|
|
19
|
+
throw new Error('--month must be YYYY-MM');
|
|
20
|
+
if (options.since && options.month)
|
|
21
|
+
throw new Error('--since and --month cannot be combined');
|
|
22
|
+
if (options.days !== undefined && (!Number.isInteger(options.days) || options.days < 0))
|
|
23
|
+
throw new Error('--days must be a non-negative integer');
|
|
24
|
+
const to = options.until ? iso(options.until) : now.toISOString();
|
|
25
|
+
const toMs = Date.parse(to);
|
|
26
|
+
if (options.month) {
|
|
27
|
+
const [year, month] = options.month.split('-').map(Number);
|
|
28
|
+
const from = new Date(Date.UTC(year, month - 1, 1));
|
|
29
|
+
const end = new Date(Date.UTC(year, month, 1));
|
|
30
|
+
if (month < 1 || month > 12 || end.getUTCMonth() !== month % 12)
|
|
31
|
+
throw new Error('--month must be a valid calendar month');
|
|
32
|
+
return { from: from.toISOString(), to: end.toISOString(), kind: 'calendar-month', granularity: options.granularity ?? 'week' };
|
|
33
|
+
}
|
|
34
|
+
const from = options.since ? iso(options.since) : new Date(toMs - (options.days ?? 30) * 86_400_000).toISOString();
|
|
35
|
+
if (Date.parse(from) > toMs)
|
|
36
|
+
throw new Error('--since must not be after --until');
|
|
37
|
+
return { from, to, kind: options.since || options.until ? 'explicit' : 'rolling', granularity: options.granularity ?? 'day' };
|
|
38
|
+
}
|
|
39
|
+
function inWindow(event, window) {
|
|
40
|
+
const time = Date.parse(event.occurredAt);
|
|
41
|
+
return time >= Date.parse(window.from) && time < Date.parse(window.to);
|
|
42
|
+
}
|
|
43
|
+
function dayStart(value, granularity) {
|
|
44
|
+
const date = new Date(value);
|
|
45
|
+
if (granularity === 'month')
|
|
46
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1)).toISOString();
|
|
47
|
+
if (granularity === 'week') {
|
|
48
|
+
const day = date.getUTCDay();
|
|
49
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() - (day === 0 ? 6 : day - 1))).toISOString();
|
|
50
|
+
}
|
|
51
|
+
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())).toISOString();
|
|
52
|
+
}
|
|
53
|
+
async function contexts(projectRoot) {
|
|
54
|
+
let ids = [];
|
|
55
|
+
try {
|
|
56
|
+
ids = await readdir(contextsRoot(projectRoot));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return { items: [], snapshots: 0 };
|
|
60
|
+
}
|
|
61
|
+
const items = [];
|
|
62
|
+
let snapshots = 0;
|
|
63
|
+
for (const id of ids) {
|
|
64
|
+
try {
|
|
65
|
+
const item = await readJson(contextMetadataPath(projectRoot, id), (value) => isRecord(value) && value.schemaVersion === 1 && value.id === id);
|
|
66
|
+
items.push(item);
|
|
67
|
+
snapshots += (await readdir(snapshotsDirectory(projectRoot, id))).filter((file) => file.endsWith('.json')).length;
|
|
68
|
+
}
|
|
69
|
+
catch { /* invalid files are reported by doctor */ }
|
|
70
|
+
}
|
|
71
|
+
return { items, snapshots };
|
|
72
|
+
}
|
|
73
|
+
function usagePayload(event) {
|
|
74
|
+
if (event.type !== 'usage.operation.completed' || !isRecord(event.payload))
|
|
75
|
+
return null;
|
|
76
|
+
const payload = event.payload;
|
|
77
|
+
return (payload.operation === 'orient' || payload.operation === 'compile' || payload.operation === 'expand' || payload.operation === 'resume')
|
|
78
|
+
&& (payload.interface === 'cli' || payload.interface === 'mcp' || payload.interface === 'agent' || payload.interface === 'hook')
|
|
79
|
+
&& typeof payload.success === 'boolean'
|
|
80
|
+
&& typeof payload.durationMs === 'number' && Number.isFinite(payload.durationMs) && payload.durationMs >= 0
|
|
81
|
+
&& typeof payload.inputChars === 'number' && Number.isFinite(payload.inputChars) && payload.inputChars >= 0
|
|
82
|
+
&& typeof payload.outputChars === 'number' && Number.isFinite(payload.outputChars) && payload.outputChars >= 0
|
|
83
|
+
&& typeof payload.inputBytes === 'number' && Number.isFinite(payload.inputBytes) && payload.inputBytes >= 0
|
|
84
|
+
&& typeof payload.outputBytes === 'number' && Number.isFinite(payload.outputBytes) && payload.outputBytes >= 0
|
|
85
|
+
&& typeof payload.estimatedInputTokens === 'number' && Number.isFinite(payload.estimatedInputTokens) && payload.estimatedInputTokens >= 0
|
|
86
|
+
&& typeof payload.estimatedOutputTokens === 'number' && Number.isFinite(payload.estimatedOutputTokens) && payload.estimatedOutputTokens >= 0
|
|
87
|
+
&& payload.estimatedMethod === 'chars-div-four'
|
|
88
|
+
? payload : null;
|
|
89
|
+
}
|
|
90
|
+
function percentile(values, fraction) {
|
|
91
|
+
if (values.length === 0)
|
|
92
|
+
return null;
|
|
93
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
94
|
+
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1)];
|
|
95
|
+
}
|
|
96
|
+
function operationStats(events) {
|
|
97
|
+
const groups = new Map();
|
|
98
|
+
for (const event of events) {
|
|
99
|
+
const payload = usagePayload(event);
|
|
100
|
+
if (!payload)
|
|
101
|
+
continue;
|
|
102
|
+
const key = payload.operation;
|
|
103
|
+
groups.set(key, [...(groups.get(key) ?? []), payload]);
|
|
104
|
+
}
|
|
105
|
+
const byOperation = {};
|
|
106
|
+
for (const [operation, values] of groups) {
|
|
107
|
+
const durations = values.map((item) => item.durationMs);
|
|
108
|
+
byOperation[operation] = {
|
|
109
|
+
calls: values.length,
|
|
110
|
+
successes: values.filter((item) => item.success).length,
|
|
111
|
+
failures: values.filter((item) => !item.success).length,
|
|
112
|
+
successRate: values.filter((item) => item.success).length / values.length,
|
|
113
|
+
errorRate: values.filter((item) => !item.success).length / values.length,
|
|
114
|
+
durationMs: { average: durations.reduce((sum, value) => sum + value, 0) / values.length, p50: percentile(durations, 0.5), p95: percentile(durations, 0.95), max: Math.max(...durations) },
|
|
115
|
+
inputChars: values.reduce((sum, item) => sum + item.inputChars, 0),
|
|
116
|
+
outputChars: values.reduce((sum, item) => sum + item.outputChars, 0),
|
|
117
|
+
inputBytes: values.reduce((sum, item) => sum + item.inputBytes, 0),
|
|
118
|
+
outputBytes: values.reduce((sum, item) => sum + item.outputBytes, 0),
|
|
119
|
+
estimatedInputTokens: values.reduce((sum, item) => sum + item.estimatedInputTokens, 0),
|
|
120
|
+
estimatedOutputTokens: values.reduce((sum, item) => sum + item.estimatedOutputTokens, 0),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return byOperation;
|
|
124
|
+
}
|
|
125
|
+
export async function buildUsageReport(startPath, options = {}) {
|
|
126
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
127
|
+
const window = windowOf(options);
|
|
128
|
+
let events = [];
|
|
129
|
+
let journalVerified = true;
|
|
130
|
+
try {
|
|
131
|
+
events = await readJournal(projectRoot);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (!(error instanceof Error && /ENOENT|manifest|not initialized/i.test(error.message)))
|
|
135
|
+
throw error;
|
|
136
|
+
journalVerified = false;
|
|
137
|
+
}
|
|
138
|
+
const scoped = events.filter((event) => inWindow(event, window));
|
|
139
|
+
const usageEvents = scoped.map((event) => ({ event, payload: usagePayload(event) })).filter((item) => item.payload !== null);
|
|
140
|
+
const knowledge = await listKnowledge(projectRoot, true);
|
|
141
|
+
const deadends = await listDeadends(projectRoot, true);
|
|
142
|
+
const notes = await listNotes(projectRoot, { limit: Number.MAX_SAFE_INTEGER, maxChars: 0 });
|
|
143
|
+
const contextData = await contexts(projectRoot);
|
|
144
|
+
const used = new Map();
|
|
145
|
+
for (const event of events.filter((item) => item.type === 'record.used')) {
|
|
146
|
+
const payload = event.payload;
|
|
147
|
+
if (typeof payload.entity === 'string' && typeof payload.entityId === 'string')
|
|
148
|
+
used.set(`${payload.entity}:${payload.entityId}`, (used.get(`${payload.entity}:${payload.entityId}`) ?? 0) + 1);
|
|
149
|
+
}
|
|
150
|
+
const verified = [...knowledge.filter((item) => item.status === 'verified').map((item) => `knowledge:${item.knowledgeId}`), ...deadends.filter((item) => item.status === 'verified').map((item) => `deadend:${item.deadendId}`)];
|
|
151
|
+
const usedVerified = verified.filter((key) => (used.get(key) ?? 0) > 0).length;
|
|
152
|
+
const recordUsed = scoped.filter((event) => event.type === 'record.used').length;
|
|
153
|
+
const lifecycle = (type) => scoped.filter((event) => event.type === type).length;
|
|
154
|
+
const buckets = new Map();
|
|
155
|
+
for (const event of scoped) {
|
|
156
|
+
const key = dayStart(event.occurredAt, window.granularity);
|
|
157
|
+
const bucket = buckets.get(key) ?? { operations: 0, usageOperations: 0, recordsUsed: 0, outputChars: 0, estimatedOutputTokens: 0, knowledgeCreated: 0, knowledgeVerified: 0, deadendsCreated: 0, deadendsVerified: 0 };
|
|
158
|
+
const payload = usagePayload(event);
|
|
159
|
+
if (payload) {
|
|
160
|
+
bucket.operations += 1;
|
|
161
|
+
bucket.usageOperations += 1;
|
|
162
|
+
bucket.outputChars += payload.outputChars;
|
|
163
|
+
bucket.estimatedOutputTokens += payload.estimatedOutputTokens;
|
|
164
|
+
}
|
|
165
|
+
if (event.type === 'record.used')
|
|
166
|
+
bucket.recordsUsed += 1;
|
|
167
|
+
if (event.type === 'knowledge.proposed')
|
|
168
|
+
bucket.knowledgeCreated += 1;
|
|
169
|
+
if (event.type === 'knowledge.verified')
|
|
170
|
+
bucket.knowledgeVerified += 1;
|
|
171
|
+
if (event.type === 'deadend.proposed')
|
|
172
|
+
bucket.deadendsCreated += 1;
|
|
173
|
+
if (event.type === 'deadend.verified')
|
|
174
|
+
bucket.deadendsVerified += 1;
|
|
175
|
+
buckets.set(key, bucket);
|
|
176
|
+
}
|
|
177
|
+
const ctxTokens = usageEvents.reduce((sum, item) => sum + item.payload.estimatedOutputTokens, 0);
|
|
178
|
+
const baseline = options.baselineTokens ?? null;
|
|
179
|
+
const savings = baseline === null ? null : baseline - ctxTokens;
|
|
180
|
+
const investment = metric(ctxTokens, 'estimated', 'tokens', 'chars-div-four');
|
|
181
|
+
const rawAssets = verified.length;
|
|
182
|
+
const usedCount = verified.reduce((sum, key) => sum + Math.min(used.get(key) ?? 0, 3), 0);
|
|
183
|
+
const trendEntries = [...buckets.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
184
|
+
let cumulativeInvestmentTokens = 0;
|
|
185
|
+
let cumulativeOutputChars = 0;
|
|
186
|
+
let paybackPeriodStart = null;
|
|
187
|
+
const trends = trendEntries.map(([periodStart, values]) => {
|
|
188
|
+
cumulativeInvestmentTokens += values.estimatedOutputTokens;
|
|
189
|
+
cumulativeOutputChars += values.outputChars;
|
|
190
|
+
const cumulativeSavings = baseline === null ? null : baseline - cumulativeInvestmentTokens;
|
|
191
|
+
if (paybackPeriodStart === null && cumulativeSavings !== null && cumulativeSavings >= cumulativeInvestmentTokens)
|
|
192
|
+
paybackPeriodStart = periodStart;
|
|
193
|
+
return {
|
|
194
|
+
periodStart,
|
|
195
|
+
...values,
|
|
196
|
+
cumulativeOutputChars,
|
|
197
|
+
cumulativeEstimatedOutputTokens: cumulativeInvestmentTokens,
|
|
198
|
+
cumulativeEstimatedSavings: cumulativeSavings,
|
|
199
|
+
};
|
|
200
|
+
});
|
|
201
|
+
const paybackReached = paybackPeriodStart !== null;
|
|
202
|
+
const paybackPeriodDays = paybackPeriodStart === null ? null : Math.max(0, (Date.parse(paybackPeriodStart) - Date.parse(window.from)) / 86_400_000);
|
|
203
|
+
return {
|
|
204
|
+
schemaVersion: 1, generatedAt: new Date().toISOString(), projectRoot, window,
|
|
205
|
+
dataQuality: { journalVerified, journalEventCount: events.length, windowEventCount: scoped.length, usageEventCount: usageEvents.length, firstUsageAt: usageEvents[0]?.event.occurredAt ?? null, lastUsageAt: usageEvents.at(-1)?.event.occurredAt ?? null, measuredMetricCount: usageEvents.length * 7, estimatedMetricCount: usageEvents.length * 2 + (ctxTokens > 0 ? 1 : 0), unavailableMetricCount: 1 },
|
|
206
|
+
assets: {
|
|
207
|
+
knowledge: { total: knowledge.length, candidate: knowledge.filter((item) => item.status === 'candidate').length, verified: knowledge.filter((item) => item.status === 'verified').length, retired: knowledge.filter((item) => ['deprecated', 'rejected'].includes(item.status)).length, created: lifecycle('knowledge.proposed'), verifiedInWindow: lifecycle('knowledge.verified') },
|
|
208
|
+
deadends: { total: deadends.length, candidate: deadends.filter((item) => item.status === 'candidate').length, verified: deadends.filter((item) => item.status === 'verified').length, retired: deadends.filter((item) => ['obsolete', 'rejected'].includes(item.status)).length, created: lifecycle('deadend.proposed'), verifiedInWindow: lifecycle('deadend.verified') },
|
|
209
|
+
notes: { total: notes.length, createdInWindow: lifecycle('note.recorded') }, contexts: { total: contextData.items.length, active: contextData.items.filter((item) => ['active', 'stable'].includes(item.status)).length, snapshots: contextData.snapshots }, rawVerifiedAssetCount: rawAssets, effectiveAssetPoints: metric(rawAssets + usedCount * 0.25, 'derived', 'asset-points'), assumptions: { reuseBonusPerUse: 0.25, maxBonusUses: 3 },
|
|
210
|
+
},
|
|
211
|
+
reuse: { recordUsedInWindow: recordUsed, recordUsedTotal: events.filter((event) => event.type === 'record.used').length, usedVerifiedAssetCount: usedVerified, verifiedAssetReuseRate: metric(rawAssets === 0 ? null : usedVerified / rawAssets, 'derived', 'ratio') },
|
|
212
|
+
investment: { contextOutputTokens: investment, providerTokens: metric(null, 'unavailable', 'tokens') },
|
|
213
|
+
payback: {
|
|
214
|
+
baselineTokens: baseline,
|
|
215
|
+
baselineSource: options.baselineSource ?? null,
|
|
216
|
+
estimatedSavings: savings === null ? metric(null, 'unavailable', 'tokens') : metric(savings, 'estimated', 'tokens'),
|
|
217
|
+
estimatedROI: baseline === null || ctxTokens === 0 ? null : savings / ctxTokens,
|
|
218
|
+
status: baseline === null ? 'no_baseline' : paybackReached ? 'reached' : 'not_reached',
|
|
219
|
+
paybackAt: paybackPeriodStart,
|
|
220
|
+
paybackPeriodDays,
|
|
221
|
+
},
|
|
222
|
+
operations: { available: usageEvents.length > 0, byOperation: operationStats(scoped), reason: usageEvents.length > 0 ? null : 'operation output telemetry is not yet recorded' },
|
|
223
|
+
trends,
|
|
224
|
+
warnings: [...(!journalVerified ? ['v2 journal unavailable; lifecycle metrics may be incomplete'] : []), 'Provider token usage is unavailable', ...(usageEvents.length === 0 ? ['Token investment is unavailable until operation output telemetry is recorded'] : [])],
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
export function formatUsageReport(report) {
|
|
228
|
+
const assets = report.assets;
|
|
229
|
+
const payback = report.payback;
|
|
230
|
+
const lines = ['fluffy-context usage report', `Window: ${String(report.window.from).slice(0, 10)} to ${String(report.window.to).slice(0, 10)}`, '', 'ASSET VALUE', ` Verified knowledge ${assets.knowledge.verified}`, ` Verified deadends ${assets.deadends.verified}`, ` Used verified assets ${report.reuse.usedVerifiedAssetCount}`, ` Asset points ${assets.effectiveAssetPoints.value.toFixed(2)} derived`, '', 'TOKEN INVESTMENT', ` Estimated tokens ${report.investment.contextOutputTokens.value} estimated`, ' Provider tokens n/a unavailable', '', 'RETURN', ` Baseline tokens ${payback.baselineTokens ?? 'n/a'}`, ` Estimated savings ${payback.estimatedSavings.value ?? 'n/a'}`, ` ROI ${payback.estimatedROI ?? 'n/a'}`, ` Payback ${payback.status}`, '', 'COMMAND OUTPUT TREND', ` Usage events ${report.dataQuality.usageEventCount}`, '', 'DATA QUALITY', ` Journal verified ${report.dataQuality.journalVerified ? 'yes' : 'no'}`, ` Window events ${report.dataQuality.windowEventCount}`, ` Warnings ${report.warnings.length}`];
|
|
231
|
+
return `${lines.join('\n')}\n`;
|
|
232
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { CognitionEventSource, UsageOperationCompletedPayload } from './types.js';
|
|
2
|
+
export type UsageOperation = UsageOperationCompletedPayload['operation'];
|
|
3
|
+
export type UsageInterface = UsageOperationCompletedPayload['interface'];
|
|
4
|
+
export declare function recordUsageOperation(startPath: string | undefined, operation: UsageOperation, source: CognitionEventSource, input: unknown, output: unknown, startedAt: number, success: boolean, error?: unknown, interfaceName?: UsageInterface): Promise<void>;
|
|
5
|
+
export declare function withUsageTelemetry<T>(startPath: string | undefined, operation: UsageOperation, source: CognitionEventSource, input: unknown, action: () => Promise<T>, interfaceName?: UsageInterface): Promise<T>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { appendEvent } from './journal.js';
|
|
2
|
+
function errorClass(error) {
|
|
3
|
+
return error instanceof Error ? error.constructor.name : typeof error;
|
|
4
|
+
}
|
|
5
|
+
function interfaceFor(source) {
|
|
6
|
+
return source === 'mcp' ? 'mcp' : source === 'claude-hook' ? 'hook' : source === 'agent' ? 'agent' : 'cli';
|
|
7
|
+
}
|
|
8
|
+
export async function recordUsageOperation(startPath, operation, source, input, output, startedAt, success, error, interfaceName = interfaceFor(source)) {
|
|
9
|
+
const inputText = typeof input === 'string' ? input : JSON.stringify(input ?? null);
|
|
10
|
+
const outputText = typeof output === 'string' ? output : JSON.stringify(output ?? null);
|
|
11
|
+
const payload = {
|
|
12
|
+
operation,
|
|
13
|
+
interface: interfaceName,
|
|
14
|
+
success,
|
|
15
|
+
durationMs: Math.max(0, Date.now() - startedAt),
|
|
16
|
+
inputChars: inputText.length,
|
|
17
|
+
outputChars: outputText.length,
|
|
18
|
+
inputBytes: Buffer.byteLength(inputText),
|
|
19
|
+
outputBytes: Buffer.byteLength(outputText),
|
|
20
|
+
estimatedInputTokens: Math.ceil(inputText.length / 4),
|
|
21
|
+
estimatedOutputTokens: Math.ceil(outputText.length / 4),
|
|
22
|
+
estimatedMethod: 'chars-div-four',
|
|
23
|
+
...(success ? {} : { errorClass: errorClass(error) }),
|
|
24
|
+
};
|
|
25
|
+
try {
|
|
26
|
+
await appendEvent(startPath, {
|
|
27
|
+
type: 'usage.operation.completed',
|
|
28
|
+
source: { kind: source },
|
|
29
|
+
idempotencyKey: `usage:${operation}:${source}:${startedAt}:${Date.now()}:${Math.random().toString(36).slice(2)}`,
|
|
30
|
+
payload,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Observability must not affect the operation being observed.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function withUsageTelemetry(startPath, operation, source, input, action, interfaceName = interfaceFor(source)) {
|
|
38
|
+
const startedAt = Date.now();
|
|
39
|
+
try {
|
|
40
|
+
const result = await action();
|
|
41
|
+
await recordUsageOperation(startPath, operation, source, input, result, startedAt, true, undefined, interfaceName);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
await recordUsageOperation(startPath, operation, source, input, { error: errorClass(error) }, startedAt, false, error, interfaceName);
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -4,6 +4,7 @@ import { queryTerms } from '../cognition/retrieval.js';
|
|
|
4
4
|
import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
|
|
5
5
|
import { listNotes } from '../runtime/notes.js';
|
|
6
6
|
import { extractActivity } from './activity.js';
|
|
7
|
+
import { graphOrdering } from './graph.js';
|
|
7
8
|
import { latestHostIntent, loadCompilerSources, sourceDescriptor } from './sources.js';
|
|
8
9
|
const scenarioKeywords = [
|
|
9
10
|
{ scenario: 'debugging', words: ['bug', 'error', 'fail', 'failure', 'exception', 'regression', 'debug', '失败', '报错', '异常', '修复'] },
|
|
@@ -137,6 +138,50 @@ function sortCandidates(left, right) {
|
|
|
137
138
|
|| right.matchedTerms.length - left.matchedTerms.length
|
|
138
139
|
|| stableCompare(left.candidateId, right.candidateId);
|
|
139
140
|
}
|
|
141
|
+
function recipeParticipation(sources) {
|
|
142
|
+
const recipe = sources.recipe?.recipe;
|
|
143
|
+
if (!recipe)
|
|
144
|
+
return undefined;
|
|
145
|
+
return {
|
|
146
|
+
recipeId: recipe.recipeId,
|
|
147
|
+
contentHash: recipe.contentHash,
|
|
148
|
+
interpreterVersion: 'recipe-interpreter-v1',
|
|
149
|
+
preferredSources: [...(recipe.preferredSources ?? [])],
|
|
150
|
+
preferredSections: [...(recipe.preferredSections ?? [])],
|
|
151
|
+
sectionBudgets: { ...(recipe.sectionBudgets ?? {}) },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function sourcePreference(candidate, recipe) {
|
|
155
|
+
const source = candidate.source.kind === 'current-context' ? 'context' : candidate.source.kind;
|
|
156
|
+
const index = recipe.preferredSources.indexOf(source);
|
|
157
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
158
|
+
}
|
|
159
|
+
function sectionPreference(candidate, recipe) {
|
|
160
|
+
const index = recipe.preferredSections.indexOf(candidate.section);
|
|
161
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
162
|
+
}
|
|
163
|
+
function recipeApplicable(recipe, sources, paths) {
|
|
164
|
+
const applicability = sources.recipe.recipe.applicability;
|
|
165
|
+
if (applicability.branchMode === 'global')
|
|
166
|
+
return true;
|
|
167
|
+
if (applicability.branchMode !== 'exact')
|
|
168
|
+
return false;
|
|
169
|
+
const branch = sources.anchor.ref ?? sources.anchor.branch ?? 'detached';
|
|
170
|
+
if (applicability.branches?.length && !applicability.branches.includes(branch) && !applicability.branches.includes(sources.anchor.branch ?? ''))
|
|
171
|
+
return false;
|
|
172
|
+
if (applicability.sourceCommit && applicability.sourceCommit !== sources.anchor.head)
|
|
173
|
+
return false;
|
|
174
|
+
if (paths.length > 0 && applicability.paths?.length && !applicability.paths.some((path) => paths.includes(path.replaceAll('\\', '/'))))
|
|
175
|
+
return false;
|
|
176
|
+
return recipe.recipeId === sources.recipe.recipe.recipeId;
|
|
177
|
+
}
|
|
178
|
+
function sortWithRecipe(recipe) {
|
|
179
|
+
if (!recipe)
|
|
180
|
+
return sortCandidates;
|
|
181
|
+
return (left, right) => sourcePreference(left, recipe) - sourcePreference(right, recipe)
|
|
182
|
+
|| sectionPreference(left, recipe) - sectionPreference(right, recipe)
|
|
183
|
+
|| sortCandidates(left, right);
|
|
184
|
+
}
|
|
140
185
|
function truncateText(value, maxChars) {
|
|
141
186
|
if (value.length <= maxChars)
|
|
142
187
|
return { text: value, truncated: false };
|
|
@@ -237,6 +282,9 @@ export async function compileContext(startPath, options = {}) {
|
|
|
237
282
|
const sources = await loadCompilerSources(startPath, options);
|
|
238
283
|
const normalizedPaths = options.paths ? await filterPaths(sources.anchor.projectRoot, options.paths) : [];
|
|
239
284
|
const intent = intentFor(sources, options, normalizedPaths);
|
|
285
|
+
const recipe = recipeParticipation(sources);
|
|
286
|
+
if (recipe && !recipeApplicable(recipe, sources, normalizedPaths))
|
|
287
|
+
throw new Error(`compiler recipe is not applicable: ${recipe.recipeId}`);
|
|
240
288
|
const orientation = sources.orientation;
|
|
241
289
|
const maxChars = intent.budget.total;
|
|
242
290
|
const notes = orientation.status === 'ready' ? (await listNotes(sources.anchor.projectRoot, { contextId: orientation.context.id, openOnly: true, limit: options.noteLimit ?? 50, maxChars: Number.MAX_SAFE_INTEGER })).filter((note) => orientation.notes.some((selected) => selected.noteId === note.noteId)) : [];
|
|
@@ -250,14 +298,31 @@ export async function compileContext(startPath, options = {}) {
|
|
|
250
298
|
if (!check.ok)
|
|
251
299
|
excluded.push({ candidateId: candidate.candidateId, source: candidate.source, reason: check.reason ?? 'query_no_match' });
|
|
252
300
|
return check.ok;
|
|
253
|
-
}).sort(
|
|
301
|
+
}).sort(sortWithRecipe(recipe));
|
|
302
|
+
const graph = options.graphHops === undefined ? undefined : graphOrdering(sources.evidenceGraph, options.graphHops, sources.anchor, eligible);
|
|
303
|
+
if (graph) {
|
|
304
|
+
eligible.sort((left, right) => {
|
|
305
|
+
if (recipe) {
|
|
306
|
+
const sourceOrder = sourcePreference(left, recipe) - sourcePreference(right, recipe);
|
|
307
|
+
if (sourceOrder)
|
|
308
|
+
return sourceOrder;
|
|
309
|
+
const sectionOrder = sectionPreference(left, recipe) - sectionPreference(right, recipe);
|
|
310
|
+
if (sectionOrder)
|
|
311
|
+
return sectionOrder;
|
|
312
|
+
}
|
|
313
|
+
return (graph.distanceFor(left) ?? Number.MAX_SAFE_INTEGER) - (graph.distanceFor(right) ?? Number.MAX_SAFE_INTEGER)
|
|
314
|
+
|| sortCandidates(left, right);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
254
317
|
const selected = [];
|
|
255
318
|
const truncation = emptyTruncation();
|
|
256
319
|
const sectionStats = {};
|
|
257
320
|
let usedChars = 0;
|
|
258
321
|
for (const candidate of eligible) {
|
|
259
322
|
const separator = selected.length > 0 ? 1 : 0;
|
|
260
|
-
const
|
|
323
|
+
const sectionBudget = recipe?.sectionBudgets[candidate.section] ?? maxChars;
|
|
324
|
+
const consumedInSection = sectionStats[candidate.section]?.consumed ?? 0;
|
|
325
|
+
const available = Math.min(maxChars - usedChars - separator, sectionBudget - consumedInSection - separator);
|
|
261
326
|
if (available <= 0) {
|
|
262
327
|
excluded.push({ candidateId: candidate.candidateId, source: candidate.source, reason: 'budget' });
|
|
263
328
|
truncation.candidatesOmittedByBudget += 1;
|
|
@@ -274,7 +339,7 @@ export async function compileContext(startPath, options = {}) {
|
|
|
274
339
|
const item = { ...candidate, text: limited.text, selectedChars: limited.text.length, selected: true, truncated: limited.truncated };
|
|
275
340
|
selected.push(item);
|
|
276
341
|
usedChars += separator + item.selectedChars;
|
|
277
|
-
const prior = sectionStats[item.section] ?? { allocated: maxChars, consumed: 0, truncated: false };
|
|
342
|
+
const prior = sectionStats[item.section] ?? { allocated: recipe?.sectionBudgets[item.section] ?? maxChars, consumed: 0, truncated: false };
|
|
278
343
|
sectionStats[item.section] = { ...prior, consumed: prior.consumed + item.selectedChars + separator, truncated: prior.truncated || limited.truncated };
|
|
279
344
|
if (limited.truncated) {
|
|
280
345
|
truncation.candidatesPartiallyTruncated += 1;
|
|
@@ -318,10 +383,12 @@ export async function compileContext(startPath, options = {}) {
|
|
|
318
383
|
selected,
|
|
319
384
|
budget: { requested: intent.budget, usedChars, remainingChars: Math.max(0, maxChars - usedChars), sections: sectionStats },
|
|
320
385
|
truncation,
|
|
386
|
+
...(recipe ? { recipe } : {}),
|
|
387
|
+
...(graph ? { graph: graph.participation } : {}),
|
|
321
388
|
});
|
|
322
389
|
return {
|
|
323
390
|
schemaVersion: 1,
|
|
324
|
-
compilerVersion: '0.7.
|
|
391
|
+
compilerVersion: '0.7.7',
|
|
325
392
|
rankingVersion: 'compiler-ranking-v2',
|
|
326
393
|
status: orientation.status,
|
|
327
394
|
projectRoot: sources.anchor.projectRoot,
|
|
@@ -348,6 +415,8 @@ export async function compileContext(startPath, options = {}) {
|
|
|
348
415
|
selectedEventIds: [...new Set(selected.flatMap((item) => item.source.provenance.eventId ? [item.source.provenance.eventId] : []))].sort(stableCompare),
|
|
349
416
|
fallbackReason: sources.fallbackReason,
|
|
350
417
|
},
|
|
418
|
+
...(recipe ? { recipe } : {}),
|
|
419
|
+
...(graph ? { graph: graph.participation } : {}),
|
|
351
420
|
text: manifestText(selected),
|
|
352
421
|
};
|
|
353
422
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EvidenceGraphProjection } from '../cognition/types.js';
|
|
2
|
+
import type { ContextAnchor, ContextCandidate, ContextGraphParticipation } from './types.js';
|
|
3
|
+
export interface GraphOrdering {
|
|
4
|
+
participation: ContextGraphParticipation;
|
|
5
|
+
distanceFor(candidate: ContextCandidate): number | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function graphOrdering(graph: EvidenceGraphProjection | null, hops: 0 | 1 | 2, anchor: ContextAnchor, eligible: ContextCandidate[]): GraphOrdering;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { EVIDENCE_GRAPH_DERIVATION_VERSION, workspaceNodeId } from '../cognition/evidence-graph.js';
|
|
2
|
+
import { contentHash } from './identity.js';
|
|
3
|
+
const MAX_VISITED_NODES = 256;
|
|
4
|
+
const MAX_TRAVERSED_EDGES = 512;
|
|
5
|
+
const MAX_FAN_OUT = 32;
|
|
6
|
+
function compare(left, right) {
|
|
7
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
8
|
+
}
|
|
9
|
+
function candidateNodeId(candidate) {
|
|
10
|
+
if (candidate.section === 'knowledge')
|
|
11
|
+
return `knowledge:${candidate.source.entityId}`;
|
|
12
|
+
if (candidate.section === 'deadend')
|
|
13
|
+
return `deadend:${candidate.source.entityId}`;
|
|
14
|
+
if (candidate.source.kind === 'current-context')
|
|
15
|
+
return `context:${candidate.source.contextId ?? candidate.source.entityId}`;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function validGraph(graph) {
|
|
19
|
+
if (graph.schemaVersion !== 1 || graph.derivationVersion !== EVIDENCE_GRAPH_DERIVATION_VERSION || !Array.isArray(graph.nodes) || !Array.isArray(graph.edges))
|
|
20
|
+
return false;
|
|
21
|
+
const nodeKinds = new Set(['context', 'snapshot', 'experience', 'proposal', 'knowledge', 'deadend', 'skill', 'recipe', 'path', 'test', 'commit', 'branch-worktree']);
|
|
22
|
+
const edgeTypes = new Set(['derived_from', 'supports', 'contradicts', 'supersedes', 'used_for', 'observed_in', 'changed_in', 'co_changed_with', 'tests']);
|
|
23
|
+
const nodes = new Set();
|
|
24
|
+
for (const node of graph.nodes) {
|
|
25
|
+
if (!node || typeof node.nodeId !== 'string' || node.nodeId.length === 0 || !nodeKinds.has(node.kind) || nodes.has(node.nodeId))
|
|
26
|
+
return false;
|
|
27
|
+
nodes.add(node.nodeId);
|
|
28
|
+
}
|
|
29
|
+
const edges = new Set();
|
|
30
|
+
for (const edge of graph.edges) {
|
|
31
|
+
if (!edge || typeof edge.edgeId !== 'string' || edge.edgeId.length === 0 || edges.has(edge.edgeId) || !nodes.has(edge.from) || !nodes.has(edge.to) || !edgeTypes.has(edge.type) || edge.derivationVersion !== EVIDENCE_GRAPH_DERIVATION_VERSION || typeof edge.occurredAt !== 'string' || edge.occurredAt.length === 0 || !Array.isArray(edge.sourceEventIds) || edge.sourceEventIds.some((id) => typeof id !== 'string' || !id) || (edge.confidence !== undefined && (typeof edge.confidence !== 'number' || !Number.isFinite(edge.confidence) || edge.confidence < 0 || edge.confidence > 1)))
|
|
32
|
+
return false;
|
|
33
|
+
edges.add(edge.edgeId);
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
function graphProjectionHash(graph) {
|
|
38
|
+
return contentHash(graph);
|
|
39
|
+
}
|
|
40
|
+
export function graphOrdering(graph, hops, anchor, eligible) {
|
|
41
|
+
const empty = (fallbackReason, seedNodeIds = []) => ({
|
|
42
|
+
participation: {
|
|
43
|
+
derivationVersion: EVIDENCE_GRAPH_DERIVATION_VERSION,
|
|
44
|
+
projectionHash: null,
|
|
45
|
+
hops,
|
|
46
|
+
seedNodeIds,
|
|
47
|
+
selectedNodeIds: [],
|
|
48
|
+
selectedEdgeIds: [],
|
|
49
|
+
fallbackReason,
|
|
50
|
+
},
|
|
51
|
+
distanceFor: () => null,
|
|
52
|
+
});
|
|
53
|
+
if (!graph)
|
|
54
|
+
return empty('graph-unavailable');
|
|
55
|
+
if (!validGraph(graph))
|
|
56
|
+
return empty('graph-invalid');
|
|
57
|
+
const projectionHash = graphProjectionHash(graph);
|
|
58
|
+
const nodes = new Set(graph.nodes.map((node) => node.nodeId));
|
|
59
|
+
const candidateNodes = new Map(eligible.map((candidate) => [candidate.candidateId, candidateNodeId(candidate)]));
|
|
60
|
+
const seedNodeIds = [...new Set([
|
|
61
|
+
workspaceNodeId(anchor),
|
|
62
|
+
...eligible.filter((candidate) => candidate.source.kind === 'current-context').map(candidateNodeId).filter((node) => node !== null),
|
|
63
|
+
].filter((node) => nodes.has(node)))].sort(compare);
|
|
64
|
+
if (seedNodeIds.length === 0)
|
|
65
|
+
return empty('graph-no-seeds');
|
|
66
|
+
if (hops === 0) {
|
|
67
|
+
const distances = new Map(seedNodeIds.map((node) => [node, 0]));
|
|
68
|
+
return {
|
|
69
|
+
participation: { derivationVersion: EVIDENCE_GRAPH_DERIVATION_VERSION, projectionHash, hops, seedNodeIds, selectedNodeIds: seedNodeIds, selectedEdgeIds: [], fallbackReason: null },
|
|
70
|
+
distanceFor: (candidate) => {
|
|
71
|
+
const node = candidateNodes.get(candidate.candidateId);
|
|
72
|
+
return node ? distances.get(node) ?? null : null;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const adjacency = new Map();
|
|
77
|
+
for (const edge of graph.edges) {
|
|
78
|
+
adjacency.set(edge.from, [...(adjacency.get(edge.from) ?? []), edge]);
|
|
79
|
+
adjacency.set(edge.to, [...(adjacency.get(edge.to) ?? []), edge]);
|
|
80
|
+
}
|
|
81
|
+
for (const edges of adjacency.values())
|
|
82
|
+
edges.sort((left, right) => compare(left.edgeId, right.edgeId));
|
|
83
|
+
const distances = new Map(seedNodeIds.map((node) => [node, 0]));
|
|
84
|
+
const queue = [...seedNodeIds];
|
|
85
|
+
const selectedEdgeIds = new Set();
|
|
86
|
+
while (queue.length > 0 && distances.size < MAX_VISITED_NODES && selectedEdgeIds.size < MAX_TRAVERSED_EDGES) {
|
|
87
|
+
const node = queue.shift();
|
|
88
|
+
const distance = distances.get(node);
|
|
89
|
+
if (distance >= hops)
|
|
90
|
+
continue;
|
|
91
|
+
for (const edge of (adjacency.get(node) ?? []).slice(0, MAX_FAN_OUT)) {
|
|
92
|
+
if (selectedEdgeIds.size >= MAX_TRAVERSED_EDGES)
|
|
93
|
+
break;
|
|
94
|
+
const adjacent = edge.from === node ? edge.to : edge.from;
|
|
95
|
+
selectedEdgeIds.add(edge.edgeId);
|
|
96
|
+
if (!distances.has(adjacent) && distances.size < MAX_VISITED_NODES) {
|
|
97
|
+
distances.set(adjacent, distance + 1);
|
|
98
|
+
queue.push(adjacent);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
participation: {
|
|
104
|
+
derivationVersion: EVIDENCE_GRAPH_DERIVATION_VERSION,
|
|
105
|
+
projectionHash,
|
|
106
|
+
hops,
|
|
107
|
+
seedNodeIds,
|
|
108
|
+
selectedNodeIds: [...distances.keys()].sort(compare),
|
|
109
|
+
selectedEdgeIds: [...selectedEdgeIds].sort(compare),
|
|
110
|
+
fallbackReason: null,
|
|
111
|
+
},
|
|
112
|
+
distanceFor: (candidate) => {
|
|
113
|
+
const node = candidateNodes.get(candidate.candidateId);
|
|
114
|
+
return node ? distances.get(node) ?? null : null;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -2,5 +2,5 @@ import type { ContextManifest } from './types.js';
|
|
|
2
2
|
export declare function canonicalJson(value: unknown): string;
|
|
3
3
|
export declare function contentHash(value: unknown): string;
|
|
4
4
|
export declare function estimateTokens(value: string): number;
|
|
5
|
-
export declare function rebuildManifestHash(manifest: Pick<ContextManifest, 'status' | 'projectRoot' | 'mode' | 'anchor' | 'intent' | 'selected' | 'budget' | 'truncation'>): string;
|
|
5
|
+
export declare function rebuildManifestHash(manifest: Pick<ContextManifest, 'status' | 'projectRoot' | 'mode' | 'anchor' | 'intent' | 'selected' | 'budget' | 'truncation' | 'recipe' | 'graph'>): string;
|
|
6
6
|
export declare function validateManifestIdentity(manifest: ContextManifest): boolean;
|
|
@@ -16,7 +16,7 @@ export function estimateTokens(value) {
|
|
|
16
16
|
export function rebuildManifestHash(manifest) {
|
|
17
17
|
return contentHash({
|
|
18
18
|
schemaVersion: 1,
|
|
19
|
-
compilerVersion: '0.7.
|
|
19
|
+
compilerVersion: '0.7.7',
|
|
20
20
|
rankingVersion: 'compiler-ranking-v2',
|
|
21
21
|
status: manifest.status,
|
|
22
22
|
projectRoot: manifest.projectRoot,
|
|
@@ -26,6 +26,8 @@ export function rebuildManifestHash(manifest) {
|
|
|
26
26
|
selectedItemHashes: manifest.selected.map((item) => item.itemHash),
|
|
27
27
|
budget: { usedChars: manifest.budget.usedChars, maxChars: manifest.budget.requested.total },
|
|
28
28
|
truncation: manifest.truncation,
|
|
29
|
+
...(manifest.recipe ? { recipe: { recipeId: manifest.recipe.recipeId, contentHash: manifest.recipe.contentHash, interpreterVersion: manifest.recipe.interpreterVersion } } : {}),
|
|
30
|
+
...(manifest.graph ? { graph: manifest.graph } : {}),
|
|
29
31
|
});
|
|
30
32
|
}
|
|
31
33
|
export function validateManifestIdentity(manifest) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BranchWorkspace, CognitionEvent } from '../cognition/types.js';
|
|
1
|
+
import type { BranchWorkspace, CognitionEvent, CompilerRecipeState, EvidenceGraphProjection } from '../cognition/types.js';
|
|
2
2
|
import type { ContextOrientResult } from '../agent/types.js';
|
|
3
3
|
import type { ContextAnchor, ContextProvenance, ContextSourceDescriptor, ContextSourceApplicability } from './types.js';
|
|
4
4
|
import type { ContextCompileOptions } from './types.js';
|
|
@@ -7,6 +7,8 @@ export interface CompilerSources {
|
|
|
7
7
|
anchor: ContextAnchor;
|
|
8
8
|
events: CognitionEvent[];
|
|
9
9
|
workspace: BranchWorkspace | null;
|
|
10
|
+
recipe: CompilerRecipeState | null;
|
|
11
|
+
evidenceGraph: EvidenceGraphProjection | null;
|
|
10
12
|
fallbackReason: string | null;
|
|
11
13
|
}
|
|
12
14
|
export declare function sourceApplicability(event: CognitionEvent | null, anchor: ContextAnchor): ContextSourceApplicability;
|
|
@@ -93,7 +93,7 @@ export async function loadCompilerSources(startPath, options) {
|
|
|
93
93
|
knowledgeLimit: options.knowledgeLimit ?? 10,
|
|
94
94
|
deadendLimit: options.deadendLimit ?? 10,
|
|
95
95
|
noteLimit: options.noteLimit ?? 20,
|
|
96
|
-
});
|
|
96
|
+
}, null);
|
|
97
97
|
const projectRoot = orientation.projectRoot;
|
|
98
98
|
const git = await readGitWorktreeState(projectRoot);
|
|
99
99
|
let events = [];
|
|
@@ -107,7 +107,16 @@ export async function loadCompilerSources(startPath, options) {
|
|
|
107
107
|
const anchor = anchorFromGit(git, projectRoot);
|
|
108
108
|
const projection = events.length > 0 ? projectEvents(events) : null;
|
|
109
109
|
const workspace = projection?.workspaces[workspaceKey(anchor)] ?? null;
|
|
110
|
-
|
|
110
|
+
const recipeId = options.recipeId?.trim();
|
|
111
|
+
if (options.recipeId !== undefined && !recipeId)
|
|
112
|
+
throw new Error('compiler recipe ID must not be empty');
|
|
113
|
+
const recipe = recipeId ? projection?.recipes[recipeId] ?? null : null;
|
|
114
|
+
if (recipeId && !recipe)
|
|
115
|
+
throw new Error(`compiler recipe not found: ${recipeId}`);
|
|
116
|
+
if (recipe && !['verified', 'published'].includes(recipe.status))
|
|
117
|
+
throw new Error(`compiler recipe is not active: ${recipe.recipe.recipeId}`);
|
|
118
|
+
const evidenceGraph = options.graphHops !== undefined ? projection?.evidenceGraph ?? null : null;
|
|
119
|
+
return { orientation, anchor, events, workspace, recipe, evidenceGraph, fallbackReason };
|
|
111
120
|
}
|
|
112
121
|
export function latestHostIntent(events, anchor) {
|
|
113
122
|
const event = [...events].reverse().find((candidate) => {
|