@hadooppei/hwcode 1.0.10 → 1.0.12

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.
@@ -8,7 +8,7 @@ import { basename, dirname, join, resolve, sep } from "node:path";
8
8
 
9
9
  import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.ts";
10
10
  import { userRuntimePaths } from "../runtime/paths.ts";
11
- import { compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.ts";
11
+ import { compactKnowledgeSummary, compactKnowledgeText, sanitizeKnowledgeText } from "./sanitize.ts";
12
12
  import type {
13
13
  KnowledgeCandidate, KnowledgeCatalog, KnowledgeCatalogEntry, KnowledgeManifest, KnowledgeReviewCursor,
14
14
  KnowledgeReviewTask, KnowledgeScope, KnowledgeSnapshot, KnowledgeTrack, PersistKnowledgeResult,
@@ -117,6 +117,56 @@ function normalizedFingerprint(candidate: KnowledgeCandidate): string {
117
117
  return createHash("sha256").update(`${candidate.scope}\0${candidate.key.trim().toLowerCase()}`).digest("hex");
118
118
  }
119
119
 
120
+ function comparisonTerms(value: string): Set<string> {
121
+ return new Set((value.normalize("NFKC").toLowerCase().match(/\p{Script=Han}+|[\p{L}\p{N}]+/gu) ?? [])
122
+ .filter((term) => term.length >= 2));
123
+ }
124
+
125
+ function containment(left: Set<string>, right: Set<string>): number {
126
+ const minimum = Math.min(left.size, right.size);
127
+ if (minimum === 0) return 0;
128
+ let shared = 0;
129
+ for (const term of left) if (right.has(term)) shared++;
130
+ return shared / minimum;
131
+ }
132
+
133
+ function semanticDuplicateScore(candidate: KnowledgeCandidate, entry: KnowledgeCatalogEntry): number {
134
+ if (candidate.scope !== entry.scope || candidate.storageHint !== entry.track) return 0;
135
+ const titleScore = containment(comparisonTerms(candidate.title), comparisonTerms(entry.title));
136
+ const keywordScore = containment(
137
+ comparisonTerms(candidate.keywords.join(" ")),
138
+ comparisonTerms(entry.keywords.join(" ")),
139
+ );
140
+ if (titleScore < 0.8 || keywordScore < 0.5) return 0;
141
+ return titleScore * 0.7 + keywordScore * 0.3;
142
+ }
143
+
144
+ function semanticDuplicateIndex(candidate: KnowledgeCandidate, catalog: KnowledgeCatalog): number {
145
+ let bestIndex = -1;
146
+ let bestScore = 0;
147
+ for (const [index, entry] of catalog.items.entries()) {
148
+ const score = semanticDuplicateScore(candidate, entry);
149
+ if (score > bestScore) { bestIndex = index; bestScore = score; }
150
+ }
151
+ return bestIndex;
152
+ }
153
+
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());
157
+ }
158
+
159
+ function contradictsVerifiedFailures(body: string, validationText: string): boolean {
160
+ const normalizedBody = body.toLowerCase();
161
+ for (const operation of unsupportedOperations(validationText)) {
162
+ const index = normalizedBody.search(new RegExp(`\\b${operation}\\b`, "u"));
163
+ if (index < 0) continue;
164
+ const context = normalizedBody.slice(Math.max(0, index - 80), index + operation.length + 80);
165
+ if (!/(?:not supported|unsupported|do not|don't|avoid|不支持|不要|避免)/u.test(context)) return true;
166
+ }
167
+ return false;
168
+ }
169
+
120
170
  function contentHash(candidate: KnowledgeCandidate): string {
121
171
  return createHash("sha256").update(`${candidate.title}\0${candidate.summary}\0${candidate.body}`).digest("hex");
122
172
  }
@@ -127,28 +177,33 @@ function asStringArray(value: unknown, maxItems: number): string[] {
127
177
  .map((item) => compactKnowledgeText(item, 240)).filter(Boolean))].slice(0, maxItems);
128
178
  }
129
179
 
130
- function normalizeCandidate(value: unknown, projectKey: string): KnowledgeCandidate | undefined {
180
+ function normalizeCandidate(value: unknown, projectKey: string, validationText: string): KnowledgeCandidate | undefined {
131
181
  if (!value || typeof value !== "object") return undefined;
132
182
  const raw = value as Record<string, unknown>;
133
183
  if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
134
184
  || typeof raw.body !== "string" || typeof raw.confidence !== "number") return undefined;
185
+ if (raw.durability !== "stable") return undefined;
135
186
  if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence) return undefined;
136
187
  const explicitUserDirective = raw.explicitUserDirective === true;
137
188
  const scope: KnowledgeScope = raw.scope === "global" && explicitUserDirective ? "global" : `project:${projectKey}`;
138
189
  const body = compactKnowledgeText(raw.body, 20_000);
139
190
  const evidence = asStringArray(raw.evidence, 8);
140
191
  if (!body || evidence.length === 0) return undefined;
192
+ if (contradictsVerifiedFailures(body, validationText)) return undefined;
141
193
  const requestedTrack: KnowledgeTrack = raw.storageHint === "rule" ? "rule" : "topic";
142
194
  const ruleEligible = body.length <= STORAGE.maxRuleChars && body.split("\n").length <= STORAGE.maxRuleFileLines
195
+ && body.split(/\n\s*\n/gu).length === 1
143
196
  && (explicitUserDirective || raw.confidence >= 0.9);
197
+ const targetId = typeof raw.targetId === "string" && SAFE_ID_RE.test(raw.targetId) ? raw.targetId : undefined;
144
198
  return {
145
- key: compactKnowledgeText(raw.key, 160), title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
146
- summary: compactKnowledgeText(raw.summary, STORAGE.maxSummaryChars),
199
+ key: compactKnowledgeText(raw.key, 160), targetId,
200
+ title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
201
+ summary: compactKnowledgeSummary(raw.summary, STORAGE.maxSummaryChars),
147
202
  keywords: asStringArray(raw.keywords, STORAGE.maxKeywordCount).map((keyword) => keyword.toLowerCase()),
148
203
  scope, body, evidence, confidence: Math.min(1, raw.confidence),
149
204
  storageHint: requestedTrack === "rule" && ruleEligible ? "rule" : "topic",
150
205
  action: raw.action === "revise" ? "revise" : raw.action === "reinforce" ? "reinforce" : "add",
151
- explicitUserDirective,
206
+ explicitUserDirective, durability: "stable",
152
207
  };
153
208
  }
154
209
 
@@ -274,13 +329,24 @@ export function commitKnowledgeReview(
274
329
  const catalog: KnowledgeCatalog = structuredClone(current.catalog);
275
330
  const result: PersistKnowledgeResult = { saved: 0, updated: 0, pending: 0, skipped: 0 };
276
331
  for (const value of values.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates)) {
277
- const candidate = normalizeCandidate(value, task.projectKey);
332
+ const candidate = normalizeCandidate(value, task.projectKey, `${task.context ?? ""}\n${task.delta}`);
278
333
  if (!candidate) { result.skipped++; continue; }
279
334
  const fingerprint = normalizedFingerprint(candidate);
280
335
  const hash = contentHash(candidate);
281
- const existingIndex = catalog.items.findIndex((item) => item.fingerprint === fingerprint);
336
+ const targetIndex = candidate.targetId
337
+ ? catalog.items.findIndex((item) => item.id === candidate.targetId && applicable(item, task.projectKey))
338
+ : -1;
339
+ if (candidate.targetId && targetIndex < 0) { result.skipped++; continue; }
340
+ let existingIndex = targetIndex >= 0
341
+ ? targetIndex
342
+ : catalog.items.findIndex((item) => item.fingerprint === fingerprint);
343
+ let semanticReinforcement = false;
344
+ if (existingIndex < 0) {
345
+ existingIndex = semanticDuplicateIndex(candidate, catalog);
346
+ semanticReinforcement = existingIndex >= 0;
347
+ }
282
348
  const existing = existingIndex >= 0 ? catalog.items[existingIndex] : undefined;
283
- if (existing?.contentHash === hash) {
349
+ if (existing && (existing.contentHash === hash || candidate.action === "reinforce" || semanticReinforcement)) {
284
350
  existing.evidenceCount += candidate.evidence.length;
285
351
  existing.updatedAt = new Date().toISOString();
286
352
  result.updated++;
@@ -303,8 +369,9 @@ export function commitKnowledgeReview(
303
369
  const relativeFile = `${track === "rule" ? "rules" : "topics"}/${id}.md`;
304
370
  atomicWrite(join(temporary, relativeFile), renderKnowledgeFile(candidate));
305
371
  const entry: KnowledgeCatalogEntry = {
306
- id, fingerprint, contentHash: hash, title: candidate.title, summary: candidate.summary,
307
- keywords: candidate.keywords, scope: candidate.scope, track, file: relativeFile,
372
+ id, fingerprint: existing?.fingerprint ?? fingerprint, contentHash: hash,
373
+ title: candidate.title, summary: candidate.summary,
374
+ keywords: candidate.keywords, scope: existing?.scope ?? candidate.scope, track, file: relativeFile,
308
375
  evidenceCount: (existing?.evidenceCount ?? 0) + candidate.evidence.length,
309
376
  createdAt: existing?.createdAt ?? now, updatedAt: now,
310
377
  };
@@ -1,9 +1,11 @@
1
1
  export type KnowledgeTrack = "rule" | "topic";
2
2
  export type KnowledgeScope = "global" | `project:${string}`;
3
3
  export type KnowledgeAction = "add" | "reinforce" | "revise";
4
+ export type KnowledgeDurability = "stable";
4
5
 
5
6
  export interface KnowledgeCandidate {
6
7
  key: string;
8
+ targetId?: string;
7
9
  title: string;
8
10
  summary: string;
9
11
  keywords: string[];
@@ -14,6 +16,7 @@ export interface KnowledgeCandidate {
14
16
  storageHint: KnowledgeTrack;
15
17
  action: KnowledgeAction;
16
18
  explicitUserDirective: boolean;
19
+ durability: KnowledgeDurability;
17
20
  }
18
21
 
19
22
  export interface KnowledgeCatalogEntry {
@@ -84,6 +87,7 @@ export interface KnowledgeReviewTask {
84
87
  projectKey: string;
85
88
  firstEntryId: string;
86
89
  lastEntryId: string;
90
+ context?: string;
87
91
  delta: string;
88
92
  deltaDigest: string;
89
93
  fileSize: number;
@@ -31,14 +31,18 @@ export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
31
31
  intervalMs: 60_000,
32
32
  idleMs: 60_000,
33
33
  capabilityPollMs: 5_000,
34
- modelTimeoutMs: 120_000,
35
- maxDeltaChars: 30_000,
36
- maxCandidates: 8,
34
+ modelTimeoutMs: 180_000,
35
+ maxDeltaChars: 12_000,
36
+ maxPriorContextChars: 4_000,
37
+ maxExistingContextItems: 5,
38
+ maxExistingBodyItems: 2,
39
+ maxExistingBodyChars: 600,
40
+ maxCandidates: 3,
37
41
  minimumConfidence: 0.72,
38
42
  }),
39
43
  storage: Object.freeze({
40
- maxRuleChars: 600,
41
- maxRuleFileLines: 40,
44
+ maxRuleChars: 320,
45
+ maxRuleFileLines: 6,
42
46
  maxRulesPromptChars: 12_000,
43
47
  maxMemoryChars: 25_000,
44
48
  maxMemoryLines: 200,
@@ -34,6 +34,7 @@ export interface UserRuntimePaths {
34
34
  knowledgeRuntime: string;
35
35
  knowledgeCoordinatorSocket: string;
36
36
  knowledgeLeader: string;
37
+ knowledgeStatus: string;
37
38
  knowledgeCommitLock: string;
38
39
  knowledgeElectionLock: string;
39
40
  }
@@ -68,6 +69,7 @@ export function userRuntimePaths(home = homedir()): UserRuntimePaths {
68
69
  // Unix-domain socket paths are short on purpose (macOS caps them at roughly 104 bytes).
69
70
  knowledgeCoordinatorSocket: join(root, "knowledge-v3.sock"),
70
71
  knowledgeLeader: join(knowledge, "runtime", "leader.json"),
72
+ knowledgeStatus: join(knowledge, "runtime", "status.json"),
71
73
  knowledgeCommitLock: join(knowledge, "runtime", "commit.lock"),
72
74
  knowledgeElectionLock: join(knowledge, "runtime", "election.lock"),
73
75
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hadooppei/hwcode",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "A customizable terminal coding agent with local-model support and HWCode workflows.",
5
5
  "type": "module",
6
6
  "bin": {