@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,453 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type {
|
|
3
|
+
OkfKnowledgeConcept,
|
|
4
|
+
OkfKnowledgeLifecycleStatus,
|
|
5
|
+
OkfKnowledgePlanCandidate,
|
|
6
|
+
} from "./index.ts";
|
|
7
|
+
|
|
8
|
+
export const OKF_KNOWLEDGE_RENDER_CONTRACT_VERSION = 2;
|
|
9
|
+
|
|
10
|
+
export type OkfKnowledgeChangeClassification =
|
|
11
|
+
| "create"
|
|
12
|
+
| "refresh"
|
|
13
|
+
| "update"
|
|
14
|
+
| "supersede"
|
|
15
|
+
| "revoke"
|
|
16
|
+
| "no_write";
|
|
17
|
+
|
|
18
|
+
export type OkfKnowledgeChangeOperation = "create" | "update" | "supersede" | "revoke";
|
|
19
|
+
|
|
20
|
+
export type OkfKnowledgeChangeState =
|
|
21
|
+
| "pending"
|
|
22
|
+
| "accepted"
|
|
23
|
+
| "rejected"
|
|
24
|
+
| "deferred"
|
|
25
|
+
| "applied"
|
|
26
|
+
| "superseded";
|
|
27
|
+
|
|
28
|
+
export interface OkfKnowledgeRuntimeProjectionV1 {
|
|
29
|
+
renderContractVersion: number;
|
|
30
|
+
type: string;
|
|
31
|
+
title: string;
|
|
32
|
+
description: string;
|
|
33
|
+
stableKey: string;
|
|
34
|
+
claim: string;
|
|
35
|
+
summary: string;
|
|
36
|
+
appliesWhen: string[];
|
|
37
|
+
howToApply: string;
|
|
38
|
+
guidance: string[];
|
|
39
|
+
antiCriteria: string[];
|
|
40
|
+
verification: string[];
|
|
41
|
+
citations: string[];
|
|
42
|
+
scopes: {
|
|
43
|
+
repoTags: string[];
|
|
44
|
+
roleTags: string[];
|
|
45
|
+
workflowTags: string[];
|
|
46
|
+
pathScopes: string[];
|
|
47
|
+
};
|
|
48
|
+
relatedConceptLinks: string[];
|
|
49
|
+
lifecycle: {
|
|
50
|
+
status: OkfKnowledgeLifecycleStatus;
|
|
51
|
+
supersedes: string[];
|
|
52
|
+
supersededBy: string | null;
|
|
53
|
+
revoked: boolean;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface OkfKnowledgeRuntimeDiffV1 {
|
|
58
|
+
runtimeFields: string[];
|
|
59
|
+
metadataFields: string[];
|
|
60
|
+
summary: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface OkfKnowledgeChangeClassificationResult {
|
|
64
|
+
classification: OkfKnowledgeChangeClassification;
|
|
65
|
+
baseRevision: string | null;
|
|
66
|
+
candidateRevision: string;
|
|
67
|
+
baseProjection: OkfKnowledgeRuntimeProjectionV1 | null;
|
|
68
|
+
candidateProjection: OkfKnowledgeRuntimeProjectionV1;
|
|
69
|
+
diff: OkfKnowledgeRuntimeDiffV1;
|
|
70
|
+
reason: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createOkfKnowledgeRuntimeProjectionFromCandidate(
|
|
74
|
+
candidate: OkfKnowledgePlanCandidate,
|
|
75
|
+
): OkfKnowledgeRuntimeProjectionV1 {
|
|
76
|
+
return normalizeRuntimeProjection({
|
|
77
|
+
renderContractVersion: OKF_KNOWLEDGE_RENDER_CONTRACT_VERSION,
|
|
78
|
+
type: candidate.okfType,
|
|
79
|
+
title: candidate.title,
|
|
80
|
+
description: candidate.description,
|
|
81
|
+
stableKey: candidate.stableKey,
|
|
82
|
+
claim: candidate.claim,
|
|
83
|
+
summary: candidate.bodySections.summary,
|
|
84
|
+
appliesWhen: candidate.bodySections.appliesWhen,
|
|
85
|
+
howToApply: candidate.howToApply,
|
|
86
|
+
guidance: candidate.bodySections.guidance,
|
|
87
|
+
antiCriteria: uniqueStrings([
|
|
88
|
+
...candidate.antiCriteria,
|
|
89
|
+
...candidate.bodySections.antiCriteria,
|
|
90
|
+
]),
|
|
91
|
+
verification: candidate.bodySections.verification,
|
|
92
|
+
citations: candidate.bodySections.citations,
|
|
93
|
+
scopes: {
|
|
94
|
+
repoTags: candidate.repoTags,
|
|
95
|
+
roleTags: candidate.roleTags,
|
|
96
|
+
workflowTags: candidate.workflowTags,
|
|
97
|
+
pathScopes: candidate.pathScopes,
|
|
98
|
+
},
|
|
99
|
+
relatedConceptLinks: candidate.relatedConceptLinks,
|
|
100
|
+
lifecycle: {
|
|
101
|
+
status: candidate.decision === "revoke" ? "revoked" : "active",
|
|
102
|
+
supersedes: [],
|
|
103
|
+
supersededBy: null,
|
|
104
|
+
revoked: candidate.decision === "revoke",
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createOkfKnowledgeRuntimeProjectionFromConcept(
|
|
110
|
+
concept: OkfKnowledgeConcept,
|
|
111
|
+
): OkfKnowledgeRuntimeProjectionV1 {
|
|
112
|
+
const sections = parseMarkdownSections(concept.body);
|
|
113
|
+
const summary = readSectionText(sections, "Summary");
|
|
114
|
+
const guidance = readSectionList(sections, "Guidance");
|
|
115
|
+
return normalizeRuntimeProjection({
|
|
116
|
+
renderContractVersion: OKF_KNOWLEDGE_RENDER_CONTRACT_VERSION,
|
|
117
|
+
type: concept.type,
|
|
118
|
+
title: concept.title,
|
|
119
|
+
description: concept.description,
|
|
120
|
+
stableKey: concept.stableKey,
|
|
121
|
+
claim: readSectionText(sections, "Claim") || summary,
|
|
122
|
+
summary,
|
|
123
|
+
appliesWhen: readSectionList(sections, "Applies When"),
|
|
124
|
+
howToApply: readSectionText(sections, "How to Apply") || guidance[0] || "",
|
|
125
|
+
guidance,
|
|
126
|
+
antiCriteria: readSectionList(sections, "Anti-Criteria"),
|
|
127
|
+
verification: readSectionList(sections, "Verification").filter(
|
|
128
|
+
(item) => item !== "No verification command is stored in this concept.",
|
|
129
|
+
),
|
|
130
|
+
citations: readSectionList(sections, "Citations").filter(
|
|
131
|
+
(item) => item !== "No external citations stored.",
|
|
132
|
+
),
|
|
133
|
+
scopes: {
|
|
134
|
+
repoTags: concept.repoTags,
|
|
135
|
+
roleTags: concept.roleTags,
|
|
136
|
+
workflowTags: concept.workflowTags,
|
|
137
|
+
pathScopes: concept.pathScopes,
|
|
138
|
+
},
|
|
139
|
+
relatedConceptLinks: readSectionList(sections, "Related Concepts").filter(
|
|
140
|
+
(item) => item !== "No related concepts yet.",
|
|
141
|
+
),
|
|
142
|
+
lifecycle: {
|
|
143
|
+
status: concept.lifecycle.status,
|
|
144
|
+
supersedes: concept.lifecycle.supersedes,
|
|
145
|
+
supersededBy: concept.lifecycle.supersededBy,
|
|
146
|
+
revoked: concept.lifecycle.status === "revoked" || concept.lifecycle.revokedAt !== null,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function createOkfKnowledgeRevision(projection: OkfKnowledgeRuntimeProjectionV1): string {
|
|
152
|
+
return `knowledge-rev-${createHash("sha256")
|
|
153
|
+
.update(stableJsonStringify(normalizeRuntimeProjection(projection)))
|
|
154
|
+
.digest("hex")
|
|
155
|
+
.slice(0, 24)}`;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function diffOkfKnowledgeRuntimeProjections(
|
|
159
|
+
base: OkfKnowledgeRuntimeProjectionV1 | null,
|
|
160
|
+
candidate: OkfKnowledgeRuntimeProjectionV1,
|
|
161
|
+
metadataFields: string[] = [],
|
|
162
|
+
): OkfKnowledgeRuntimeDiffV1 {
|
|
163
|
+
const runtimeFields = base === null ? ["concept"] : listChangedProjectionFields(base, candidate);
|
|
164
|
+
const normalizedMetadataFields = uniqueStrings(metadataFields).sort();
|
|
165
|
+
const summary =
|
|
166
|
+
runtimeFields.length === 0
|
|
167
|
+
? normalizedMetadataFields.length === 0
|
|
168
|
+
? "No effective knowledge change."
|
|
169
|
+
: `Metadata refresh: ${normalizedMetadataFields.join(", ")}.`
|
|
170
|
+
: base === null
|
|
171
|
+
? "Create a new active knowledge concept."
|
|
172
|
+
: `Runtime knowledge changed: ${runtimeFields.join(", ")}.`;
|
|
173
|
+
return {
|
|
174
|
+
runtimeFields,
|
|
175
|
+
metadataFields: normalizedMetadataFields,
|
|
176
|
+
summary,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export function classifyOkfKnowledgeCandidate(input: {
|
|
181
|
+
candidate: OkfKnowledgePlanCandidate;
|
|
182
|
+
existingConcept?: OkfKnowledgeConcept | null;
|
|
183
|
+
metadataFields?: string[];
|
|
184
|
+
}): OkfKnowledgeChangeClassificationResult {
|
|
185
|
+
const existing = input.existingConcept ?? null;
|
|
186
|
+
const baseProjection =
|
|
187
|
+
existing === null ? null : createOkfKnowledgeRuntimeProjectionFromConcept(existing);
|
|
188
|
+
let candidateProjection = createOkfKnowledgeRuntimeProjectionFromCandidate(input.candidate);
|
|
189
|
+
if (baseProjection !== null && existing?.stableKey === input.candidate.stableKey) {
|
|
190
|
+
candidateProjection =
|
|
191
|
+
input.candidate.decision === "revoke"
|
|
192
|
+
? normalizeRuntimeProjection({
|
|
193
|
+
...baseProjection,
|
|
194
|
+
lifecycle: {
|
|
195
|
+
...baseProjection.lifecycle,
|
|
196
|
+
status: "revoked",
|
|
197
|
+
revoked: true,
|
|
198
|
+
},
|
|
199
|
+
})
|
|
200
|
+
: {
|
|
201
|
+
...candidateProjection,
|
|
202
|
+
lifecycle: baseProjection.lifecycle,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const candidateRevision = createOkfKnowledgeRevision(candidateProjection);
|
|
206
|
+
const baseRevision = baseProjection === null ? null : createOkfKnowledgeRevision(baseProjection);
|
|
207
|
+
const diff = diffOkfKnowledgeRuntimeProjections(
|
|
208
|
+
baseProjection,
|
|
209
|
+
candidateProjection,
|
|
210
|
+
input.metadataFields ?? ["evidenceRefs", "verifiedAt"],
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (input.candidate.decision === "no_write" || input.candidate.decision === "skip") {
|
|
214
|
+
return {
|
|
215
|
+
classification: "no_write",
|
|
216
|
+
baseRevision,
|
|
217
|
+
candidateRevision,
|
|
218
|
+
baseProjection,
|
|
219
|
+
candidateProjection,
|
|
220
|
+
diff,
|
|
221
|
+
reason: input.candidate.decisionReason || "Candidate is not eligible for a knowledge write.",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (input.candidate.decision === "revoke" && existing === null) {
|
|
225
|
+
return {
|
|
226
|
+
classification: "no_write",
|
|
227
|
+
baseRevision,
|
|
228
|
+
candidateRevision,
|
|
229
|
+
baseProjection,
|
|
230
|
+
candidateProjection,
|
|
231
|
+
diff,
|
|
232
|
+
reason: "No active concept uses this stable key, so there is nothing to revoke.",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (existing === null) {
|
|
236
|
+
return {
|
|
237
|
+
classification: "create",
|
|
238
|
+
baseRevision,
|
|
239
|
+
candidateRevision,
|
|
240
|
+
baseProjection,
|
|
241
|
+
candidateProjection,
|
|
242
|
+
diff,
|
|
243
|
+
reason: "No active concept uses this stable key.",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
if (existing.stableKey !== input.candidate.stableKey) {
|
|
247
|
+
return {
|
|
248
|
+
classification: "no_write",
|
|
249
|
+
baseRevision,
|
|
250
|
+
candidateRevision,
|
|
251
|
+
baseProjection,
|
|
252
|
+
candidateProjection,
|
|
253
|
+
diff,
|
|
254
|
+
reason: "Existing concept identity does not match the candidate stable key.",
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
if (input.candidate.decision === "revoke") {
|
|
258
|
+
return {
|
|
259
|
+
classification: "revoke",
|
|
260
|
+
baseRevision,
|
|
261
|
+
candidateRevision,
|
|
262
|
+
baseProjection,
|
|
263
|
+
candidateProjection,
|
|
264
|
+
diff,
|
|
265
|
+
reason: input.candidate.decisionReason,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
if (shouldSupersede(existing, input.candidate)) {
|
|
269
|
+
return {
|
|
270
|
+
classification: "supersede",
|
|
271
|
+
baseRevision,
|
|
272
|
+
candidateRevision,
|
|
273
|
+
baseProjection,
|
|
274
|
+
candidateProjection,
|
|
275
|
+
diff,
|
|
276
|
+
reason: "The candidate changes the concept identity, canonical path, or incompatible scope.",
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (diff.runtimeFields.length === 0) {
|
|
280
|
+
return {
|
|
281
|
+
classification: "refresh",
|
|
282
|
+
baseRevision,
|
|
283
|
+
candidateRevision,
|
|
284
|
+
baseProjection,
|
|
285
|
+
candidateProjection,
|
|
286
|
+
diff,
|
|
287
|
+
reason: "Runtime projection is unchanged; only verification or provenance metadata changed.",
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
classification: "update",
|
|
292
|
+
baseRevision,
|
|
293
|
+
candidateRevision,
|
|
294
|
+
baseProjection,
|
|
295
|
+
candidateProjection,
|
|
296
|
+
diff,
|
|
297
|
+
reason: diff.summary,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function shouldSupersede(
|
|
302
|
+
concept: OkfKnowledgeConcept,
|
|
303
|
+
candidate: OkfKnowledgePlanCandidate,
|
|
304
|
+
): boolean {
|
|
305
|
+
const existingTargetPath = concept.sourceLink.replace(/^\/+/u, "");
|
|
306
|
+
const candidateTargetPath = candidate.targetPath.replace(/^\/+/u, "");
|
|
307
|
+
if (existingTargetPath !== candidateTargetPath) return true;
|
|
308
|
+
if (normalizeText(concept.type) !== normalizeText(candidate.okfType)) return true;
|
|
309
|
+
return (
|
|
310
|
+
scopesAreIncompatible(concept.repoTags, candidate.repoTags) ||
|
|
311
|
+
scopesAreIncompatible(concept.roleTags, candidate.roleTags) ||
|
|
312
|
+
scopesAreIncompatible(concept.workflowTags, candidate.workflowTags)
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function scopesAreIncompatible(left: string[], right: string[]): boolean {
|
|
317
|
+
if (left.length === 0 || right.length === 0) return false;
|
|
318
|
+
const normalizedRight = new Set(right.map(normalizeText));
|
|
319
|
+
return !left.some((value) => normalizedRight.has(normalizeText(value)));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function normalizeRuntimeProjection(
|
|
323
|
+
value: OkfKnowledgeRuntimeProjectionV1,
|
|
324
|
+
): OkfKnowledgeRuntimeProjectionV1 {
|
|
325
|
+
return {
|
|
326
|
+
renderContractVersion: value.renderContractVersion,
|
|
327
|
+
type: normalizeText(value.type),
|
|
328
|
+
title: normalizeText(value.title),
|
|
329
|
+
description: normalizeText(value.description),
|
|
330
|
+
stableKey: normalizeText(value.stableKey),
|
|
331
|
+
claim: normalizeMarkdown(value.claim),
|
|
332
|
+
summary: normalizeMarkdown(value.summary),
|
|
333
|
+
appliesWhen: normalizeArray(value.appliesWhen),
|
|
334
|
+
howToApply: normalizeMarkdown(value.howToApply),
|
|
335
|
+
guidance: normalizeArray(value.guidance),
|
|
336
|
+
antiCriteria: normalizeArray(value.antiCriteria),
|
|
337
|
+
verification: normalizeArray(value.verification),
|
|
338
|
+
citations: normalizeArray(value.citations),
|
|
339
|
+
scopes: {
|
|
340
|
+
repoTags: normalizeArray(value.scopes.repoTags),
|
|
341
|
+
roleTags: normalizeArray(value.scopes.roleTags),
|
|
342
|
+
workflowTags: normalizeArray(value.scopes.workflowTags),
|
|
343
|
+
pathScopes: normalizeArray(value.scopes.pathScopes),
|
|
344
|
+
},
|
|
345
|
+
relatedConceptLinks: normalizeArray(value.relatedConceptLinks),
|
|
346
|
+
lifecycle: {
|
|
347
|
+
status: value.lifecycle.status,
|
|
348
|
+
supersedes: normalizeArray(value.lifecycle.supersedes),
|
|
349
|
+
supersededBy:
|
|
350
|
+
value.lifecycle.supersededBy === null ? null : normalizeText(value.lifecycle.supersededBy),
|
|
351
|
+
revoked: value.lifecycle.revoked,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function listChangedProjectionFields(
|
|
357
|
+
base: OkfKnowledgeRuntimeProjectionV1,
|
|
358
|
+
candidate: OkfKnowledgeRuntimeProjectionV1,
|
|
359
|
+
): string[] {
|
|
360
|
+
const fields: Array<[string, unknown, unknown]> = [
|
|
361
|
+
["type", base.type, candidate.type],
|
|
362
|
+
["title", base.title, candidate.title],
|
|
363
|
+
["description", base.description, candidate.description],
|
|
364
|
+
["stableKey", base.stableKey, candidate.stableKey],
|
|
365
|
+
["claim", base.claim, candidate.claim],
|
|
366
|
+
["summary", base.summary, candidate.summary],
|
|
367
|
+
["appliesWhen", base.appliesWhen, candidate.appliesWhen],
|
|
368
|
+
["howToApply", base.howToApply, candidate.howToApply],
|
|
369
|
+
["guidance", base.guidance, candidate.guidance],
|
|
370
|
+
["antiCriteria", base.antiCriteria, candidate.antiCriteria],
|
|
371
|
+
["verification", base.verification, candidate.verification],
|
|
372
|
+
["citations", base.citations, candidate.citations],
|
|
373
|
+
["scopes.repoTags", base.scopes.repoTags, candidate.scopes.repoTags],
|
|
374
|
+
["scopes.roleTags", base.scopes.roleTags, candidate.scopes.roleTags],
|
|
375
|
+
["scopes.workflowTags", base.scopes.workflowTags, candidate.scopes.workflowTags],
|
|
376
|
+
["scopes.pathScopes", base.scopes.pathScopes, candidate.scopes.pathScopes],
|
|
377
|
+
["relatedConceptLinks", base.relatedConceptLinks, candidate.relatedConceptLinks],
|
|
378
|
+
["lifecycle", base.lifecycle, candidate.lifecycle],
|
|
379
|
+
];
|
|
380
|
+
return fields
|
|
381
|
+
.filter(([, left, right]) => stableJsonStringify(left) !== stableJsonStringify(right))
|
|
382
|
+
.map(([field]) => field);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function parseMarkdownSections(body: string): Map<string, string> {
|
|
386
|
+
const sections = new Map<string, string>();
|
|
387
|
+
let heading = "";
|
|
388
|
+
let lines: string[] = [];
|
|
389
|
+
const flush = () => {
|
|
390
|
+
if (heading !== "") sections.set(heading, lines.join("\n").trim());
|
|
391
|
+
};
|
|
392
|
+
for (const line of body.replace(/\r\n?/gu, "\n").split("\n")) {
|
|
393
|
+
const match = line.match(/^#\s+(.+?)\s*$/u);
|
|
394
|
+
if (match?.[1] !== undefined) {
|
|
395
|
+
flush();
|
|
396
|
+
heading = match[1];
|
|
397
|
+
lines = [];
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
lines.push(line);
|
|
401
|
+
}
|
|
402
|
+
flush();
|
|
403
|
+
return sections;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function readSectionText(sections: Map<string, string>, heading: string): string {
|
|
407
|
+
return normalizeMarkdown(sections.get(heading) ?? "");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function readSectionList(sections: Map<string, string>, heading: string): string[] {
|
|
411
|
+
const section = sections.get(heading) ?? "";
|
|
412
|
+
const items = section
|
|
413
|
+
.split("\n")
|
|
414
|
+
.map((line) => line.trim())
|
|
415
|
+
.filter((line) => /^[-*]\s+/u.test(line))
|
|
416
|
+
.map((line) => line.replace(/^[-*]\s+/u, ""));
|
|
417
|
+
if (items.length > 0) return normalizeArray(items);
|
|
418
|
+
const text = normalizeMarkdown(section);
|
|
419
|
+
return text === "" ? [] : [text];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function normalizeText(value: string): string {
|
|
423
|
+
return value.replace(/\s+/gu, " ").trim();
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function normalizeMarkdown(value: string): string {
|
|
427
|
+
return value
|
|
428
|
+
.replace(/\r\n?/gu, "\n")
|
|
429
|
+
.split("\n")
|
|
430
|
+
.map((line) => line.trimEnd())
|
|
431
|
+
.join("\n")
|
|
432
|
+
.replace(/\n{3,}/gu, "\n\n")
|
|
433
|
+
.trim();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function normalizeArray(values: string[]): string[] {
|
|
437
|
+
return uniqueStrings(values.map(normalizeMarkdown).filter((value) => value !== "")).sort();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function uniqueStrings(values: string[]): string[] {
|
|
441
|
+
return [...new Set(values)];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function stableJsonStringify(value: unknown): string {
|
|
445
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
446
|
+
if (Array.isArray(value)) return `[${value.map(stableJsonStringify).join(",")}]`;
|
|
447
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
448
|
+
.filter(([, child]) => child !== undefined)
|
|
449
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
450
|
+
return `{${entries
|
|
451
|
+
.map(([key, child]) => `${JSON.stringify(key)}:${stableJsonStringify(child)}`)
|
|
452
|
+
.join(",")}}`;
|
|
453
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { OkfKnowledgeConcept, OkfKnowledgePlanCandidate } from "./index.ts";
|
|
2
|
+
|
|
3
|
+
export type OkfKnowledgeFreshness = "verified" | "review-due" | "unknown" | "stale";
|
|
4
|
+
|
|
5
|
+
export interface OkfKnowledgeVerificationSnapshotV1 {
|
|
6
|
+
schemaVersion: 1;
|
|
7
|
+
verifiedAt: string;
|
|
8
|
+
evidenceRefs: string[];
|
|
9
|
+
repository: null | {
|
|
10
|
+
projectKey: string;
|
|
11
|
+
head: string | null;
|
|
12
|
+
checkedPaths: Array<{
|
|
13
|
+
path: string;
|
|
14
|
+
sha256: string;
|
|
15
|
+
}>;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface OkfKnowledgeFreshnessResult {
|
|
20
|
+
freshness: OkfKnowledgeFreshness;
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function deriveOkfKnowledgeFreshness(input: {
|
|
25
|
+
concept: OkfKnowledgeConcept;
|
|
26
|
+
now?: string | Date;
|
|
27
|
+
repositoryFingerprintChanged?: boolean;
|
|
28
|
+
verifiedContradiction?: boolean;
|
|
29
|
+
}): OkfKnowledgeFreshnessResult {
|
|
30
|
+
const now = normalizeNow(input.now);
|
|
31
|
+
const concept = input.concept;
|
|
32
|
+
if (
|
|
33
|
+
input.verifiedContradiction === true ||
|
|
34
|
+
concept.lifecycle.status === "stale" ||
|
|
35
|
+
concept.lifecycle.status === "deprecated" ||
|
|
36
|
+
concept.lifecycle.status === "revoked" ||
|
|
37
|
+
concept.lifecycle.status === "superseded" ||
|
|
38
|
+
concept.reviewState === "stale" ||
|
|
39
|
+
concept.reviewState === "deprecated" ||
|
|
40
|
+
concept.reviewState === "revoked" ||
|
|
41
|
+
concept.reviewState === "superseded" ||
|
|
42
|
+
isDue(concept.lifecycle.staleAfter, now)
|
|
43
|
+
) {
|
|
44
|
+
return {
|
|
45
|
+
freshness: "stale",
|
|
46
|
+
reason:
|
|
47
|
+
input.verifiedContradiction === true
|
|
48
|
+
? "Verified evidence contradicts the active claim."
|
|
49
|
+
: "Lifecycle status or staleAfter excludes this concept from new runtime context.",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (concept.verificationSnapshot === null) {
|
|
53
|
+
return {
|
|
54
|
+
freshness: "unknown",
|
|
55
|
+
reason: "No verifiable knowledge snapshot is stored for this legacy concept.",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (input.repositoryFingerprintChanged === true || isDue(concept.lifecycle.reviewAfter, now)) {
|
|
59
|
+
return {
|
|
60
|
+
freshness: "review-due",
|
|
61
|
+
reason:
|
|
62
|
+
input.repositoryFingerprintChanged === true
|
|
63
|
+
? "A checked repository path changed after the last verification."
|
|
64
|
+
: `Review was due at ${concept.lifecycle.reviewAfter}.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
freshness: "verified",
|
|
69
|
+
reason: `Verified at ${concept.verificationSnapshot.verifiedAt}.`,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createOkfKnowledgeVerificationSnapshot(input: {
|
|
74
|
+
candidate: OkfKnowledgePlanCandidate;
|
|
75
|
+
verifiedAt: string;
|
|
76
|
+
projectKey: string;
|
|
77
|
+
}): OkfKnowledgeVerificationSnapshotV1 | null {
|
|
78
|
+
const hasVerification =
|
|
79
|
+
input.candidate.bodySections.verification.length > 0 ||
|
|
80
|
+
input.candidate.verificationNotApplicableReason !== undefined;
|
|
81
|
+
if (!hasVerification) return null;
|
|
82
|
+
const verifiedAt = normalizeIso(input.verifiedAt);
|
|
83
|
+
if (verifiedAt === null) return null;
|
|
84
|
+
return {
|
|
85
|
+
schemaVersion: 1,
|
|
86
|
+
verifiedAt,
|
|
87
|
+
evidenceRefs: [...new Set(input.candidate.evidenceRefs)].sort(),
|
|
88
|
+
repository: null,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function normalizeNow(value: string | Date | undefined): Date {
|
|
93
|
+
if (value instanceof Date && Number.isFinite(value.getTime())) return value;
|
|
94
|
+
if (typeof value === "string") {
|
|
95
|
+
const normalized = normalizeIso(value);
|
|
96
|
+
if (normalized !== null) return new Date(normalized);
|
|
97
|
+
}
|
|
98
|
+
return new Date();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isDue(value: string, now: Date): boolean {
|
|
102
|
+
const normalized = normalizeIso(value);
|
|
103
|
+
return normalized !== null && Date.parse(normalized) <= now.getTime();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeIso(value: string): string | null {
|
|
107
|
+
const time = Date.parse(value);
|
|
108
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : null;
|
|
109
|
+
}
|