@evo-dev/core 0.0.1-alpha.15 → 0.0.1-alpha.17
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/dist/config/index.js +45 -2
- package/dist/index.js +12899 -11408
- package/package.json +1 -1
- package/src/config/settings.ts +45 -7
- package/src/evolution/candidates/index.ts +50 -0
- package/src/evolution/evidence/session-memory/retention.ts +2 -1
- package/src/evolution/knowledge/change-store.ts +558 -0
- package/src/evolution/knowledge/changes.ts +453 -0
- package/src/evolution/knowledge/freshness.ts +109 -0
- package/src/evolution/knowledge/index.ts +471 -55
- package/src/evolution/knowledge/review.ts +446 -0
- package/src/evolution/processor/process.ts +5 -1
- package/src/evolution/schema.ts +29 -1
- package/src/evolution/shared.ts +108 -0
- package/src/index.ts +4 -0
- package/src/team/index.ts +6 -0
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import { detectSessionMemorySensitivity } from "../evidence/session-memory/sensitivity.ts";
|
|
2
|
+
import {
|
|
3
|
+
type OkfKnowledgeChangeCandidateV1,
|
|
4
|
+
readOkfKnowledgeChangeCandidate,
|
|
5
|
+
replaceOkfKnowledgeChangeCandidate,
|
|
6
|
+
withOkfKnowledgeChangeLock,
|
|
7
|
+
} from "./change-store.ts";
|
|
8
|
+
import {
|
|
9
|
+
type OkfKnowledgeChangeState,
|
|
10
|
+
createOkfKnowledgeRevision,
|
|
11
|
+
createOkfKnowledgeRuntimeProjectionFromConcept,
|
|
12
|
+
} from "./changes.ts";
|
|
13
|
+
import {
|
|
14
|
+
type OkfKnowledgeActivationResult,
|
|
15
|
+
type OkfKnowledgePlan,
|
|
16
|
+
activateOkfKnowledgePlan,
|
|
17
|
+
listOkfKnowledgeConcepts,
|
|
18
|
+
readOkfKnowledgeConcept,
|
|
19
|
+
revokeOkfKnowledgeConcept,
|
|
20
|
+
supersedeOkfKnowledgeConcept,
|
|
21
|
+
} from "./index.ts";
|
|
22
|
+
|
|
23
|
+
export type OkfKnowledgeChangeReviewDecision = "accept" | "reject" | "defer";
|
|
24
|
+
|
|
25
|
+
export interface OkfKnowledgeChangeDecisionPreview {
|
|
26
|
+
changeId: string;
|
|
27
|
+
operation: OkfKnowledgeChangeCandidateV1["operation"];
|
|
28
|
+
currentState: OkfKnowledgeChangeState;
|
|
29
|
+
decision: OkfKnowledgeChangeReviewDecision;
|
|
30
|
+
baseRevision: string | null;
|
|
31
|
+
candidateRevision: string;
|
|
32
|
+
runtimeFields: string[];
|
|
33
|
+
willMutateActiveKnowledge: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OkfKnowledgeChangeDecisionResult extends OkfKnowledgeChangeDecisionPreview {
|
|
37
|
+
state: OkfKnowledgeChangeState;
|
|
38
|
+
activation: OkfKnowledgeActivationResult | null;
|
|
39
|
+
conceptPaths: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function previewOkfKnowledgeChangeDecision(input: {
|
|
43
|
+
homeDir: string;
|
|
44
|
+
changeId: string;
|
|
45
|
+
decision: OkfKnowledgeChangeReviewDecision;
|
|
46
|
+
expectedState: OkfKnowledgeChangeState;
|
|
47
|
+
expectedBaseRevision: string | null;
|
|
48
|
+
}): Promise<OkfKnowledgeChangeDecisionPreview> {
|
|
49
|
+
const { change } = await readOkfKnowledgeChangeCandidate({
|
|
50
|
+
homeDir: input.homeDir,
|
|
51
|
+
changeId: input.changeId,
|
|
52
|
+
});
|
|
53
|
+
await assertReviewPreconditions({
|
|
54
|
+
homeDir: input.homeDir,
|
|
55
|
+
change,
|
|
56
|
+
expectedState: input.expectedState,
|
|
57
|
+
expectedBaseRevision: input.expectedBaseRevision,
|
|
58
|
+
});
|
|
59
|
+
return toPreview(change, input.decision);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function decideOkfKnowledgeChange(input: {
|
|
63
|
+
homeDir: string;
|
|
64
|
+
changeId: string;
|
|
65
|
+
decision: OkfKnowledgeChangeReviewDecision;
|
|
66
|
+
expectedState: OkfKnowledgeChangeState;
|
|
67
|
+
expectedBaseRevision: string | null;
|
|
68
|
+
reason?: string | null;
|
|
69
|
+
now?: string | Date;
|
|
70
|
+
}): Promise<OkfKnowledgeChangeDecisionResult> {
|
|
71
|
+
const initial = await readOkfKnowledgeChangeCandidate({
|
|
72
|
+
homeDir: input.homeDir,
|
|
73
|
+
changeId: input.changeId,
|
|
74
|
+
});
|
|
75
|
+
return await withOkfKnowledgeChangeLock({
|
|
76
|
+
homeDir: input.homeDir,
|
|
77
|
+
projectKey: initial.change.projectKey,
|
|
78
|
+
stableKey: initial.change.stableKey,
|
|
79
|
+
conceptId: initial.change.base?.conceptId ?? null,
|
|
80
|
+
run: async () => {
|
|
81
|
+
const { change } = await readOkfKnowledgeChangeCandidate({
|
|
82
|
+
homeDir: input.homeDir,
|
|
83
|
+
changeId: input.changeId,
|
|
84
|
+
projectKey: initial.change.projectKey,
|
|
85
|
+
});
|
|
86
|
+
assertReviewInputPreconditions({
|
|
87
|
+
change,
|
|
88
|
+
expectedState: input.expectedState,
|
|
89
|
+
expectedBaseRevision: input.expectedBaseRevision,
|
|
90
|
+
});
|
|
91
|
+
if (change.state === "accepted" && input.decision !== "accept") {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"Knowledge change state conflict: an accepted change can only resume acceptance.",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (change.state === "accepted" && input.decision === "accept") {
|
|
97
|
+
const recovered = await readAlreadyAppliedKnowledgeChange({
|
|
98
|
+
homeDir: input.homeDir,
|
|
99
|
+
change,
|
|
100
|
+
});
|
|
101
|
+
if (recovered !== null) {
|
|
102
|
+
const completed: OkfKnowledgeChangeCandidateV1 = {
|
|
103
|
+
...change,
|
|
104
|
+
state: "applied",
|
|
105
|
+
updatedAt: resolveNow(input.now),
|
|
106
|
+
};
|
|
107
|
+
await replaceOkfKnowledgeChangeCandidate({
|
|
108
|
+
homeDir: input.homeDir,
|
|
109
|
+
previous: change,
|
|
110
|
+
next: completed,
|
|
111
|
+
reason: input.reason ?? null,
|
|
112
|
+
});
|
|
113
|
+
return {
|
|
114
|
+
...toPreview(change, input.decision),
|
|
115
|
+
state: "applied",
|
|
116
|
+
activation: null,
|
|
117
|
+
conceptPaths: recovered.conceptPaths,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await assertReviewPreconditions({
|
|
123
|
+
homeDir: input.homeDir,
|
|
124
|
+
change,
|
|
125
|
+
expectedState: input.expectedState,
|
|
126
|
+
expectedBaseRevision: input.expectedBaseRevision,
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (isBaseRevisionConflict(error) && change.state !== "superseded") {
|
|
130
|
+
const superseded = {
|
|
131
|
+
...change,
|
|
132
|
+
state: "superseded" as const,
|
|
133
|
+
updatedAt: resolveNow(input.now),
|
|
134
|
+
};
|
|
135
|
+
await replaceOkfKnowledgeChangeCandidate({
|
|
136
|
+
homeDir: input.homeDir,
|
|
137
|
+
previous: change,
|
|
138
|
+
next: superseded,
|
|
139
|
+
reason: "Base revision changed before review.",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const reason = normalizeDecisionReason(input.reason);
|
|
146
|
+
const decidedAt = resolveNow(input.now);
|
|
147
|
+
const decisionState =
|
|
148
|
+
input.decision === "accept"
|
|
149
|
+
? ("accepted" as const)
|
|
150
|
+
: input.decision === "reject"
|
|
151
|
+
? ("rejected" as const)
|
|
152
|
+
: ("deferred" as const);
|
|
153
|
+
const decided =
|
|
154
|
+
change.state === "accepted" && input.decision === "accept"
|
|
155
|
+
? change
|
|
156
|
+
: {
|
|
157
|
+
...change,
|
|
158
|
+
state: decisionState,
|
|
159
|
+
decision: {
|
|
160
|
+
state: decisionState,
|
|
161
|
+
reason,
|
|
162
|
+
decidedAt,
|
|
163
|
+
},
|
|
164
|
+
updatedAt: decidedAt,
|
|
165
|
+
};
|
|
166
|
+
if (decided !== change) {
|
|
167
|
+
await replaceOkfKnowledgeChangeCandidate({
|
|
168
|
+
homeDir: input.homeDir,
|
|
169
|
+
previous: change,
|
|
170
|
+
next: decided,
|
|
171
|
+
reason,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const preview = toPreview(change, input.decision);
|
|
175
|
+
if (input.decision !== "accept") {
|
|
176
|
+
return {
|
|
177
|
+
...preview,
|
|
178
|
+
state: decided.state,
|
|
179
|
+
activation: null,
|
|
180
|
+
conceptPaths: [],
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const applied = await applyAcceptedChange({
|
|
185
|
+
homeDir: input.homeDir,
|
|
186
|
+
change: decided,
|
|
187
|
+
now: decidedAt,
|
|
188
|
+
reason,
|
|
189
|
+
});
|
|
190
|
+
const current = (
|
|
191
|
+
await readOkfKnowledgeChangeCandidate({
|
|
192
|
+
homeDir: input.homeDir,
|
|
193
|
+
changeId: input.changeId,
|
|
194
|
+
projectKey: change.projectKey,
|
|
195
|
+
})
|
|
196
|
+
).change;
|
|
197
|
+
const completed: OkfKnowledgeChangeCandidateV1 = {
|
|
198
|
+
...current,
|
|
199
|
+
state: "applied",
|
|
200
|
+
updatedAt: resolveNow(input.now),
|
|
201
|
+
};
|
|
202
|
+
await replaceOkfKnowledgeChangeCandidate({
|
|
203
|
+
homeDir: input.homeDir,
|
|
204
|
+
previous: current,
|
|
205
|
+
next: completed,
|
|
206
|
+
reason,
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
...preview,
|
|
210
|
+
state: "applied",
|
|
211
|
+
activation: applied.activation,
|
|
212
|
+
conceptPaths: applied.conceptPaths,
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function assertReviewPreconditions(input: {
|
|
219
|
+
homeDir: string;
|
|
220
|
+
change: OkfKnowledgeChangeCandidateV1;
|
|
221
|
+
expectedState: OkfKnowledgeChangeState;
|
|
222
|
+
expectedBaseRevision: string | null;
|
|
223
|
+
}): Promise<void> {
|
|
224
|
+
assertReviewInputPreconditions(input);
|
|
225
|
+
if (input.change.base === null) {
|
|
226
|
+
const concepts = await listOkfKnowledgeConcepts({ homeDir: input.homeDir });
|
|
227
|
+
if (
|
|
228
|
+
concepts.some(
|
|
229
|
+
(concept) =>
|
|
230
|
+
concept.stableKey === input.change.stableKey ||
|
|
231
|
+
concept.id === conceptIdFromTargetPath(input.change.targetPath),
|
|
232
|
+
)
|
|
233
|
+
) {
|
|
234
|
+
throw createBaseRevisionConflict(
|
|
235
|
+
"Knowledge change base revision conflict: a concept now uses this stable key or target path.",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const current = await readOkfKnowledgeConcept({
|
|
241
|
+
homeDir: input.homeDir,
|
|
242
|
+
conceptId: input.change.base.conceptId,
|
|
243
|
+
});
|
|
244
|
+
const currentRevision = createOkfKnowledgeRevision(
|
|
245
|
+
createOkfKnowledgeRuntimeProjectionFromConcept(current),
|
|
246
|
+
);
|
|
247
|
+
if (currentRevision !== input.change.base.revision) {
|
|
248
|
+
throw createBaseRevisionConflict(
|
|
249
|
+
`Knowledge change base revision conflict: expected ${input.change.base.revision}, found ${currentRevision}.`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function assertReviewInputPreconditions(input: {
|
|
255
|
+
change: OkfKnowledgeChangeCandidateV1;
|
|
256
|
+
expectedState: OkfKnowledgeChangeState;
|
|
257
|
+
expectedBaseRevision: string | null;
|
|
258
|
+
}): void {
|
|
259
|
+
if (input.change.state !== input.expectedState) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
`Knowledge change state conflict: expected ${input.expectedState}, found ${input.change.state}.`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const storedBaseRevision = input.change.base?.revision ?? null;
|
|
265
|
+
if (storedBaseRevision !== input.expectedBaseRevision) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`Knowledge change base revision input does not match candidate: expected ${storedBaseRevision ?? "none"}.`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function readAlreadyAppliedKnowledgeChange(input: {
|
|
273
|
+
homeDir: string;
|
|
274
|
+
change: OkfKnowledgeChangeCandidateV1;
|
|
275
|
+
}): Promise<{ conceptPaths: string[] } | null> {
|
|
276
|
+
if (input.change.operation === "supersede") {
|
|
277
|
+
if (input.change.base === null) return null;
|
|
278
|
+
const replacement = await readConceptIfPresent(
|
|
279
|
+
input.homeDir,
|
|
280
|
+
conceptIdFromTargetPath(input.change.targetPath),
|
|
281
|
+
);
|
|
282
|
+
const previous = await readConceptIfPresent(input.homeDir, input.change.base.conceptId);
|
|
283
|
+
if (
|
|
284
|
+
replacement === null ||
|
|
285
|
+
previous === null ||
|
|
286
|
+
replacement.stableKey !== input.change.stableKey ||
|
|
287
|
+
previous.lifecycle.status !== "superseded" ||
|
|
288
|
+
previous.lifecycle.supersededBy !== replacement.id ||
|
|
289
|
+
!replacement.lifecycle.supersedes.includes(previous.id)
|
|
290
|
+
) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
return { conceptPaths: [previous.path, replacement.path] };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const conceptId =
|
|
297
|
+
input.change.operation === "create"
|
|
298
|
+
? conceptIdFromTargetPath(input.change.targetPath)
|
|
299
|
+
: input.change.base?.conceptId;
|
|
300
|
+
if (conceptId === undefined) return null;
|
|
301
|
+
const concept = await readConceptIfPresent(input.homeDir, conceptId);
|
|
302
|
+
if (concept === null || concept.stableKey !== input.change.stableKey) return null;
|
|
303
|
+
const revision = createOkfKnowledgeRevision(
|
|
304
|
+
createOkfKnowledgeRuntimeProjectionFromConcept(concept),
|
|
305
|
+
);
|
|
306
|
+
return revision === input.change.candidate.revision ? { conceptPaths: [concept.path] } : null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function readConceptIfPresent(
|
|
310
|
+
homeDir: string,
|
|
311
|
+
conceptId: string,
|
|
312
|
+
): Promise<Awaited<ReturnType<typeof readOkfKnowledgeConcept>> | null> {
|
|
313
|
+
try {
|
|
314
|
+
return await readOkfKnowledgeConcept({ homeDir, conceptId });
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (error instanceof Error && /not found/iu.test(error.message)) return null;
|
|
317
|
+
throw error;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function conceptIdFromTargetPath(targetPath: string): string {
|
|
322
|
+
return targetPath.replace(/^\/+/u, "").replace(/\.md$/u, "");
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function createBaseRevisionConflict(message: string): Error {
|
|
326
|
+
return Object.assign(new Error(message), {
|
|
327
|
+
code: "KNOWLEDGE_BASE_REVISION_CONFLICT",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
async function applyAcceptedChange(input: {
|
|
332
|
+
homeDir: string;
|
|
333
|
+
change: OkfKnowledgeChangeCandidateV1;
|
|
334
|
+
now: string;
|
|
335
|
+
reason: string | null;
|
|
336
|
+
}): Promise<{ activation: OkfKnowledgeActivationResult | null; conceptPaths: string[] }> {
|
|
337
|
+
if (input.change.operation === "revoke") {
|
|
338
|
+
if (input.change.base === null) throw new Error("Knowledge revoke requires an active base.");
|
|
339
|
+
const result = await revokeOkfKnowledgeConcept({
|
|
340
|
+
homeDir: input.homeDir,
|
|
341
|
+
conceptId: input.change.base.conceptId,
|
|
342
|
+
reason: input.reason ?? input.change.candidate.planCandidate.decisionReason.slice(0, 500),
|
|
343
|
+
now: input.now,
|
|
344
|
+
});
|
|
345
|
+
return { activation: null, conceptPaths: result.conceptPaths };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const planCandidate = {
|
|
349
|
+
...input.change.candidate.planCandidate,
|
|
350
|
+
decision: input.change.operation === "update" ? ("update" as const) : ("create" as const),
|
|
351
|
+
reviewState: "accepted" as const,
|
|
352
|
+
targetPath:
|
|
353
|
+
input.change.operation === "update" && input.change.base !== null
|
|
354
|
+
? input.change.base.sourceLink.replace(/^\/+/u, "")
|
|
355
|
+
: input.change.targetPath,
|
|
356
|
+
};
|
|
357
|
+
const plan: OkfKnowledgePlan = {
|
|
358
|
+
schemaVersion: 1,
|
|
359
|
+
kind: "evodev-knowledge-plan",
|
|
360
|
+
projectKey: input.change.projectKey,
|
|
361
|
+
runId: input.change.runId,
|
|
362
|
+
createdAt: input.now,
|
|
363
|
+
evidenceWindowId: input.change.provenance.evidenceWindowId,
|
|
364
|
+
summary: `Apply reviewed knowledge change ${input.change.id}.`,
|
|
365
|
+
evidenceRefs: input.change.provenance.evidenceRefs.map((id) => ({
|
|
366
|
+
id,
|
|
367
|
+
kind: "knowledge-change-evidence",
|
|
368
|
+
source: `knowledge-change:${input.change.id}`,
|
|
369
|
+
rawContentStored: false,
|
|
370
|
+
externalContentCopied: false,
|
|
371
|
+
})),
|
|
372
|
+
evoEvalSets: input.change.candidate.evalSets,
|
|
373
|
+
candidates: [planCandidate],
|
|
374
|
+
droppedSignals: [],
|
|
375
|
+
conflicts: [],
|
|
376
|
+
privacyCheck: planCandidate.privacyCheck,
|
|
377
|
+
};
|
|
378
|
+
const activation = await activateOkfKnowledgePlan({
|
|
379
|
+
homeDir: input.homeDir,
|
|
380
|
+
plan,
|
|
381
|
+
overwrite: input.change.operation === "update",
|
|
382
|
+
reviewedChangeId: input.change.id,
|
|
383
|
+
});
|
|
384
|
+
const conceptPaths = [...activation.conceptPaths];
|
|
385
|
+
if (input.change.operation === "supersede") {
|
|
386
|
+
if (input.change.base === null) throw new Error("Knowledge supersede requires an active base.");
|
|
387
|
+
const replacementId = input.change.targetPath.replace(/^\/+/u, "").replace(/\.md$/u, "");
|
|
388
|
+
const lifecycle = await supersedeOkfKnowledgeConcept({
|
|
389
|
+
homeDir: input.homeDir,
|
|
390
|
+
oldConceptId: input.change.base.conceptId,
|
|
391
|
+
newConceptId: replacementId,
|
|
392
|
+
now: input.now,
|
|
393
|
+
});
|
|
394
|
+
conceptPaths.push(...lifecycle.conceptPaths);
|
|
395
|
+
}
|
|
396
|
+
return { activation, conceptPaths: [...new Set(conceptPaths)] };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function toPreview(
|
|
400
|
+
change: OkfKnowledgeChangeCandidateV1,
|
|
401
|
+
decision: OkfKnowledgeChangeReviewDecision,
|
|
402
|
+
): OkfKnowledgeChangeDecisionPreview {
|
|
403
|
+
return {
|
|
404
|
+
changeId: change.id,
|
|
405
|
+
operation: change.operation,
|
|
406
|
+
currentState: change.state,
|
|
407
|
+
decision,
|
|
408
|
+
baseRevision: change.base?.revision ?? null,
|
|
409
|
+
candidateRevision: change.candidate.revision,
|
|
410
|
+
runtimeFields: change.diff.runtimeFields,
|
|
411
|
+
willMutateActiveKnowledge: decision === "accept",
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function normalizeDecisionReason(value: string | null | undefined): string | null {
|
|
416
|
+
if (value === undefined || value === null || value.trim() === "") return null;
|
|
417
|
+
const reason = [...value]
|
|
418
|
+
.filter((character) => {
|
|
419
|
+
const code = character.charCodeAt(0);
|
|
420
|
+
return code === 9 || code === 10 || code === 13 || (code >= 32 && code !== 127);
|
|
421
|
+
})
|
|
422
|
+
.join("")
|
|
423
|
+
.trim();
|
|
424
|
+
if (reason.length > 500)
|
|
425
|
+
throw new Error("Knowledge review reason must be 500 characters or less.");
|
|
426
|
+
if (detectSessionMemorySensitivity(reason).classification === "credential") {
|
|
427
|
+
throw new Error("Knowledge review reason contains a credential.");
|
|
428
|
+
}
|
|
429
|
+
return reason;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function resolveNow(value: string | Date | undefined): string {
|
|
433
|
+
const date =
|
|
434
|
+
value instanceof Date ? value : typeof value === "string" ? new Date(value) : new Date();
|
|
435
|
+
if (!Number.isFinite(date.getTime())) throw new Error("Invalid knowledge review timestamp.");
|
|
436
|
+
return date.toISOString();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isBaseRevisionConflict(error: unknown): boolean {
|
|
440
|
+
return (
|
|
441
|
+
typeof error === "object" &&
|
|
442
|
+
error !== null &&
|
|
443
|
+
"code" in error &&
|
|
444
|
+
error.code === "KNOWLEDGE_BASE_REVISION_CONFLICT"
|
|
445
|
+
);
|
|
446
|
+
}
|
|
@@ -62,6 +62,7 @@ export async function processEvolutionTriggers(
|
|
|
62
62
|
pending: 0,
|
|
63
63
|
triggerIds: [],
|
|
64
64
|
batchIds: [],
|
|
65
|
+
pendingChangeIds: [],
|
|
65
66
|
warnings,
|
|
66
67
|
dryRun,
|
|
67
68
|
};
|
|
@@ -108,6 +109,7 @@ export async function processEvolutionTriggers(
|
|
|
108
109
|
...pending.map((trigger) => trigger.id),
|
|
109
110
|
],
|
|
110
111
|
batchIds: [],
|
|
112
|
+
pendingChangeIds: [],
|
|
111
113
|
warnings,
|
|
112
114
|
dryRun,
|
|
113
115
|
};
|
|
@@ -215,6 +217,7 @@ export async function processEvolutionTriggers(
|
|
|
215
217
|
});
|
|
216
218
|
result.consumed += 1;
|
|
217
219
|
result.batchIds.push(batch.id);
|
|
220
|
+
result.pendingChangeIds.push(...curated.activation.okf.pendingChangeIds);
|
|
218
221
|
await updateSegmentTriggers(input.homeDir, [attemptedTrigger], {
|
|
219
222
|
status: "consumed",
|
|
220
223
|
updatedAt: now,
|
|
@@ -303,13 +306,14 @@ export async function processEvolutionTriggers(
|
|
|
303
306
|
now,
|
|
304
307
|
});
|
|
305
308
|
if (lock !== null) await assertEvolutionProcessLockOwned(lock);
|
|
306
|
-
await activateEvolutionDistillationBatch({
|
|
309
|
+
const activation = await activateEvolutionDistillationBatch({
|
|
307
310
|
homeDir: input.homeDir,
|
|
308
311
|
batch,
|
|
309
312
|
overwrite: true,
|
|
310
313
|
});
|
|
311
314
|
result.consumed += triggers.length;
|
|
312
315
|
result.batchIds.push(batch.id);
|
|
316
|
+
result.pendingChangeIds.push(...activation.okf.pendingChangeIds);
|
|
313
317
|
await updateTriggers(input.homeDir, attemptedTriggers, {
|
|
314
318
|
status: "consumed",
|
|
315
319
|
updatedAt: now,
|
package/src/evolution/schema.ts
CHANGED
|
@@ -372,6 +372,30 @@ export interface EvolutionEvosCase {
|
|
|
372
372
|
privacy: EvolutionPrivacyFields;
|
|
373
373
|
}
|
|
374
374
|
|
|
375
|
+
export interface EvolutionRepoProposalCurrentState {
|
|
376
|
+
checkedAt: string;
|
|
377
|
+
headSha: string;
|
|
378
|
+
statusFingerprint: string;
|
|
379
|
+
verdict: "unresolved" | "resolved" | "unknown";
|
|
380
|
+
ownership: "governance" | "implementation" | "not-applicable";
|
|
381
|
+
explicitUserRequest: boolean;
|
|
382
|
+
checkedPaths: string[];
|
|
383
|
+
relatedCommits: string[];
|
|
384
|
+
summary: string;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
export interface EvolutionRepoProposalRecurrence {
|
|
388
|
+
gapKey: string;
|
|
389
|
+
independentSessionCount: number;
|
|
390
|
+
sourceSessionKeys: string[];
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export interface EvolutionRepoProposalSupersession {
|
|
394
|
+
policy: "session-proposal-v2";
|
|
395
|
+
reason: string;
|
|
396
|
+
supersededAt: string;
|
|
397
|
+
}
|
|
398
|
+
|
|
375
399
|
export interface EvolutionRepoProposal {
|
|
376
400
|
schemaVersion: 1;
|
|
377
401
|
id: string;
|
|
@@ -382,10 +406,13 @@ export interface EvolutionRepoProposal {
|
|
|
382
406
|
rationale: string;
|
|
383
407
|
roleTags: string[];
|
|
384
408
|
tags: string[];
|
|
385
|
-
reviewState: "pending" | "accepted" | "rejected" | "deferred" | "applied";
|
|
409
|
+
reviewState: "pending" | "accepted" | "rejected" | "deferred" | "applied" | "superseded";
|
|
386
410
|
reviewStateChangedAt?: string;
|
|
387
411
|
confidence: EvolutionConfidence;
|
|
388
412
|
targetRepoPath: string | null;
|
|
413
|
+
currentState?: EvolutionRepoProposalCurrentState;
|
|
414
|
+
recurrence?: EvolutionRepoProposalRecurrence;
|
|
415
|
+
supersession?: EvolutionRepoProposalSupersession;
|
|
389
416
|
plannedFiles: Array<{
|
|
390
417
|
relativePath: string;
|
|
391
418
|
action: "create" | "update";
|
|
@@ -606,6 +633,7 @@ export interface EvolutionProcessResult {
|
|
|
606
633
|
pending: number;
|
|
607
634
|
triggerIds: string[];
|
|
608
635
|
batchIds: string[];
|
|
636
|
+
pendingChangeIds: string[];
|
|
609
637
|
warnings: string[];
|
|
610
638
|
dryRun: boolean;
|
|
611
639
|
}
|
package/src/evolution/shared.ts
CHANGED
|
@@ -363,6 +363,33 @@ export function createEvolutionRepoProposal(
|
|
|
363
363
|
? {}
|
|
364
364
|
: { proposedChange: sanitizeProposedChange(file.proposedChange) }),
|
|
365
365
|
})),
|
|
366
|
+
...(input.currentState === undefined
|
|
367
|
+
? {}
|
|
368
|
+
: {
|
|
369
|
+
currentState: {
|
|
370
|
+
...input.currentState,
|
|
371
|
+
checkedPaths: uniqueSorted(input.currentState.checkedPaths.map(sanitizeRelativePath)),
|
|
372
|
+
relatedCommits: uniqueSanitizedIds(input.currentState.relatedCommits),
|
|
373
|
+
summary: sanitizeText(input.currentState.summary),
|
|
374
|
+
},
|
|
375
|
+
}),
|
|
376
|
+
...(input.recurrence === undefined
|
|
377
|
+
? {}
|
|
378
|
+
: {
|
|
379
|
+
recurrence: {
|
|
380
|
+
...input.recurrence,
|
|
381
|
+
gapKey: sanitizeStorageId("gapKey", input.recurrence.gapKey),
|
|
382
|
+
sourceSessionKeys: uniqueSanitizedIds(input.recurrence.sourceSessionKeys),
|
|
383
|
+
},
|
|
384
|
+
}),
|
|
385
|
+
...(input.supersession === undefined
|
|
386
|
+
? {}
|
|
387
|
+
: {
|
|
388
|
+
supersession: {
|
|
389
|
+
...input.supersession,
|
|
390
|
+
reason: sanitizeText(input.supersession.reason),
|
|
391
|
+
},
|
|
392
|
+
}),
|
|
366
393
|
...(input.improvementEval === undefined
|
|
367
394
|
? {}
|
|
368
395
|
: { improvementEval: sanitizeEvolutionImprovementEval(input.improvementEval) }),
|
|
@@ -394,6 +421,7 @@ export function validateEvolutionRepoProposal(proposal: EvolutionRepoProposal):
|
|
|
394
421
|
"rejected",
|
|
395
422
|
"deferred",
|
|
396
423
|
"applied",
|
|
424
|
+
"superseded",
|
|
397
425
|
]);
|
|
398
426
|
if (
|
|
399
427
|
proposal.reviewStateChangedAt !== undefined &&
|
|
@@ -406,6 +434,72 @@ export function validateEvolutionRepoProposal(proposal: EvolutionRepoProposal):
|
|
|
406
434
|
throw new Error("Repo proposal proposedChange must not be empty.");
|
|
407
435
|
}
|
|
408
436
|
}
|
|
437
|
+
if (proposal.currentState !== undefined) {
|
|
438
|
+
if (!isRecord(proposal.currentState)) {
|
|
439
|
+
throw new Error("Repo proposal currentState must be an object.");
|
|
440
|
+
}
|
|
441
|
+
assertTimestamp("currentState.checkedAt", proposal.currentState.checkedAt);
|
|
442
|
+
if (!/^[a-f0-9]{40}([a-f0-9]{24})?$/.test(proposal.currentState.headSha)) {
|
|
443
|
+
throw new Error("Repo proposal currentState.headSha must be a Git object id.");
|
|
444
|
+
}
|
|
445
|
+
if (!/^[a-f0-9]{64}$/.test(proposal.currentState.statusFingerprint)) {
|
|
446
|
+
throw new Error("Repo proposal currentState.statusFingerprint must be a SHA-256 digest.");
|
|
447
|
+
}
|
|
448
|
+
assertEnum("currentState.verdict", proposal.currentState.verdict, [
|
|
449
|
+
"unresolved",
|
|
450
|
+
"resolved",
|
|
451
|
+
"unknown",
|
|
452
|
+
]);
|
|
453
|
+
assertEnum("currentState.ownership", proposal.currentState.ownership, [
|
|
454
|
+
"governance",
|
|
455
|
+
"implementation",
|
|
456
|
+
"not-applicable",
|
|
457
|
+
]);
|
|
458
|
+
if (typeof proposal.currentState.explicitUserRequest !== "boolean") {
|
|
459
|
+
throw new Error("Repo proposal currentState.explicitUserRequest must be a boolean.");
|
|
460
|
+
}
|
|
461
|
+
assertStringArray("currentState.checkedPaths", proposal.currentState.checkedPaths);
|
|
462
|
+
assertStringArray("currentState.relatedCommits", proposal.currentState.relatedCommits);
|
|
463
|
+
for (const commit of proposal.currentState.relatedCommits) {
|
|
464
|
+
if (!/^[a-f0-9]{7,64}$/.test(commit)) {
|
|
465
|
+
throw new Error("Repo proposal currentState.relatedCommits must contain Git object ids.");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
assertString("currentState.summary", proposal.currentState.summary);
|
|
469
|
+
}
|
|
470
|
+
if (proposal.recurrence !== undefined) {
|
|
471
|
+
if (!isRecord(proposal.recurrence)) {
|
|
472
|
+
throw new Error("Repo proposal recurrence must be an object.");
|
|
473
|
+
}
|
|
474
|
+
assertString("recurrence.gapKey", proposal.recurrence.gapKey);
|
|
475
|
+
assertStringArray("recurrence.sourceSessionKeys", proposal.recurrence.sourceSessionKeys);
|
|
476
|
+
if (
|
|
477
|
+
!Number.isInteger(proposal.recurrence.independentSessionCount) ||
|
|
478
|
+
proposal.recurrence.independentSessionCount < 2 ||
|
|
479
|
+
proposal.recurrence.independentSessionCount !==
|
|
480
|
+
new Set(proposal.recurrence.sourceSessionKeys).size
|
|
481
|
+
) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
"Repo proposal recurrence must contain at least two distinct source sessions.",
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (proposal.supersession !== undefined) {
|
|
488
|
+
if (!isRecord(proposal.supersession)) {
|
|
489
|
+
throw new Error("Repo proposal supersession must be an object.");
|
|
490
|
+
}
|
|
491
|
+
if (proposal.supersession.policy !== "session-proposal-v2") {
|
|
492
|
+
throw new Error("Repo proposal supersession policy is invalid.");
|
|
493
|
+
}
|
|
494
|
+
assertString("supersession.reason", proposal.supersession.reason);
|
|
495
|
+
assertTimestamp("supersession.supersededAt", proposal.supersession.supersededAt);
|
|
496
|
+
}
|
|
497
|
+
if (proposal.reviewState === "superseded" && proposal.supersession === undefined) {
|
|
498
|
+
throw new Error("A superseded repo proposal must include supersession metadata.");
|
|
499
|
+
}
|
|
500
|
+
if (proposal.reviewState !== "superseded" && proposal.supersession !== undefined) {
|
|
501
|
+
throw new Error("Repo proposal supersession metadata requires superseded review state.");
|
|
502
|
+
}
|
|
409
503
|
if (proposal.lastDecision !== undefined) {
|
|
410
504
|
assertEnum("lastDecision.state", proposal.lastDecision.state, [
|
|
411
505
|
"accepted",
|
|
@@ -601,6 +695,20 @@ function assertNullableTimestamp(field: string, value: string | null): void {
|
|
|
601
695
|
}
|
|
602
696
|
}
|
|
603
697
|
|
|
698
|
+
function assertTimestamp(field: string, value: unknown): asserts value is string {
|
|
699
|
+
assertString(field, value);
|
|
700
|
+
if (Number.isNaN(new Date(value).getTime())) {
|
|
701
|
+
throw new Error(`${field} must be a valid timestamp.`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function assertStringArray(field: string, value: unknown): asserts value is string[] {
|
|
706
|
+
if (!Array.isArray(value)) throw new Error(`${field} must be an array.`);
|
|
707
|
+
for (const [index, item] of value.entries()) {
|
|
708
|
+
assertString(`${field}[${index}]`, item);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
604
712
|
function isNonNegativeInteger(value: unknown): value is number {
|
|
605
713
|
return typeof value === "number" && Number.isInteger(value) && value >= 0;
|
|
606
714
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,10 @@ export * as evolutionEvidence from "./evolution/evidence/index.ts";
|
|
|
10
10
|
export * from "./evolution/evidence/session-memory/index.ts";
|
|
11
11
|
export * from "./evolution/knowledge/index.ts";
|
|
12
12
|
export * as evolutionKnowledge from "./evolution/knowledge/index.ts";
|
|
13
|
+
export * from "./evolution/knowledge/changes.ts";
|
|
14
|
+
export * from "./evolution/knowledge/change-store.ts";
|
|
15
|
+
export * from "./evolution/knowledge/freshness.ts";
|
|
16
|
+
export * from "./evolution/knowledge/review.ts";
|
|
13
17
|
export * as evolutionProcessor from "./evolution/processor/index.ts";
|
|
14
18
|
export * from "./evolution/review/index.ts";
|
|
15
19
|
export * as evolutionReview from "./evolution/review/index.ts";
|