fluffy-context 0.7.6 → 0.8.0
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 +115 -36
- package/dist/src/agent/api.d.ts +49 -4
- package/dist/src/agent/api.js +183 -131
- package/dist/src/agent/index.d.ts +7 -1
- package/dist/src/agent/index.js +1 -0
- package/dist/src/cli/main.js +301 -15
- 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/journal.js +143 -2
- 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/skills.d.ts +45 -0
- package/dist/src/cognition/skills.js +184 -0
- package/dist/src/cognition/types.d.ts +196 -2
- package/dist/src/cognition/usage-report.d.ts +40 -0
- package/dist/src/cognition/usage-report.js +232 -0
- package/dist/src/cognition/usage.d.ts +5 -0
- package/dist/src/cognition/usage.js +48 -0
- package/dist/src/compiler/compile.js +73 -4
- 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 +3 -1
- package/dist/src/compiler/sources.d.ts +3 -1
- package/dist/src/compiler/sources.js +11 -2
- package/dist/src/compiler/types.d.ts +23 -2
- package/dist/src/mcp/server.js +228 -6
- package/dist/src/storage/layout.d.ts +5 -0
- package/dist/src/storage/layout.js +15 -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 +53 -5
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EvolutionArtifactKind, EvolutionArtifactStatus, EvolutionLineageReference, EvolutionProposalState } from './types.js';
|
|
2
|
+
type ProposalTargetKind = Extract<EvolutionArtifactKind, 'skill' | 'compiler-recipe'>;
|
|
3
|
+
export interface ProposeEvolutionInput {
|
|
4
|
+
eventIds?: string[];
|
|
5
|
+
recentLimit?: number;
|
|
6
|
+
targetKind?: ProposalTargetKind;
|
|
7
|
+
}
|
|
8
|
+
export interface ProposeEvolutionResult {
|
|
9
|
+
experienceId: string;
|
|
10
|
+
experienceEventId: string;
|
|
11
|
+
proposalId: string | null;
|
|
12
|
+
proposalEventId: string | null;
|
|
13
|
+
recorded: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface AcceptEvolutionCandidateInput {
|
|
16
|
+
proposalId: string;
|
|
17
|
+
rationale: string;
|
|
18
|
+
}
|
|
19
|
+
export interface TransitionEvolutionProposalInput {
|
|
20
|
+
proposalId: string;
|
|
21
|
+
to: Extract<EvolutionArtifactStatus, 'verified' | 'rejected' | 'deprecated' | 'superseded'>;
|
|
22
|
+
rationale: string;
|
|
23
|
+
supersedes?: EvolutionLineageReference;
|
|
24
|
+
}
|
|
25
|
+
export declare function proposeEvolution(startPath: string | undefined, input?: ProposeEvolutionInput): Promise<ProposeEvolutionResult>;
|
|
26
|
+
export declare function listEvolutionProposals(startPath?: string): Promise<EvolutionProposalState[]>;
|
|
27
|
+
export declare function inspectEvolutionProposal(startPath: string | undefined, proposalId: string): Promise<EvolutionProposalState>;
|
|
28
|
+
export declare function acceptEvolutionCandidate(startPath: string | undefined, input: AcceptEvolutionCandidateInput): Promise<{
|
|
29
|
+
accepted: boolean;
|
|
30
|
+
eventId: string;
|
|
31
|
+
}>;
|
|
32
|
+
export declare function transitionEvolutionProposal(startPath: string | undefined, input: TransitionEvolutionProposalInput): Promise<{
|
|
33
|
+
transitioned: boolean;
|
|
34
|
+
eventId: string;
|
|
35
|
+
}>;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { readGitWorktreeState } from '../git/git-adapter.js';
|
|
3
|
+
import { resolveProjectRoot } from '../project/project-resolver.js';
|
|
4
|
+
import { appendEvent, readJournal } from './journal.js';
|
|
5
|
+
import { v2Enabled } from './orient.js';
|
|
6
|
+
import { projectEvents, rebuildProjections } from './projections.js';
|
|
7
|
+
const extractor = { id: 'deterministic-maintainer', version: '1' };
|
|
8
|
+
const maxEvidenceEvents = 32;
|
|
9
|
+
const allowedEvidenceTypes = new Set([
|
|
10
|
+
'host.prompt.submitted',
|
|
11
|
+
'context.checkpoint.imported', 'context.state.recorded',
|
|
12
|
+
'note.recorded', 'note.resolved', 'note.absorbed',
|
|
13
|
+
'git.state.observed', 'git.branch.changed', 'git.head.changed',
|
|
14
|
+
'workspace.paths.changed', 'record.used', 'usage.operation.completed',
|
|
15
|
+
]);
|
|
16
|
+
function canonical(value) {
|
|
17
|
+
if (value === null || typeof value !== 'object')
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
if (Array.isArray(value))
|
|
20
|
+
return `[${value.map(canonical).join(',')}]`;
|
|
21
|
+
const record = value;
|
|
22
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonical(record[key])}`).join(',')}}`;
|
|
23
|
+
}
|
|
24
|
+
function hash(value) {
|
|
25
|
+
return crypto.createHash('sha256').update(canonical(value)).digest('hex');
|
|
26
|
+
}
|
|
27
|
+
function anchor(event) {
|
|
28
|
+
return { worktreeId: event.git.worktreeId, branch: event.git.branch, ref: event.git.ref, head: event.git.head };
|
|
29
|
+
}
|
|
30
|
+
function currentAnchor(state) {
|
|
31
|
+
return { worktreeId: state.worktreeId, branch: state.branch, ref: state.ref, head: state.commit };
|
|
32
|
+
}
|
|
33
|
+
function requireReason(reason) {
|
|
34
|
+
const value = reason.trim();
|
|
35
|
+
if (!value)
|
|
36
|
+
throw new Error('evolution rationale must not be empty');
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function selectEvidence(events, input) {
|
|
40
|
+
const selected = input.eventIds
|
|
41
|
+
? input.eventIds.map((eventId) => {
|
|
42
|
+
if (!eventId.trim())
|
|
43
|
+
throw new Error('evidence event ID must not be empty');
|
|
44
|
+
return eventId;
|
|
45
|
+
})
|
|
46
|
+
: events.filter((event) => allowedEvidenceTypes.has(event.type)).slice(-(input.recentLimit ?? maxEvidenceEvents)).map((event) => event.eventId);
|
|
47
|
+
if (selected.length === 0)
|
|
48
|
+
throw new Error('evolution extraction requires evidence events');
|
|
49
|
+
if (selected.length > maxEvidenceEvents)
|
|
50
|
+
throw new Error(`evolution extraction supports at most ${maxEvidenceEvents} evidence events`);
|
|
51
|
+
if (new Set(selected).size !== selected.length)
|
|
52
|
+
throw new Error('evolution evidence event IDs must be unique');
|
|
53
|
+
const byId = new Map(events.map((event) => [event.eventId, event]));
|
|
54
|
+
const evidence = selected.map((eventId) => byId.get(eventId));
|
|
55
|
+
if (evidence.some((event) => !event))
|
|
56
|
+
throw new Error('evolution evidence event was not found');
|
|
57
|
+
if (evidence.some((event) => !allowedEvidenceTypes.has(event.type)))
|
|
58
|
+
throw new Error('evolution evidence event type is not allowed');
|
|
59
|
+
return evidence;
|
|
60
|
+
}
|
|
61
|
+
function stringArray(value) {
|
|
62
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === 'string' && item.trim().length > 0) : [];
|
|
63
|
+
}
|
|
64
|
+
function sourceReferences(events) {
|
|
65
|
+
const contextIds = new Set();
|
|
66
|
+
const snapshotIds = new Set();
|
|
67
|
+
const paths = new Set();
|
|
68
|
+
for (const event of events) {
|
|
69
|
+
const payload = event.payload;
|
|
70
|
+
const record = payload.record;
|
|
71
|
+
if (record && typeof record === 'object' && !Array.isArray(record)) {
|
|
72
|
+
const item = record;
|
|
73
|
+
const snapshot = item.snapshot;
|
|
74
|
+
if (snapshot && typeof snapshot === 'object' && !Array.isArray(snapshot)) {
|
|
75
|
+
const value = snapshot;
|
|
76
|
+
if (typeof value.contextId === 'string')
|
|
77
|
+
contextIds.add(value.contextId);
|
|
78
|
+
if (typeof value.snapshotId === 'string')
|
|
79
|
+
snapshotIds.add(value.snapshotId);
|
|
80
|
+
}
|
|
81
|
+
if (typeof item.contextId === 'string')
|
|
82
|
+
contextIds.add(item.contextId);
|
|
83
|
+
if (typeof item.snapshotId === 'string')
|
|
84
|
+
snapshotIds.add(item.snapshotId);
|
|
85
|
+
}
|
|
86
|
+
for (const value of stringArray(payload.paths))
|
|
87
|
+
paths.add(value.replaceAll('\\', '/'));
|
|
88
|
+
}
|
|
89
|
+
return { contextIds: [...contextIds].sort(), snapshotIds: [...snapshotIds].sort(), paths: [...paths].sort() };
|
|
90
|
+
}
|
|
91
|
+
function privacy(events) {
|
|
92
|
+
const redactionCount = events.reduce((total, event) => total + event.privacy.redactionCount, 0);
|
|
93
|
+
return { classification: events.some((event) => event.privacy.classification === 'redacted-text') ? 'redacted-text' : 'metadata', redactionCount };
|
|
94
|
+
}
|
|
95
|
+
function applicability(event) {
|
|
96
|
+
if (event.git.ref === null && event.git.branch === null)
|
|
97
|
+
return { project: true, branchMode: 'global' };
|
|
98
|
+
return { project: true, branchMode: 'exact', branches: [event.git.ref ?? event.git.branch ?? 'detached'], sourceCommit: event.git.head };
|
|
99
|
+
}
|
|
100
|
+
function candidateEligible(events) {
|
|
101
|
+
const hasContext = events.some((event) => event.type === 'context.checkpoint.imported' || event.type === 'context.state.recorded');
|
|
102
|
+
const hasActivity = events.some((event) => event.type.startsWith('git.') || event.type === 'workspace.paths.changed');
|
|
103
|
+
const hasUse = events.some((event) => event.type === 'record.used');
|
|
104
|
+
return hasContext && hasActivity && hasUse;
|
|
105
|
+
}
|
|
106
|
+
async function enabledProject(startPath) {
|
|
107
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
108
|
+
if (!await v2Enabled(projectRoot))
|
|
109
|
+
throw new Error('evolution requires an enabled v2 runtime');
|
|
110
|
+
return projectRoot;
|
|
111
|
+
}
|
|
112
|
+
export async function proposeEvolution(startPath, input = {}) {
|
|
113
|
+
const projectRoot = await enabledProject(startPath);
|
|
114
|
+
const events = await readJournal(projectRoot);
|
|
115
|
+
const evidence = selectEvidence(events, input);
|
|
116
|
+
const references = sourceReferences(evidence);
|
|
117
|
+
const sourceEventIds = evidence.map((event) => event.eventId);
|
|
118
|
+
const last = evidence.at(-1);
|
|
119
|
+
const experienceBasis = { sourceEventIds, sourceContextIds: references.contextIds, sourceSnapshotIds: references.snapshotIds, paths: references.paths, git: anchor(last), privacy: privacy(evidence), extractor, createdAt: last.occurredAt };
|
|
120
|
+
const experience = {
|
|
121
|
+
experienceId: `exp-${hash(experienceBasis).slice(0, 20)}`,
|
|
122
|
+
schemaVersion: 1,
|
|
123
|
+
...experienceBasis,
|
|
124
|
+
contentHash: hash(experienceBasis),
|
|
125
|
+
summary: `Metadata-only experience from ${sourceEventIds.length} journal events.`,
|
|
126
|
+
};
|
|
127
|
+
const experienceResult = await appendEvent(projectRoot, {
|
|
128
|
+
type: 'experience.recorded', source: { kind: 'agent' }, occurredAt: experience.createdAt,
|
|
129
|
+
idempotencyKey: `experience:${experience.experienceId}`, git: experience.git,
|
|
130
|
+
privacy: experience.privacy, payload: { record: experience },
|
|
131
|
+
});
|
|
132
|
+
let proposalResult = null;
|
|
133
|
+
let proposal = null;
|
|
134
|
+
if (candidateEligible(evidence)) {
|
|
135
|
+
const targetKind = input.targetKind ?? 'skill';
|
|
136
|
+
const proposalBasis = { sourceExperienceIds: [experience.experienceId], evidence: sourceEventIds.map((eventId) => ({ eventId, relation: 'derived_from' })), applicability: applicability(last), targetKind, extractor, createdAt: experience.createdAt };
|
|
137
|
+
const contentHash = hash(proposalBasis);
|
|
138
|
+
proposal = {
|
|
139
|
+
proposalId: `proposal-${contentHash.slice(0, 20)}`,
|
|
140
|
+
schemaVersion: 1,
|
|
141
|
+
targetKind,
|
|
142
|
+
targetId: `${targetKind}-${contentHash.slice(0, 20)}`,
|
|
143
|
+
status: 'candidate',
|
|
144
|
+
sourceExperienceIds: proposalBasis.sourceExperienceIds,
|
|
145
|
+
evidence: proposalBasis.evidence,
|
|
146
|
+
lineage: [],
|
|
147
|
+
applicability: proposalBasis.applicability,
|
|
148
|
+
contentHash,
|
|
149
|
+
summary: `Candidate ${targetKind} derived from metadata-only experience ${experience.experienceId}.`,
|
|
150
|
+
extractor,
|
|
151
|
+
createdAt: experience.createdAt,
|
|
152
|
+
};
|
|
153
|
+
proposalResult = await appendEvent(projectRoot, {
|
|
154
|
+
type: 'evolution.proposed', source: { kind: 'agent' }, occurredAt: proposal.createdAt,
|
|
155
|
+
idempotencyKey: `evolution-proposal:${proposal.proposalId}`, git: experience.git,
|
|
156
|
+
privacy: experience.privacy, payload: { record: proposal },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (experienceResult.appended || proposalResult?.appended)
|
|
160
|
+
await rebuildProjections(projectRoot);
|
|
161
|
+
return { experienceId: experience.experienceId, experienceEventId: experienceResult.event.eventId, proposalId: proposal?.proposalId ?? null, proposalEventId: proposalResult?.event.eventId ?? null, recorded: experienceResult.appended || Boolean(proposalResult?.appended) };
|
|
162
|
+
}
|
|
163
|
+
export async function listEvolutionProposals(startPath) {
|
|
164
|
+
const projectRoot = await enabledProject(startPath);
|
|
165
|
+
return Object.values(projectEvents(await readJournal(projectRoot)).proposals).sort((left, right) => left.proposal.proposalId.localeCompare(right.proposal.proposalId));
|
|
166
|
+
}
|
|
167
|
+
export async function inspectEvolutionProposal(startPath, proposalId) {
|
|
168
|
+
const proposal = (await listEvolutionProposals(startPath)).find((item) => item.proposal.proposalId === proposalId);
|
|
169
|
+
if (!proposal)
|
|
170
|
+
throw new Error(`evolution proposal not found: ${proposalId}`);
|
|
171
|
+
return proposal;
|
|
172
|
+
}
|
|
173
|
+
export async function acceptEvolutionCandidate(startPath, input) {
|
|
174
|
+
const projectRoot = await enabledProject(startPath);
|
|
175
|
+
const rationale = requireReason(input.rationale);
|
|
176
|
+
const state = await inspectEvolutionProposal(projectRoot, input.proposalId);
|
|
177
|
+
if (state.proposal.targetKind !== 'skill' && state.proposal.targetKind !== 'compiler-recipe')
|
|
178
|
+
throw new Error('only skill and compiler-recipe proposals can be accepted in Phase 1');
|
|
179
|
+
if (state.status !== 'candidate')
|
|
180
|
+
throw new Error(`candidate acceptance requires a candidate proposal: ${input.proposalId}`);
|
|
181
|
+
if (state.acceptedArtifact)
|
|
182
|
+
throw new Error(`candidate artifact already accepted: ${input.proposalId}`);
|
|
183
|
+
const git = await readGitWorktreeState(projectRoot);
|
|
184
|
+
const artifact = {
|
|
185
|
+
artifactId: state.proposal.targetId,
|
|
186
|
+
schemaVersion: 1,
|
|
187
|
+
proposalId: state.proposal.proposalId,
|
|
188
|
+
kind: state.proposal.targetKind,
|
|
189
|
+
status: 'candidate',
|
|
190
|
+
applicability: state.proposal.applicability,
|
|
191
|
+
contentHash: state.proposal.contentHash,
|
|
192
|
+
lineage: state.proposal.lineage,
|
|
193
|
+
rationale,
|
|
194
|
+
createdAt: new Date().toISOString(),
|
|
195
|
+
};
|
|
196
|
+
const result = await appendEvent(projectRoot, {
|
|
197
|
+
type: 'evolution.candidate.accepted', source: { kind: 'cli' }, idempotencyKey: `evolution-accept:${artifact.artifactId}:${hash(rationale).slice(0, 16)}`,
|
|
198
|
+
git: currentAnchor(git), payload: { record: artifact },
|
|
199
|
+
});
|
|
200
|
+
if (result.appended)
|
|
201
|
+
await rebuildProjections(projectRoot);
|
|
202
|
+
return { accepted: result.appended, eventId: result.event.eventId };
|
|
203
|
+
}
|
|
204
|
+
export async function transitionEvolutionProposal(startPath, input) {
|
|
205
|
+
const projectRoot = await enabledProject(startPath);
|
|
206
|
+
const rationale = requireReason(input.rationale);
|
|
207
|
+
const state = await inspectEvolutionProposal(projectRoot, input.proposalId);
|
|
208
|
+
if (input.to === 'superseded' && !input.supersedes)
|
|
209
|
+
throw new Error('superseding an evolution proposal requires lineage');
|
|
210
|
+
const git = await readGitWorktreeState(projectRoot);
|
|
211
|
+
const result = await appendEvent(projectRoot, {
|
|
212
|
+
type: `evolution.${input.to}`,
|
|
213
|
+
source: { kind: 'cli' },
|
|
214
|
+
idempotencyKey: `evolution-transition:${input.proposalId}:${state.status}:${input.to}:${hash({ rationale, supersedes: input.supersedes }).slice(0, 16)}`,
|
|
215
|
+
git: currentAnchor(git),
|
|
216
|
+
payload: { transition: { entity: 'proposal', entityId: input.proposalId, from: state.status, to: input.to, reason: rationale, ...(input.supersedes ? { supersedes: input.supersedes } : {}) } },
|
|
217
|
+
});
|
|
218
|
+
if (result.appended)
|
|
219
|
+
await rebuildProjections(projectRoot);
|
|
220
|
+
return { transitioned: result.appended, eventId: result.event.eventId };
|
|
221
|
+
}
|
|
@@ -10,10 +10,14 @@ const eventTypes = new Set([
|
|
|
10
10
|
'git.state.observed', 'git.branch.changed', 'git.head.changed', 'workspace.paths.changed',
|
|
11
11
|
'context.checkpoint.imported', 'context.state.recorded', 'note.recorded', 'note.resolved', 'note.absorbed',
|
|
12
12
|
'knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected',
|
|
13
|
-
'deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected',
|
|
13
|
+
'deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected',
|
|
14
|
+
'experience.recorded',
|
|
15
|
+
'evolution.proposed', 'evolution.candidate.accepted', 'evolution.verified', 'evolution.published', 'evolution.rejected', 'evolution.deprecated', 'evolution.superseded',
|
|
16
|
+
'skill.materialized', 'recipe.materialized', 'skill.invoked', 'skill.outcome.recorded', 'record.used',
|
|
17
|
+
'usage.operation.completed',
|
|
14
18
|
'projection.advanced', 'projection.failed',
|
|
15
19
|
]);
|
|
16
|
-
const sourceKinds = new Set(['cli', 'claude-hook', 'mcp', 'git-observer', 'fs-observer', 'migration']);
|
|
20
|
+
const sourceKinds = new Set(['cli', 'claude-hook', 'mcp', 'agent', 'git-observer', 'fs-observer', 'migration']);
|
|
17
21
|
const segmentName = '00000001.ndjson';
|
|
18
22
|
function canonical(value) {
|
|
19
23
|
if (value === null || typeof value !== 'object')
|
|
@@ -55,6 +59,79 @@ function validGitAnchor(value) {
|
|
|
55
59
|
function validIsoDate(value) {
|
|
56
60
|
return typeof value === 'string' && Number.isFinite(Date.parse(value));
|
|
57
61
|
}
|
|
62
|
+
function validEvolutionId(value, label) {
|
|
63
|
+
if (typeof value !== 'string' || value.trim().length === 0)
|
|
64
|
+
throw new Error(`${label} must not be empty`);
|
|
65
|
+
}
|
|
66
|
+
function validNonNegativeInteger(value) {
|
|
67
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
68
|
+
}
|
|
69
|
+
function validLineage(value) {
|
|
70
|
+
return isRecord(value)
|
|
71
|
+
&& (value.entity === 'knowledge' || value.entity === 'deadend' || value.entity === 'skill' || value.entity === 'compiler-recipe' || value.entity === 'experience' || value.entity === 'proposal')
|
|
72
|
+
&& typeof value.entityId === 'string' && value.entityId.trim().length > 0
|
|
73
|
+
&& (value.relation === 'supports' || value.relation === 'contradicts' || value.relation === 'supersedes' || value.relation === 'used_for');
|
|
74
|
+
}
|
|
75
|
+
function validEvolutionEvidence(value) {
|
|
76
|
+
return isRecord(value)
|
|
77
|
+
&& typeof value.eventId === 'string' && value.eventId.trim().length > 0
|
|
78
|
+
&& (value.relation === 'derived_from' || value.relation === 'supports' || value.relation === 'contradicts' || value.relation === 'validated_by');
|
|
79
|
+
}
|
|
80
|
+
function validApplicability(value) {
|
|
81
|
+
return isRecord(value)
|
|
82
|
+
&& value.project === true
|
|
83
|
+
&& (value.branchMode === 'global' || value.branchMode === 'exact' || value.branchMode === 'ancestor-of-source' || value.branchMode === 'descendant-of-source')
|
|
84
|
+
&& (value.branches === undefined || Array.isArray(value.branches) && value.branches.every((item) => typeof item === 'string' && item.trim().length > 0))
|
|
85
|
+
&& (value.paths === undefined || Array.isArray(value.paths) && value.paths.every((item) => typeof item === 'string' && item.trim().length > 0))
|
|
86
|
+
&& (value.sourceCommit === undefined || typeof value.sourceCommit === 'string' || value.sourceCommit === null)
|
|
87
|
+
&& (value.dependencyConstraints === undefined || Array.isArray(value.dependencyConstraints) && value.dependencyConstraints.every((item) => typeof item === 'string' && item.trim().length > 0));
|
|
88
|
+
}
|
|
89
|
+
function validEvolutionTransition(from, to) {
|
|
90
|
+
if ((from !== 'candidate' && from !== 'verified' && from !== 'published' && from !== 'rejected' && from !== 'deprecated' && from !== 'superseded' && from !== 'obsolete') || (to !== 'verified' && to !== 'published' && to !== 'rejected' && to !== 'deprecated' && to !== 'superseded' && to !== 'obsolete'))
|
|
91
|
+
return false;
|
|
92
|
+
return (from === 'candidate' && (to === 'verified' || to === 'rejected' || to === 'deprecated'))
|
|
93
|
+
|| (from === 'verified' && (to === 'published' || to === 'deprecated' || to === 'superseded' || to === 'obsolete'))
|
|
94
|
+
|| (from === 'published' && (to === 'deprecated' || to === 'superseded' || to === 'obsolete'));
|
|
95
|
+
}
|
|
96
|
+
function validSkill(value) {
|
|
97
|
+
return isRecord(value)
|
|
98
|
+
&& typeof value.skillId === 'string' && value.skillId.trim().length > 0
|
|
99
|
+
&& value.schemaVersion === 1
|
|
100
|
+
&& typeof value.proposalId === 'string' && value.proposalId.trim().length > 0
|
|
101
|
+
&& typeof value.title === 'string' && value.title.trim().length > 0
|
|
102
|
+
&& Array.isArray(value.procedure) && value.procedure.length > 0 && value.procedure.length <= 32 && value.procedure.every((step) => typeof step === 'string' && step.trim().length > 0)
|
|
103
|
+
&& validApplicability(value.applicability)
|
|
104
|
+
&& value.status === 'candidate'
|
|
105
|
+
&& typeof value.contentHash === 'string' && value.contentHash.trim().length > 0
|
|
106
|
+
&& Array.isArray(value.lineage) && value.lineage.every(validLineage)
|
|
107
|
+
&& validIsoDate(value.createdAt);
|
|
108
|
+
}
|
|
109
|
+
const recipeSources = new Set(['context', 'knowledge', 'deadend', 'note']);
|
|
110
|
+
const recipeSections = new Set(['summary', 'pending', 'decision', 'risk', 'note', 'knowledge', 'deadend', 'activity']);
|
|
111
|
+
function validRecipe(value) {
|
|
112
|
+
if (!isRecord(value))
|
|
113
|
+
return false;
|
|
114
|
+
const allowed = new Set(['recipeId', 'schemaVersion', 'proposalId', 'title', 'applicability', 'preferredSources', 'preferredSections', 'sectionBudgets', 'status', 'contentHash', 'lineage', 'createdAt']);
|
|
115
|
+
if (Object.keys(value).some((key) => !allowed.has(key)))
|
|
116
|
+
return false;
|
|
117
|
+
const validPreferences = (items, allowedItems) => items === undefined
|
|
118
|
+
|| (Array.isArray(items) && items.length <= allowedItems.size && items.every((item) => typeof item === 'string' && allowedItems.has(item)) && new Set(items).size === items.length);
|
|
119
|
+
const sectionBudgets = isRecord(value.sectionBudgets) ? value.sectionBudgets : null;
|
|
120
|
+
const validBudgets = value.sectionBudgets === undefined || (sectionBudgets !== null
|
|
121
|
+
&& Object.keys(sectionBudgets).every((key) => recipeSections.has(key) && validNonNegativeInteger(sectionBudgets[key]) && sectionBudgets[key] <= 100_000));
|
|
122
|
+
return typeof value.recipeId === 'string' && value.recipeId.trim().length > 0
|
|
123
|
+
&& value.schemaVersion === 1
|
|
124
|
+
&& typeof value.proposalId === 'string' && value.proposalId.trim().length > 0
|
|
125
|
+
&& typeof value.title === 'string' && value.title.trim().length > 0
|
|
126
|
+
&& validApplicability(value.applicability)
|
|
127
|
+
&& validPreferences(value.preferredSources, recipeSources)
|
|
128
|
+
&& validPreferences(value.preferredSections, recipeSections)
|
|
129
|
+
&& validBudgets
|
|
130
|
+
&& value.status === 'candidate'
|
|
131
|
+
&& typeof value.contentHash === 'string' && value.contentHash.trim().length > 0
|
|
132
|
+
&& Array.isArray(value.lineage) && value.lineage.every(validLineage)
|
|
133
|
+
&& validIsoDate(value.createdAt);
|
|
134
|
+
}
|
|
58
135
|
function validatePayloadBoundary(input) {
|
|
59
136
|
const payload = input.payload;
|
|
60
137
|
if (input.type === 'host.prompt.submitted' && (typeof payload.intent !== 'string' || payload.intent.trim().length === 0)) {
|
|
@@ -65,11 +142,74 @@ function validatePayloadBoundary(input) {
|
|
|
65
142
|
if (!isRecord(record) || typeof record.message !== 'string' || record.message.trim().length === 0)
|
|
66
143
|
throw new Error('note message must not be empty');
|
|
67
144
|
}
|
|
145
|
+
if (input.type === 'usage.operation.completed') {
|
|
146
|
+
const requiredNumbers = ['durationMs', 'inputChars', 'outputChars', 'inputBytes', 'outputBytes', 'estimatedInputTokens', 'estimatedOutputTokens'];
|
|
147
|
+
if (payload.operation !== 'orient' && payload.operation !== 'compile' && payload.operation !== 'expand' && payload.operation !== 'resume')
|
|
148
|
+
throw new Error('invalid usage operation');
|
|
149
|
+
if (payload.interface !== 'cli' && payload.interface !== 'mcp' && payload.interface !== 'agent' && payload.interface !== 'hook')
|
|
150
|
+
throw new Error('invalid usage interface');
|
|
151
|
+
if (typeof payload.success !== 'boolean' || payload.estimatedMethod !== 'chars-div-four' || requiredNumbers.some((key) => typeof payload[key] !== 'number' || !Number.isFinite(payload[key]) || payload[key] < 0))
|
|
152
|
+
throw new Error('invalid usage telemetry payload');
|
|
153
|
+
if (!payload.success && typeof payload.errorClass !== 'string')
|
|
154
|
+
throw new Error('usage errors require an error class');
|
|
155
|
+
}
|
|
68
156
|
if (input.type === 'workspace.paths.changed') {
|
|
69
157
|
const changes = payload.changes;
|
|
70
158
|
if (!Array.isArray(changes) || changes.length === 0)
|
|
71
159
|
throw new Error('workspace activity must contain changes');
|
|
72
160
|
}
|
|
161
|
+
if (input.type === 'experience.recorded') {
|
|
162
|
+
const experience = payload.record;
|
|
163
|
+
if (!isRecord(experience))
|
|
164
|
+
throw new Error('experience record is required');
|
|
165
|
+
validEvolutionId(experience.experienceId, 'experience id');
|
|
166
|
+
if (experience.schemaVersion !== 1 || !Array.isArray(experience.sourceEventIds) || experience.sourceEventIds.length === 0 || !experience.sourceEventIds.every((value) => typeof value === 'string' && value.trim().length > 0) || !Array.isArray(experience.sourceContextIds) || !experience.sourceContextIds.every((value) => typeof value === 'string') || !Array.isArray(experience.sourceSnapshotIds) || !experience.sourceSnapshotIds.every((value) => typeof value === 'string') || !Array.isArray(experience.paths) || !experience.paths.every((value) => typeof value === 'string') || !validGitAnchor(experience.git) || !isRecord(experience.privacy) || (experience.privacy.classification !== 'metadata' && experience.privacy.classification !== 'redacted-text') || !validNonNegativeInteger(experience.privacy.redactionCount) || !isRecord(experience.extractor) || typeof experience.extractor.id !== 'string' || experience.extractor.id.trim().length === 0 || typeof experience.extractor.version !== 'string' || experience.extractor.version.trim().length === 0 || typeof experience.contentHash !== 'string' || experience.contentHash.trim().length === 0 || typeof experience.summary !== 'string' || experience.summary.trim().length === 0 || !validIsoDate(experience.createdAt))
|
|
167
|
+
throw new Error('invalid experience record');
|
|
168
|
+
}
|
|
169
|
+
if (input.type === 'evolution.proposed') {
|
|
170
|
+
const proposal = payload.record;
|
|
171
|
+
if (!isRecord(proposal))
|
|
172
|
+
throw new Error('evolution proposal record is required');
|
|
173
|
+
validEvolutionId(proposal.proposalId, 'proposal id');
|
|
174
|
+
if (proposal.schemaVersion !== 1 || (proposal.targetKind !== 'knowledge' && proposal.targetKind !== 'deadend' && proposal.targetKind !== 'skill' && proposal.targetKind !== 'compiler-recipe') || typeof proposal.targetId !== 'string' || proposal.targetId.trim().length === 0 || proposal.status !== 'candidate' || !Array.isArray(proposal.sourceExperienceIds) || proposal.sourceExperienceIds.length === 0 || !proposal.sourceExperienceIds.every((value) => typeof value === 'string' && value.trim().length > 0) || !Array.isArray(proposal.evidence) || proposal.evidence.length === 0 || !proposal.evidence.every(validEvolutionEvidence) || !Array.isArray(proposal.lineage) || !proposal.lineage.every(validLineage) || !validApplicability(proposal.applicability) || typeof proposal.contentHash !== 'string' || proposal.contentHash.trim().length === 0 || typeof proposal.summary !== 'string' || proposal.summary.trim().length === 0 || !isRecord(proposal.extractor) || typeof proposal.extractor.id !== 'string' || proposal.extractor.id.trim().length === 0 || typeof proposal.extractor.version !== 'string' || proposal.extractor.version.trim().length === 0 || !validIsoDate(proposal.createdAt))
|
|
175
|
+
throw new Error('invalid evolution proposal record');
|
|
176
|
+
}
|
|
177
|
+
if (input.type === 'evolution.candidate.accepted') {
|
|
178
|
+
const artifact = payload.record;
|
|
179
|
+
if (!isRecord(artifact) || typeof artifact.artifactId !== 'string' || artifact.artifactId.trim().length === 0 || artifact.schemaVersion !== 1 || typeof artifact.proposalId !== 'string' || artifact.proposalId.trim().length === 0 || (artifact.kind !== 'skill' && artifact.kind !== 'compiler-recipe') || artifact.status !== 'candidate' || !validApplicability(artifact.applicability) || typeof artifact.contentHash !== 'string' || artifact.contentHash.trim().length === 0 || !Array.isArray(artifact.lineage) || !artifact.lineage.every(validLineage) || typeof artifact.rationale !== 'string' || artifact.rationale.trim().length === 0 || !validIsoDate(artifact.createdAt))
|
|
180
|
+
throw new Error('invalid evolution candidate artifact');
|
|
181
|
+
}
|
|
182
|
+
if (['evolution.verified', 'evolution.published', 'evolution.rejected', 'evolution.deprecated', 'evolution.superseded'].includes(input.type)) {
|
|
183
|
+
const transition = payload.transition;
|
|
184
|
+
if (!isRecord(transition) || (transition.entity !== 'knowledge' && transition.entity !== 'deadend' && transition.entity !== 'skill' && transition.entity !== 'compiler-recipe' && transition.entity !== 'proposal') || typeof transition.entityId !== 'string' || transition.entityId.trim().length === 0 || !validEvolutionTransition(transition.from, transition.to) || typeof transition.reason !== 'string' || transition.reason.trim().length === 0 || (transition.supersedes !== undefined && !validLineage(transition.supersedes)))
|
|
185
|
+
throw new Error('invalid evolution lifecycle transition');
|
|
186
|
+
if ((input.type === 'evolution.verified' && transition.to !== 'verified') || (input.type === 'evolution.published' && transition.to !== 'published') || (input.type === 'evolution.rejected' && transition.to !== 'rejected') || (input.type === 'evolution.deprecated' && transition.to !== 'deprecated') || (input.type === 'evolution.superseded' && (transition.to !== 'superseded' || !validLineage(transition.supersedes))))
|
|
187
|
+
throw new Error('evolution lifecycle event does not match transition');
|
|
188
|
+
}
|
|
189
|
+
if (input.type === 'skill.materialized') {
|
|
190
|
+
if (!validSkill(payload.record))
|
|
191
|
+
throw new Error('invalid skill record');
|
|
192
|
+
}
|
|
193
|
+
if (input.type === 'recipe.materialized') {
|
|
194
|
+
if (!validRecipe(payload.record))
|
|
195
|
+
throw new Error('invalid compiler recipe record');
|
|
196
|
+
}
|
|
197
|
+
if (input.type === 'skill.invoked') {
|
|
198
|
+
const invocation = payload.record;
|
|
199
|
+
if (!isRecord(invocation))
|
|
200
|
+
throw new Error('skill invocation record is required');
|
|
201
|
+
validEvolutionId(invocation.invocationId, 'invocation id');
|
|
202
|
+
if (typeof invocation.skillId !== 'string' || invocation.skillId.trim().length === 0 || typeof invocation.callerEventId !== 'string' || invocation.callerEventId.trim().length === 0 || typeof invocation.inputHash !== 'string' || invocation.inputHash.trim().length === 0 || !validIsoDate(invocation.occurredAt))
|
|
203
|
+
throw new Error('invalid skill invocation record');
|
|
204
|
+
}
|
|
205
|
+
if (input.type === 'skill.outcome.recorded') {
|
|
206
|
+
const outcome = payload.record;
|
|
207
|
+
if (!isRecord(outcome))
|
|
208
|
+
throw new Error('skill outcome record is required');
|
|
209
|
+
validEvolutionId(outcome.outcomeId, 'outcome id');
|
|
210
|
+
if (typeof outcome.invocationId !== 'string' || outcome.invocationId.trim().length === 0 || typeof outcome.callerEventId !== 'string' || outcome.callerEventId.trim().length === 0 || (outcome.outcome !== 'success' && outcome.outcome !== 'failure' && outcome.outcome !== 'partial' && outcome.outcome !== 'irrelevant' && outcome.outcome !== 'unknown') || typeof outcome.summary !== 'string' || outcome.summary.trim().length === 0 || !validIsoDate(outcome.occurredAt))
|
|
211
|
+
throw new Error('invalid skill outcome record');
|
|
212
|
+
}
|
|
73
213
|
}
|
|
74
214
|
function validateEventShape(event, projectId, file, line) {
|
|
75
215
|
if (!isRecord(event)
|
|
@@ -142,6 +282,7 @@ export async function readJournal(startPath, options = {}) {
|
|
|
142
282
|
throw new Error(`invalid journal record ${file}:${index + 1}: ${error instanceof Error ? error.message : 'invalid JSON'}`);
|
|
143
283
|
}
|
|
144
284
|
validateEventShape(event, projectId, file, index + 1);
|
|
285
|
+
validatePayloadBoundary(event);
|
|
145
286
|
const { hash, ...withoutHash } = event;
|
|
146
287
|
if (event.previousHash !== previousHash || hash !== eventHash(withoutHash)) {
|
|
147
288
|
throw new Error(`invalid journal hash chain at ${file}:${index + 1}`);
|
|
@@ -2,9 +2,10 @@ import crypto from 'node:crypto';
|
|
|
2
2
|
import { readdir, rm } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { atomicWriteJson } from '../storage/atomic-write.js';
|
|
5
|
-
import { cognitionBranchesDirectory, cognitionContextsProjectionPath, cognitionDeadendsProjectionPath, cognitionKnowledgeProjectionPath, cognitionNotesProjectionPath, cognitionProjectionCursorPath, cognitionRetrievalProjectionPath, } from '../storage/layout.js';
|
|
5
|
+
import { cognitionBranchesDirectory, cognitionContextsProjectionPath, cognitionDeadendsProjectionPath, cognitionExperiencesProjectionPath, cognitionEvidenceGraphProjectionPath, cognitionKnowledgeProjectionPath, cognitionNotesProjectionPath, cognitionProjectionCursorPath, cognitionProposalsProjectionPath, cognitionRetrievalProjectionPath, cognitionSkillsProjectionPath, cognitionRecipesProjectionPath, } from '../storage/layout.js';
|
|
6
6
|
import { readJournal } from './journal.js';
|
|
7
7
|
import { buildRetrievalProjection } from './retrieval.js';
|
|
8
|
+
import { buildEvidenceGraph } from './evidence-graph.js';
|
|
8
9
|
export function workspaceKey(anchor) {
|
|
9
10
|
return crypto.createHash('sha256')
|
|
10
11
|
.update(`${anchor.worktreeId ?? 'default'}\0${anchor.ref ?? anchor.branch ?? 'detached'}`)
|
|
@@ -48,6 +49,55 @@ function pathFocusFrom(event) {
|
|
|
48
49
|
.sort()
|
|
49
50
|
.slice(-100);
|
|
50
51
|
}
|
|
52
|
+
function evolutionRecord(event) {
|
|
53
|
+
const payload = event.payload;
|
|
54
|
+
return isRecord(payload.record) ? payload.record : null;
|
|
55
|
+
}
|
|
56
|
+
function isRecord(value) {
|
|
57
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
58
|
+
}
|
|
59
|
+
function lifecycleEntry(event) {
|
|
60
|
+
const payload = event.payload;
|
|
61
|
+
const transition = payload.transition;
|
|
62
|
+
if (!isRecord(transition) || transition.entity !== 'proposal' || typeof transition.entityId !== 'string' || typeof transition.from !== 'string' || typeof transition.to !== 'string' || typeof transition.reason !== 'string')
|
|
63
|
+
return null;
|
|
64
|
+
return {
|
|
65
|
+
eventId: event.eventId,
|
|
66
|
+
occurredAt: event.occurredAt,
|
|
67
|
+
from: transition.from,
|
|
68
|
+
to: transition.to,
|
|
69
|
+
reason: transition.reason,
|
|
70
|
+
...(isRecord(transition.supersedes) ? { supersedes: transition.supersedes } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function skillLifecycleEntry(event) {
|
|
74
|
+
const payload = event.payload;
|
|
75
|
+
const transition = payload.transition;
|
|
76
|
+
if (!isRecord(transition) || transition.entity !== 'skill' || typeof transition.entityId !== 'string' || typeof transition.from !== 'string' || typeof transition.to !== 'string' || typeof transition.reason !== 'string')
|
|
77
|
+
return null;
|
|
78
|
+
return {
|
|
79
|
+
eventId: event.eventId,
|
|
80
|
+
occurredAt: event.occurredAt,
|
|
81
|
+
from: transition.from,
|
|
82
|
+
to: transition.to,
|
|
83
|
+
reason: transition.reason,
|
|
84
|
+
...(isRecord(transition.supersedes) ? { supersedes: transition.supersedes } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function recipeLifecycleEntry(event) {
|
|
88
|
+
const payload = event.payload;
|
|
89
|
+
const transition = payload.transition;
|
|
90
|
+
if (!isRecord(transition) || transition.entity !== 'compiler-recipe' || typeof transition.entityId !== 'string' || typeof transition.from !== 'string' || typeof transition.to !== 'string' || typeof transition.reason !== 'string')
|
|
91
|
+
return null;
|
|
92
|
+
return {
|
|
93
|
+
eventId: event.eventId,
|
|
94
|
+
occurredAt: event.occurredAt,
|
|
95
|
+
from: transition.from,
|
|
96
|
+
to: transition.to,
|
|
97
|
+
reason: transition.reason,
|
|
98
|
+
...(isRecord(transition.supersedes) ? { supersedes: transition.supersedes } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
51
101
|
function latestOpenObservationIds(events, notes) {
|
|
52
102
|
const latest = new Map();
|
|
53
103
|
for (const event of events) {
|
|
@@ -66,7 +116,7 @@ function latestOpenObservationIds(events, notes) {
|
|
|
66
116
|
return new Set([...latest.values()].flatMap(({ eventId, record }) => record.status === 'open' && record.absorbedBySnapshotId === null ? [eventId] : []));
|
|
67
117
|
}
|
|
68
118
|
export function projectEvents(events) {
|
|
69
|
-
const projection = { schemaVersion: 2, contexts: {}, notes: {}, knowledge: {}, deadends: {}, workspaces: {} };
|
|
119
|
+
const projection = { schemaVersion: 2, contexts: {}, notes: {}, knowledge: {}, deadends: {}, experiences: {}, proposals: {}, skills: {}, recipes: {}, evidenceGraph: { schemaVersion: 1, derivationVersion: 'evidence-graph-v1', nodes: [], edges: [] }, workspaces: {} };
|
|
70
120
|
const openObservationIds = latestOpenObservationIds(events, projection.notes);
|
|
71
121
|
for (const event of events) {
|
|
72
122
|
const key = workspaceKey(event.git);
|
|
@@ -96,6 +146,72 @@ export function projectEvents(events) {
|
|
|
96
146
|
if (deadendId)
|
|
97
147
|
projection.deadends[deadendId] = payload;
|
|
98
148
|
}
|
|
149
|
+
if (event.type === 'experience.recorded') {
|
|
150
|
+
const experience = evolutionRecord(event);
|
|
151
|
+
if (experience && typeof experience.experienceId === 'string')
|
|
152
|
+
projection.experiences[experience.experienceId] = experience;
|
|
153
|
+
}
|
|
154
|
+
if (event.type === 'evolution.proposed') {
|
|
155
|
+
const proposal = evolutionRecord(event);
|
|
156
|
+
if (proposal && typeof proposal.proposalId === 'string') {
|
|
157
|
+
projection.proposals[proposal.proposalId] = { proposal, status: proposal.status, lifecycle: [], acceptedArtifact: null };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (event.type === 'evolution.candidate.accepted') {
|
|
161
|
+
const artifact = evolutionRecord(event);
|
|
162
|
+
if (artifact && projection.proposals[artifact.proposalId])
|
|
163
|
+
projection.proposals[artifact.proposalId].acceptedArtifact = artifact;
|
|
164
|
+
}
|
|
165
|
+
if (['evolution.verified', 'evolution.published', 'evolution.rejected', 'evolution.deprecated', 'evolution.superseded'].includes(event.type)) {
|
|
166
|
+
const entry = lifecycleEntry(event);
|
|
167
|
+
const state = entry ? projection.proposals[event.payload.transition.entityId] : null;
|
|
168
|
+
if (entry && state && state.status === entry.from) {
|
|
169
|
+
state.status = entry.to;
|
|
170
|
+
state.lifecycle.push(entry);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (event.type === 'skill.materialized') {
|
|
174
|
+
const skill = evolutionRecord(event);
|
|
175
|
+
if (skill && typeof skill.skillId === 'string' && !projection.skills[skill.skillId]) {
|
|
176
|
+
projection.skills[skill.skillId] = { skill, status: skill.status, lifecycle: [], invocations: [], outcomes: [] };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (['evolution.verified', 'evolution.published', 'evolution.rejected', 'evolution.deprecated', 'evolution.superseded'].includes(event.type)) {
|
|
180
|
+
const skillEntry = skillLifecycleEntry(event);
|
|
181
|
+
const skillState = skillEntry ? projection.skills[event.payload.transition.entityId] : null;
|
|
182
|
+
if (skillEntry && skillState && skillState.status === skillEntry.from) {
|
|
183
|
+
skillState.status = skillEntry.to;
|
|
184
|
+
skillState.lifecycle.push(skillEntry);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (event.type === 'recipe.materialized') {
|
|
188
|
+
const recipe = evolutionRecord(event);
|
|
189
|
+
if (recipe && typeof recipe.recipeId === 'string' && !projection.recipes[recipe.recipeId]) {
|
|
190
|
+
projection.recipes[recipe.recipeId] = { recipe, status: recipe.status, lifecycle: [] };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (['evolution.verified', 'evolution.published', 'evolution.rejected', 'evolution.deprecated', 'evolution.superseded'].includes(event.type)) {
|
|
194
|
+
const recipeEntry = recipeLifecycleEntry(event);
|
|
195
|
+
const recipeState = recipeEntry ? projection.recipes[event.payload.transition.entityId] : null;
|
|
196
|
+
if (recipeEntry && recipeState && recipeState.status === recipeEntry.from) {
|
|
197
|
+
recipeState.status = recipeEntry.to;
|
|
198
|
+
recipeState.lifecycle.push(recipeEntry);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (event.type === 'skill.invoked') {
|
|
202
|
+
const invocation = evolutionRecord(event);
|
|
203
|
+
const skillState = invocation ? projection.skills[invocation.skillId] : null;
|
|
204
|
+
if (invocation && skillState && skillState.status === 'published' && !skillState.invocations.some((item) => item.invocationId === invocation.invocationId))
|
|
205
|
+
skillState.invocations.push(invocation);
|
|
206
|
+
}
|
|
207
|
+
if (event.type === 'skill.outcome.recorded') {
|
|
208
|
+
const outcome = evolutionRecord(event);
|
|
209
|
+
if (outcome) {
|
|
210
|
+
const skillState = Object.values(projection.skills).find((state) => state.invocations.some((invocation) => invocation.invocationId === outcome.invocationId));
|
|
211
|
+
if (skillState && !skillState.outcomes.some((item) => item.invocationId === outcome.invocationId))
|
|
212
|
+
skillState.outcomes.push(outcome);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
99
215
|
if (event.type === 'note.recorded' && openObservationIds.has(event.eventId)) {
|
|
100
216
|
workspace.openObservationEventIds = [...workspace.openObservationEventIds, event.eventId].slice(-50);
|
|
101
217
|
}
|
|
@@ -104,6 +220,7 @@ export function projectEvents(events) {
|
|
|
104
220
|
}
|
|
105
221
|
projection.workspaces[key] = workspace;
|
|
106
222
|
}
|
|
223
|
+
projection.evidenceGraph = buildEvidenceGraph(events);
|
|
107
224
|
return projection;
|
|
108
225
|
}
|
|
109
226
|
async function removeObsoleteWorkspaceFiles(projectRoot, workspaceKeys) {
|
|
@@ -136,6 +253,11 @@ export async function rebuildProjections(projectRoot) {
|
|
|
136
253
|
atomicWriteJson(cognitionNotesProjectionPath(projectRoot), projection.notes),
|
|
137
254
|
atomicWriteJson(cognitionKnowledgeProjectionPath(projectRoot), projection.knowledge),
|
|
138
255
|
atomicWriteJson(cognitionDeadendsProjectionPath(projectRoot), projection.deadends),
|
|
256
|
+
atomicWriteJson(cognitionExperiencesProjectionPath(projectRoot), projection.experiences),
|
|
257
|
+
atomicWriteJson(cognitionProposalsProjectionPath(projectRoot), projection.proposals),
|
|
258
|
+
atomicWriteJson(cognitionSkillsProjectionPath(projectRoot), projection.skills),
|
|
259
|
+
atomicWriteJson(cognitionRecipesProjectionPath(projectRoot), projection.recipes),
|
|
260
|
+
atomicWriteJson(cognitionEvidenceGraphProjectionPath(projectRoot), projection.evidenceGraph),
|
|
139
261
|
atomicWriteJson(cognitionRetrievalProjectionPath(projectRoot), retrieval),
|
|
140
262
|
...Object.values(projection.workspaces).map((workspace) => atomicWriteJson(`${cognitionBranchesDirectory(projectRoot)}/${workspace.workspaceKey}.json`, workspace)),
|
|
141
263
|
]);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CompilerRecipeSection, CompilerRecipeSource, CompilerRecipeState, EvolutionArtifactStatus, EvolutionLineageReference } from './types.js';
|
|
2
|
+
export interface MaterializeRecipeInput {
|
|
3
|
+
proposalId: string;
|
|
4
|
+
title: string;
|
|
5
|
+
preferredSources?: CompilerRecipeSource[];
|
|
6
|
+
preferredSections?: CompilerRecipeSection[];
|
|
7
|
+
sectionBudgets?: Partial<Record<CompilerRecipeSection, number>>;
|
|
8
|
+
}
|
|
9
|
+
export interface TransitionRecipeInput {
|
|
10
|
+
recipeId: string;
|
|
11
|
+
to: Extract<EvolutionArtifactStatus, 'verified' | 'published' | 'deprecated' | 'superseded'>;
|
|
12
|
+
rationale: string;
|
|
13
|
+
supersedes?: EvolutionLineageReference;
|
|
14
|
+
}
|
|
15
|
+
export declare function listRecipes(startPath?: string): Promise<CompilerRecipeState[]>;
|
|
16
|
+
export declare function inspectRecipe(startPath: string | undefined, recipeId: string): Promise<CompilerRecipeState>;
|
|
17
|
+
export declare function materializeRecipe(startPath: string | undefined, input: MaterializeRecipeInput): Promise<{
|
|
18
|
+
materialized: boolean;
|
|
19
|
+
recipeId: string;
|
|
20
|
+
eventId: string;
|
|
21
|
+
}>;
|
|
22
|
+
export declare function transitionRecipe(startPath: string | undefined, input: TransitionRecipeInput): Promise<{
|
|
23
|
+
transitioned: boolean;
|
|
24
|
+
eventId: string;
|
|
25
|
+
}>;
|