fluffy-context 0.7.7 → 1.0.0-beta.1
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/CHANGELOG.md +35 -0
- package/README.md +110 -553
- package/ROADMAP-1.0.md +70 -0
- package/dist/src/agent/api.d.ts +41 -0
- package/dist/src/agent/api.js +37 -0
- package/dist/src/agent/index.d.ts +9 -1
- package/dist/src/agent/index.js +3 -0
- package/dist/src/capture/context-filter.js +2 -0
- package/dist/src/cli/main.js +293 -15
- package/dist/src/cognition/admission.d.ts +8 -0
- package/dist/src/cognition/admission.js +24 -0
- package/dist/src/cognition/evidence-graph.d.ts +4 -0
- package/dist/src/cognition/evidence-graph.js +187 -0
- package/dist/src/cognition/evolution.d.ts +36 -0
- package/dist/src/cognition/evolution.js +221 -0
- package/dist/src/cognition/import-memory.d.ts +18 -0
- package/dist/src/cognition/import-memory.js +124 -0
- package/dist/src/cognition/journal-segments.d.ts +11 -0
- package/dist/src/cognition/journal-segments.js +129 -0
- package/dist/src/cognition/journal.d.ts +13 -0
- package/dist/src/cognition/journal.js +186 -20
- package/dist/src/cognition/migration-v1.js +39 -3
- package/dist/src/cognition/projections.js +124 -2
- package/dist/src/cognition/recipes.d.ts +25 -0
- package/dist/src/cognition/recipes.js +149 -0
- package/dist/src/cognition/runtime.js +4 -0
- package/dist/src/cognition/skills.d.ts +45 -0
- package/dist/src/cognition/skills.js +184 -0
- package/dist/src/cognition/types.d.ts +181 -1
- package/dist/src/cognition/usage-report.js +30 -15
- package/dist/src/compiler/compile.js +101 -31
- package/dist/src/compiler/graph.d.ts +7 -0
- package/dist/src/compiler/graph.js +117 -0
- package/dist/src/compiler/identity.d.ts +1 -1
- package/dist/src/compiler/identity.js +2 -0
- package/dist/src/compiler/inject.d.ts +23 -0
- package/dist/src/compiler/inject.js +27 -0
- package/dist/src/compiler/sources.d.ts +3 -1
- package/dist/src/compiler/sources.js +10 -1
- package/dist/src/compiler/types.d.ts +22 -1
- package/dist/src/hooks/claude-code.js +17 -56
- package/dist/src/integrations/claude-code.js +30 -8
- package/dist/src/mcp/server.js +228 -1
- package/dist/src/runtime/knowledge.js +57 -8
- package/dist/src/runtime/setup.d.ts +27 -0
- package/dist/src/runtime/setup.js +85 -0
- package/dist/src/storage/layout.d.ts +5 -0
- package/dist/src/storage/layout.js +15 -0
- package/dist/src/storage/lock.js +19 -2
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +4 -1
- package/skills/fluffy-context/SKILL.md +46 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare const COGNITION_LAYOUT_VERSION: "event-journal-v2";
|
|
2
2
|
export type CognitionEventSource = 'cli' | 'claude-hook' | 'mcp' | 'agent' | 'git-observer' | 'fs-observer' | 'migration';
|
|
3
|
-
export type CognitionEventType = 'runtime.enabled' | 'runtime.started' | 'runtime.stopped' | 'host.session.started' | 'host.prompt.submitted' | 'git.state.observed' | 'git.branch.changed' | 'git.head.changed' | 'workspace.paths.changed' | 'context.checkpoint.imported' | 'context.state.recorded' | 'note.recorded' | 'note.resolved' | 'note.absorbed' | 'knowledge.proposed' | 'knowledge.verified' | 'knowledge.deprecated' | 'knowledge.rejected' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'deadend.rejected' | 'record.used' | 'usage.operation.completed' | 'projection.advanced' | 'projection.failed';
|
|
3
|
+
export type CognitionEventType = 'runtime.enabled' | 'runtime.started' | 'runtime.stopped' | 'host.session.started' | 'host.prompt.submitted' | 'git.state.observed' | 'git.branch.changed' | 'git.head.changed' | 'workspace.paths.changed' | 'context.checkpoint.imported' | 'context.state.recorded' | 'note.recorded' | 'note.resolved' | 'note.absorbed' | 'knowledge.proposed' | 'knowledge.verified' | 'knowledge.deprecated' | 'knowledge.rejected' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'deadend.rejected' | 'experience.recorded' | 'evolution.proposed' | 'evolution.candidate.accepted' | 'evolution.verified' | 'evolution.published' | 'evolution.rejected' | 'evolution.deprecated' | 'evolution.superseded' | 'skill.materialized' | 'recipe.materialized' | 'skill.invoked' | 'skill.outcome.recorded' | 'record.used' | 'usage.operation.completed' | 'projection.advanced' | 'projection.failed';
|
|
4
4
|
export interface CognitionGitAnchor {
|
|
5
5
|
worktreeId: string | null;
|
|
6
6
|
branch: string | null;
|
|
@@ -61,6 +61,181 @@ export interface CognitionLifecycleTransition {
|
|
|
61
61
|
to: 'verified' | 'deprecated' | 'rejected' | 'obsolete';
|
|
62
62
|
reason?: string;
|
|
63
63
|
}
|
|
64
|
+
export type EvolutionArtifactKind = 'knowledge' | 'deadend' | 'skill' | 'compiler-recipe';
|
|
65
|
+
export type EvolutionArtifactStatus = 'candidate' | 'verified' | 'published' | 'rejected' | 'deprecated' | 'superseded' | 'obsolete';
|
|
66
|
+
export type EvolutionLineageRelation = 'derived_from' | 'supports' | 'contradicts' | 'supersedes' | 'validated_by' | 'used_for';
|
|
67
|
+
export interface EvolutionEvidenceReference {
|
|
68
|
+
eventId: string;
|
|
69
|
+
relation: Extract<EvolutionLineageRelation, 'derived_from' | 'supports' | 'contradicts' | 'validated_by'>;
|
|
70
|
+
}
|
|
71
|
+
export interface EvolutionLineageReference {
|
|
72
|
+
entity: EvolutionArtifactKind | 'experience' | 'proposal';
|
|
73
|
+
entityId: string;
|
|
74
|
+
relation: Extract<EvolutionLineageRelation, 'supports' | 'contradicts' | 'supersedes' | 'used_for'>;
|
|
75
|
+
}
|
|
76
|
+
export interface Experience {
|
|
77
|
+
experienceId: string;
|
|
78
|
+
schemaVersion: 1;
|
|
79
|
+
sourceEventIds: string[];
|
|
80
|
+
sourceContextIds: string[];
|
|
81
|
+
sourceSnapshotIds: string[];
|
|
82
|
+
paths: string[];
|
|
83
|
+
git: CognitionGitAnchor;
|
|
84
|
+
privacy: {
|
|
85
|
+
classification: 'metadata' | 'redacted-text';
|
|
86
|
+
redactionCount: number;
|
|
87
|
+
};
|
|
88
|
+
extractor: {
|
|
89
|
+
id: string;
|
|
90
|
+
version: string;
|
|
91
|
+
};
|
|
92
|
+
contentHash: string;
|
|
93
|
+
summary: string;
|
|
94
|
+
createdAt: string;
|
|
95
|
+
}
|
|
96
|
+
export interface EvolutionProposal {
|
|
97
|
+
proposalId: string;
|
|
98
|
+
schemaVersion: 1;
|
|
99
|
+
targetKind: EvolutionArtifactKind;
|
|
100
|
+
targetId: string;
|
|
101
|
+
status: 'candidate';
|
|
102
|
+
sourceExperienceIds: string[];
|
|
103
|
+
evidence: EvolutionEvidenceReference[];
|
|
104
|
+
lineage: EvolutionLineageReference[];
|
|
105
|
+
applicability: Applicability;
|
|
106
|
+
contentHash: string;
|
|
107
|
+
summary: string;
|
|
108
|
+
extractor: {
|
|
109
|
+
id: string;
|
|
110
|
+
version: string;
|
|
111
|
+
};
|
|
112
|
+
createdAt: string;
|
|
113
|
+
}
|
|
114
|
+
export interface EvolutionCandidateArtifact {
|
|
115
|
+
artifactId: string;
|
|
116
|
+
schemaVersion: 1;
|
|
117
|
+
proposalId: string;
|
|
118
|
+
kind: Extract<EvolutionArtifactKind, 'skill' | 'compiler-recipe'>;
|
|
119
|
+
status: 'candidate';
|
|
120
|
+
applicability: Applicability;
|
|
121
|
+
contentHash: string;
|
|
122
|
+
lineage: EvolutionLineageReference[];
|
|
123
|
+
rationale: string;
|
|
124
|
+
createdAt: string;
|
|
125
|
+
}
|
|
126
|
+
export interface EvolutionProposalLifecycleEntry {
|
|
127
|
+
eventId: string;
|
|
128
|
+
occurredAt: string;
|
|
129
|
+
from: EvolutionArtifactStatus;
|
|
130
|
+
to: EvolutionArtifactStatus;
|
|
131
|
+
reason: string;
|
|
132
|
+
supersedes?: EvolutionLineageReference;
|
|
133
|
+
}
|
|
134
|
+
export interface EvolutionProposalState {
|
|
135
|
+
proposal: EvolutionProposal;
|
|
136
|
+
status: EvolutionArtifactStatus;
|
|
137
|
+
lifecycle: EvolutionProposalLifecycleEntry[];
|
|
138
|
+
acceptedArtifact: EvolutionCandidateArtifact | null;
|
|
139
|
+
}
|
|
140
|
+
export interface Skill {
|
|
141
|
+
skillId: string;
|
|
142
|
+
schemaVersion: 1;
|
|
143
|
+
proposalId: string;
|
|
144
|
+
title: string;
|
|
145
|
+
procedure: string[];
|
|
146
|
+
applicability: Applicability;
|
|
147
|
+
status: 'candidate';
|
|
148
|
+
contentHash: string;
|
|
149
|
+
lineage: EvolutionLineageReference[];
|
|
150
|
+
createdAt: string;
|
|
151
|
+
}
|
|
152
|
+
export interface SkillLifecycleEntry {
|
|
153
|
+
eventId: string;
|
|
154
|
+
occurredAt: string;
|
|
155
|
+
from: EvolutionArtifactStatus;
|
|
156
|
+
to: EvolutionArtifactStatus;
|
|
157
|
+
reason: string;
|
|
158
|
+
supersedes?: EvolutionLineageReference;
|
|
159
|
+
}
|
|
160
|
+
export interface SkillState {
|
|
161
|
+
skill: Skill;
|
|
162
|
+
status: EvolutionArtifactStatus;
|
|
163
|
+
lifecycle: SkillLifecycleEntry[];
|
|
164
|
+
invocations: SkillInvocation[];
|
|
165
|
+
outcomes: SkillOutcome[];
|
|
166
|
+
}
|
|
167
|
+
export type CompilerRecipeSource = 'context' | 'knowledge' | 'deadend' | 'note';
|
|
168
|
+
export type CompilerRecipeSection = 'summary' | 'pending' | 'decision' | 'risk' | 'note' | 'knowledge' | 'deadend' | 'activity';
|
|
169
|
+
export interface CompilerRecipe {
|
|
170
|
+
recipeId: string;
|
|
171
|
+
schemaVersion: 1;
|
|
172
|
+
proposalId: string;
|
|
173
|
+
title: string;
|
|
174
|
+
applicability: Applicability;
|
|
175
|
+
preferredSources?: CompilerRecipeSource[];
|
|
176
|
+
preferredSections?: CompilerRecipeSection[];
|
|
177
|
+
sectionBudgets?: Partial<Record<CompilerRecipeSection, number>>;
|
|
178
|
+
status: 'candidate';
|
|
179
|
+
contentHash: string;
|
|
180
|
+
lineage: EvolutionLineageReference[];
|
|
181
|
+
createdAt: string;
|
|
182
|
+
}
|
|
183
|
+
export interface CompilerRecipeLifecycleEntry {
|
|
184
|
+
eventId: string;
|
|
185
|
+
occurredAt: string;
|
|
186
|
+
from: EvolutionArtifactStatus;
|
|
187
|
+
to: EvolutionArtifactStatus;
|
|
188
|
+
reason: string;
|
|
189
|
+
supersedes?: EvolutionLineageReference;
|
|
190
|
+
}
|
|
191
|
+
export interface CompilerRecipeState {
|
|
192
|
+
recipe: CompilerRecipe;
|
|
193
|
+
status: EvolutionArtifactStatus;
|
|
194
|
+
lifecycle: CompilerRecipeLifecycleEntry[];
|
|
195
|
+
}
|
|
196
|
+
export interface EvolutionLifecycleTransition {
|
|
197
|
+
entity: EvolutionArtifactKind | 'proposal';
|
|
198
|
+
entityId: string;
|
|
199
|
+
from: EvolutionArtifactStatus;
|
|
200
|
+
to: EvolutionArtifactStatus;
|
|
201
|
+
reason: string;
|
|
202
|
+
supersedes?: EvolutionLineageReference;
|
|
203
|
+
}
|
|
204
|
+
export interface SkillInvocation {
|
|
205
|
+
invocationId: string;
|
|
206
|
+
skillId: string;
|
|
207
|
+
callerEventId: string;
|
|
208
|
+
inputHash: string;
|
|
209
|
+
occurredAt: string;
|
|
210
|
+
}
|
|
211
|
+
export interface SkillOutcome {
|
|
212
|
+
outcomeId: string;
|
|
213
|
+
invocationId: string;
|
|
214
|
+
callerEventId: string;
|
|
215
|
+
outcome: 'success' | 'failure' | 'partial' | 'irrelevant' | 'unknown';
|
|
216
|
+
summary: string;
|
|
217
|
+
occurredAt: string;
|
|
218
|
+
}
|
|
219
|
+
export interface EvidenceGraphNode {
|
|
220
|
+
nodeId: string;
|
|
221
|
+
kind: 'context' | 'snapshot' | 'experience' | 'proposal' | 'knowledge' | 'deadend' | 'skill' | 'recipe' | 'path' | 'test' | 'commit' | 'branch-worktree';
|
|
222
|
+
}
|
|
223
|
+
export interface EvidenceGraphEdge {
|
|
224
|
+
edgeId: string;
|
|
225
|
+
from: string;
|
|
226
|
+
to: string;
|
|
227
|
+
type: 'derived_from' | 'supports' | 'contradicts' | 'supersedes' | 'used_for' | 'observed_in' | 'changed_in' | 'co_changed_with' | 'tests';
|
|
228
|
+
sourceEventIds: string[];
|
|
229
|
+
derivationVersion: string;
|
|
230
|
+
occurredAt: string;
|
|
231
|
+
confidence?: number;
|
|
232
|
+
}
|
|
233
|
+
export interface EvidenceGraphProjection {
|
|
234
|
+
schemaVersion: 1;
|
|
235
|
+
derivationVersion: 'evidence-graph-v1';
|
|
236
|
+
nodes: EvidenceGraphNode[];
|
|
237
|
+
edges: EvidenceGraphEdge[];
|
|
238
|
+
}
|
|
64
239
|
export interface CognitionUseFeedback {
|
|
65
240
|
feedbackId: string;
|
|
66
241
|
entity: 'knowledge' | 'deadend';
|
|
@@ -177,6 +352,11 @@ export interface CognitionProjection {
|
|
|
177
352
|
notes: Record<string, CognitionRecordPayload>;
|
|
178
353
|
knowledge: Record<string, CognitionRecordPayload>;
|
|
179
354
|
deadends: Record<string, CognitionRecordPayload>;
|
|
355
|
+
experiences: Record<string, Experience>;
|
|
356
|
+
proposals: Record<string, EvolutionProposalState>;
|
|
357
|
+
skills: Record<string, SkillState>;
|
|
358
|
+
recipes: Record<string, CompilerRecipeState>;
|
|
359
|
+
evidenceGraph: EvidenceGraphProjection;
|
|
180
360
|
workspaces: Record<string, BranchWorkspace>;
|
|
181
361
|
}
|
|
182
362
|
export interface RuntimeLease {
|
|
@@ -2,6 +2,8 @@ import { readdir } from 'node:fs/promises';
|
|
|
2
2
|
import { resolveProjectRoot } from '../project/project-resolver.js';
|
|
3
3
|
import { readJson, isRecord } from '../storage/json-store.js';
|
|
4
4
|
import { contextsRoot, contextMetadataPath, snapshotsDirectory } from '../storage/layout.js';
|
|
5
|
+
import { journalStorage } from './journal-segments.js';
|
|
6
|
+
import { readAdmissionStats } from './admission.js';
|
|
5
7
|
import { readJournal } from './journal.js';
|
|
6
8
|
import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
|
|
7
9
|
import { listNotes } from '../runtime/notes.js';
|
|
@@ -123,6 +125,8 @@ function operationStats(events) {
|
|
|
123
125
|
return byOperation;
|
|
124
126
|
}
|
|
125
127
|
export async function buildUsageReport(startPath, options = {}) {
|
|
128
|
+
if (options.baselineTokens !== undefined && (!Number.isSafeInteger(options.baselineTokens) || options.baselineTokens < 0))
|
|
129
|
+
throw new Error('baselineTokens must be a non-negative safe integer');
|
|
126
130
|
const projectRoot = await resolveProjectRoot(startPath);
|
|
127
131
|
const window = windowOf(options);
|
|
128
132
|
let events = [];
|
|
@@ -136,6 +140,13 @@ export async function buildUsageReport(startPath, options = {}) {
|
|
|
136
140
|
journalVerified = false;
|
|
137
141
|
}
|
|
138
142
|
const scoped = events.filter((event) => inWindow(event, window));
|
|
143
|
+
const storage = await journalStorage(projectRoot, true);
|
|
144
|
+
const admission = await readAdmissionStats(projectRoot);
|
|
145
|
+
const journalBytes = storage.bytes;
|
|
146
|
+
const eventTypes = {};
|
|
147
|
+
for (const event of scoped)
|
|
148
|
+
eventTypes[event.type] = (eventTypes[event.type] ?? 0) + 1;
|
|
149
|
+
const semanticEvents = scoped.filter((event) => /^(context\.|knowledge\.|deadend\.|note\.|experience\.|evolution\.|skill\.|recipe\.|record\.used)/.test(event.type)).length;
|
|
139
150
|
const usageEvents = scoped.map((event) => ({ event, payload: usagePayload(event) })).filter((item) => item.payload !== null);
|
|
140
151
|
const knowledge = await listKnowledge(projectRoot, true);
|
|
141
152
|
const deadends = await listDeadends(projectRoot, true);
|
|
@@ -175,21 +186,21 @@ export async function buildUsageReport(startPath, options = {}) {
|
|
|
175
186
|
buckets.set(key, bucket);
|
|
176
187
|
}
|
|
177
188
|
const ctxTokens = usageEvents.reduce((sum, item) => sum + item.payload.estimatedOutputTokens, 0);
|
|
189
|
+
const inputTokens = usageEvents.reduce((sum, item) => sum + item.payload.estimatedInputTokens, 0);
|
|
190
|
+
const totalTokens = ctxTokens + inputTokens;
|
|
178
191
|
const baseline = options.baselineTokens ?? null;
|
|
179
|
-
const savings = baseline === null ? null : baseline -
|
|
192
|
+
const savings = baseline === null || usageEvents.length === 0 ? null : baseline - totalTokens;
|
|
180
193
|
const investment = metric(ctxTokens, 'estimated', 'tokens', 'chars-div-four');
|
|
181
194
|
const rawAssets = verified.length;
|
|
182
195
|
const usedCount = verified.reduce((sum, key) => sum + Math.min(used.get(key) ?? 0, 3), 0);
|
|
183
196
|
const trendEntries = [...buckets.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
184
197
|
let cumulativeInvestmentTokens = 0;
|
|
185
198
|
let cumulativeOutputChars = 0;
|
|
186
|
-
let paybackPeriodStart = null;
|
|
187
199
|
const trends = trendEntries.map(([periodStart, values]) => {
|
|
188
200
|
cumulativeInvestmentTokens += values.estimatedOutputTokens;
|
|
189
201
|
cumulativeOutputChars += values.outputChars;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
paybackPeriodStart = periodStart;
|
|
202
|
+
// A window-total baseline cannot establish when savings occurred.
|
|
203
|
+
const cumulativeSavings = null;
|
|
193
204
|
return {
|
|
194
205
|
periodStart,
|
|
195
206
|
...values,
|
|
@@ -198,35 +209,39 @@ export async function buildUsageReport(startPath, options = {}) {
|
|
|
198
209
|
cumulativeEstimatedSavings: cumulativeSavings,
|
|
199
210
|
};
|
|
200
211
|
});
|
|
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
212
|
return {
|
|
204
213
|
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 },
|
|
214
|
+
dataQuality: { journalVerified, journalBytes, journalStorage: storage, admission, eventTypes, semanticEventRatio: metric(scoped.length ? semanticEvents / scoped.length : null, 'derived', 'ratio', 'semantic lifecycle events / window events; not a correctness score'), 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
215
|
assets: {
|
|
207
216
|
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
217
|
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
218
|
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
219
|
},
|
|
211
220
|
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') },
|
|
221
|
+
investment: { contextOutputTokens: investment, contextInputTokens: metric(inputTokens, 'estimated', 'tokens', 'chars-div-four'), totalContextTokens: metric(totalTokens, 'estimated', 'tokens', 'chars-div-four'), providerTokens: metric(null, 'unavailable', 'tokens') },
|
|
213
222
|
payback: {
|
|
214
223
|
baselineTokens: baseline,
|
|
215
224
|
baselineSource: options.baselineSource ?? null,
|
|
216
225
|
estimatedSavings: savings === null ? metric(null, 'unavailable', 'tokens') : metric(savings, 'estimated', 'tokens'),
|
|
217
|
-
estimatedROI:
|
|
218
|
-
status: baseline === null ? 'no_baseline' :
|
|
219
|
-
paybackAt:
|
|
220
|
-
paybackPeriodDays,
|
|
226
|
+
estimatedROI: savings === null || totalTokens === 0 ? null : savings / totalTokens,
|
|
227
|
+
status: baseline === null ? 'no_baseline' : 'unpaired_baseline',
|
|
228
|
+
paybackAt: null,
|
|
229
|
+
paybackPeriodDays: null,
|
|
221
230
|
},
|
|
222
231
|
operations: { available: usageEvents.length > 0, byOperation: operationStats(scoped), reason: usageEvents.length > 0 ? null : 'operation output telemetry is not yet recorded' },
|
|
223
232
|
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'] : [])],
|
|
233
|
+
warnings: [...(!journalVerified ? ['v2 journal unavailable; lifecycle metrics may be incomplete'] : []), 'Provider token usage is unavailable', 'chars-div-four is a rough estimate, especially for CJK; context compression is not proven task savings', ...(baseline !== null ? ['Window baseline is unpaired; no causal savings or payback date can be established'] : []), ...(usageEvents.length === 0 ? ['Token investment is unavailable until operation output telemetry is recorded'] : [])],
|
|
225
234
|
};
|
|
226
235
|
}
|
|
227
236
|
export function formatUsageReport(report) {
|
|
228
237
|
const assets = report.assets;
|
|
229
238
|
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.
|
|
239
|
+
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.totalContextTokens.value} estimated (input + output)`, ' 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}`];
|
|
240
|
+
lines.push(` Journal bytes ${report.dataQuality.journalBytes}`, ` Semantic event ratio ${report.dataQuality.semanticEventRatio.value ?? 'n/a'} derived`, '', 'EVENT TYPES');
|
|
241
|
+
lines.push(` Archives ${report.dataQuality.journalStorage.archives}`, ` Expanded journal bytes ${report.dataQuality.journalStorage.expandedBytes}`);
|
|
242
|
+
for (const [type, count] of Object.entries(report.dataQuality.eventTypes))
|
|
243
|
+
lines.push(` ${type}: ${count}`);
|
|
244
|
+
lines.push(` Budget-rejected observations ${Object.values(report.dataQuality.admission.dropped).reduce((sum, count) => Number(sum) + Number(count), 0)}`);
|
|
245
|
+
lines.push('', ...report.warnings.map((warning) => ` ${warning}`));
|
|
231
246
|
return `${lines.join('\n')}\n`;
|
|
232
247
|
}
|
|
@@ -4,6 +4,7 @@ import { queryTerms } from '../cognition/retrieval.js';
|
|
|
4
4
|
import { listDeadends, listKnowledge } from '../runtime/knowledge.js';
|
|
5
5
|
import { listNotes } from '../runtime/notes.js';
|
|
6
6
|
import { extractActivity } from './activity.js';
|
|
7
|
+
import { graphOrdering } from './graph.js';
|
|
7
8
|
import { latestHostIntent, loadCompilerSources, sourceDescriptor } from './sources.js';
|
|
8
9
|
const scenarioKeywords = [
|
|
9
10
|
{ scenario: 'debugging', words: ['bug', 'error', 'fail', 'failure', 'exception', 'regression', 'debug', '失败', '报错', '异常', '修复'] },
|
|
@@ -137,6 +138,50 @@ function sortCandidates(left, right) {
|
|
|
137
138
|
|| right.matchedTerms.length - left.matchedTerms.length
|
|
138
139
|
|| stableCompare(left.candidateId, right.candidateId);
|
|
139
140
|
}
|
|
141
|
+
function recipeParticipation(sources) {
|
|
142
|
+
const recipe = sources.recipe?.recipe;
|
|
143
|
+
if (!recipe)
|
|
144
|
+
return undefined;
|
|
145
|
+
return {
|
|
146
|
+
recipeId: recipe.recipeId,
|
|
147
|
+
contentHash: recipe.contentHash,
|
|
148
|
+
interpreterVersion: 'recipe-interpreter-v1',
|
|
149
|
+
preferredSources: [...(recipe.preferredSources ?? [])],
|
|
150
|
+
preferredSections: [...(recipe.preferredSections ?? [])],
|
|
151
|
+
sectionBudgets: { ...(recipe.sectionBudgets ?? {}) },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function sourcePreference(candidate, recipe) {
|
|
155
|
+
const source = candidate.source.kind === 'current-context' ? 'context' : candidate.source.kind;
|
|
156
|
+
const index = recipe.preferredSources.indexOf(source);
|
|
157
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
158
|
+
}
|
|
159
|
+
function sectionPreference(candidate, recipe) {
|
|
160
|
+
const index = recipe.preferredSections.indexOf(candidate.section);
|
|
161
|
+
return index === -1 ? Number.MAX_SAFE_INTEGER : index;
|
|
162
|
+
}
|
|
163
|
+
function recipeApplicable(recipe, sources, paths) {
|
|
164
|
+
const applicability = sources.recipe.recipe.applicability;
|
|
165
|
+
if (applicability.branchMode === 'global')
|
|
166
|
+
return true;
|
|
167
|
+
if (applicability.branchMode !== 'exact')
|
|
168
|
+
return false;
|
|
169
|
+
const branch = sources.anchor.ref ?? sources.anchor.branch ?? 'detached';
|
|
170
|
+
if (applicability.branches?.length && !applicability.branches.includes(branch) && !applicability.branches.includes(sources.anchor.branch ?? ''))
|
|
171
|
+
return false;
|
|
172
|
+
if (applicability.sourceCommit && applicability.sourceCommit !== sources.anchor.head)
|
|
173
|
+
return false;
|
|
174
|
+
if (paths.length > 0 && applicability.paths?.length && !applicability.paths.some((path) => paths.includes(path.replaceAll('\\', '/'))))
|
|
175
|
+
return false;
|
|
176
|
+
return recipe.recipeId === sources.recipe.recipe.recipeId;
|
|
177
|
+
}
|
|
178
|
+
function sortWithRecipe(recipe) {
|
|
179
|
+
if (!recipe)
|
|
180
|
+
return sortCandidates;
|
|
181
|
+
return (left, right) => sourcePreference(left, recipe) - sourcePreference(right, recipe)
|
|
182
|
+
|| sectionPreference(left, recipe) - sectionPreference(right, recipe)
|
|
183
|
+
|| sortCandidates(left, right);
|
|
184
|
+
}
|
|
140
185
|
function truncateText(value, maxChars) {
|
|
141
186
|
if (value.length <= maxChars)
|
|
142
187
|
return { text: value, truncated: false };
|
|
@@ -152,32 +197,32 @@ function activityText(record) {
|
|
|
152
197
|
return `activity: ${changes}`;
|
|
153
198
|
}
|
|
154
199
|
function createCandidates(sources, intent, notes, knowledge, deadends, activities) {
|
|
155
|
-
if (sources.orientation.status !== 'ready')
|
|
156
|
-
return [];
|
|
157
|
-
const { context, snapshot, resumeSummary } = sources.orientation;
|
|
158
200
|
const terms = intent.normalizedTerms;
|
|
159
201
|
const candidates = [];
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
202
|
+
if (sources.orientation.status === 'ready') {
|
|
203
|
+
const { context, snapshot, resumeSummary } = sources.orientation;
|
|
204
|
+
const contextSource = sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'current-context', context.id, context.id, snapshot.snapshotId, context.status, null, resumeSummary.relatedFiles);
|
|
205
|
+
const summaryFields = [
|
|
206
|
+
['title', context.title, 'summary'],
|
|
207
|
+
['progressSummary', resumeSummary.progressSummary, 'summary'],
|
|
208
|
+
['lastError', resumeSummary.lastError, 'risk'],
|
|
209
|
+
];
|
|
210
|
+
for (const [field, value, section] of summaryFields) {
|
|
211
|
+
const text = textForSummary(field, value);
|
|
212
|
+
if (text)
|
|
213
|
+
candidates.push(makeCandidate(section, contextSource, { field, value }, text, terms, [field], field));
|
|
214
|
+
}
|
|
215
|
+
const listFields = [
|
|
216
|
+
['pendingTasks', resumeSummary.pendingTasks, 'pending'],
|
|
217
|
+
['completed', resumeSummary.completed, 'summary'],
|
|
218
|
+
['decisions', resumeSummary.decisions, 'decision'],
|
|
219
|
+
['risks', resumeSummary.risks, 'risk'],
|
|
220
|
+
['relatedFiles', resumeSummary.relatedFiles, 'summary'],
|
|
221
|
+
];
|
|
222
|
+
for (const [field, values, section] of listFields) {
|
|
223
|
+
for (const [index, value] of values.entries()) {
|
|
224
|
+
candidates.push(makeCandidate(section, contextSource, { field, value }, `${field}: ${value}`, terms, [field], `${field}:${index}`));
|
|
225
|
+
}
|
|
181
226
|
}
|
|
182
227
|
}
|
|
183
228
|
for (const note of notes) {
|
|
@@ -237,11 +282,14 @@ export async function compileContext(startPath, options = {}) {
|
|
|
237
282
|
const sources = await loadCompilerSources(startPath, options);
|
|
238
283
|
const normalizedPaths = options.paths ? await filterPaths(sources.anchor.projectRoot, options.paths) : [];
|
|
239
284
|
const intent = intentFor(sources, options, normalizedPaths);
|
|
285
|
+
const recipe = recipeParticipation(sources);
|
|
286
|
+
if (recipe && !recipeApplicable(recipe, sources, normalizedPaths))
|
|
287
|
+
throw new Error(`compiler recipe is not applicable: ${recipe.recipeId}`);
|
|
240
288
|
const orientation = sources.orientation;
|
|
241
289
|
const maxChars = intent.budget.total;
|
|
242
290
|
const notes = orientation.status === 'ready' ? (await listNotes(sources.anchor.projectRoot, { contextId: orientation.context.id, openOnly: true, limit: options.noteLimit ?? 50, maxChars: Number.MAX_SAFE_INTEGER })).filter((note) => orientation.notes.some((selected) => selected.noteId === note.noteId)) : [];
|
|
243
|
-
const knowledge =
|
|
244
|
-
const deadends =
|
|
291
|
+
const knowledge = intent.query ? await listKnowledge(sources.anchor.projectRoot, true) : [];
|
|
292
|
+
const deadends = intent.query ? await listDeadends(sources.anchor.projectRoot, true) : [];
|
|
245
293
|
const activities = await extractActivity(sources.anchor.projectRoot, sources.events, sources.anchor, sources.workspace, options.activityLimit ?? 20);
|
|
246
294
|
const all = createCandidates(sources, intent, notes, knowledge, deadends, activities);
|
|
247
295
|
const excluded = [];
|
|
@@ -250,14 +298,31 @@ export async function compileContext(startPath, options = {}) {
|
|
|
250
298
|
if (!check.ok)
|
|
251
299
|
excluded.push({ candidateId: candidate.candidateId, source: candidate.source, reason: check.reason ?? 'query_no_match' });
|
|
252
300
|
return check.ok;
|
|
253
|
-
}).sort(
|
|
301
|
+
}).sort(sortWithRecipe(recipe));
|
|
302
|
+
const graph = options.graphHops === undefined ? undefined : graphOrdering(sources.evidenceGraph, options.graphHops, sources.anchor, eligible);
|
|
303
|
+
if (graph) {
|
|
304
|
+
eligible.sort((left, right) => {
|
|
305
|
+
if (recipe) {
|
|
306
|
+
const sourceOrder = sourcePreference(left, recipe) - sourcePreference(right, recipe);
|
|
307
|
+
if (sourceOrder)
|
|
308
|
+
return sourceOrder;
|
|
309
|
+
const sectionOrder = sectionPreference(left, recipe) - sectionPreference(right, recipe);
|
|
310
|
+
if (sectionOrder)
|
|
311
|
+
return sectionOrder;
|
|
312
|
+
}
|
|
313
|
+
return (graph.distanceFor(left) ?? Number.MAX_SAFE_INTEGER) - (graph.distanceFor(right) ?? Number.MAX_SAFE_INTEGER)
|
|
314
|
+
|| sortCandidates(left, right);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
254
317
|
const selected = [];
|
|
255
318
|
const truncation = emptyTruncation();
|
|
256
319
|
const sectionStats = {};
|
|
257
320
|
let usedChars = 0;
|
|
258
321
|
for (const candidate of eligible) {
|
|
259
322
|
const separator = selected.length > 0 ? 1 : 0;
|
|
260
|
-
const
|
|
323
|
+
const sectionBudget = recipe?.sectionBudgets[candidate.section] ?? maxChars;
|
|
324
|
+
const consumedInSection = sectionStats[candidate.section]?.consumed ?? 0;
|
|
325
|
+
const available = Math.min(maxChars - usedChars - separator, sectionBudget - consumedInSection - separator);
|
|
261
326
|
if (available <= 0) {
|
|
262
327
|
excluded.push({ candidateId: candidate.candidateId, source: candidate.source, reason: 'budget' });
|
|
263
328
|
truncation.candidatesOmittedByBudget += 1;
|
|
@@ -274,7 +339,7 @@ export async function compileContext(startPath, options = {}) {
|
|
|
274
339
|
const item = { ...candidate, text: limited.text, selectedChars: limited.text.length, selected: true, truncated: limited.truncated };
|
|
275
340
|
selected.push(item);
|
|
276
341
|
usedChars += separator + item.selectedChars;
|
|
277
|
-
const prior = sectionStats[item.section] ?? { allocated: maxChars, consumed: 0, truncated: false };
|
|
342
|
+
const prior = sectionStats[item.section] ?? { allocated: recipe?.sectionBudgets[item.section] ?? maxChars, consumed: 0, truncated: false };
|
|
278
343
|
sectionStats[item.section] = { ...prior, consumed: prior.consumed + item.selectedChars + separator, truncated: prior.truncated || limited.truncated };
|
|
279
344
|
if (limited.truncated) {
|
|
280
345
|
truncation.candidatesPartiallyTruncated += 1;
|
|
@@ -309,8 +374,9 @@ export async function compileContext(startPath, options = {}) {
|
|
|
309
374
|
};
|
|
310
375
|
const tokenEstimate = estimateTokens(manifestText(selected));
|
|
311
376
|
const selectedItemHashes = selected.map((item) => item.itemHash);
|
|
377
|
+
const status = orientation.status === 'ready' || selected.length > 0 ? 'ready' : 'no_context';
|
|
312
378
|
const manifestHash = rebuildManifestHash({
|
|
313
|
-
status
|
|
379
|
+
status,
|
|
314
380
|
projectRoot: sources.anchor.projectRoot,
|
|
315
381
|
mode: orientation.explanation.mode,
|
|
316
382
|
anchor: sources.anchor,
|
|
@@ -318,12 +384,14 @@ export async function compileContext(startPath, options = {}) {
|
|
|
318
384
|
selected,
|
|
319
385
|
budget: { requested: intent.budget, usedChars, remainingChars: Math.max(0, maxChars - usedChars), sections: sectionStats },
|
|
320
386
|
truncation,
|
|
387
|
+
...(recipe ? { recipe } : {}),
|
|
388
|
+
...(graph ? { graph: graph.participation } : {}),
|
|
321
389
|
});
|
|
322
390
|
return {
|
|
323
391
|
schemaVersion: 1,
|
|
324
392
|
compilerVersion: '0.7.7',
|
|
325
393
|
rankingVersion: 'compiler-ranking-v2',
|
|
326
|
-
status
|
|
394
|
+
status,
|
|
327
395
|
projectRoot: sources.anchor.projectRoot,
|
|
328
396
|
mode: orientation.explanation.mode,
|
|
329
397
|
anchor: sources.anchor,
|
|
@@ -348,6 +416,8 @@ export async function compileContext(startPath, options = {}) {
|
|
|
348
416
|
selectedEventIds: [...new Set(selected.flatMap((item) => item.source.provenance.eventId ? [item.source.provenance.eventId] : []))].sort(stableCompare),
|
|
349
417
|
fallbackReason: sources.fallbackReason,
|
|
350
418
|
},
|
|
419
|
+
...(recipe ? { recipe } : {}),
|
|
420
|
+
...(graph ? { graph: graph.participation } : {}),
|
|
351
421
|
text: manifestText(selected),
|
|
352
422
|
};
|
|
353
423
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { EvidenceGraphProjection } from '../cognition/types.js';
|
|
2
|
+
import type { ContextAnchor, ContextCandidate, ContextGraphParticipation } from './types.js';
|
|
3
|
+
export interface GraphOrdering {
|
|
4
|
+
participation: ContextGraphParticipation;
|
|
5
|
+
distanceFor(candidate: ContextCandidate): number | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function graphOrdering(graph: EvidenceGraphProjection | null, hops: 0 | 1 | 2, anchor: ContextAnchor, eligible: ContextCandidate[]): GraphOrdering;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { EVIDENCE_GRAPH_DERIVATION_VERSION, workspaceNodeId } from '../cognition/evidence-graph.js';
|
|
2
|
+
import { contentHash } from './identity.js';
|
|
3
|
+
const MAX_VISITED_NODES = 256;
|
|
4
|
+
const MAX_TRAVERSED_EDGES = 512;
|
|
5
|
+
const MAX_FAN_OUT = 32;
|
|
6
|
+
function compare(left, right) {
|
|
7
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
8
|
+
}
|
|
9
|
+
function candidateNodeId(candidate) {
|
|
10
|
+
if (candidate.section === 'knowledge')
|
|
11
|
+
return `knowledge:${candidate.source.entityId}`;
|
|
12
|
+
if (candidate.section === 'deadend')
|
|
13
|
+
return `deadend:${candidate.source.entityId}`;
|
|
14
|
+
if (candidate.source.kind === 'current-context')
|
|
15
|
+
return `context:${candidate.source.contextId ?? candidate.source.entityId}`;
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
function validGraph(graph) {
|
|
19
|
+
if (graph.schemaVersion !== 1 || graph.derivationVersion !== EVIDENCE_GRAPH_DERIVATION_VERSION || !Array.isArray(graph.nodes) || !Array.isArray(graph.edges))
|
|
20
|
+
return false;
|
|
21
|
+
const nodeKinds = new Set(['context', 'snapshot', 'experience', 'proposal', 'knowledge', 'deadend', 'skill', 'recipe', 'path', 'test', 'commit', 'branch-worktree']);
|
|
22
|
+
const edgeTypes = new Set(['derived_from', 'supports', 'contradicts', 'supersedes', 'used_for', 'observed_in', 'changed_in', 'co_changed_with', 'tests']);
|
|
23
|
+
const nodes = new Set();
|
|
24
|
+
for (const node of graph.nodes) {
|
|
25
|
+
if (!node || typeof node.nodeId !== 'string' || node.nodeId.length === 0 || !nodeKinds.has(node.kind) || nodes.has(node.nodeId))
|
|
26
|
+
return false;
|
|
27
|
+
nodes.add(node.nodeId);
|
|
28
|
+
}
|
|
29
|
+
const edges = new Set();
|
|
30
|
+
for (const edge of graph.edges) {
|
|
31
|
+
if (!edge || typeof edge.edgeId !== 'string' || edge.edgeId.length === 0 || edges.has(edge.edgeId) || !nodes.has(edge.from) || !nodes.has(edge.to) || !edgeTypes.has(edge.type) || edge.derivationVersion !== EVIDENCE_GRAPH_DERIVATION_VERSION || typeof edge.occurredAt !== 'string' || edge.occurredAt.length === 0 || !Array.isArray(edge.sourceEventIds) || edge.sourceEventIds.some((id) => typeof id !== 'string' || !id) || (edge.confidence !== undefined && (typeof edge.confidence !== 'number' || !Number.isFinite(edge.confidence) || edge.confidence < 0 || edge.confidence > 1)))
|
|
32
|
+
return false;
|
|
33
|
+
edges.add(edge.edgeId);
|
|
34
|
+
}
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
function graphProjectionHash(graph) {
|
|
38
|
+
return contentHash(graph);
|
|
39
|
+
}
|
|
40
|
+
export function graphOrdering(graph, hops, anchor, eligible) {
|
|
41
|
+
const empty = (fallbackReason, seedNodeIds = []) => ({
|
|
42
|
+
participation: {
|
|
43
|
+
derivationVersion: EVIDENCE_GRAPH_DERIVATION_VERSION,
|
|
44
|
+
projectionHash: null,
|
|
45
|
+
hops,
|
|
46
|
+
seedNodeIds,
|
|
47
|
+
selectedNodeIds: [],
|
|
48
|
+
selectedEdgeIds: [],
|
|
49
|
+
fallbackReason,
|
|
50
|
+
},
|
|
51
|
+
distanceFor: () => null,
|
|
52
|
+
});
|
|
53
|
+
if (!graph)
|
|
54
|
+
return empty('graph-unavailable');
|
|
55
|
+
if (!validGraph(graph))
|
|
56
|
+
return empty('graph-invalid');
|
|
57
|
+
const projectionHash = graphProjectionHash(graph);
|
|
58
|
+
const nodes = new Set(graph.nodes.map((node) => node.nodeId));
|
|
59
|
+
const candidateNodes = new Map(eligible.map((candidate) => [candidate.candidateId, candidateNodeId(candidate)]));
|
|
60
|
+
const seedNodeIds = [...new Set([
|
|
61
|
+
workspaceNodeId(anchor),
|
|
62
|
+
...eligible.filter((candidate) => candidate.source.kind === 'current-context').map(candidateNodeId).filter((node) => node !== null),
|
|
63
|
+
].filter((node) => nodes.has(node)))].sort(compare);
|
|
64
|
+
if (seedNodeIds.length === 0)
|
|
65
|
+
return empty('graph-no-seeds');
|
|
66
|
+
if (hops === 0) {
|
|
67
|
+
const distances = new Map(seedNodeIds.map((node) => [node, 0]));
|
|
68
|
+
return {
|
|
69
|
+
participation: { derivationVersion: EVIDENCE_GRAPH_DERIVATION_VERSION, projectionHash, hops, seedNodeIds, selectedNodeIds: seedNodeIds, selectedEdgeIds: [], fallbackReason: null },
|
|
70
|
+
distanceFor: (candidate) => {
|
|
71
|
+
const node = candidateNodes.get(candidate.candidateId);
|
|
72
|
+
return node ? distances.get(node) ?? null : null;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const adjacency = new Map();
|
|
77
|
+
for (const edge of graph.edges) {
|
|
78
|
+
adjacency.set(edge.from, [...(adjacency.get(edge.from) ?? []), edge]);
|
|
79
|
+
adjacency.set(edge.to, [...(adjacency.get(edge.to) ?? []), edge]);
|
|
80
|
+
}
|
|
81
|
+
for (const edges of adjacency.values())
|
|
82
|
+
edges.sort((left, right) => compare(left.edgeId, right.edgeId));
|
|
83
|
+
const distances = new Map(seedNodeIds.map((node) => [node, 0]));
|
|
84
|
+
const queue = [...seedNodeIds];
|
|
85
|
+
const selectedEdgeIds = new Set();
|
|
86
|
+
while (queue.length > 0 && distances.size < MAX_VISITED_NODES && selectedEdgeIds.size < MAX_TRAVERSED_EDGES) {
|
|
87
|
+
const node = queue.shift();
|
|
88
|
+
const distance = distances.get(node);
|
|
89
|
+
if (distance >= hops)
|
|
90
|
+
continue;
|
|
91
|
+
for (const edge of (adjacency.get(node) ?? []).slice(0, MAX_FAN_OUT)) {
|
|
92
|
+
if (selectedEdgeIds.size >= MAX_TRAVERSED_EDGES)
|
|
93
|
+
break;
|
|
94
|
+
const adjacent = edge.from === node ? edge.to : edge.from;
|
|
95
|
+
selectedEdgeIds.add(edge.edgeId);
|
|
96
|
+
if (!distances.has(adjacent) && distances.size < MAX_VISITED_NODES) {
|
|
97
|
+
distances.set(adjacent, distance + 1);
|
|
98
|
+
queue.push(adjacent);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
participation: {
|
|
104
|
+
derivationVersion: EVIDENCE_GRAPH_DERIVATION_VERSION,
|
|
105
|
+
projectionHash,
|
|
106
|
+
hops,
|
|
107
|
+
seedNodeIds,
|
|
108
|
+
selectedNodeIds: [...distances.keys()].sort(compare),
|
|
109
|
+
selectedEdgeIds: [...selectedEdgeIds].sort(compare),
|
|
110
|
+
fallbackReason: null,
|
|
111
|
+
},
|
|
112
|
+
distanceFor: (candidate) => {
|
|
113
|
+
const node = candidateNodes.get(candidate.candidateId);
|
|
114
|
+
return node ? distances.get(node) ?? null : null;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|