fluffy-context 0.5.0 → 0.7.2
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 +121 -9
- package/dist/src/agent/api.d.ts +5 -1
- package/dist/src/agent/api.js +41 -12
- package/dist/src/agent/index.d.ts +3 -1
- package/dist/src/agent/index.js +1 -0
- package/dist/src/agent/types.d.ts +21 -0
- package/dist/src/cli/main.js +150 -12
- package/dist/src/cognition/feedback.d.ts +9 -0
- package/dist/src/cognition/feedback.js +30 -0
- package/dist/src/cognition/filesystem-observer.d.ts +41 -0
- package/dist/src/cognition/filesystem-observer.js +164 -0
- package/dist/src/cognition/git-observer.d.ts +1 -0
- package/dist/src/cognition/git-observer.js +3 -2
- package/dist/src/cognition/journal.d.ts +4 -1
- package/dist/src/cognition/journal.js +83 -4
- package/dist/src/cognition/migration-v1.js +13 -11
- package/dist/src/cognition/orient.d.ts +4 -0
- package/dist/src/cognition/orient.js +14 -4
- package/dist/src/cognition/policy.d.ts +4 -0
- package/dist/src/cognition/policy.js +49 -0
- package/dist/src/cognition/projections.js +0 -0
- package/dist/src/cognition/retrieval.d.ts +26 -0
- package/dist/src/cognition/retrieval.js +273 -0
- package/dist/src/cognition/runtime.d.ts +20 -1
- package/dist/src/cognition/runtime.js +229 -53
- package/dist/src/cognition/types.d.ts +55 -1
- package/dist/src/cognition/v1-bridge.js +13 -4
- package/dist/src/compiler/activity.d.ts +3 -0
- package/dist/src/compiler/activity.js +79 -0
- package/dist/src/compiler/compile.d.ts +2 -0
- package/dist/src/compiler/compile.js +330 -0
- package/dist/src/compiler/sources.d.ts +19 -0
- package/dist/src/compiler/sources.js +130 -0
- package/dist/src/compiler/types.d.ts +187 -0
- package/dist/src/compiler/types.js +1 -0
- package/dist/src/hooks/claude-code.js +7 -15
- package/dist/src/mcp/main.js +2 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/runtime/knowledge.d.ts +2 -0
- package/dist/src/runtime/knowledge.js +63 -0
- package/dist/src/runtime/types.d.ts +19 -0
- package/dist/src/storage/layout.d.ts +2 -0
- package/dist/src/storage/layout.js +6 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +2 -2
- package/skills/fluffy-context/SKILL.md +2 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { filterPaths } from '../capture/context-filter.js';
|
|
2
|
+
import { queryTerms } from '../cognition/retrieval.js';
|
|
3
|
+
import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
|
|
4
|
+
import { listNotes } from '../runtime/notes.js';
|
|
5
|
+
import { extractActivity } from './activity.js';
|
|
6
|
+
import { latestHostIntent, loadCompilerSources, sourceDescriptor } from './sources.js';
|
|
7
|
+
const scenarioKeywords = [
|
|
8
|
+
{ scenario: 'debugging', words: ['bug', 'error', 'fail', 'failure', 'exception', 'regression', 'debug', '失败', '报错', '异常', '修复'] },
|
|
9
|
+
{ scenario: 'verification', words: ['test', 'verify', 'check', 'build', '测试', '验证', '检查', '构建'] },
|
|
10
|
+
{ scenario: 'handoff', words: ['handoff', 'summary', '交接', '总结', '移交'] },
|
|
11
|
+
{ scenario: 'implementation', words: ['implement', 'add', 'change', 'feature', '实现', '新增', '修改', '功能'] },
|
|
12
|
+
{ scenario: 'exploration', words: ['search', 'investigate', 'find', 'explore', '查找', '调查', '分析', '探索'] },
|
|
13
|
+
{ scenario: 'resume', words: ['resume', 'continue', 'restore', '继续', '恢复'] },
|
|
14
|
+
];
|
|
15
|
+
const priorities = {
|
|
16
|
+
summary: 100,
|
|
17
|
+
pending: 95,
|
|
18
|
+
risk: 94,
|
|
19
|
+
decision: 92,
|
|
20
|
+
note: 85,
|
|
21
|
+
knowledge: 75,
|
|
22
|
+
deadend: 73,
|
|
23
|
+
activity: 70,
|
|
24
|
+
};
|
|
25
|
+
function clean(value) {
|
|
26
|
+
return value.normalize('NFC').trim().toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
function stableCompare(left, right) {
|
|
29
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
30
|
+
}
|
|
31
|
+
function anchorFromSources(sources) {
|
|
32
|
+
return sources.anchor;
|
|
33
|
+
}
|
|
34
|
+
function signal(source, value, origin, confidence) {
|
|
35
|
+
return { source, value, origin, confidence };
|
|
36
|
+
}
|
|
37
|
+
function inferScenario(query, explicit) {
|
|
38
|
+
if (explicit)
|
|
39
|
+
return { scenario: explicit, signals: [signal('explicit-scenario', explicit, 'explicit', 'high')] };
|
|
40
|
+
if (!query)
|
|
41
|
+
return { scenario: 'resume', signals: [] };
|
|
42
|
+
const normalized = clean(query);
|
|
43
|
+
const matches = scenarioKeywords.flatMap(({ scenario, words }) => words.filter((word) => normalized.includes(clean(word))).map((word) => ({ scenario, word })));
|
|
44
|
+
if (matches.length === 0)
|
|
45
|
+
return { scenario: 'unknown', signals: [] };
|
|
46
|
+
const selected = matches[0].scenario;
|
|
47
|
+
return {
|
|
48
|
+
scenario: selected,
|
|
49
|
+
signals: matches.map(({ scenario, word }) => signal('context-state', `${scenario}:${word}`, 'deterministic', 'medium')),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function sourcePaths(record) {
|
|
53
|
+
const applicability = record.applicability;
|
|
54
|
+
if (!applicability || typeof applicability !== 'object' || Array.isArray(applicability))
|
|
55
|
+
return [];
|
|
56
|
+
const paths = applicability.paths;
|
|
57
|
+
return Array.isArray(paths) ? paths.filter((value) => typeof value === 'string') : [];
|
|
58
|
+
}
|
|
59
|
+
function sourceApplicability(record, descriptor) {
|
|
60
|
+
const applicability = record.applicability;
|
|
61
|
+
if (!applicability || typeof applicability !== 'object' || Array.isArray(applicability))
|
|
62
|
+
return descriptor;
|
|
63
|
+
const value = applicability;
|
|
64
|
+
const branchMode = value.branchMode === 'global' || value.branchMode === 'exact' ? value.branchMode : descriptor.applicability.branchMode;
|
|
65
|
+
const branches = Array.isArray(value.branches) ? value.branches.filter((item) => typeof item === 'string').map(clean).sort(stableCompare) : descriptor.applicability.branches;
|
|
66
|
+
const paths = sourcePaths(record).map((item) => item.replaceAll('\\', '/')).sort(stableCompare);
|
|
67
|
+
return {
|
|
68
|
+
...descriptor,
|
|
69
|
+
scope: typeof record.scope === 'string' ? record.scope : descriptor.scope,
|
|
70
|
+
paths,
|
|
71
|
+
applicability: {
|
|
72
|
+
...descriptor.applicability,
|
|
73
|
+
branchMode,
|
|
74
|
+
branches,
|
|
75
|
+
paths,
|
|
76
|
+
ref: descriptor.applicability.ref,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function matched(text, terms) {
|
|
81
|
+
const normalized = clean(text);
|
|
82
|
+
const matchedTerms = terms.filter((term) => normalized.includes(term));
|
|
83
|
+
return { score: matchedTerms.length, matchedTerms };
|
|
84
|
+
}
|
|
85
|
+
function makeCandidate(section, source, content, text, terms, matchedFields, discriminator) {
|
|
86
|
+
const hit = matched(text, terms);
|
|
87
|
+
return {
|
|
88
|
+
candidateId: `${section}:${source.sourceId}:${matchedFields.join('.')}:${discriminator}`,
|
|
89
|
+
section,
|
|
90
|
+
source,
|
|
91
|
+
content,
|
|
92
|
+
text,
|
|
93
|
+
priority: priorities[section],
|
|
94
|
+
score: hit.score,
|
|
95
|
+
matchedTerms: hit.matchedTerms,
|
|
96
|
+
matchedFields,
|
|
97
|
+
originalChars: text.length,
|
|
98
|
+
selectedChars: 0,
|
|
99
|
+
selected: false,
|
|
100
|
+
truncated: false,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function applicable(candidate, intent) {
|
|
104
|
+
const source = candidate.source;
|
|
105
|
+
if (source.privacy.classification === 'unknown')
|
|
106
|
+
return { ok: false, reason: 'v2_unavailable' };
|
|
107
|
+
if (source.status && ['candidate', 'deprecated', 'rejected', 'obsolete'].includes(source.status))
|
|
108
|
+
return { ok: false, reason: 'status_not_verified' };
|
|
109
|
+
if (source.applicability.branchMode === 'unsupported')
|
|
110
|
+
return { ok: false, reason: 'unsupported_applicability' };
|
|
111
|
+
if (source.applicability.branchMode === 'exact') {
|
|
112
|
+
const branch = intent.anchor.ref ?? intent.anchor.branch ?? 'detached';
|
|
113
|
+
if (source.applicability.branches.length > 0 && !source.applicability.branches.includes(branch) && !source.applicability.branches.includes(intent.anchor.branch ?? ''))
|
|
114
|
+
return { ok: false, reason: 'applicability_mismatch' };
|
|
115
|
+
if (source.applicability.worktreeId && source.applicability.worktreeId !== intent.anchor.worktreeId)
|
|
116
|
+
return { ok: false, reason: 'applicability_mismatch' };
|
|
117
|
+
if (source.applicability.ref && source.applicability.ref !== intent.anchor.ref)
|
|
118
|
+
return { ok: false, reason: 'applicability_mismatch' };
|
|
119
|
+
}
|
|
120
|
+
if (intent.scope && source.scope && clean(intent.scope) !== clean(source.scope))
|
|
121
|
+
return { ok: false, reason: 'scope_mismatch' };
|
|
122
|
+
if (intent.paths.length > 0 && source.paths.length > 0 && !source.paths.some((path) => intent.paths.includes(path)))
|
|
123
|
+
return { ok: false, reason: 'path_mismatch' };
|
|
124
|
+
if (intent.query && ['knowledge', 'deadend'].includes(candidate.section) && candidate.matchedTerms.length === 0)
|
|
125
|
+
return { ok: false, reason: 'query_no_match' };
|
|
126
|
+
return { ok: true };
|
|
127
|
+
}
|
|
128
|
+
function sortCandidates(left, right) {
|
|
129
|
+
return right.priority - left.priority
|
|
130
|
+
|| right.score - left.score
|
|
131
|
+
|| right.matchedTerms.length - left.matchedTerms.length
|
|
132
|
+
|| stableCompare(left.candidateId, right.candidateId);
|
|
133
|
+
}
|
|
134
|
+
function truncateText(value, maxChars) {
|
|
135
|
+
if (value.length <= maxChars)
|
|
136
|
+
return { text: value, truncated: false };
|
|
137
|
+
if (maxChars <= 0)
|
|
138
|
+
return { text: '', truncated: true };
|
|
139
|
+
return { text: `${value.slice(0, maxChars - 1)}…`, truncated: true };
|
|
140
|
+
}
|
|
141
|
+
function textForSummary(label, value) {
|
|
142
|
+
return value ? `${label}: ${value}` : '';
|
|
143
|
+
}
|
|
144
|
+
function activityText(record) {
|
|
145
|
+
const changes = record.changes.map((change) => `${change.path} (${change.kind})`).join(', ');
|
|
146
|
+
return `activity: ${changes}`;
|
|
147
|
+
}
|
|
148
|
+
function createCandidates(sources, intent, notes, knowledge, deadends, activities) {
|
|
149
|
+
if (sources.orientation.status !== 'ready')
|
|
150
|
+
return [];
|
|
151
|
+
const { context, snapshot, resumeSummary } = sources.orientation;
|
|
152
|
+
const terms = intent.normalizedTerms;
|
|
153
|
+
const candidates = [];
|
|
154
|
+
const contextSource = sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'current-context', context.id, context.id, snapshot.snapshotId, context.status, null, resumeSummary.relatedFiles);
|
|
155
|
+
const summaryFields = [
|
|
156
|
+
['title', context.title, 'summary'],
|
|
157
|
+
['progressSummary', resumeSummary.progressSummary, 'summary'],
|
|
158
|
+
['lastError', resumeSummary.lastError, 'risk'],
|
|
159
|
+
];
|
|
160
|
+
for (const [field, value, section] of summaryFields) {
|
|
161
|
+
const text = textForSummary(field, value);
|
|
162
|
+
if (text)
|
|
163
|
+
candidates.push(makeCandidate(section, contextSource, { field, value }, text, terms, [field], field));
|
|
164
|
+
}
|
|
165
|
+
const listFields = [
|
|
166
|
+
['pendingTasks', resumeSummary.pendingTasks, 'pending'],
|
|
167
|
+
['completed', resumeSummary.completed, 'summary'],
|
|
168
|
+
['decisions', resumeSummary.decisions, 'decision'],
|
|
169
|
+
['risks', resumeSummary.risks, 'risk'],
|
|
170
|
+
['relatedFiles', resumeSummary.relatedFiles, 'summary'],
|
|
171
|
+
];
|
|
172
|
+
for (const [field, values, section] of listFields) {
|
|
173
|
+
for (const [index, value] of values.entries()) {
|
|
174
|
+
candidates.push(makeCandidate(section, contextSource, { field, value }, `${field}: ${value}`, terms, [field], `${field}:${index}`));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const note of notes) {
|
|
178
|
+
const source = sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'note', note.noteId, note.contextId, note.snapshotId, note.status, null, note.relatedFiles);
|
|
179
|
+
candidates.push(makeCandidate('note', source, note, `${note.kind}: ${note.message}`, terms, ['kind', 'message'], note.noteId));
|
|
180
|
+
}
|
|
181
|
+
for (const item of knowledge) {
|
|
182
|
+
const source = sourceApplicability(item, sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'knowledge', item.knowledgeId, item.sourceContextIds[0] ?? null, item.sourceSnapshotIds[0] ?? null, item.status, item.scope, sourcePaths(item)));
|
|
183
|
+
candidates.push(makeCandidate('knowledge', source, item, item.statement, terms, ['statement', 'kind', 'scope'], item.knowledgeId));
|
|
184
|
+
}
|
|
185
|
+
for (const item of deadends) {
|
|
186
|
+
const source = sourceApplicability(item, sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'deadend', item.deadendId, item.sourceContextId, item.sourceSnapshotId, item.status, item.scope, sourcePaths(item)));
|
|
187
|
+
candidates.push(makeCandidate('deadend', source, item, `${item.attempt}: ${item.reason}`, terms, ['attempt', 'reason', 'scope'], item.deadendId));
|
|
188
|
+
}
|
|
189
|
+
for (const item of activities) {
|
|
190
|
+
const source = sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'journal', item.eventId, null, null, null, null, item.paths);
|
|
191
|
+
candidates.push(makeCandidate('activity', source, item, activityText(item), terms, ['paths'], item.eventId));
|
|
192
|
+
}
|
|
193
|
+
return candidates;
|
|
194
|
+
}
|
|
195
|
+
function emptyTruncation() {
|
|
196
|
+
return { truncated: false, summary: false, pending: false, decisions: false, risks: false, notes: false, knowledge: false, deadends: false, activity: false, candidatesOmittedByBudget: 0, candidatesPartiallyTruncated: 0 };
|
|
197
|
+
}
|
|
198
|
+
function manifestText(selected) {
|
|
199
|
+
return selected.map((candidate) => candidate.text).join('\n');
|
|
200
|
+
}
|
|
201
|
+
function intentFor(sources, options, normalizedPaths) {
|
|
202
|
+
const orientation = sources.orientation;
|
|
203
|
+
const host = latestHostIntent(sources.events, sources.anchor);
|
|
204
|
+
const query = options.query?.trim() || host?.value.trim() || (orientation.status === 'ready' ? orientation.context.title : null);
|
|
205
|
+
const inferred = inferScenario(query, options.scenario);
|
|
206
|
+
const signals = [
|
|
207
|
+
...(options.query ? [signal('explicit-query', options.query.trim(), 'explicit', 'high')] : []),
|
|
208
|
+
...(host && !options.query ? [signal('host-prompt', host.value, 'deterministic', 'medium')] : []),
|
|
209
|
+
...(normalizedPaths.map((value) => signal('path', value, 'explicit', 'high'))),
|
|
210
|
+
...(inferred.signals),
|
|
211
|
+
];
|
|
212
|
+
return {
|
|
213
|
+
schemaVersion: 1,
|
|
214
|
+
trigger: options.trigger ?? 'manual',
|
|
215
|
+
query,
|
|
216
|
+
normalizedTerms: query ? queryTerms(query).map(clean).sort(stableCompare) : [],
|
|
217
|
+
scenario: inferred.scenario,
|
|
218
|
+
scope: options.scope ?? null,
|
|
219
|
+
paths: normalizedPaths,
|
|
220
|
+
contextId: options.contextId ?? (orientation.status === 'ready' ? orientation.context.id : null),
|
|
221
|
+
anchor: anchorFromSources(sources),
|
|
222
|
+
signals,
|
|
223
|
+
requestedSections: ['summary', 'pending', 'decision', 'risk', 'note', 'knowledge', 'deadend', 'activity'],
|
|
224
|
+
budget: { unit: 'chars', total: Math.max(0, options.maxChars ?? 4000), reservedForRenderer: 0 },
|
|
225
|
+
adapter: options.adapter ?? 'generic',
|
|
226
|
+
sessionId: options.sessionId ?? null,
|
|
227
|
+
eventId: options.eventId ?? null,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export async function compileContext(startPath, options = {}) {
|
|
231
|
+
const sources = await loadCompilerSources(startPath, options);
|
|
232
|
+
const normalizedPaths = options.paths ? await filterPaths(sources.anchor.projectRoot, options.paths) : [];
|
|
233
|
+
const intent = intentFor(sources, options, normalizedPaths);
|
|
234
|
+
const orientation = sources.orientation;
|
|
235
|
+
const maxChars = intent.budget.total;
|
|
236
|
+
const notes = orientation.status === 'ready' ? (await listNotes(sources.anchor.projectRoot, { contextId: orientation.context.id, openOnly: true, limit: Number.MAX_SAFE_INTEGER, maxChars: Number.MAX_SAFE_INTEGER })).filter((note) => !orientation || orientation.notes.some((selected) => selected.noteId === note.noteId)) : [];
|
|
237
|
+
const knowledge = orientation.status === 'ready' && intent.query ? await listKnowledge(sources.anchor.projectRoot, true) : [];
|
|
238
|
+
const deadends = orientation.status === 'ready' && intent.query ? await listDeadends(sources.anchor.projectRoot, true) : [];
|
|
239
|
+
const activities = await extractActivity(sources.anchor.projectRoot, sources.events, sources.anchor, sources.workspace, options.activityLimit ?? 20);
|
|
240
|
+
const all = createCandidates(sources, intent, notes, knowledge, deadends, activities);
|
|
241
|
+
const excluded = [];
|
|
242
|
+
const eligible = all.filter((candidate) => {
|
|
243
|
+
const check = applicable(candidate, intent);
|
|
244
|
+
if (!check.ok)
|
|
245
|
+
excluded.push({ candidateId: candidate.candidateId, source: candidate.source, reason: check.reason ?? 'query_no_match' });
|
|
246
|
+
return check.ok;
|
|
247
|
+
}).sort(sortCandidates);
|
|
248
|
+
const selected = [];
|
|
249
|
+
const truncation = emptyTruncation();
|
|
250
|
+
const sectionStats = {};
|
|
251
|
+
let usedChars = 0;
|
|
252
|
+
for (const candidate of eligible) {
|
|
253
|
+
const separator = selected.length > 0 ? 1 : 0;
|
|
254
|
+
const available = maxChars - usedChars - separator;
|
|
255
|
+
if (available <= 0) {
|
|
256
|
+
excluded.push({ candidateId: candidate.candidateId, source: candidate.source, reason: 'budget' });
|
|
257
|
+
truncation.candidatesOmittedByBudget += 1;
|
|
258
|
+
truncation.truncated = true;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
const limited = truncateText(candidate.text, available);
|
|
262
|
+
if (!limited.text) {
|
|
263
|
+
excluded.push({ candidateId: candidate.candidateId, source: candidate.source, reason: 'budget' });
|
|
264
|
+
truncation.candidatesOmittedByBudget += 1;
|
|
265
|
+
truncation.truncated = true;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const item = { ...candidate, text: limited.text, selectedChars: limited.text.length, selected: true, truncated: limited.truncated };
|
|
269
|
+
selected.push(item);
|
|
270
|
+
usedChars += separator + item.selectedChars;
|
|
271
|
+
const prior = sectionStats[item.section] ?? { allocated: maxChars, consumed: 0, truncated: false };
|
|
272
|
+
sectionStats[item.section] = { ...prior, consumed: prior.consumed + item.selectedChars + separator, truncated: prior.truncated || limited.truncated };
|
|
273
|
+
if (limited.truncated) {
|
|
274
|
+
truncation.candidatesPartiallyTruncated += 1;
|
|
275
|
+
truncation.truncated = true;
|
|
276
|
+
if (item.section === 'summary')
|
|
277
|
+
truncation.summary = true;
|
|
278
|
+
if (item.section === 'pending')
|
|
279
|
+
truncation.pending = true;
|
|
280
|
+
if (item.section === 'decision')
|
|
281
|
+
truncation.decisions = true;
|
|
282
|
+
if (item.section === 'risk')
|
|
283
|
+
truncation.risks = true;
|
|
284
|
+
if (item.section === 'note')
|
|
285
|
+
truncation.notes = true;
|
|
286
|
+
if (item.section === 'knowledge')
|
|
287
|
+
truncation.knowledge = true;
|
|
288
|
+
if (item.section === 'deadend')
|
|
289
|
+
truncation.deadends = true;
|
|
290
|
+
if (item.section === 'activity')
|
|
291
|
+
truncation.activity = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const sections = {
|
|
295
|
+
summary: selected.filter((item) => item.section === 'summary'),
|
|
296
|
+
pending: selected.filter((item) => item.section === 'pending'),
|
|
297
|
+
decision: selected.filter((item) => item.section === 'decision'),
|
|
298
|
+
risk: selected.filter((item) => item.section === 'risk'),
|
|
299
|
+
notes: selected.filter((item) => item.section === 'note'),
|
|
300
|
+
knowledge: selected.filter((item) => item.section === 'knowledge'),
|
|
301
|
+
deadends: selected.filter((item) => item.section === 'deadend'),
|
|
302
|
+
activity: selected.filter((item) => item.section === 'activity'),
|
|
303
|
+
};
|
|
304
|
+
return {
|
|
305
|
+
schemaVersion: 1,
|
|
306
|
+
compilerVersion: '0.7.2',
|
|
307
|
+
rankingVersion: 'compiler-ranking-v2',
|
|
308
|
+
status: orientation.status,
|
|
309
|
+
projectRoot: sources.anchor.projectRoot,
|
|
310
|
+
mode: orientation.explanation.mode,
|
|
311
|
+
anchor: sources.anchor,
|
|
312
|
+
intent,
|
|
313
|
+
context: orientation.context,
|
|
314
|
+
snapshot: orientation.snapshot,
|
|
315
|
+
resumeSummary: orientation.resumeSummary,
|
|
316
|
+
sections,
|
|
317
|
+
selected,
|
|
318
|
+
excluded,
|
|
319
|
+
budget: { requested: intent.budget, usedChars, remainingChars: Math.max(0, maxChars - usedChars), sections: sectionStats },
|
|
320
|
+
truncation,
|
|
321
|
+
provenance: {
|
|
322
|
+
journalRead: sources.events.length > 0,
|
|
323
|
+
journalEventCount: sources.events.length,
|
|
324
|
+
selectedSourceIds: [...new Set(selected.map((item) => item.source.sourceId))].sort(stableCompare),
|
|
325
|
+
selectedEventIds: [...new Set(selected.flatMap((item) => item.source.provenance.eventId ? [item.source.provenance.eventId] : []))].sort(stableCompare),
|
|
326
|
+
fallbackReason: sources.fallbackReason,
|
|
327
|
+
},
|
|
328
|
+
text: manifestText(selected),
|
|
329
|
+
};
|
|
330
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BranchWorkspace, CognitionEvent } from '../cognition/types.js';
|
|
2
|
+
import type { ContextOrientResult } from '../agent/types.js';
|
|
3
|
+
import type { ContextAnchor, ContextProvenance, ContextSourceDescriptor, ContextSourceApplicability } from './types.js';
|
|
4
|
+
import type { ContextCompileOptions } from './types.js';
|
|
5
|
+
export interface CompilerSources {
|
|
6
|
+
orientation: ContextOrientResult;
|
|
7
|
+
anchor: ContextAnchor;
|
|
8
|
+
events: CognitionEvent[];
|
|
9
|
+
workspace: BranchWorkspace | null;
|
|
10
|
+
fallbackReason: string | null;
|
|
11
|
+
}
|
|
12
|
+
export declare function sourceApplicability(event: CognitionEvent | null, anchor: ContextAnchor): ContextSourceApplicability;
|
|
13
|
+
export declare function provenanceFor(events: CognitionEvent[], entityId: string, anchor: ContextAnchor): ContextProvenance;
|
|
14
|
+
export declare function sourceDescriptor(projectRoot: string, anchor: ContextAnchor, events: CognitionEvent[], kind: ContextSourceDescriptor['kind'], entityId: string, contextId: string | null, snapshotId: string | null, status: string | null, scope: string | null, paths: string[]): ContextSourceDescriptor;
|
|
15
|
+
export declare function loadCompilerSources(startPath: string | undefined, options: ContextCompileOptions): Promise<CompilerSources>;
|
|
16
|
+
export declare function latestHostIntent(events: CognitionEvent[], anchor: ContextAnchor): {
|
|
17
|
+
value: string;
|
|
18
|
+
eventId: string;
|
|
19
|
+
} | null;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readGitWorktreeState } from '../git/git-adapter.js';
|
|
2
|
+
import { contextOrient } from '../agent/api.js';
|
|
3
|
+
import { readJournal } from '../cognition/journal.js';
|
|
4
|
+
import { projectEvents, workspaceKey } from '../cognition/projections.js';
|
|
5
|
+
function anchorFromGit(git, projectRoot) {
|
|
6
|
+
return {
|
|
7
|
+
projectRoot,
|
|
8
|
+
worktreeId: git.worktreeId,
|
|
9
|
+
branch: git.branch,
|
|
10
|
+
ref: git.ref,
|
|
11
|
+
head: git.commit,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function payloadRecord(event) {
|
|
15
|
+
const payload = event.payload;
|
|
16
|
+
return payload.record && typeof payload.record === 'object' && !Array.isArray(payload.record)
|
|
17
|
+
? payload.record
|
|
18
|
+
: null;
|
|
19
|
+
}
|
|
20
|
+
function matchesRecord(event, entityId) {
|
|
21
|
+
if (event.eventId === entityId)
|
|
22
|
+
return true;
|
|
23
|
+
const record = payloadRecord(event);
|
|
24
|
+
return record?.contextId === entityId
|
|
25
|
+
|| record?.knowledgeId === entityId
|
|
26
|
+
|| record?.deadendId === entityId
|
|
27
|
+
|| record?.noteId === entityId
|
|
28
|
+
|| record?.snapshotId === entityId;
|
|
29
|
+
}
|
|
30
|
+
export function sourceApplicability(event, anchor) {
|
|
31
|
+
if (!event || (event.git.branch === null && event.git.ref === null)) {
|
|
32
|
+
return { project: true, branchMode: 'global', branches: [], paths: [], worktreeId: null, ref: null };
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
project: true,
|
|
36
|
+
branchMode: 'exact',
|
|
37
|
+
branches: [event.git.ref ?? event.git.branch ?? 'detached'],
|
|
38
|
+
paths: [],
|
|
39
|
+
worktreeId: event.git.worktreeId,
|
|
40
|
+
ref: event.git.ref,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function provenanceFor(events, entityId, anchor) {
|
|
44
|
+
const event = [...events].reverse().find((item) => matchesRecord(item, entityId));
|
|
45
|
+
if (!event) {
|
|
46
|
+
return {
|
|
47
|
+
origin: 'v1-file',
|
|
48
|
+
eventId: null,
|
|
49
|
+
eventType: null,
|
|
50
|
+
occurredAt: null,
|
|
51
|
+
sourcePath: null,
|
|
52
|
+
sourceHash: null,
|
|
53
|
+
anchorProvenance: null,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const payload = event.payload;
|
|
57
|
+
return {
|
|
58
|
+
origin: 'v2-journal',
|
|
59
|
+
eventId: event.eventId,
|
|
60
|
+
eventType: event.type,
|
|
61
|
+
occurredAt: event.occurredAt,
|
|
62
|
+
sourcePath: typeof payload.sourcePath === 'string' ? payload.sourcePath : null,
|
|
63
|
+
sourceHash: typeof payload.sourceHash === 'string' ? payload.sourceHash : null,
|
|
64
|
+
anchorProvenance: payload.anchorProvenance === 'snapshot' || payload.anchorProvenance === 'live' || payload.anchorProvenance === 'global'
|
|
65
|
+
? payload.anchorProvenance
|
|
66
|
+
: null,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function sourceDescriptor(projectRoot, anchor, events, kind, entityId, contextId, snapshotId, status, scope, paths) {
|
|
70
|
+
const provenance = provenanceFor(events, entityId, anchor);
|
|
71
|
+
const event = provenance.eventId ? events.find((item) => item.eventId === provenance.eventId) ?? null : null;
|
|
72
|
+
return {
|
|
73
|
+
sourceId: `${kind}:${entityId}`,
|
|
74
|
+
kind,
|
|
75
|
+
entityId,
|
|
76
|
+
contextId,
|
|
77
|
+
snapshotId,
|
|
78
|
+
status,
|
|
79
|
+
scope,
|
|
80
|
+
paths: [...new Set(paths)].sort(),
|
|
81
|
+
anchor,
|
|
82
|
+
applicability: sourceApplicability(event, anchor),
|
|
83
|
+
privacy: { classification: provenance.origin === 'v2-journal' ? (event?.privacy.classification ?? 'metadata') : 'metadata', redactionCount: event?.privacy.redactionCount ?? 0 },
|
|
84
|
+
provenance,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export async function loadCompilerSources(startPath, options) {
|
|
88
|
+
const orientation = await contextOrient(startPath, {
|
|
89
|
+
contextId: options.contextId,
|
|
90
|
+
query: options.query,
|
|
91
|
+
scope: options.scope,
|
|
92
|
+
maxChars: Number.MAX_SAFE_INTEGER,
|
|
93
|
+
knowledgeLimit: options.knowledgeLimit ?? 10,
|
|
94
|
+
deadendLimit: options.deadendLimit ?? 10,
|
|
95
|
+
noteLimit: options.noteLimit ?? 20,
|
|
96
|
+
});
|
|
97
|
+
const projectRoot = orientation.projectRoot;
|
|
98
|
+
const git = await readGitWorktreeState(projectRoot);
|
|
99
|
+
let events = [];
|
|
100
|
+
let fallbackReason = null;
|
|
101
|
+
try {
|
|
102
|
+
events = await readJournal(projectRoot);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
fallbackReason = error instanceof Error ? error.message : 'journal-unavailable';
|
|
106
|
+
}
|
|
107
|
+
const anchor = anchorFromGit(git, projectRoot);
|
|
108
|
+
const projection = events.length > 0 ? projectEvents(events) : null;
|
|
109
|
+
const workspace = projection?.workspaces[workspaceKey(anchor)] ?? null;
|
|
110
|
+
return { orientation, anchor, events, workspace, fallbackReason };
|
|
111
|
+
}
|
|
112
|
+
export function latestHostIntent(events, anchor) {
|
|
113
|
+
return [...events].reverse().find((event) => {
|
|
114
|
+
if (event.type !== 'host.prompt.submitted')
|
|
115
|
+
return false;
|
|
116
|
+
if (event.git.worktreeId !== null && event.git.worktreeId !== anchor.worktreeId)
|
|
117
|
+
return false;
|
|
118
|
+
if (event.git.ref !== null && event.git.ref !== anchor.ref)
|
|
119
|
+
return false;
|
|
120
|
+
const payload = event.payload;
|
|
121
|
+
return typeof payload.intent === 'string' && payload.intent.trim().length > 0;
|
|
122
|
+
})?.payload
|
|
123
|
+
? (() => {
|
|
124
|
+
const event = [...events].reverse().find((item) => item.type === 'host.prompt.submitted' && item.git.worktreeId === anchor.worktreeId && item.git.ref === anchor.ref && typeof item.payload.intent === 'string');
|
|
125
|
+
if (!event)
|
|
126
|
+
return null;
|
|
127
|
+
return { value: event.payload.intent, eventId: event.eventId };
|
|
128
|
+
})()
|
|
129
|
+
: null;
|
|
130
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import type { CognitionGitAnchor, WorkspacePathChange } from '../cognition/types.js';
|
|
2
|
+
import type { ContextOrientResult } from '../agent/types.js';
|
|
3
|
+
import type { Deadend, Knowledge, Note, ResumeSummary, StructuredContext } from '../runtime/types.js';
|
|
4
|
+
export type ContextScenario = 'resume' | 'implementation' | 'debugging' | 'verification' | 'handoff' | 'exploration' | 'unknown';
|
|
5
|
+
export type ContextSourceKind = 'current-context' | 'checkpoint' | 'note' | 'journal' | 'knowledge' | 'deadend';
|
|
6
|
+
export type ContextCandidateSection = 'summary' | 'pending' | 'decision' | 'risk' | 'note' | 'knowledge' | 'deadend' | 'activity';
|
|
7
|
+
export type ContextSignalOrigin = 'explicit' | 'deterministic';
|
|
8
|
+
export interface ContextAnchor {
|
|
9
|
+
projectRoot: string;
|
|
10
|
+
worktreeId: string | null;
|
|
11
|
+
branch: string | null;
|
|
12
|
+
ref: string | null;
|
|
13
|
+
head: string | null;
|
|
14
|
+
}
|
|
15
|
+
export interface ContextIntentSignal {
|
|
16
|
+
source: 'explicit-query' | 'explicit-scenario' | 'context-state' | 'host-prompt' | 'path' | 'git';
|
|
17
|
+
value: string;
|
|
18
|
+
origin: ContextSignalOrigin;
|
|
19
|
+
confidence: 'high' | 'medium' | 'low';
|
|
20
|
+
}
|
|
21
|
+
export interface ContextIntent {
|
|
22
|
+
schemaVersion: 1;
|
|
23
|
+
trigger: 'session-start' | 'prompt' | 'manual' | 'tool-call';
|
|
24
|
+
query: string | null;
|
|
25
|
+
normalizedTerms: string[];
|
|
26
|
+
scenario: ContextScenario;
|
|
27
|
+
scope: string | null;
|
|
28
|
+
paths: string[];
|
|
29
|
+
contextId: string | null;
|
|
30
|
+
anchor: ContextAnchor;
|
|
31
|
+
signals: ContextIntentSignal[];
|
|
32
|
+
requestedSections: ContextCandidateSection[];
|
|
33
|
+
budget: ContextBudget;
|
|
34
|
+
adapter: string;
|
|
35
|
+
sessionId: string | null;
|
|
36
|
+
eventId: string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface ContextBudget {
|
|
39
|
+
unit: 'chars';
|
|
40
|
+
total: number;
|
|
41
|
+
reservedForRenderer: number;
|
|
42
|
+
sections?: Partial<Record<ContextCandidateSection, number>>;
|
|
43
|
+
}
|
|
44
|
+
export interface ContextSourceApplicability {
|
|
45
|
+
project: true;
|
|
46
|
+
branchMode: 'global' | 'exact' | 'unsupported';
|
|
47
|
+
branches: string[];
|
|
48
|
+
paths: string[];
|
|
49
|
+
worktreeId: string | null;
|
|
50
|
+
ref: string | null;
|
|
51
|
+
}
|
|
52
|
+
export interface ContextProvenance {
|
|
53
|
+
origin: 'v1-file' | 'v2-journal' | 'mixed' | 'live';
|
|
54
|
+
eventId: string | null;
|
|
55
|
+
eventType: string | null;
|
|
56
|
+
occurredAt: string | null;
|
|
57
|
+
sourcePath: string | null;
|
|
58
|
+
sourceHash: string | null;
|
|
59
|
+
anchorProvenance: 'snapshot' | 'live' | 'global' | null;
|
|
60
|
+
}
|
|
61
|
+
export interface ContextSourceDescriptor {
|
|
62
|
+
sourceId: string;
|
|
63
|
+
kind: ContextSourceKind;
|
|
64
|
+
entityId: string;
|
|
65
|
+
contextId: string | null;
|
|
66
|
+
snapshotId: string | null;
|
|
67
|
+
status: string | null;
|
|
68
|
+
scope: string | null;
|
|
69
|
+
paths: string[];
|
|
70
|
+
anchor: ContextAnchor;
|
|
71
|
+
applicability: ContextSourceApplicability;
|
|
72
|
+
privacy: {
|
|
73
|
+
classification: 'metadata' | 'redacted-text' | 'unknown';
|
|
74
|
+
redactionCount: number;
|
|
75
|
+
};
|
|
76
|
+
provenance: ContextProvenance;
|
|
77
|
+
}
|
|
78
|
+
export interface ContextActivityRecord {
|
|
79
|
+
eventId: string;
|
|
80
|
+
occurredAt: string;
|
|
81
|
+
anchor: ContextAnchor;
|
|
82
|
+
paths: string[];
|
|
83
|
+
changes: WorkspacePathChange[];
|
|
84
|
+
pathCount: number;
|
|
85
|
+
coalesced: boolean;
|
|
86
|
+
truncated: boolean;
|
|
87
|
+
focusMatch: boolean;
|
|
88
|
+
}
|
|
89
|
+
export interface ContextCandidate<T = unknown> {
|
|
90
|
+
candidateId: string;
|
|
91
|
+
section: ContextCandidateSection;
|
|
92
|
+
source: ContextSourceDescriptor;
|
|
93
|
+
content: T;
|
|
94
|
+
text: string;
|
|
95
|
+
priority: number;
|
|
96
|
+
score: number;
|
|
97
|
+
matchedTerms: string[];
|
|
98
|
+
matchedFields: string[];
|
|
99
|
+
originalChars: number;
|
|
100
|
+
selectedChars: number;
|
|
101
|
+
selected: boolean;
|
|
102
|
+
truncated: boolean;
|
|
103
|
+
}
|
|
104
|
+
export type ContextExclusionReason = 'no_active_context' | 'context_mismatch' | 'status_not_verified' | 'not_open' | 'absorbed' | 'applicability_mismatch' | 'scope_mismatch' | 'path_mismatch' | 'query_no_match' | 'result_limit' | 'budget' | 'unsupported_applicability' | 'v2_unavailable';
|
|
105
|
+
export interface ContextExclusion {
|
|
106
|
+
candidateId: string;
|
|
107
|
+
source: ContextSourceDescriptor | null;
|
|
108
|
+
reason: ContextExclusionReason;
|
|
109
|
+
detail?: string;
|
|
110
|
+
}
|
|
111
|
+
export interface ContextTruncation {
|
|
112
|
+
truncated: boolean;
|
|
113
|
+
summary: boolean;
|
|
114
|
+
pending: boolean;
|
|
115
|
+
decisions: boolean;
|
|
116
|
+
risks: boolean;
|
|
117
|
+
notes: boolean;
|
|
118
|
+
knowledge: boolean;
|
|
119
|
+
deadends: boolean;
|
|
120
|
+
activity: boolean;
|
|
121
|
+
candidatesOmittedByBudget: number;
|
|
122
|
+
candidatesPartiallyTruncated: number;
|
|
123
|
+
}
|
|
124
|
+
export interface ContextBudgetReport {
|
|
125
|
+
requested: ContextBudget;
|
|
126
|
+
usedChars: number;
|
|
127
|
+
remainingChars: number;
|
|
128
|
+
sections: Partial<Record<ContextCandidateSection, {
|
|
129
|
+
allocated: number;
|
|
130
|
+
consumed: number;
|
|
131
|
+
truncated: boolean;
|
|
132
|
+
}>>;
|
|
133
|
+
}
|
|
134
|
+
export interface ContextManifestProvenance {
|
|
135
|
+
journalRead: boolean;
|
|
136
|
+
journalEventCount: number;
|
|
137
|
+
selectedSourceIds: string[];
|
|
138
|
+
selectedEventIds: string[];
|
|
139
|
+
fallbackReason: string | null;
|
|
140
|
+
}
|
|
141
|
+
export interface ContextCompileOptions {
|
|
142
|
+
contextId?: string;
|
|
143
|
+
query?: string;
|
|
144
|
+
scenario?: ContextScenario;
|
|
145
|
+
scope?: string;
|
|
146
|
+
paths?: string[];
|
|
147
|
+
maxChars?: number;
|
|
148
|
+
knowledgeLimit?: number;
|
|
149
|
+
deadendLimit?: number;
|
|
150
|
+
noteLimit?: number;
|
|
151
|
+
activityLimit?: number;
|
|
152
|
+
trigger?: ContextIntent['trigger'];
|
|
153
|
+
adapter?: string;
|
|
154
|
+
sessionId?: string;
|
|
155
|
+
eventId?: string;
|
|
156
|
+
}
|
|
157
|
+
export interface ContextManifest {
|
|
158
|
+
schemaVersion: 1;
|
|
159
|
+
compilerVersion: '0.7.2';
|
|
160
|
+
rankingVersion: 'compiler-ranking-v2';
|
|
161
|
+
status: 'ready' | 'no_context';
|
|
162
|
+
projectRoot: string;
|
|
163
|
+
mode: 'v2' | 'v1-fallback';
|
|
164
|
+
anchor: ContextAnchor;
|
|
165
|
+
intent: ContextIntent;
|
|
166
|
+
context: ContextOrientResult['context'];
|
|
167
|
+
snapshot: ContextOrientResult['snapshot'];
|
|
168
|
+
resumeSummary: ResumeSummary | null;
|
|
169
|
+
sections: {
|
|
170
|
+
summary: ContextCandidate[];
|
|
171
|
+
pending: ContextCandidate[];
|
|
172
|
+
decision: ContextCandidate[];
|
|
173
|
+
risk: ContextCandidate[];
|
|
174
|
+
notes: ContextCandidate[];
|
|
175
|
+
knowledge: ContextCandidate[];
|
|
176
|
+
deadends: ContextCandidate[];
|
|
177
|
+
activity: ContextCandidate<ContextActivityRecord>[];
|
|
178
|
+
};
|
|
179
|
+
selected: ContextCandidate[];
|
|
180
|
+
excluded: ContextExclusion[];
|
|
181
|
+
budget: ContextBudgetReport;
|
|
182
|
+
truncation: ContextTruncation;
|
|
183
|
+
provenance: ContextManifestProvenance;
|
|
184
|
+
text: string;
|
|
185
|
+
}
|
|
186
|
+
export type CompilerRecord = StructuredContext | Note | Knowledge | Deadend;
|
|
187
|
+
export type CompilerGitAnchor = CognitionGitAnchor;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|