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.
Files changed (53) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +110 -553
  3. package/ROADMAP-1.0.md +70 -0
  4. package/dist/src/agent/api.d.ts +41 -0
  5. package/dist/src/agent/api.js +37 -0
  6. package/dist/src/agent/index.d.ts +9 -1
  7. package/dist/src/agent/index.js +3 -0
  8. package/dist/src/capture/context-filter.js +2 -0
  9. package/dist/src/cli/main.js +293 -15
  10. package/dist/src/cognition/admission.d.ts +8 -0
  11. package/dist/src/cognition/admission.js +24 -0
  12. package/dist/src/cognition/evidence-graph.d.ts +4 -0
  13. package/dist/src/cognition/evidence-graph.js +187 -0
  14. package/dist/src/cognition/evolution.d.ts +36 -0
  15. package/dist/src/cognition/evolution.js +221 -0
  16. package/dist/src/cognition/import-memory.d.ts +18 -0
  17. package/dist/src/cognition/import-memory.js +124 -0
  18. package/dist/src/cognition/journal-segments.d.ts +11 -0
  19. package/dist/src/cognition/journal-segments.js +129 -0
  20. package/dist/src/cognition/journal.d.ts +13 -0
  21. package/dist/src/cognition/journal.js +186 -20
  22. package/dist/src/cognition/migration-v1.js +39 -3
  23. package/dist/src/cognition/projections.js +124 -2
  24. package/dist/src/cognition/recipes.d.ts +25 -0
  25. package/dist/src/cognition/recipes.js +149 -0
  26. package/dist/src/cognition/runtime.js +4 -0
  27. package/dist/src/cognition/skills.d.ts +45 -0
  28. package/dist/src/cognition/skills.js +184 -0
  29. package/dist/src/cognition/types.d.ts +181 -1
  30. package/dist/src/cognition/usage-report.js +30 -15
  31. package/dist/src/compiler/compile.js +101 -31
  32. package/dist/src/compiler/graph.d.ts +7 -0
  33. package/dist/src/compiler/graph.js +117 -0
  34. package/dist/src/compiler/identity.d.ts +1 -1
  35. package/dist/src/compiler/identity.js +2 -0
  36. package/dist/src/compiler/inject.d.ts +23 -0
  37. package/dist/src/compiler/inject.js +27 -0
  38. package/dist/src/compiler/sources.d.ts +3 -1
  39. package/dist/src/compiler/sources.js +10 -1
  40. package/dist/src/compiler/types.d.ts +22 -1
  41. package/dist/src/hooks/claude-code.js +17 -56
  42. package/dist/src/integrations/claude-code.js +30 -8
  43. package/dist/src/mcp/server.js +228 -1
  44. package/dist/src/runtime/knowledge.js +57 -8
  45. package/dist/src/runtime/setup.d.ts +27 -0
  46. package/dist/src/runtime/setup.js +85 -0
  47. package/dist/src/storage/layout.d.ts +5 -0
  48. package/dist/src/storage/layout.js +15 -0
  49. package/dist/src/storage/lock.js +19 -2
  50. package/dist/src/version.d.ts +1 -1
  51. package/dist/src/version.js +1 -1
  52. package/package.json +4 -1
  53. 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 - ctxTokens;
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
- const cumulativeSavings = baseline === null ? null : baseline - cumulativeInvestmentTokens;
191
- if (paybackPeriodStart === null && cumulativeSavings !== null && cumulativeSavings >= cumulativeInvestmentTokens)
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: baseline === null || ctxTokens === 0 ? null : savings / ctxTokens,
218
- status: baseline === null ? 'no_baseline' : paybackReached ? 'reached' : 'not_reached',
219
- paybackAt: paybackPeriodStart,
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.contextOutputTokens.value} estimated`, ' Provider tokens n/a unavailable', '', 'RETURN', ` Baseline tokens ${payback.baselineTokens ?? 'n/a'}`, ` Estimated savings ${payback.estimatedSavings.value ?? 'n/a'}`, ` ROI ${payback.estimatedROI ?? 'n/a'}`, ` Payback ${payback.status}`, '', 'COMMAND OUTPUT TREND', ` Usage events ${report.dataQuality.usageEventCount}`, '', 'DATA QUALITY', ` Journal verified ${report.dataQuality.journalVerified ? 'yes' : 'no'}`, ` Window events ${report.dataQuality.windowEventCount}`, ` Warnings ${report.warnings.length}`];
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
- const contextSource = sourceDescriptor(sources.anchor.projectRoot, sources.anchor, sources.events, 'current-context', context.id, context.id, snapshot.snapshotId, context.status, null, resumeSummary.relatedFiles);
161
- const summaryFields = [
162
- ['title', context.title, 'summary'],
163
- ['progressSummary', resumeSummary.progressSummary, 'summary'],
164
- ['lastError', resumeSummary.lastError, 'risk'],
165
- ];
166
- for (const [field, value, section] of summaryFields) {
167
- const text = textForSummary(field, value);
168
- if (text)
169
- candidates.push(makeCandidate(section, contextSource, { field, value }, text, terms, [field], field));
170
- }
171
- const listFields = [
172
- ['pendingTasks', resumeSummary.pendingTasks, 'pending'],
173
- ['completed', resumeSummary.completed, 'summary'],
174
- ['decisions', resumeSummary.decisions, 'decision'],
175
- ['risks', resumeSummary.risks, 'risk'],
176
- ['relatedFiles', resumeSummary.relatedFiles, 'summary'],
177
- ];
178
- for (const [field, values, section] of listFields) {
179
- for (const [index, value] of values.entries()) {
180
- candidates.push(makeCandidate(section, contextSource, { field, value }, `${field}: ${value}`, terms, [field], `${field}:${index}`));
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 = orientation.status === 'ready' && intent.query ? await listKnowledge(sources.anchor.projectRoot, true) : [];
244
- const deadends = orientation.status === 'ready' && intent.query ? await listDeadends(sources.anchor.projectRoot, true) : [];
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(sortCandidates);
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 available = maxChars - usedChars - separator;
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: orientation.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: orientation.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
+ }