fluffy-context 0.7.2 → 0.7.7
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 +481 -175
- package/dist/src/agent/api.d.ts +8 -2
- package/dist/src/agent/api.js +148 -128
- package/dist/src/agent/index.d.ts +3 -1
- package/dist/src/agent/index.js +2 -0
- package/dist/src/cli/main.js +112 -9
- package/dist/src/cognition/journal.js +30 -1
- package/dist/src/cognition/projections.js +0 -0
- package/dist/src/cognition/types.d.ts +16 -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 +26 -3
- package/dist/src/compiler/expand.d.ts +2 -0
- package/dist/src/compiler/expand.js +187 -0
- package/dist/src/compiler/identity.d.ts +6 -0
- package/dist/src/compiler/identity.js +36 -0
- package/dist/src/compiler/sources.js +10 -14
- package/dist/src/compiler/types.d.ts +47 -1
- package/dist/src/mcp/server.js +186 -11
- package/dist/src/runtime/runtime.js +10 -0
- package/dist/src/runtime/types.d.ts +1 -1
- 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 +28 -2
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type UsageReportFormat = 'json' | 'ascii';
|
|
2
|
+
export type UsageGranularity = 'day' | 'week' | 'month';
|
|
3
|
+
export interface UsageReportOptions {
|
|
4
|
+
since?: string;
|
|
5
|
+
until?: string;
|
|
6
|
+
days?: number;
|
|
7
|
+
month?: string;
|
|
8
|
+
granularity?: UsageGranularity;
|
|
9
|
+
baselineTokens?: number;
|
|
10
|
+
baselineSource?: 'manual' | 'recorded-control';
|
|
11
|
+
}
|
|
12
|
+
export interface UsageMetric<T> {
|
|
13
|
+
value: T | null;
|
|
14
|
+
quality: 'measured' | 'estimated' | 'derived' | 'unavailable';
|
|
15
|
+
unit: string;
|
|
16
|
+
method?: string;
|
|
17
|
+
}
|
|
18
|
+
interface Window {
|
|
19
|
+
from: string;
|
|
20
|
+
to: string;
|
|
21
|
+
kind: 'rolling' | 'calendar-month' | 'explicit';
|
|
22
|
+
granularity: UsageGranularity;
|
|
23
|
+
}
|
|
24
|
+
export interface UsageReport {
|
|
25
|
+
schemaVersion: 1;
|
|
26
|
+
generatedAt: string;
|
|
27
|
+
projectRoot: string;
|
|
28
|
+
window: Window;
|
|
29
|
+
dataQuality: Record<string, unknown>;
|
|
30
|
+
assets: Record<string, unknown>;
|
|
31
|
+
reuse: Record<string, unknown>;
|
|
32
|
+
investment: Record<string, unknown>;
|
|
33
|
+
payback: Record<string, unknown>;
|
|
34
|
+
operations: Record<string, unknown>;
|
|
35
|
+
trends: Array<Record<string, unknown>>;
|
|
36
|
+
warnings: string[];
|
|
37
|
+
}
|
|
38
|
+
export declare function buildUsageReport(startPath: string | undefined, options?: UsageReportOptions): Promise<UsageReport>;
|
|
39
|
+
export declare function formatUsageReport(report: Record<string, any>): string;
|
|
40
|
+
export {};
|
|
@@ -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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { filterPaths } from '../capture/context-filter.js';
|
|
2
|
+
import { contentHash, estimateTokens, rebuildManifestHash } from './identity.js';
|
|
2
3
|
import { queryTerms } from '../cognition/retrieval.js';
|
|
3
4
|
import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
|
|
4
5
|
import { listNotes } from '../runtime/notes.js';
|
|
@@ -84,8 +85,9 @@ function matched(text, terms) {
|
|
|
84
85
|
}
|
|
85
86
|
function makeCandidate(section, source, content, text, terms, matchedFields, discriminator) {
|
|
86
87
|
const hit = matched(text, terms);
|
|
88
|
+
const candidateId = `${section}:${source.sourceId}:${matchedFields.join('.')}:${discriminator}`;
|
|
87
89
|
return {
|
|
88
|
-
candidateId
|
|
90
|
+
candidateId,
|
|
89
91
|
section,
|
|
90
92
|
source,
|
|
91
93
|
content,
|
|
@@ -98,6 +100,10 @@ function makeCandidate(section, source, content, text, terms, matchedFields, dis
|
|
|
98
100
|
selectedChars: 0,
|
|
99
101
|
selected: false,
|
|
100
102
|
truncated: false,
|
|
103
|
+
level: 'summary',
|
|
104
|
+
itemHash: contentHash({ candidateId, source, content, text }),
|
|
105
|
+
tokenEstimate: estimateTokens(text),
|
|
106
|
+
expandable: true,
|
|
101
107
|
};
|
|
102
108
|
}
|
|
103
109
|
function applicable(candidate, intent) {
|
|
@@ -233,7 +239,7 @@ export async function compileContext(startPath, options = {}) {
|
|
|
233
239
|
const intent = intentFor(sources, options, normalizedPaths);
|
|
234
240
|
const orientation = sources.orientation;
|
|
235
241
|
const maxChars = intent.budget.total;
|
|
236
|
-
const notes = orientation.status === 'ready' ? (await listNotes(sources.anchor.projectRoot, { contextId: orientation.context.id, openOnly: true, limit:
|
|
242
|
+
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)) : [];
|
|
237
243
|
const knowledge = orientation.status === 'ready' && intent.query ? await listKnowledge(sources.anchor.projectRoot, true) : [];
|
|
238
244
|
const deadends = orientation.status === 'ready' && intent.query ? await listDeadends(sources.anchor.projectRoot, true) : [];
|
|
239
245
|
const activities = await extractActivity(sources.anchor.projectRoot, sources.events, sources.anchor, sources.workspace, options.activityLimit ?? 20);
|
|
@@ -301,9 +307,21 @@ export async function compileContext(startPath, options = {}) {
|
|
|
301
307
|
deadends: selected.filter((item) => item.section === 'deadend'),
|
|
302
308
|
activity: selected.filter((item) => item.section === 'activity'),
|
|
303
309
|
};
|
|
310
|
+
const tokenEstimate = estimateTokens(manifestText(selected));
|
|
311
|
+
const selectedItemHashes = selected.map((item) => item.itemHash);
|
|
312
|
+
const manifestHash = rebuildManifestHash({
|
|
313
|
+
status: orientation.status,
|
|
314
|
+
projectRoot: sources.anchor.projectRoot,
|
|
315
|
+
mode: orientation.explanation.mode,
|
|
316
|
+
anchor: sources.anchor,
|
|
317
|
+
intent,
|
|
318
|
+
selected,
|
|
319
|
+
budget: { requested: intent.budget, usedChars, remainingChars: Math.max(0, maxChars - usedChars), sections: sectionStats },
|
|
320
|
+
truncation,
|
|
321
|
+
});
|
|
304
322
|
return {
|
|
305
323
|
schemaVersion: 1,
|
|
306
|
-
compilerVersion: '0.7.
|
|
324
|
+
compilerVersion: '0.7.7',
|
|
307
325
|
rankingVersion: 'compiler-ranking-v2',
|
|
308
326
|
status: orientation.status,
|
|
309
327
|
projectRoot: sources.anchor.projectRoot,
|
|
@@ -316,6 +334,11 @@ export async function compileContext(startPath, options = {}) {
|
|
|
316
334
|
sections,
|
|
317
335
|
selected,
|
|
318
336
|
excluded,
|
|
337
|
+
identity: {
|
|
338
|
+
manifestHash,
|
|
339
|
+
selectedItemHashes,
|
|
340
|
+
tokenEstimate: { input: tokenEstimate, output: tokenEstimate, unit: 'estimated-tokens', method: 'chars-div-four' },
|
|
341
|
+
},
|
|
319
342
|
budget: { requested: intent.budget, usedChars, remainingChars: Math.max(0, maxChars - usedChars), sections: sectionStats },
|
|
320
343
|
truncation,
|
|
321
344
|
provenance: {
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { compileContext } from './compile.js';
|
|
2
|
+
import { extractActivity } from './activity.js';
|
|
3
|
+
import { validateManifestIdentity } from './identity.js';
|
|
4
|
+
import { readJournal } from '../cognition/journal.js';
|
|
5
|
+
import { projectEvents, workspaceKey } from '../cognition/projections.js';
|
|
6
|
+
import { rebuildSnapshot } from '../runtime/runtime.js';
|
|
7
|
+
import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
|
|
8
|
+
import { listNotes } from '../runtime/notes.js';
|
|
9
|
+
function truncateText(value, maxChars) {
|
|
10
|
+
if (value.length <= maxChars)
|
|
11
|
+
return { text: value, truncated: false };
|
|
12
|
+
if (maxChars <= 0)
|
|
13
|
+
return { text: '', truncated: true };
|
|
14
|
+
return { text: `${value.slice(0, maxChars - 1)}…`, truncated: true };
|
|
15
|
+
}
|
|
16
|
+
function isExpandLevel(value) {
|
|
17
|
+
return value === 'structured' || value === 'evidence';
|
|
18
|
+
}
|
|
19
|
+
function validateInput(input) {
|
|
20
|
+
if (!input.manifestHash.trim())
|
|
21
|
+
throw new Error('context expand manifest hash must not be empty');
|
|
22
|
+
if (!input.candidateId?.trim() && !input.itemHash?.trim())
|
|
23
|
+
throw new Error('context expand requires candidateId or itemHash');
|
|
24
|
+
if (!isExpandLevel(input.level))
|
|
25
|
+
throw new Error('context expand level must be structured or evidence');
|
|
26
|
+
if (input.maxChars !== undefined && (!Number.isSafeInteger(input.maxChars) || input.maxChars < 0))
|
|
27
|
+
throw new Error('context expand maxChars must be a non-negative integer');
|
|
28
|
+
}
|
|
29
|
+
function selectedCandidate(manifest, input) {
|
|
30
|
+
const byId = input.candidateId ? manifest.selected.find((item) => item.candidateId === input.candidateId) : undefined;
|
|
31
|
+
const byHash = input.itemHash ? manifest.selected.find((item) => item.itemHash === input.itemHash) : undefined;
|
|
32
|
+
if (byId && byHash && byId.candidateId !== byHash.candidateId)
|
|
33
|
+
throw new Error('context expand candidate selectors do not match');
|
|
34
|
+
const candidate = byId ?? byHash;
|
|
35
|
+
if (!candidate)
|
|
36
|
+
throw new Error('context expand candidate not selected');
|
|
37
|
+
if (!candidate.expandable)
|
|
38
|
+
throw new Error('context expand candidate is not expandable');
|
|
39
|
+
if (candidate.level !== 'summary')
|
|
40
|
+
throw new Error('context expand candidate is already expanded');
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
function contextField(content, candidate) {
|
|
44
|
+
const field = candidate.content.field;
|
|
45
|
+
if (typeof field !== 'string')
|
|
46
|
+
throw new Error('context expand current context field is invalid');
|
|
47
|
+
if (field === 'title')
|
|
48
|
+
return candidate.content.value ?? '';
|
|
49
|
+
if (field === 'progressSummary' || field === 'lastError')
|
|
50
|
+
return content[field];
|
|
51
|
+
if (field === 'pendingTasks' || field === 'completed' || field === 'decisions' || field === 'risks' || field === 'relatedFiles') {
|
|
52
|
+
const discriminator = candidate.candidateId.split(':').at(-1) ?? '';
|
|
53
|
+
const index = Number(discriminator);
|
|
54
|
+
const values = content[field];
|
|
55
|
+
if (!Number.isInteger(index) || index < 0 || index >= values.length)
|
|
56
|
+
throw new Error(`context expand current context item is unavailable: ${field}`);
|
|
57
|
+
return values[index];
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`context expand unsupported current context field: ${field}`);
|
|
60
|
+
}
|
|
61
|
+
function currentContextText(value, candidate) {
|
|
62
|
+
const field = candidate.content.field;
|
|
63
|
+
if (typeof field !== 'string')
|
|
64
|
+
throw new Error('context expand current context field is invalid');
|
|
65
|
+
return `${field}: ${Array.isArray(value) ? value.join(', ') : value ?? ''}`;
|
|
66
|
+
}
|
|
67
|
+
function recordText(source, value) {
|
|
68
|
+
if (source.kind === 'note') {
|
|
69
|
+
const note = value;
|
|
70
|
+
return `${note.kind}: ${note.message}`;
|
|
71
|
+
}
|
|
72
|
+
if (source.kind === 'knowledge')
|
|
73
|
+
return value.statement;
|
|
74
|
+
const deadend = value;
|
|
75
|
+
return `${deadend.attempt}: ${deadend.reason}`;
|
|
76
|
+
}
|
|
77
|
+
function evidenceText(source, value, structuredText) {
|
|
78
|
+
return `${structuredText}\nevidence: ${JSON.stringify({
|
|
79
|
+
source: source.provenance,
|
|
80
|
+
...(source.kind === 'knowledge' ? { supportingEvidence: value.supportingEvidence } : {}),
|
|
81
|
+
...(source.kind === 'deadend' ? { observedEvidence: value.observedEvidence } : {}),
|
|
82
|
+
...(source.kind === 'note' ? { relatedFiles: value.relatedFiles } : {}),
|
|
83
|
+
})}`;
|
|
84
|
+
}
|
|
85
|
+
function recordApplicability(value) {
|
|
86
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
87
|
+
return undefined;
|
|
88
|
+
const applicability = value.applicability;
|
|
89
|
+
if (!applicability || typeof applicability !== 'object' || Array.isArray(applicability))
|
|
90
|
+
return undefined;
|
|
91
|
+
return applicability;
|
|
92
|
+
}
|
|
93
|
+
function ensureApplicable(source, value, manifest) {
|
|
94
|
+
const applicability = recordApplicability(value);
|
|
95
|
+
if (!applicability)
|
|
96
|
+
return;
|
|
97
|
+
if (applicability.branchMode !== source.applicability.branchMode)
|
|
98
|
+
throw new Error(`context expand source applicability changed: ${source.entityId}`);
|
|
99
|
+
if (applicability.branchMode === 'exact') {
|
|
100
|
+
const branch = manifest.anchor.ref ?? manifest.anchor.branch ?? 'detached';
|
|
101
|
+
const branches = (applicability.branches ?? []).map((item) => item.toLowerCase());
|
|
102
|
+
if (branches.length > 0 && !branches.includes(branch.toLowerCase()) && !branches.includes((manifest.anchor.branch ?? '').toLowerCase()))
|
|
103
|
+
throw new Error(`context expand source is not applicable: ${source.entityId}`);
|
|
104
|
+
if (applicability.sourceCommit && applicability.sourceCommit !== manifest.anchor.head)
|
|
105
|
+
throw new Error(`context expand source commit is not applicable: ${source.entityId}`);
|
|
106
|
+
if (source.applicability.worktreeId && source.applicability.worktreeId !== manifest.anchor.worktreeId)
|
|
107
|
+
throw new Error(`context expand source worktree is not applicable: ${source.entityId}`);
|
|
108
|
+
if (source.applicability.ref && source.applicability.ref !== manifest.anchor.ref)
|
|
109
|
+
throw new Error(`context expand source ref is not applicable: ${source.entityId}`);
|
|
110
|
+
}
|
|
111
|
+
if (source.scope && typeof value.scope === 'string' && source.scope !== value.scope)
|
|
112
|
+
throw new Error(`context expand source scope changed: ${source.entityId}`);
|
|
113
|
+
const paths = applicability.paths ?? [];
|
|
114
|
+
if (source.paths.length > 0 && paths.length > 0 && !source.paths.some((path) => paths.includes(path)))
|
|
115
|
+
throw new Error(`context expand source paths changed: ${source.entityId}`);
|
|
116
|
+
}
|
|
117
|
+
async function resolveCandidate(projectRoot, manifest, candidate, level) {
|
|
118
|
+
const source = candidate.source;
|
|
119
|
+
if (source.kind === 'current-context') {
|
|
120
|
+
if (!source.contextId || !source.snapshotId)
|
|
121
|
+
throw new Error('context expand current context source is incomplete');
|
|
122
|
+
const content = await rebuildSnapshot(projectRoot, source.contextId, source.snapshotId);
|
|
123
|
+
const value = contextField(content, candidate);
|
|
124
|
+
const text = currentContextText(value, candidate);
|
|
125
|
+
return { content: level === 'evidence' ? { field: candidate.content.field, value, source: source.provenance } : { field: candidate.content.field, value }, text: level === 'evidence' ? evidenceText(source, { value }, text) : text };
|
|
126
|
+
}
|
|
127
|
+
if (source.kind === 'note') {
|
|
128
|
+
const note = (await listNotes(projectRoot, { contextId: source.contextId ?? undefined, limit: Number.MAX_SAFE_INTEGER, maxChars: Number.MAX_SAFE_INTEGER })).find((item) => item.noteId === source.entityId);
|
|
129
|
+
if (!note)
|
|
130
|
+
throw new Error(`context expand note not found: ${source.entityId}`);
|
|
131
|
+
const text = recordText(source, note);
|
|
132
|
+
return { content: level === 'evidence' ? { ...note, provenance: source.provenance } : note, text: level === 'evidence' ? evidenceText(source, note, text) : text };
|
|
133
|
+
}
|
|
134
|
+
if (source.kind === 'knowledge') {
|
|
135
|
+
const knowledge = (await listKnowledge(projectRoot, true)).find((item) => item.knowledgeId === source.entityId);
|
|
136
|
+
if (!knowledge || knowledge.status !== 'verified')
|
|
137
|
+
throw new Error(`context expand knowledge is no longer verified: ${source.entityId}`);
|
|
138
|
+
ensureApplicable(source, knowledge, manifest);
|
|
139
|
+
const text = recordText(source, knowledge);
|
|
140
|
+
return { content: level === 'evidence' ? { ...knowledge, provenance: source.provenance } : knowledge, text: level === 'evidence' ? evidenceText(source, knowledge, text) : text };
|
|
141
|
+
}
|
|
142
|
+
if (source.kind === 'deadend') {
|
|
143
|
+
const deadend = (await listDeadends(projectRoot, true)).find((item) => item.deadendId === source.entityId);
|
|
144
|
+
if (!deadend || deadend.status !== 'verified')
|
|
145
|
+
throw new Error(`context expand deadend is no longer verified: ${source.entityId}`);
|
|
146
|
+
ensureApplicable(source, deadend, manifest);
|
|
147
|
+
const text = recordText(source, deadend);
|
|
148
|
+
return { content: level === 'evidence' ? { ...deadend, provenance: source.provenance } : deadend, text: level === 'evidence' ? evidenceText(source, deadend, text) : text };
|
|
149
|
+
}
|
|
150
|
+
if (source.kind === 'journal') {
|
|
151
|
+
const events = manifest.provenance.journalRead ? await readJournal(projectRoot) : [];
|
|
152
|
+
const projection = events.length > 0 ? projectEvents(events) : null;
|
|
153
|
+
const workspace = projection?.workspaces[workspaceKey(manifest.anchor)] ?? null;
|
|
154
|
+
const records = await extractActivity(projectRoot, events, manifest.anchor, workspace, Number.MAX_SAFE_INTEGER);
|
|
155
|
+
const activity = records.find((item) => item.eventId === source.entityId);
|
|
156
|
+
if (!activity)
|
|
157
|
+
throw new Error(`context expand activity not found: ${source.entityId}`);
|
|
158
|
+
return { content: activity, text: `activity: ${activity.changes.map((change) => `${change.path} (${change.kind})`).join(', ')}` };
|
|
159
|
+
}
|
|
160
|
+
throw new Error(`context expand source kind is not supported: ${source.kind}`);
|
|
161
|
+
}
|
|
162
|
+
export async function expandContext(startPath, input) {
|
|
163
|
+
validateInput(input);
|
|
164
|
+
const manifest = await compileContext(startPath, input.compile);
|
|
165
|
+
if (!validateManifestIdentity(manifest) || manifest.identity.manifestHash !== input.manifestHash)
|
|
166
|
+
throw new Error('context expand manifest hash mismatch');
|
|
167
|
+
const candidate = selectedCandidate(manifest, input);
|
|
168
|
+
const resolved = await resolveCandidate(manifest.projectRoot, manifest, candidate, input.level);
|
|
169
|
+
const limited = truncateText(resolved.text, input.maxChars ?? Number.MAX_SAFE_INTEGER);
|
|
170
|
+
const content = limited.truncated ? null : resolved.content;
|
|
171
|
+
const requestedChars = input.maxChars ?? resolved.text.length;
|
|
172
|
+
return {
|
|
173
|
+
status: limited.truncated ? 'partial' : 'complete',
|
|
174
|
+
projectRoot: manifest.projectRoot,
|
|
175
|
+
manifestHash: manifest.identity.manifestHash,
|
|
176
|
+
candidateId: candidate.candidateId,
|
|
177
|
+
itemHash: candidate.itemHash,
|
|
178
|
+
previousLevel: candidate.level,
|
|
179
|
+
requestedLevel: input.level,
|
|
180
|
+
resolvedLevel: input.level,
|
|
181
|
+
source: candidate.source,
|
|
182
|
+
content,
|
|
183
|
+
text: limited.text,
|
|
184
|
+
budget: { requestedChars, usedChars: limited.text.length, remainingChars: Math.max(0, requestedChars - limited.text.length), truncated: limited.truncated },
|
|
185
|
+
provenance: manifest.provenance,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ContextManifest } from './types.js';
|
|
2
|
+
export declare function canonicalJson(value: unknown): string;
|
|
3
|
+
export declare function contentHash(value: unknown): string;
|
|
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;
|
|
6
|
+
export declare function validateManifestIdentity(manifest: ContextManifest): boolean;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
export function canonicalJson(value) {
|
|
3
|
+
if (value === null || typeof value !== 'object')
|
|
4
|
+
return JSON.stringify(value) ?? 'null';
|
|
5
|
+
if (Array.isArray(value))
|
|
6
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
7
|
+
const record = value;
|
|
8
|
+
return `{${Object.keys(record).filter((key) => record[key] !== undefined).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
|
|
9
|
+
}
|
|
10
|
+
export function contentHash(value) {
|
|
11
|
+
return crypto.createHash('sha256').update(canonicalJson(value)).digest('hex');
|
|
12
|
+
}
|
|
13
|
+
export function estimateTokens(value) {
|
|
14
|
+
return value.length === 0 ? 0 : Math.ceil(value.length / 4);
|
|
15
|
+
}
|
|
16
|
+
export function rebuildManifestHash(manifest) {
|
|
17
|
+
return contentHash({
|
|
18
|
+
schemaVersion: 1,
|
|
19
|
+
compilerVersion: '0.7.7',
|
|
20
|
+
rankingVersion: 'compiler-ranking-v2',
|
|
21
|
+
status: manifest.status,
|
|
22
|
+
projectRoot: manifest.projectRoot,
|
|
23
|
+
mode: manifest.mode,
|
|
24
|
+
anchor: manifest.anchor,
|
|
25
|
+
intent: manifest.intent,
|
|
26
|
+
selectedItemHashes: manifest.selected.map((item) => item.itemHash),
|
|
27
|
+
budget: { usedChars: manifest.budget.usedChars, maxChars: manifest.budget.requested.total },
|
|
28
|
+
truncation: manifest.truncation,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
export function validateManifestIdentity(manifest) {
|
|
32
|
+
return manifest.identity.manifestHash === rebuildManifestHash(manifest)
|
|
33
|
+
&& JSON.stringify(manifest.identity.selectedItemHashes) === JSON.stringify(manifest.selected.map((item) => item.itemHash))
|
|
34
|
+
&& manifest.identity.tokenEstimate.input === estimateTokens(manifest.text)
|
|
35
|
+
&& manifest.identity.tokenEstimate.output === estimateTokens(manifest.text);
|
|
36
|
+
}
|