@hadooppei/hwcode 1.0.16 → 1.0.20

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.
@@ -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, KnowledgeManifest, KnowledgeReviewCursor,
14
- KnowledgeReviewTask, KnowledgeScope, KnowledgeSkipReason, KnowledgeSnapshot, KnowledgeTrack,
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
- return createHash("sha256").update(`${candidate.scope}\0${candidate.key.trim().toLowerCase()}`).digest("hex");
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, projectKey: string, projectRoot: string, validationText: string,
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 evidence = asStringArray(raw.evidence, 8);
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), targetId,
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: raw.action === "revise" ? "revise" : raw.action === "reinforce" ? "reinforce" : "add",
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.projectKey, task.projectRoot, `${task.context ?? ""}\n${task.delta}`,
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
- let semanticReinforcement = false;
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
- if (existing && (existing.contentHash === hash || candidate.action === "reinforce" || semanticReinforcement)) {
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
- if (existing && !(candidate.action === "revise" && (candidate.explicitUserDirective || candidate.confidence >= 0.92))) {
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
- if (result.skipped > 0 && result.saved === 0 && result.updated === 0 && result.pending === 0) {
455
- for (const item of rejected) writeRejectedPending(temporary, item.reason, item.value);
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" | "revise";
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;
@@ -13,7 +13,7 @@ export type KnowledgeWorkerOutput =
13
13
  | { type: "review_request"; leaderToken: string; requestId: string; task: KnowledgeReviewTask }
14
14
  | { type: "review_cancel"; requestId: string; reason: string }
15
15
  | { type: "review_saved"; requestId: string; generationId?: string; result: PersistKnowledgeResult }
16
- | { type: "review_failed"; requestId?: string; error: string }
16
+ | { type: "review_failed"; requestId?: string; error: string; terminal?: boolean }
17
17
  | { type: "generation_changed"; generationId: string };
18
18
 
19
19
  export interface CoordinatorBroadcast {
@@ -32,8 +32,14 @@ export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
32
32
  idleMs: 60_000,
33
33
  capabilityPollMs: 5_000,
34
34
  modelTimeoutMs: 180_000,
35
+ maxAttempts: 2,
36
+ maxFailureDetailChars: 4_000,
35
37
  maxDeltaChars: 12_000,
36
38
  maxPriorContextChars: 4_000,
39
+ maxNarrativeChars: 6_000,
40
+ maxEvidenceItems: 24,
41
+ maxEvidenceChars: 12_000,
42
+ mandatoryEpisodeScore: 45,
37
43
  maxExistingContextItems: 5,
38
44
  maxExistingBodyItems: 2,
39
45
  maxExistingBodyChars: 600,
@@ -18,6 +18,25 @@ export interface ProcessResult {
18
18
  spawnErrorCode?: string;
19
19
  }
20
20
 
21
+ const BRACKETED_PROVIDER_ERROR_RE = /\[(?:USE|CLI|AUTH|HTTP|SERVER|SYSTEM|SDK)_ERROR\][^\r\n]*/iu;
22
+ const STRUCTURED_PROVIDER_FAILURE_RE = /"kind"\s*:\s*"Status"[\s\S]{0,500}"status"\s*:\s*"(?:failure|failed|error)"/iu;
23
+ const STRUCTURED_ERROR_CODE_RE = /"(?:errorCode|error_code|code)"\s*:\s*(?:"[^"\r\n]+"|\d+)/iu;
24
+
25
+ /** Detect CLIs that report an API failure in output while incorrectly exiting with code 0. */
26
+ export function cloudProviderFailureReason(stdout: string, stderr: string): string | undefined {
27
+ const output = `${stderr}\n${stdout}`.trim();
28
+ if (!output) return undefined;
29
+ const bracketed = BRACKETED_PROVIDER_ERROR_RE.exec(output)?.[0]?.trim();
30
+ if (bracketed) return bracketed.slice(0, 2_000);
31
+ if (STRUCTURED_PROVIDER_FAILURE_RE.test(output) && STRUCTURED_ERROR_CODE_RE.test(output)) {
32
+ const status = STRUCTURED_PROVIDER_FAILURE_RE.exec(output)?.[0];
33
+ const code = STRUCTURED_ERROR_CODE_RE.exec(output)?.[0];
34
+ const message = /"message"\s*:\s*"([^"\r\n]+)"/iu.exec(output)?.[1];
35
+ return [status, code, message].filter(Boolean).join("; ").slice(0, 2_000);
36
+ }
37
+ return undefined;
38
+ }
39
+
21
40
  function captureOutput(current: string, chunk: Buffer): string {
22
41
  if (Buffer.byteLength(current, "utf8") >= MAX_CAPTURE_BYTES) return current;
23
42
  const remaining = MAX_CAPTURE_BYTES - Buffer.byteLength(current, "utf8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadooppei/hwcode",
3
- "version": "1.0.16",
3
+ "version": "1.0.20",
4
4
  "description": "A customizable terminal coding agent with local-model support and HWCode workflows.",
5
5
  "type": "module",
6
6
  "bin": {