@hadooppei/hwcode 1.0.12 → 1.0.15

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.
@@ -11,13 +11,20 @@ import { userRuntimePaths } from "../runtime/paths.ts";
11
11
  import { compactKnowledgeSummary, compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.ts";
12
12
  import type {
13
13
  KnowledgeCandidate, KnowledgeCatalog, KnowledgeCatalogEntry, KnowledgeManifest, KnowledgeReviewCursor,
14
- KnowledgeReviewTask, KnowledgeScope, KnowledgeSnapshot, KnowledgeTrack, PersistKnowledgeResult,
14
+ KnowledgeReviewTask, KnowledgeScope, KnowledgeSkipReason, KnowledgeSnapshot, KnowledgeTrack,
15
+ PersistKnowledgeResult,
15
16
  } from "./types.ts";
16
17
 
17
18
  const STORAGE = KNOWLEDGE_RUNTIME_DEFAULTS.storage;
18
19
  const SAFE_ID_RE = /^[a-z0-9][a-z0-9-]{0,79}$/u;
19
20
  const SAFE_GENERATION_RE = /^[0-9TZ-]+-[a-f0-9]{12}$/u;
20
21
  const PROJECT_SCOPE_RE = /^project:[a-f0-9]{16}$/u;
22
+ const UNVERIFIED_CLAIM_RE = /(?:待验证|尚未(?:验证|确认)|未经(?:验证|确认)|未(?:经)?证实|推测|猜测|疑似|可能是|可能由于|可能导致|或许|unverified|unconfirmed|speculat(?:e|ive|ion)|hypothes(?:is|ize)|suspect(?:ed|ion)?|possibly|probably)/iu;
23
+ 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
+ ]);
21
28
 
22
29
  export class KnowledgeCommitBusyError extends Error {}
23
30
 
@@ -137,8 +144,14 @@ function semanticDuplicateScore(candidate: KnowledgeCandidate, entry: KnowledgeC
137
144
  comparisonTerms(candidate.keywords.join(" ")),
138
145
  comparisonTerms(entry.keywords.join(" ")),
139
146
  );
140
- if (titleScore < 0.8 || keywordScore < 0.5) return 0;
141
- return titleScore * 0.7 + keywordScore * 0.3;
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;
142
155
  }
143
156
 
144
157
  function semanticDuplicateIndex(candidate: KnowledgeCandidate, catalog: KnowledgeCatalog): number {
@@ -151,17 +164,28 @@ function semanticDuplicateIndex(candidate: KnowledgeCandidate, catalog: Knowledg
151
164
  return bestIndex;
152
165
  }
153
166
 
154
- function unsupportedOperations(value: string): string[] {
155
- return [...value.matchAll(/\bOperation\s+([A-Za-z][A-Za-z0-9]+)\s+is not supported\b/gu)]
156
- .map((match) => match[1]!.toLowerCase());
167
+ interface UnsupportedOperation {
168
+ operation: string;
169
+ service?: string;
170
+ }
171
+
172
+ function unsupportedOperations(value: string): UnsupportedOperation[] {
173
+ return [...value.matchAll(/\bOperation\s+([A-Za-z][A-Za-z0-9]+)\s+is not supported\b/gu)].map((match) => {
174
+ const suffix = value.slice(match.index + match[0].length, match.index + match[0].length + 500);
175
+ const service = /\bhcloud\s+([A-Za-z][A-Za-z0-9-]*)\s+--help\b/iu.exec(suffix)?.[1]?.toLowerCase();
176
+ return { operation: match[1]!.toLowerCase(), service };
177
+ });
157
178
  }
158
179
 
159
180
  function contradictsVerifiedFailures(body: string, validationText: string): boolean {
160
181
  const normalizedBody = body.toLowerCase();
161
- for (const operation of unsupportedOperations(validationText)) {
162
- const index = normalizedBody.search(new RegExp(`\\b${operation}\\b`, "u"));
182
+ for (const unsupported of unsupportedOperations(validationText)) {
183
+ const operationExpression = unsupported.service
184
+ ? new RegExp(`\\b(?:hcloud\\s+)?${unsupported.service}\\b[^\\n]{0,120}\\b${unsupported.operation}\\b`, "u")
185
+ : new RegExp(`\\b${unsupported.operation}\\b`, "u");
186
+ const index = normalizedBody.search(operationExpression);
163
187
  if (index < 0) continue;
164
- const context = normalizedBody.slice(Math.max(0, index - 80), index + operation.length + 80);
188
+ const context = normalizedBody.slice(Math.max(0, index - 80), index + 240);
165
189
  if (!/(?:not supported|unsupported|do not|don't|avoid|不支持|不要|避免)/u.test(context)) return true;
166
190
  }
167
191
  return false;
@@ -177,25 +201,48 @@ function asStringArray(value: unknown, maxItems: number): string[] {
177
201
  .map((item) => compactKnowledgeText(item, 240)).filter(Boolean))].slice(0, maxItems);
178
202
  }
179
203
 
180
- function normalizeCandidate(value: unknown, projectKey: string, validationText: string): KnowledgeCandidate | undefined {
181
- if (!value || typeof value !== "object") return undefined;
204
+ type CandidateNormalization = { candidate: KnowledgeCandidate } | { reason: KnowledgeSkipReason };
205
+
206
+ function withoutLeadingTitle(body: string): string {
207
+ return body.replace(/^#\s+[^\n]*(?:\n+|$)/u, "").trim();
208
+ }
209
+
210
+ function containsVolatileDetail(body: string, projectRoot: string): boolean {
211
+ const projectName = basename(resolve(projectRoot));
212
+ const escapedProjectName = projectName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
213
+ const namedProject = projectName.length >= 5
214
+ && new RegExp(`(?:项目|project)\\s*(?:[::=]|is)?\\s*[\`'\"]?${escapedProjectName}(?![\\p{L}\\p{N}-])`, "iu").test(body);
215
+ return VOLATILE_ARGUMENT_RE.test(body)
216
+ || namedProject
217
+ || /\.hwcode\/cloud\/runs\//iu.test(body);
218
+ }
219
+
220
+ function normalizeCandidate(
221
+ value: unknown, projectKey: string, projectRoot: string, validationText: string,
222
+ ): CandidateNormalization {
223
+ if (!value || typeof value !== "object") return { reason: "invalid-schema" };
182
224
  const raw = value as Record<string, unknown>;
183
225
  if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
184
- || typeof raw.body !== "string" || typeof raw.confidence !== "number") return undefined;
185
- if (raw.durability !== "stable") return undefined;
186
- if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence) return undefined;
226
+ || typeof raw.body !== "string" || typeof raw.confidence !== "number") return { reason: "invalid-schema" };
227
+ if (raw.durability !== "stable") return { reason: "unstable" };
228
+ if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence) return { reason: "low-confidence" };
229
+ if (raw.targetId !== undefined && raw.targetId !== null
230
+ && (typeof raw.targetId !== "string" || !SAFE_ID_RE.test(raw.targetId))) return { reason: "unknown-target" };
187
231
  const explicitUserDirective = raw.explicitUserDirective === true;
188
232
  const scope: KnowledgeScope = raw.scope === "global" && explicitUserDirective ? "global" : `project:${projectKey}`;
189
- const body = compactKnowledgeText(raw.body, 20_000);
233
+ const body = withoutLeadingTitle(compactKnowledgeText(raw.body, 20_000));
190
234
  const evidence = asStringArray(raw.evidence, 8);
191
- if (!body || evidence.length === 0) return undefined;
192
- if (contradictsVerifiedFailures(body, validationText)) return undefined;
235
+ if (!body || evidence.length === 0) return { reason: "missing-body-or-evidence" };
236
+ const candidateClaims = `${raw.title}\n${raw.summary}\n${body}`;
237
+ if (!explicitUserDirective && UNVERIFIED_CLAIM_RE.test(candidateClaims)) return { reason: "unverified-claim" };
238
+ if (containsVolatileDetail(body, projectRoot)) return { reason: "volatile-detail" };
239
+ if (contradictsVerifiedFailures(body, validationText)) return { reason: "contradicts-verified-failure" };
193
240
  const requestedTrack: KnowledgeTrack = raw.storageHint === "rule" ? "rule" : "topic";
194
241
  const ruleEligible = body.length <= STORAGE.maxRuleChars && body.split("\n").length <= STORAGE.maxRuleFileLines
195
242
  && body.split(/\n\s*\n/gu).length === 1
196
243
  && (explicitUserDirective || raw.confidence >= 0.9);
197
244
  const targetId = typeof raw.targetId === "string" && SAFE_ID_RE.test(raw.targetId) ? raw.targetId : undefined;
198
- return {
245
+ return { candidate: {
199
246
  key: compactKnowledgeText(raw.key, 160), targetId,
200
247
  title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
201
248
  summary: compactKnowledgeSummary(raw.summary, STORAGE.maxSummaryChars),
@@ -204,7 +251,21 @@ function normalizeCandidate(value: unknown, projectKey: string, validationText:
204
251
  storageHint: requestedTrack === "rule" && ruleEligible ? "rule" : "topic",
205
252
  action: raw.action === "revise" ? "revise" : raw.action === "reinforce" ? "reinforce" : "add",
206
253
  explicitUserDirective, durability: "stable",
207
- };
254
+ } };
255
+ }
256
+
257
+ function recordSkip(result: PersistKnowledgeResult, reason: KnowledgeSkipReason): void {
258
+ result.skipped++;
259
+ result.skippedReasons ??= {};
260
+ result.skippedReasons[reason] = (result.skippedReasons[reason] ?? 0) + 1;
261
+ }
262
+
263
+ function writeRejectedPending(directory: string, reason: KnowledgeSkipReason, value: unknown): void {
264
+ let serialized: string;
265
+ try { serialized = JSON.stringify(value); } catch { serialized = String(value); }
266
+ const candidate = sanitizeKnowledgeText(serialized).slice(0, 10_000);
267
+ const name = `${Date.now()}-rejected-${reason}-${randomUUID().slice(0, 8)}.json`;
268
+ atomicWrite(join(directory, "pending", name), `${JSON.stringify({ reason, candidate }, null, 2)}\n`);
208
269
  }
209
270
 
210
271
  function renderKnowledgeFile(candidate: KnowledgeCandidate): string {
@@ -328,15 +389,27 @@ export function commitKnowledgeReview(
328
389
  copyCurrentContent(current, temporary, home);
329
390
  const catalog: KnowledgeCatalog = structuredClone(current.catalog);
330
391
  const result: PersistKnowledgeResult = { saved: 0, updated: 0, pending: 0, skipped: 0 };
392
+ const rejected: Array<{ reason: KnowledgeSkipReason; value: unknown }> = [];
331
393
  for (const value of values.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates)) {
332
- const candidate = normalizeCandidate(value, task.projectKey, `${task.context ?? ""}\n${task.delta}`);
333
- if (!candidate) { result.skipped++; continue; }
394
+ const normalized = normalizeCandidate(
395
+ value, task.projectKey, task.projectRoot, `${task.context ?? ""}\n${task.delta}`,
396
+ );
397
+ if (!("candidate" in normalized)) {
398
+ recordSkip(result, normalized.reason);
399
+ rejected.push({ reason: normalized.reason, value });
400
+ continue;
401
+ }
402
+ const candidate = normalized.candidate;
334
403
  const fingerprint = normalizedFingerprint(candidate);
335
404
  const hash = contentHash(candidate);
336
405
  const targetIndex = candidate.targetId
337
406
  ? catalog.items.findIndex((item) => item.id === candidate.targetId && applicable(item, task.projectKey))
338
407
  : -1;
339
- if (candidate.targetId && targetIndex < 0) { result.skipped++; continue; }
408
+ if (candidate.targetId && targetIndex < 0) {
409
+ recordSkip(result, "unknown-target");
410
+ rejected.push({ reason: "unknown-target", value });
411
+ continue;
412
+ }
340
413
  let existingIndex = targetIndex >= 0
341
414
  ? targetIndex
342
415
  : catalog.items.findIndex((item) => item.fingerprint === fingerprint);
@@ -378,6 +451,10 @@ export function commitKnowledgeReview(
378
451
  if (existingIndex >= 0) { catalog.items[existingIndex] = entry; result.updated++; }
379
452
  else { catalog.items.push(entry); result.saved++; }
380
453
  }
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
+ }
381
458
  catalog.updatedAt = new Date().toISOString();
382
459
  const manifest: KnowledgeManifest = {
383
460
  version: 3, generationId, createdAt: new Date().toISOString(), leaderToken, catalog,
@@ -75,8 +75,19 @@ export interface PersistKnowledgeResult {
75
75
  updated: number;
76
76
  pending: number;
77
77
  skipped: number;
78
+ skippedReasons?: Partial<Record<KnowledgeSkipReason, number>>;
78
79
  }
79
80
 
81
+ export type KnowledgeSkipReason =
82
+ | "invalid-schema"
83
+ | "unstable"
84
+ | "unverified-claim"
85
+ | "volatile-detail"
86
+ | "low-confidence"
87
+ | "missing-body-or-evidence"
88
+ | "contradicts-verified-failure"
89
+ | "unknown-target";
90
+
80
91
  export interface KnowledgeReviewTask {
81
92
  requestId: string;
82
93
  reviewKey: string;
@@ -89,6 +100,8 @@ export interface KnowledgeReviewTask {
89
100
  lastEntryId: string;
90
101
  context?: string;
91
102
  delta: string;
103
+ recallQuery?: string;
104
+ loadedKnowledgeIds?: string[];
92
105
  deltaDigest: string;
93
106
  fileSize: number;
94
107
  fileMtimeMs: number;
@@ -2,6 +2,7 @@ import type { KnowledgeReviewTask, PersistKnowledgeResult } from "./types.ts";
2
2
 
3
3
  export type KnowledgeWorkerInput =
4
4
  | { type: "configure"; modelAvailable: boolean; sessionsRoot: string }
5
+ | { type: "review_deferred"; leaderToken: string; requestId: string; reason: string }
5
6
  | { type: "review_result"; leaderToken: string; requestId: string; raw?: string; error?: string }
6
7
  | { type: "scan_now" }
7
8
  | { type: "stop" };
@@ -189,6 +189,16 @@ export function getActiveWorkflowRoot(entries: readonly SessionEntry[]): string
189
189
  return activeWorkflow(entries)?.root;
190
190
  }
191
191
 
192
+ export function findLatestWorkflowRoot(entries: readonly SessionEntry[]): string | undefined {
193
+ for (const entry of [...entries].reverse()) {
194
+ if (entry.type !== "custom" || entry.customType !== "hwcode-workflow-state"
195
+ || !entry.data || typeof entry.data !== "object") continue;
196
+ const root = (entry.data as Record<string, unknown>).root;
197
+ if (typeof root === "string" && root) return root;
198
+ }
199
+ return undefined;
200
+ }
201
+
192
202
  export function shellQuote(value: string): string {
193
203
  return `'${value.replaceAll("'", `'"'"'`)}'`;
194
204
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadooppei/hwcode",
3
- "version": "1.0.12",
3
+ "version": "1.0.15",
4
4
  "description": "A customizable terminal coding agent with local-model support and HWCode workflows.",
5
5
  "type": "module",
6
6
  "bin": {