fluffy-context 0.6.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 +99 -8
- package/dist/src/agent/index.d.ts +2 -0
- package/dist/src/agent/index.js +1 -0
- package/dist/src/cli/main.js +69 -5
- 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 +1 -1
- package/dist/src/cognition/orient.js +4 -3
- 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/runtime.d.ts +20 -1
- package/dist/src/cognition/runtime.js +229 -53
- package/dist/src/cognition/types.d.ts +16 -0
- package/dist/src/cognition/v1-bridge.js +3 -2
- 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/storage/layout.d.ts +1 -0
- package/dist/src/storage/layout.js +3 -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 +2 -1
|
@@ -32,6 +32,7 @@ export interface CognitionEvent<T = Record<string, unknown>> {
|
|
|
32
32
|
export interface CognitionRuntimePolicy {
|
|
33
33
|
schemaVersion: 2;
|
|
34
34
|
enabled: boolean;
|
|
35
|
+
autoStart: boolean;
|
|
35
36
|
allowHookStartup: boolean;
|
|
36
37
|
inactivityTimeoutMs: number;
|
|
37
38
|
promptIntentMaxChars: number;
|
|
@@ -141,6 +142,21 @@ export interface CognitionRecordPayload<T extends Record<string, unknown> = Reco
|
|
|
141
142
|
sourceHash?: string;
|
|
142
143
|
anchorProvenance?: 'snapshot' | 'live' | 'global';
|
|
143
144
|
}
|
|
145
|
+
export interface WorkspacePathChange {
|
|
146
|
+
path: string;
|
|
147
|
+
kind: 'created' | 'modified' | 'deleted' | 'renamed' | 'unknown';
|
|
148
|
+
fileType: 'file';
|
|
149
|
+
sizeBytes?: number;
|
|
150
|
+
mtimeMs?: number;
|
|
151
|
+
}
|
|
152
|
+
export interface WorkspacePathsChangedPayload {
|
|
153
|
+
schemaVersion: 1;
|
|
154
|
+
paths: string[];
|
|
155
|
+
changes: WorkspacePathChange[];
|
|
156
|
+
pathCount: number;
|
|
157
|
+
coalesced: boolean;
|
|
158
|
+
truncated: boolean;
|
|
159
|
+
}
|
|
144
160
|
export interface CognitionProjection {
|
|
145
161
|
schemaVersion: 2;
|
|
146
162
|
contexts: Record<string, CognitionRecordPayload>;
|
|
@@ -2,11 +2,12 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { access, readFile, readdir } from 'node:fs/promises';
|
|
4
4
|
import { readGitWorktreeState } from '../git/git-adapter.js';
|
|
5
|
-
import { cognitionManifestPath,
|
|
5
|
+
import { cognitionManifestPath, cognitionV1SyncPath, contextsRoot, deadendsPath, knowledgePath, notesPath, snapshotsDirectory } from '../storage/layout.js';
|
|
6
6
|
import { readJson } from '../storage/json-store.js';
|
|
7
7
|
import { atomicWriteJson } from '../storage/atomic-write.js';
|
|
8
8
|
import { appendEvent } from './journal.js';
|
|
9
9
|
import { rebuildProjections } from './projections.js';
|
|
10
|
+
import { loadRuntimePolicy } from './policy.js';
|
|
10
11
|
function digest(record) {
|
|
11
12
|
return crypto.createHash('sha256').update(JSON.stringify(record)).digest('hex');
|
|
12
13
|
}
|
|
@@ -34,7 +35,7 @@ export async function v1Synced(projectRoot) {
|
|
|
34
35
|
async function enabled(projectRoot) {
|
|
35
36
|
try {
|
|
36
37
|
await access(cognitionManifestPath(projectRoot));
|
|
37
|
-
return (await
|
|
38
|
+
return (await loadRuntimePolicy(projectRoot)).enabled;
|
|
38
39
|
}
|
|
39
40
|
catch {
|
|
40
41
|
return false;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { BranchWorkspace, CognitionEvent } from '../cognition/types.js';
|
|
2
|
+
import type { ContextActivityRecord, ContextAnchor } from './types.js';
|
|
3
|
+
export declare function extractActivity(projectRoot: string, events: CognitionEvent[], anchor: ContextAnchor, workspace: BranchWorkspace | null, limit: number): Promise<ContextActivityRecord[]>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { filterPaths } from '../capture/context-filter.js';
|
|
2
|
+
import { workspaceKey } from '../cognition/projections.js';
|
|
3
|
+
const changeKinds = new Set(['created', 'modified', 'deleted', 'renamed', 'unknown']);
|
|
4
|
+
function normalizedPath(value) {
|
|
5
|
+
if (typeof value !== 'string')
|
|
6
|
+
return null;
|
|
7
|
+
const normalized = value.replaceAll('\\', '/');
|
|
8
|
+
return normalized.length > 0 ? normalized : null;
|
|
9
|
+
}
|
|
10
|
+
function stableCompare(left, right) {
|
|
11
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
12
|
+
}
|
|
13
|
+
function metadataNumber(value) {
|
|
14
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
15
|
+
}
|
|
16
|
+
function parsePayload(event) {
|
|
17
|
+
const payload = event.payload;
|
|
18
|
+
return payload.schemaVersion === 1 && typeof payload === 'object' && payload !== null ? payload : null;
|
|
19
|
+
}
|
|
20
|
+
async function activityForEvent(projectRoot, event, focus) {
|
|
21
|
+
const payload = parsePayload(event);
|
|
22
|
+
if (!payload)
|
|
23
|
+
return null;
|
|
24
|
+
const rawPaths = Array.isArray(payload.paths) ? payload.paths.map(normalizedPath).filter((value) => value !== null) : [];
|
|
25
|
+
const rawChanges = Array.isArray(payload.changes) ? payload.changes : [];
|
|
26
|
+
const changePaths = rawChanges.map((change) => normalizedPath(change?.path)).filter((value) => value !== null);
|
|
27
|
+
const allowed = await filterPaths(projectRoot, [...new Set([...rawPaths, ...changePaths])]);
|
|
28
|
+
if (allowed.length === 0)
|
|
29
|
+
return null;
|
|
30
|
+
const allowedSet = new Set(allowed);
|
|
31
|
+
const changes = [];
|
|
32
|
+
for (const change of rawChanges) {
|
|
33
|
+
const path = normalizedPath(change?.path);
|
|
34
|
+
if (!path || !allowedSet.has(path))
|
|
35
|
+
continue;
|
|
36
|
+
changes.push({
|
|
37
|
+
path,
|
|
38
|
+
kind: changeKinds.has(change?.kind) ? change.kind : 'unknown',
|
|
39
|
+
fileType: 'file',
|
|
40
|
+
...(metadataNumber(change?.sizeBytes) !== undefined ? { sizeBytes: metadataNumber(change?.sizeBytes) } : {}),
|
|
41
|
+
...(metadataNumber(change?.mtimeMs) !== undefined ? { mtimeMs: metadataNumber(change?.mtimeMs) } : {}),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const fallbackChanges = changes.length > 0 ? changes : allowed.map((path) => ({ path, kind: 'unknown', fileType: 'file' }));
|
|
45
|
+
const paths = [...new Set(fallbackChanges.map((change) => change.path))].sort(stableCompare);
|
|
46
|
+
return {
|
|
47
|
+
eventId: event.eventId,
|
|
48
|
+
occurredAt: event.occurredAt,
|
|
49
|
+
anchor: {
|
|
50
|
+
projectRoot,
|
|
51
|
+
worktreeId: event.git.worktreeId,
|
|
52
|
+
branch: event.git.branch,
|
|
53
|
+
ref: event.git.ref,
|
|
54
|
+
head: event.git.head,
|
|
55
|
+
},
|
|
56
|
+
paths,
|
|
57
|
+
changes: fallbackChanges.sort((left, right) => stableCompare(left.path, right.path)),
|
|
58
|
+
pathCount: typeof payload.pathCount === 'number' && Number.isSafeInteger(payload.pathCount) && payload.pathCount >= paths.length ? payload.pathCount : paths.length,
|
|
59
|
+
coalesced: payload.coalesced === true,
|
|
60
|
+
truncated: payload.truncated === true,
|
|
61
|
+
focusMatch: paths.some((path) => focus.includes(path)),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export async function extractActivity(projectRoot, events, anchor, workspace, limit) {
|
|
65
|
+
if (limit <= 0 || !workspace)
|
|
66
|
+
return [];
|
|
67
|
+
const currentWorkspace = workspaceKey(anchor);
|
|
68
|
+
const matching = events
|
|
69
|
+
.filter((event) => event.type === 'workspace.paths.changed' && workspaceKey(event.git) === currentWorkspace)
|
|
70
|
+
.sort((left, right) => right.occurredAt.localeCompare(left.occurredAt) || right.eventId.localeCompare(left.eventId))
|
|
71
|
+
.slice(0, limit);
|
|
72
|
+
const records = [];
|
|
73
|
+
for (const event of matching) {
|
|
74
|
+
const record = await activityForEvent(projectRoot, event, workspace.pathFocus);
|
|
75
|
+
if (record)
|
|
76
|
+
records.push(record);
|
|
77
|
+
}
|
|
78
|
+
return records;
|
|
79
|
+
}
|
|
@@ -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
|
+
}
|