@hadooppei/hwcode 1.0.16 → 1.0.19
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/.pi/dist/lib/knowledge/evidence.js +294 -0
- package/.pi/dist/lib/knowledge/extractor.js +44 -5
- package/.pi/dist/lib/knowledge/review-worker.js +3 -2
- package/.pi/dist/lib/knowledge/session-scanner.js +10 -1
- package/.pi/dist/lib/knowledge/store.js +215 -69
- package/.pi/dist/lib/runtime/defaults.js +4 -0
- package/.pi/extensions/knowledge.ts +65 -5
- package/.pi/lib/knowledge/evidence.ts +308 -0
- package/.pi/lib/knowledge/extractor.ts +51 -4
- package/.pi/lib/knowledge/review-worker.ts +3 -2
- package/.pi/lib/knowledge/session-scanner.ts +10 -1
- package/.pi/lib/knowledge/store.ts +220 -69
- package/.pi/lib/knowledge/types.ts +59 -2
- package/.pi/lib/runtime/defaults.ts +4 -0
- package/package.json +1 -1
|
@@ -10,8 +10,9 @@ import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.ts";
|
|
|
10
10
|
import { userRuntimePaths } from "../runtime/paths.ts";
|
|
11
11
|
import { compactKnowledgeSummary, compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.ts";
|
|
12
12
|
import type {
|
|
13
|
-
KnowledgeCandidate, KnowledgeCatalog, KnowledgeCatalogEntry,
|
|
14
|
-
|
|
13
|
+
KnowledgeCandidate, KnowledgeCatalog, KnowledgeCatalogEntry, KnowledgeClaim, KnowledgeEpisodeDecision,
|
|
14
|
+
KnowledgeEvidence, KnowledgeManifest, KnowledgeReviewCursor, KnowledgeReviewTask, KnowledgeScope,
|
|
15
|
+
KnowledgeSkipReason, KnowledgeSnapshot, KnowledgeTrack,
|
|
15
16
|
PersistKnowledgeResult,
|
|
16
17
|
} from "./types.ts";
|
|
17
18
|
|
|
@@ -21,10 +22,6 @@ const SAFE_GENERATION_RE = /^[0-9TZ-]+-[a-f0-9]{12}$/u;
|
|
|
21
22
|
const PROJECT_SCOPE_RE = /^project:[a-f0-9]{16}$/u;
|
|
22
23
|
const UNVERIFIED_CLAIM_RE = /(?:待验证|尚未(?:验证|确认)|未经(?:验证|确认)|未(?:经)?证实|推测|猜测|疑似|可能是|可能由于|可能导致|或许|unverified|unconfirmed|speculat(?:e|ive|ion)|hypothes(?:is|ize)|suspect(?:ed|ion)?|possibly|probably)/iu;
|
|
23
24
|
const VOLATILE_ARGUMENT_RE = /--(?:namespace|name|server_name|security_group_id|subnet_id|vpc_id|image_id|project_id)(?:\.\d+)?=(?!<[^>]+>|\$?\{)[^\s`"']+/iu;
|
|
24
|
-
const DISTINCTIVE_COMMON_TERMS = new Set([
|
|
25
|
-
"cloud", "huawei", "huaweicloud", "hcloud", "topic", "rule", "sop", "project", "region", "create",
|
|
26
|
-
"deploy", "deployment", "service", "workflow", "cn", "south",
|
|
27
|
-
]);
|
|
28
25
|
|
|
29
26
|
export class KnowledgeCommitBusyError extends Error {}
|
|
30
27
|
|
|
@@ -121,47 +118,8 @@ function normalizeSlug(value: string): string {
|
|
|
121
118
|
}
|
|
122
119
|
|
|
123
120
|
function normalizedFingerprint(candidate: KnowledgeCandidate): string {
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function comparisonTerms(value: string): Set<string> {
|
|
128
|
-
return new Set((value.normalize("NFKC").toLowerCase().match(/\p{Script=Han}+|[\p{L}\p{N}]+/gu) ?? [])
|
|
129
|
-
.filter((term) => term.length >= 2));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function containment(left: Set<string>, right: Set<string>): number {
|
|
133
|
-
const minimum = Math.min(left.size, right.size);
|
|
134
|
-
if (minimum === 0) return 0;
|
|
135
|
-
let shared = 0;
|
|
136
|
-
for (const term of left) if (right.has(term)) shared++;
|
|
137
|
-
return shared / minimum;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function semanticDuplicateScore(candidate: KnowledgeCandidate, entry: KnowledgeCatalogEntry): number {
|
|
141
|
-
if (candidate.scope !== entry.scope || candidate.storageHint !== entry.track) return 0;
|
|
142
|
-
const titleScore = containment(comparisonTerms(candidate.title), comparisonTerms(entry.title));
|
|
143
|
-
const keywordScore = containment(
|
|
144
|
-
comparisonTerms(candidate.keywords.join(" ")),
|
|
145
|
-
comparisonTerms(entry.keywords.join(" ")),
|
|
146
|
-
);
|
|
147
|
-
if (titleScore >= 0.8 && keywordScore >= 0.5) return titleScore * 0.7 + keywordScore * 0.3;
|
|
148
|
-
const candidateTerms = comparisonTerms(`${candidate.title} ${candidate.keywords.join(" ")}`);
|
|
149
|
-
const entryTerms = comparisonTerms(`${entry.title} ${entry.keywords.join(" ")}`);
|
|
150
|
-
const distinctiveShared = [...candidateTerms].filter((term) => (
|
|
151
|
-
entryTerms.has(term) && term.length >= 3 && !DISTINCTIVE_COMMON_TERMS.has(term)
|
|
152
|
-
)).length;
|
|
153
|
-
if (keywordScore < 0.5 || distinctiveShared < 3) return 0;
|
|
154
|
-
return keywordScore * 0.7 + Math.min(distinctiveShared / 6, 1) * 0.3;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function semanticDuplicateIndex(candidate: KnowledgeCandidate, catalog: KnowledgeCatalog): number {
|
|
158
|
-
let bestIndex = -1;
|
|
159
|
-
let bestScore = 0;
|
|
160
|
-
for (const [index, entry] of catalog.items.entries()) {
|
|
161
|
-
const score = semanticDuplicateScore(candidate, entry);
|
|
162
|
-
if (score > bestScore) { bestIndex = index; bestScore = score; }
|
|
163
|
-
}
|
|
164
|
-
return bestIndex;
|
|
121
|
+
const identity = candidate.identityKey?.trim().toLowerCase() || candidate.key.trim().toLowerCase();
|
|
122
|
+
return createHash("sha256").update(`${candidate.scope}\0${candidate.storageHint}\0${identity}`).digest("hex");
|
|
165
123
|
}
|
|
166
124
|
|
|
167
125
|
interface UnsupportedOperation {
|
|
@@ -201,6 +159,72 @@ function asStringArray(value: unknown, maxItems: number): string[] {
|
|
|
201
159
|
.map((item) => compactKnowledgeText(item, 240)).filter(Boolean))].slice(0, maxItems);
|
|
202
160
|
}
|
|
203
161
|
|
|
162
|
+
function asSafeReferenceArray(value: unknown, maxItems: number): string[] {
|
|
163
|
+
if (!Array.isArray(value)) return [];
|
|
164
|
+
return [...new Set(value.filter((item): item is string => (
|
|
165
|
+
typeof item === "string" && /^[a-z0-9][a-z0-9-]{0,127}$/u.test(item)
|
|
166
|
+
)))].slice(0, maxItems);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function normalizedClaimKey(value: string): string {
|
|
170
|
+
return compactKnowledgeText(value, 160).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ".").replace(/^\.+|\.+$/gu, "");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function parseClaims(value: unknown): KnowledgeClaim[] {
|
|
174
|
+
if (!Array.isArray(value)) return [];
|
|
175
|
+
const claims: KnowledgeClaim[] = [];
|
|
176
|
+
for (const raw of value.slice(0, 12)) {
|
|
177
|
+
if (!raw || typeof raw !== "object") continue;
|
|
178
|
+
const item = raw as Record<string, unknown>;
|
|
179
|
+
if (typeof item.text !== "string") continue;
|
|
180
|
+
const text = compactKnowledgeText(item.text, 500);
|
|
181
|
+
const key = typeof item.key === "string" ? normalizedClaimKey(item.key) : normalizedClaimKey(text);
|
|
182
|
+
const evidenceIds = asSafeReferenceArray(item.evidenceIds, 12);
|
|
183
|
+
if (key && text && evidenceIds.length > 0) claims.push({ key, text, evidenceIds });
|
|
184
|
+
}
|
|
185
|
+
return claims;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function evidenceMap(task: KnowledgeReviewTask): Map<string, KnowledgeEvidence> {
|
|
189
|
+
return new Map((task.evidence ?? []).map((item) => [item.id, item]));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const UNIVERSAL_CLAIM_RE = /(?:\b(?:all|always|any|every)\b|全部|所有|任何|始终|均需)/iu;
|
|
193
|
+
const BROAD_REQUIRED_CLAIM_RE = /(?:\b(?:apis?|operations?|services?)\b|接口|操作|服务)[^\n]{0,80}(?:\b(?:must|require[sd]?)\b|必须)/iu;
|
|
194
|
+
const TECHNICAL_TOKEN_RE = /\p{Script=Han}{2,}|[a-z][a-z0-9_.\/-]{2,}/giu;
|
|
195
|
+
const CLAIM_COMMON_TERMS = new Set(["should", "must", "always", "before", "after", "使用", "必须", "应该", "需要", "首先"]);
|
|
196
|
+
|
|
197
|
+
function claimTerms(value: string): Set<string> {
|
|
198
|
+
return new Set((value.toLowerCase().match(TECHNICAL_TOKEN_RE) ?? [])
|
|
199
|
+
.filter((term) => !CLAIM_COMMON_TERMS.has(term)));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function claimHasDirectSupport(claim: KnowledgeClaim, ledger: Map<string, KnowledgeEvidence>): boolean {
|
|
203
|
+
const terms = claimTerms(claim.text);
|
|
204
|
+
if (terms.size === 0) return false;
|
|
205
|
+
for (const id of claim.evidenceIds) {
|
|
206
|
+
const evidence = ledger.get(id);
|
|
207
|
+
if (!evidence || evidence.kind === "assistant-claim") continue;
|
|
208
|
+
const haystack = `${evidence.subject}\n${evidence.operation ?? ""}\n${evidence.excerpt}`.toLowerCase();
|
|
209
|
+
if ([...terms].some((term) => haystack.includes(term))) return true;
|
|
210
|
+
}
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function claimIsOverbroad(claim: KnowledgeClaim, ledger: Map<string, KnowledgeEvidence>): boolean {
|
|
215
|
+
if (!UNIVERSAL_CLAIM_RE.test(claim.text) && !BROAD_REQUIRED_CLAIM_RE.test(claim.text)) return false;
|
|
216
|
+
const operations = new Set(claim.evidenceIds.map((id) => ledger.get(id)?.operation).filter(Boolean));
|
|
217
|
+
return operations.size <= 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function claimContradictsSuccess(claim: KnowledgeClaim, task: KnowledgeReviewTask): boolean {
|
|
221
|
+
if (!/(?:not supported|unsupported|不支持|不可用|必须.*\/v\d|must.*\/v\d)/iu.test(claim.text)) return false;
|
|
222
|
+
const claimLower = claim.text.toLowerCase();
|
|
223
|
+
return (task.evidence ?? []).some((item) => item.outcome === "success" && item.operation
|
|
224
|
+
&& claimLower.includes(item.operation.split(/\s+/u).at(-1)!.toLowerCase().replace(/\/v\d$/u, ""))
|
|
225
|
+
&& !item.operation.toLowerCase().match(/\/v\d$/u));
|
|
226
|
+
}
|
|
227
|
+
|
|
204
228
|
type CandidateNormalization = { candidate: KnowledgeCandidate } | { reason: KnowledgeSkipReason };
|
|
205
229
|
|
|
206
230
|
function withoutLeadingTitle(body: string): string {
|
|
@@ -218,7 +242,7 @@ function containsVolatileDetail(body: string, projectRoot: string): boolean {
|
|
|
218
242
|
}
|
|
219
243
|
|
|
220
244
|
function normalizeCandidate(
|
|
221
|
-
value: unknown,
|
|
245
|
+
value: unknown, task: KnowledgeReviewTask, validationText: string,
|
|
222
246
|
): CandidateNormalization {
|
|
223
247
|
if (!value || typeof value !== "object") return { reason: "invalid-schema" };
|
|
224
248
|
const raw = value as Record<string, unknown>;
|
|
@@ -229,27 +253,62 @@ function normalizeCandidate(
|
|
|
229
253
|
if (raw.targetId !== undefined && raw.targetId !== null
|
|
230
254
|
&& (typeof raw.targetId !== "string" || !SAFE_ID_RE.test(raw.targetId))) return { reason: "unknown-target" };
|
|
231
255
|
const explicitUserDirective = raw.explicitUserDirective === true;
|
|
232
|
-
const scope: KnowledgeScope = raw.scope === "global" && explicitUserDirective ? "global" : `project:${projectKey}`;
|
|
256
|
+
const scope: KnowledgeScope = raw.scope === "global" && explicitUserDirective ? "global" : `project:${task.projectKey}`;
|
|
233
257
|
const body = withoutLeadingTitle(compactKnowledgeText(raw.body, 20_000));
|
|
234
|
-
const
|
|
258
|
+
const ledger = evidenceMap(task);
|
|
259
|
+
const claims = parseClaims(raw.claims);
|
|
260
|
+
const episodeIds = asSafeReferenceArray(raw.episodeIds, 8);
|
|
261
|
+
const evidenceIds = [...new Set([
|
|
262
|
+
...asSafeReferenceArray(raw.evidenceIds, 16),
|
|
263
|
+
...claims.flatMap((claim) => claim.evidenceIds),
|
|
264
|
+
])];
|
|
265
|
+
let evidence = asStringArray(raw.evidence, 8);
|
|
266
|
+
if (ledger.size > 0) {
|
|
267
|
+
if (typeof raw.identityKey !== "string" || !raw.identityKey.trim()) return { reason: "invalid-schema" };
|
|
268
|
+
if (claims.length === 0 || evidenceIds.length === 0) return { reason: "missing-body-or-evidence" };
|
|
269
|
+
if (evidenceIds.some((id) => !ledger.has(id))) return { reason: "unknown-evidence" };
|
|
270
|
+
if (episodeIds.length === 0 || episodeIds.some((id) => !(task.episodes ?? []).some((episode) => episode.id === id))) {
|
|
271
|
+
return { reason: "invalid-schema" };
|
|
272
|
+
}
|
|
273
|
+
const episodeEvidenceIds = new Set((task.episodes ?? [])
|
|
274
|
+
.filter((episode) => episodeIds.includes(episode.id)).flatMap((episode) => episode.evidenceIds));
|
|
275
|
+
if (evidenceIds.some((id) => !episodeEvidenceIds.has(id))) return { reason: "unknown-evidence" };
|
|
276
|
+
if (!explicitUserDirective && claims.some((claim) => !claimHasDirectSupport(claim, ledger))) {
|
|
277
|
+
return { reason: "unsupported-claim" };
|
|
278
|
+
}
|
|
279
|
+
if (!explicitUserDirective && claims.some((claim) => claimIsOverbroad(claim, ledger))) {
|
|
280
|
+
return { reason: "overbroad-claim" };
|
|
281
|
+
}
|
|
282
|
+
if (claims.some((claim) => claimContradictsSuccess(claim, task))) {
|
|
283
|
+
return { reason: "contradicts-verified-failure" };
|
|
284
|
+
}
|
|
285
|
+
evidence = evidenceIds.slice(0, 8).map((id) => {
|
|
286
|
+
const item = ledger.get(id)!;
|
|
287
|
+
return `${id}: ${compactKnowledgeText(`${item.subject}: ${item.excerpt}`, 220)}`;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
235
290
|
if (!body || evidence.length === 0) return { reason: "missing-body-or-evidence" };
|
|
236
291
|
const candidateClaims = `${raw.title}\n${raw.summary}\n${body}`;
|
|
237
292
|
if (!explicitUserDirective && UNVERIFIED_CLAIM_RE.test(candidateClaims)) return { reason: "unverified-claim" };
|
|
238
|
-
if (containsVolatileDetail(body, projectRoot)) return { reason: "volatile-detail" };
|
|
293
|
+
if (containsVolatileDetail(body, task.projectRoot)) return { reason: "volatile-detail" };
|
|
239
294
|
if (contradictsVerifiedFailures(body, validationText)) return { reason: "contradicts-verified-failure" };
|
|
240
295
|
const requestedTrack: KnowledgeTrack = raw.storageHint === "rule" ? "rule" : "topic";
|
|
241
296
|
const ruleEligible = body.length <= STORAGE.maxRuleChars && body.split("\n").length <= STORAGE.maxRuleFileLines
|
|
242
297
|
&& body.split(/\n\s*\n/gu).length === 1
|
|
243
298
|
&& (explicitUserDirective || raw.confidence >= 0.9);
|
|
244
299
|
const targetId = typeof raw.targetId === "string" && SAFE_ID_RE.test(raw.targetId) ? raw.targetId : undefined;
|
|
300
|
+
const action = raw.action === "reinforce" || raw.action === "extend" || raw.action === "correct"
|
|
301
|
+
|| raw.action === "retire" ? raw.action : "add";
|
|
245
302
|
return { candidate: {
|
|
246
|
-
key: compactKnowledgeText(raw.key, 160),
|
|
303
|
+
key: compactKnowledgeText(raw.key, 160),
|
|
304
|
+
identityKey: typeof raw.identityKey === "string" ? compactKnowledgeText(raw.identityKey, 240).toLowerCase() : undefined,
|
|
305
|
+
targetId,
|
|
247
306
|
title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
|
|
248
307
|
summary: compactKnowledgeSummary(raw.summary, STORAGE.maxSummaryChars),
|
|
249
308
|
keywords: asStringArray(raw.keywords, STORAGE.maxKeywordCount).map((keyword) => keyword.toLowerCase()),
|
|
250
|
-
scope, body, evidence, confidence: Math.min(1, raw.confidence),
|
|
309
|
+
scope, body, evidence, evidenceIds, episodeIds, claims, confidence: Math.min(1, raw.confidence),
|
|
251
310
|
storageHint: requestedTrack === "rule" && ruleEligible ? "rule" : "topic",
|
|
252
|
-
action
|
|
311
|
+
action,
|
|
253
312
|
explicitUserDirective, durability: "stable",
|
|
254
313
|
} };
|
|
255
314
|
}
|
|
@@ -276,6 +335,17 @@ function renderKnowledgeFile(candidate: KnowledgeCandidate): string {
|
|
|
276
335
|
].join("\n");
|
|
277
336
|
}
|
|
278
337
|
|
|
338
|
+
function extendKnowledgeFile(content: string, candidate: KnowledgeCandidate): string {
|
|
339
|
+
const evidenceHeading = "\n## Evidence\n";
|
|
340
|
+
const index = content.indexOf(evidenceHeading);
|
|
341
|
+
const addition = candidate.body.trim();
|
|
342
|
+
const evidenceLines = candidate.evidence.map((item) => `- ${item}`).join("\n");
|
|
343
|
+
if (index < 0) return `${content.trim()}\n\n${addition}\n\n## Evidence\n\n${evidenceLines}\n`;
|
|
344
|
+
const before = content.slice(0, index).trim();
|
|
345
|
+
const after = content.slice(index + evidenceHeading.length).trim();
|
|
346
|
+
return `${before}\n\n${addition}\n\n## Evidence\n\n${after}${after ? "\n" : ""}${evidenceLines}\n`;
|
|
347
|
+
}
|
|
348
|
+
|
|
279
349
|
function applicable(entry: KnowledgeCatalogEntry, projectKey: string): boolean {
|
|
280
350
|
return entry.scope === "global" || entry.scope === `project:${projectKey}`;
|
|
281
351
|
}
|
|
@@ -340,11 +410,11 @@ function newGenerationId(): string {
|
|
|
340
410
|
}
|
|
341
411
|
|
|
342
412
|
function copyCurrentContent(manifest: KnowledgeManifest, target: string, home: string): void {
|
|
343
|
-
for (const name of ["rules", "topics", "pending"]) ensureDirectory(join(target, name));
|
|
413
|
+
for (const name of ["rules", "topics", "pending", "audits"]) ensureDirectory(join(target, name));
|
|
344
414
|
if (!manifest.generationId) return;
|
|
345
415
|
const source = generationDirectory(manifest.generationId, home);
|
|
346
416
|
if (!source) return;
|
|
347
|
-
for (const name of ["rules", "topics", "pending"]) {
|
|
417
|
+
for (const name of ["rules", "topics", "pending", "audits"]) {
|
|
348
418
|
const sourceDirectory = join(source, name);
|
|
349
419
|
if (!existsSync(sourceDirectory)) continue;
|
|
350
420
|
for (const entry of readdirSync(sourceDirectory, { withFileTypes: true })) {
|
|
@@ -371,9 +441,23 @@ function reviewCursor(task: KnowledgeReviewTask): KnowledgeReviewCursor {
|
|
|
371
441
|
};
|
|
372
442
|
}
|
|
373
443
|
|
|
444
|
+
function validateEpisodeDecisions(task: KnowledgeReviewTask, decisions: KnowledgeEpisodeDecision[]): void {
|
|
445
|
+
const mandatory = (task.episodes ?? []).filter((episode) => (
|
|
446
|
+
episode.score >= KNOWLEDGE_RUNTIME_DEFAULTS.review.mandatoryEpisodeScore
|
|
447
|
+
));
|
|
448
|
+
for (const episode of mandatory) {
|
|
449
|
+
const decision = decisions.find((item) => item.episodeId === episode.id);
|
|
450
|
+
if (!decision || typeof decision.reason !== "string" || !decision.reason.trim()) {
|
|
451
|
+
throw new Error(`Knowledge reviewer omitted mandatory episode ${episode.id}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
374
456
|
export function commitKnowledgeReview(
|
|
375
457
|
values: unknown[], task: KnowledgeReviewTask, leaderToken: string, home = homedir(),
|
|
458
|
+
decisions: KnowledgeEpisodeDecision[] = [],
|
|
376
459
|
): PersistKnowledgeResult {
|
|
460
|
+
validateEpisodeDecisions(task, decisions);
|
|
377
461
|
ensureKnowledgeDirectories(home);
|
|
378
462
|
const release = acquireCommitLock(leaderToken, home);
|
|
379
463
|
const paths = userRuntimePaths(home);
|
|
@@ -392,7 +476,7 @@ export function commitKnowledgeReview(
|
|
|
392
476
|
const rejected: Array<{ reason: KnowledgeSkipReason; value: unknown }> = [];
|
|
393
477
|
for (const value of values.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates)) {
|
|
394
478
|
const normalized = normalizeCandidate(
|
|
395
|
-
value, task
|
|
479
|
+
value, task, `${task.context ?? ""}\n${task.delta}`,
|
|
396
480
|
);
|
|
397
481
|
if (!("candidate" in normalized)) {
|
|
398
482
|
recordSkip(result, normalized.reason);
|
|
@@ -412,20 +496,76 @@ export function commitKnowledgeReview(
|
|
|
412
496
|
}
|
|
413
497
|
let existingIndex = targetIndex >= 0
|
|
414
498
|
? targetIndex
|
|
415
|
-
: catalog.items.findIndex((item) => item.fingerprint === fingerprint
|
|
416
|
-
|
|
417
|
-
if (existingIndex < 0) {
|
|
418
|
-
existingIndex = semanticDuplicateIndex(candidate, catalog);
|
|
419
|
-
semanticReinforcement = existingIndex >= 0;
|
|
420
|
-
}
|
|
499
|
+
: catalog.items.findIndex((item) => item.fingerprint === fingerprint
|
|
500
|
+
|| Boolean(candidate.identityKey && item.identityKey === candidate.identityKey && item.track === candidate.storageHint));
|
|
421
501
|
const existing = existingIndex >= 0 ? catalog.items[existingIndex] : undefined;
|
|
422
|
-
|
|
502
|
+
|
|
503
|
+
if (candidate.action === "reinforce") {
|
|
504
|
+
const claimKeys = candidate.claims?.map((claim) => claim.key) ?? [];
|
|
505
|
+
const covered = claimKeys.length > 0 && claimKeys.every((key) => existing?.claimKeys?.includes(key));
|
|
506
|
+
if (!existing || (!covered && existing.contentHash !== hash)) {
|
|
507
|
+
recordSkip(result, "unsafe-reinforcement");
|
|
508
|
+
rejected.push({ reason: "unsafe-reinforcement", value });
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
existing.evidenceCount += candidate.evidence.length;
|
|
512
|
+
existing.updatedAt = new Date().toISOString();
|
|
513
|
+
result.updated++;
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (candidate.action === "retire") {
|
|
518
|
+
if (!existing || !candidate.targetId || (!candidate.explicitUserDirective && candidate.confidence < 0.98)) {
|
|
519
|
+
recordSkip(result, "unsafe-reinforcement");
|
|
520
|
+
rejected.push({ reason: "unsafe-reinforcement", value });
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
const existingPath = resolve(temporary, existing.file);
|
|
524
|
+
if (existingPath.startsWith(`${temporary}${sep}`)) rmSync(existingPath, { force: true });
|
|
525
|
+
catalog.items.splice(existingIndex, 1);
|
|
526
|
+
result.updated++;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (candidate.action === "extend") {
|
|
531
|
+
if (!existing || !candidate.targetId || existing.track !== "topic") {
|
|
532
|
+
recordSkip(result, "unknown-target");
|
|
533
|
+
rejected.push({ reason: "unknown-target", value });
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
const newClaimKeys = (candidate.claims ?? []).map((claim) => claim.key)
|
|
537
|
+
.filter((key) => !existing.claimKeys?.includes(key));
|
|
538
|
+
if (newClaimKeys.length === 0 && existing.claimKeys?.length) {
|
|
539
|
+
existing.evidenceCount += candidate.evidence.length;
|
|
540
|
+
existing.updatedAt = new Date().toISOString();
|
|
541
|
+
result.updated++;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const existingPath = resolve(temporary, existing.file);
|
|
545
|
+
if (!existingPath.startsWith(`${temporary}${sep}`) || !existsSync(existingPath)) {
|
|
546
|
+
recordSkip(result, "unknown-target");
|
|
547
|
+
rejected.push({ reason: "unknown-target", value });
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
const content = extendKnowledgeFile(readFileSync(existingPath, "utf8"), candidate);
|
|
551
|
+
atomicWrite(existingPath, content);
|
|
552
|
+
existing.contentHash = createHash("sha256").update(content).digest("hex");
|
|
553
|
+
existing.keywords = [...new Set([...existing.keywords, ...candidate.keywords])].slice(0, STORAGE.maxKeywordCount);
|
|
554
|
+
existing.claimKeys = [...new Set([...(existing.claimKeys ?? []), ...newClaimKeys])];
|
|
423
555
|
existing.evidenceCount += candidate.evidence.length;
|
|
424
556
|
existing.updatedAt = new Date().toISOString();
|
|
425
557
|
result.updated++;
|
|
426
558
|
continue;
|
|
427
559
|
}
|
|
428
|
-
|
|
560
|
+
|
|
561
|
+
if (existing && candidate.action !== "correct") {
|
|
562
|
+
const name = `${Date.now()}-${normalizeSlug(candidate.title)}-${randomUUID().slice(0, 8)}.json`;
|
|
563
|
+
atomicWrite(join(temporary, "pending", name), `${JSON.stringify({ reason: "conflicting-or-ambiguous-update", candidate }, null, 2)}\n`);
|
|
564
|
+
result.pending++;
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
if (existing && candidate.action === "correct"
|
|
568
|
+
&& (!candidate.targetId || (!candidate.explicitUserDirective && candidate.confidence < 0.92))) {
|
|
429
569
|
const name = `${Date.now()}-${normalizeSlug(candidate.title)}-${randomUUID().slice(0, 8)}.json`;
|
|
430
570
|
atomicWrite(join(temporary, "pending", name), `${JSON.stringify({ reason: "conflicting-or-ambiguous-update", candidate }, null, 2)}\n`);
|
|
431
571
|
result.pending++;
|
|
@@ -446,21 +586,32 @@ export function commitKnowledgeReview(
|
|
|
446
586
|
title: candidate.title, summary: candidate.summary,
|
|
447
587
|
keywords: candidate.keywords, scope: existing?.scope ?? candidate.scope, track, file: relativeFile,
|
|
448
588
|
evidenceCount: (existing?.evidenceCount ?? 0) + candidate.evidence.length,
|
|
589
|
+
identityKey: candidate.identityKey ?? existing?.identityKey,
|
|
590
|
+
claimKeys: candidate.claims?.map((claim) => claim.key) ?? existing?.claimKeys,
|
|
449
591
|
createdAt: existing?.createdAt ?? now, updatedAt: now,
|
|
450
592
|
};
|
|
451
593
|
if (existingIndex >= 0) { catalog.items[existingIndex] = entry; result.updated++; }
|
|
452
594
|
else { catalog.items.push(entry); result.saved++; }
|
|
453
595
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
result.pending += rejected.length;
|
|
457
|
-
}
|
|
596
|
+
for (const item of rejected) writeRejectedPending(temporary, item.reason, item.value);
|
|
597
|
+
result.pending += rejected.length;
|
|
458
598
|
catalog.updatedAt = new Date().toISOString();
|
|
459
599
|
const manifest: KnowledgeManifest = {
|
|
460
600
|
version: 3, generationId, createdAt: new Date().toISOString(), leaderToken, catalog,
|
|
461
601
|
reviews: { ...current.reviews, [task.sessionKey]: reviewCursor(task) },
|
|
462
602
|
};
|
|
463
603
|
atomicWrite(join(temporary, "MEMORY.md"), generateMemory(catalog));
|
|
604
|
+
atomicWrite(join(temporary, "audits", `${task.reviewKey}.json`), `${JSON.stringify({
|
|
605
|
+
reviewKey: task.reviewKey,
|
|
606
|
+
sessionKey: task.sessionKey,
|
|
607
|
+
episodes: (task.episodes ?? []).map((episode) => ({
|
|
608
|
+
id: episode.id, title: episode.title, score: episode.score,
|
|
609
|
+
failureCount: episode.failureCount, successCount: episode.successCount,
|
|
610
|
+
})),
|
|
611
|
+
decisions,
|
|
612
|
+
result,
|
|
613
|
+
rejected: rejected.map((item) => ({ reason: item.reason })),
|
|
614
|
+
}, null, 2)}\n`);
|
|
464
615
|
atomicWrite(join(temporary, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
465
616
|
if (!validManifestAt(manifest, temporary)) throw new Error("Refusing to publish an incomplete knowledge generation");
|
|
466
617
|
const finalDirectory = join(paths.knowledgeGenerations, generationId);
|
|
@@ -1,10 +1,56 @@
|
|
|
1
1
|
export type KnowledgeTrack = "rule" | "topic";
|
|
2
2
|
export type KnowledgeScope = "global" | `project:${string}`;
|
|
3
|
-
export type KnowledgeAction = "add" | "reinforce" | "
|
|
3
|
+
export type KnowledgeAction = "add" | "reinforce" | "extend" | "correct" | "retire";
|
|
4
4
|
export type KnowledgeDurability = "stable";
|
|
5
5
|
|
|
6
|
+
export type KnowledgeEvidenceKind =
|
|
7
|
+
| "user-directive"
|
|
8
|
+
| "user-correction"
|
|
9
|
+
| "tool-success"
|
|
10
|
+
| "tool-failure"
|
|
11
|
+
| "verification"
|
|
12
|
+
| "assistant-claim";
|
|
13
|
+
|
|
14
|
+
export interface KnowledgeEvidence {
|
|
15
|
+
id: string;
|
|
16
|
+
entryId: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
kind: KnowledgeEvidenceKind;
|
|
19
|
+
subject: string;
|
|
20
|
+
operation?: string;
|
|
21
|
+
outcome: "success" | "failure" | "unknown";
|
|
22
|
+
excerpt: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface KnowledgeEpisode {
|
|
26
|
+
id: string;
|
|
27
|
+
title: string;
|
|
28
|
+
query: string;
|
|
29
|
+
evidenceIds: string[];
|
|
30
|
+
score: number;
|
|
31
|
+
failureCount: number;
|
|
32
|
+
successCount: number;
|
|
33
|
+
hypothesisReversalCount: number;
|
|
34
|
+
elapsedMs: number;
|
|
35
|
+
userSignal: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface KnowledgeClaim {
|
|
39
|
+
key: string;
|
|
40
|
+
text: string;
|
|
41
|
+
evidenceIds: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface KnowledgeEpisodeDecision {
|
|
45
|
+
episodeId: string;
|
|
46
|
+
decision: "add" | "reinforce" | "extend" | "correct" | "retire" | "skip";
|
|
47
|
+
targetId?: string;
|
|
48
|
+
reason: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
6
51
|
export interface KnowledgeCandidate {
|
|
7
52
|
key: string;
|
|
53
|
+
identityKey?: string;
|
|
8
54
|
targetId?: string;
|
|
9
55
|
title: string;
|
|
10
56
|
summary: string;
|
|
@@ -12,6 +58,9 @@ export interface KnowledgeCandidate {
|
|
|
12
58
|
scope: KnowledgeScope;
|
|
13
59
|
body: string;
|
|
14
60
|
evidence: string[];
|
|
61
|
+
evidenceIds?: string[];
|
|
62
|
+
episodeIds?: string[];
|
|
63
|
+
claims?: KnowledgeClaim[];
|
|
15
64
|
confidence: number;
|
|
16
65
|
storageHint: KnowledgeTrack;
|
|
17
66
|
action: KnowledgeAction;
|
|
@@ -30,6 +79,8 @@ export interface KnowledgeCatalogEntry {
|
|
|
30
79
|
track: KnowledgeTrack;
|
|
31
80
|
file: string;
|
|
32
81
|
evidenceCount: number;
|
|
82
|
+
identityKey?: string;
|
|
83
|
+
claimKeys?: string[];
|
|
33
84
|
createdAt: string;
|
|
34
85
|
updatedAt: string;
|
|
35
86
|
}
|
|
@@ -86,7 +137,11 @@ export type KnowledgeSkipReason =
|
|
|
86
137
|
| "low-confidence"
|
|
87
138
|
| "missing-body-or-evidence"
|
|
88
139
|
| "contradicts-verified-failure"
|
|
89
|
-
| "unknown-target"
|
|
140
|
+
| "unknown-target"
|
|
141
|
+
| "unknown-evidence"
|
|
142
|
+
| "unsupported-claim"
|
|
143
|
+
| "overbroad-claim"
|
|
144
|
+
| "unsafe-reinforcement";
|
|
90
145
|
|
|
91
146
|
export interface KnowledgeReviewTask {
|
|
92
147
|
requestId: string;
|
|
@@ -102,6 +157,8 @@ export interface KnowledgeReviewTask {
|
|
|
102
157
|
delta: string;
|
|
103
158
|
recallQuery?: string;
|
|
104
159
|
loadedKnowledgeIds?: string[];
|
|
160
|
+
evidence?: KnowledgeEvidence[];
|
|
161
|
+
episodes?: KnowledgeEpisode[];
|
|
105
162
|
deltaDigest: string;
|
|
106
163
|
fileSize: number;
|
|
107
164
|
fileMtimeMs: number;
|
|
@@ -34,6 +34,10 @@ export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
|
|
|
34
34
|
modelTimeoutMs: 180_000,
|
|
35
35
|
maxDeltaChars: 12_000,
|
|
36
36
|
maxPriorContextChars: 4_000,
|
|
37
|
+
maxNarrativeChars: 6_000,
|
|
38
|
+
maxEvidenceItems: 24,
|
|
39
|
+
maxEvidenceChars: 12_000,
|
|
40
|
+
mandatoryEpisodeScore: 45,
|
|
37
41
|
maxExistingContextItems: 5,
|
|
38
42
|
maxExistingBodyItems: 2,
|
|
39
43
|
maxExistingBodyChars: 600,
|