@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.
- package/.pi/dist/lib/knowledge/extractor.js +2 -0
- package/.pi/dist/lib/knowledge/review-worker.js +30 -0
- package/.pi/dist/lib/knowledge/session-scanner.js +128 -6
- package/.pi/dist/lib/knowledge/store.js +92 -29
- package/.pi/dist/lib/runtime/session-state.js +9 -0
- package/.pi/dist/lib/workflows/state.js +159 -0
- package/.pi/dist/lib/working-directory.js +170 -0
- package/.pi/extensions/knowledge.ts +63 -22
- package/.pi/lib/knowledge/extractor.ts +2 -0
- package/.pi/lib/knowledge/matcher.ts +36 -1
- package/.pi/lib/knowledge/review-status.ts +6 -0
- package/.pi/lib/knowledge/review-worker.ts +25 -0
- package/.pi/lib/knowledge/session-scanner.ts +112 -5
- package/.pi/lib/knowledge/store.ts +99 -22
- package/.pi/lib/knowledge/types.ts +13 -0
- package/.pi/lib/knowledge/worker-protocol.ts +1 -0
- package/.pi/lib/working-directory.ts +10 -0
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
|
141
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
162
|
-
const
|
|
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 +
|
|
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
|
-
|
|
181
|
-
|
|
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
|
|
185
|
-
if (raw.durability !== "stable") return
|
|
186
|
-
if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence) return
|
|
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
|
|
192
|
-
|
|
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
|
|
333
|
-
|
|
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) {
|
|
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
|
}
|