@hadooppei/hwcode 1.0.11 → 1.0.13
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 +17 -6
- package/.pi/dist/lib/knowledge/review-worker.js +30 -0
- package/.pi/dist/lib/knowledge/session-scanner.js +62 -13
- package/.pi/dist/lib/knowledge/store.js +128 -22
- package/.pi/dist/lib/runtime/defaults.js +8 -4
- 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 +58 -10
- package/.pi/lib/knowledge/extractor.ts +31 -6
- 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 +57 -12
- package/.pi/lib/knowledge/store.ts +126 -16
- package/.pi/lib/knowledge/types.ts +11 -0
- package/.pi/lib/knowledge/worker-protocol.ts +1 -0
- package/.pi/lib/runtime/defaults.ts +8 -4
- package/.pi/lib/working-directory.ts +10 -0
- package/package.json +1 -1
|
@@ -11,7 +11,8 @@ 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;
|
|
@@ -117,6 +118,67 @@ function normalizedFingerprint(candidate: KnowledgeCandidate): string {
|
|
|
117
118
|
return createHash("sha256").update(`${candidate.scope}\0${candidate.key.trim().toLowerCase()}`).digest("hex");
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
function comparisonTerms(value: string): Set<string> {
|
|
122
|
+
return new Set((value.normalize("NFKC").toLowerCase().match(/\p{Script=Han}+|[\p{L}\p{N}]+/gu) ?? [])
|
|
123
|
+
.filter((term) => term.length >= 2));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function containment(left: Set<string>, right: Set<string>): number {
|
|
127
|
+
const minimum = Math.min(left.size, right.size);
|
|
128
|
+
if (minimum === 0) return 0;
|
|
129
|
+
let shared = 0;
|
|
130
|
+
for (const term of left) if (right.has(term)) shared++;
|
|
131
|
+
return shared / minimum;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function semanticDuplicateScore(candidate: KnowledgeCandidate, entry: KnowledgeCatalogEntry): number {
|
|
135
|
+
if (candidate.scope !== entry.scope || candidate.storageHint !== entry.track) return 0;
|
|
136
|
+
const titleScore = containment(comparisonTerms(candidate.title), comparisonTerms(entry.title));
|
|
137
|
+
const keywordScore = containment(
|
|
138
|
+
comparisonTerms(candidate.keywords.join(" ")),
|
|
139
|
+
comparisonTerms(entry.keywords.join(" ")),
|
|
140
|
+
);
|
|
141
|
+
if (titleScore < 0.8 || keywordScore < 0.5) return 0;
|
|
142
|
+
return titleScore * 0.7 + keywordScore * 0.3;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function semanticDuplicateIndex(candidate: KnowledgeCandidate, catalog: KnowledgeCatalog): number {
|
|
146
|
+
let bestIndex = -1;
|
|
147
|
+
let bestScore = 0;
|
|
148
|
+
for (const [index, entry] of catalog.items.entries()) {
|
|
149
|
+
const score = semanticDuplicateScore(candidate, entry);
|
|
150
|
+
if (score > bestScore) { bestIndex = index; bestScore = score; }
|
|
151
|
+
}
|
|
152
|
+
return bestIndex;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
interface UnsupportedOperation {
|
|
156
|
+
operation: string;
|
|
157
|
+
service?: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function unsupportedOperations(value: string): UnsupportedOperation[] {
|
|
161
|
+
return [...value.matchAll(/\bOperation\s+([A-Za-z][A-Za-z0-9]+)\s+is not supported\b/gu)].map((match) => {
|
|
162
|
+
const suffix = value.slice(match.index + match[0].length, match.index + match[0].length + 500);
|
|
163
|
+
const service = /\bhcloud\s+([A-Za-z][A-Za-z0-9-]*)\s+--help\b/iu.exec(suffix)?.[1]?.toLowerCase();
|
|
164
|
+
return { operation: match[1]!.toLowerCase(), service };
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function contradictsVerifiedFailures(body: string, validationText: string): boolean {
|
|
169
|
+
const normalizedBody = body.toLowerCase();
|
|
170
|
+
for (const unsupported of unsupportedOperations(validationText)) {
|
|
171
|
+
const operationExpression = unsupported.service
|
|
172
|
+
? new RegExp(`\\b(?:hcloud\\s+)?${unsupported.service}\\b[^\\n]{0,120}\\b${unsupported.operation}\\b`, "u")
|
|
173
|
+
: new RegExp(`\\b${unsupported.operation}\\b`, "u");
|
|
174
|
+
const index = normalizedBody.search(operationExpression);
|
|
175
|
+
if (index < 0) continue;
|
|
176
|
+
const context = normalizedBody.slice(Math.max(0, index - 80), index + 240);
|
|
177
|
+
if (!/(?:not supported|unsupported|do not|don't|avoid|不支持|不要|避免)/u.test(context)) return true;
|
|
178
|
+
}
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
|
|
120
182
|
function contentHash(candidate: KnowledgeCandidate): string {
|
|
121
183
|
return createHash("sha256").update(`${candidate.title}\0${candidate.summary}\0${candidate.body}`).digest("hex");
|
|
122
184
|
}
|
|
@@ -127,30 +189,52 @@ function asStringArray(value: unknown, maxItems: number): string[] {
|
|
|
127
189
|
.map((item) => compactKnowledgeText(item, 240)).filter(Boolean))].slice(0, maxItems);
|
|
128
190
|
}
|
|
129
191
|
|
|
130
|
-
|
|
131
|
-
|
|
192
|
+
type CandidateNormalization = { candidate: KnowledgeCandidate } | { reason: KnowledgeSkipReason };
|
|
193
|
+
|
|
194
|
+
function normalizeCandidate(value: unknown, projectKey: string, validationText: string): CandidateNormalization {
|
|
195
|
+
if (!value || typeof value !== "object") return { reason: "invalid-schema" };
|
|
132
196
|
const raw = value as Record<string, unknown>;
|
|
133
197
|
if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
|
|
134
|
-
|| typeof raw.body !== "string" || typeof raw.confidence !== "number") return
|
|
135
|
-
if (raw.durability !== "stable") return
|
|
136
|
-
if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence) return
|
|
198
|
+
|| typeof raw.body !== "string" || typeof raw.confidence !== "number") return { reason: "invalid-schema" };
|
|
199
|
+
if (raw.durability !== "stable") return { reason: "unstable" };
|
|
200
|
+
if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence) return { reason: "low-confidence" };
|
|
201
|
+
if (raw.targetId !== undefined && raw.targetId !== null
|
|
202
|
+
&& (typeof raw.targetId !== "string" || !SAFE_ID_RE.test(raw.targetId))) return { reason: "unknown-target" };
|
|
137
203
|
const explicitUserDirective = raw.explicitUserDirective === true;
|
|
138
204
|
const scope: KnowledgeScope = raw.scope === "global" && explicitUserDirective ? "global" : `project:${projectKey}`;
|
|
139
205
|
const body = compactKnowledgeText(raw.body, 20_000);
|
|
140
206
|
const evidence = asStringArray(raw.evidence, 8);
|
|
141
|
-
if (!body || evidence.length === 0) return
|
|
207
|
+
if (!body || evidence.length === 0) return { reason: "missing-body-or-evidence" };
|
|
208
|
+
if (contradictsVerifiedFailures(body, validationText)) return { reason: "contradicts-verified-failure" };
|
|
142
209
|
const requestedTrack: KnowledgeTrack = raw.storageHint === "rule" ? "rule" : "topic";
|
|
143
210
|
const ruleEligible = body.length <= STORAGE.maxRuleChars && body.split("\n").length <= STORAGE.maxRuleFileLines
|
|
211
|
+
&& body.split(/\n\s*\n/gu).length === 1
|
|
144
212
|
&& (explicitUserDirective || raw.confidence >= 0.9);
|
|
145
|
-
|
|
146
|
-
|
|
213
|
+
const targetId = typeof raw.targetId === "string" && SAFE_ID_RE.test(raw.targetId) ? raw.targetId : undefined;
|
|
214
|
+
return { candidate: {
|
|
215
|
+
key: compactKnowledgeText(raw.key, 160), targetId,
|
|
216
|
+
title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
|
|
147
217
|
summary: compactKnowledgeSummary(raw.summary, STORAGE.maxSummaryChars),
|
|
148
218
|
keywords: asStringArray(raw.keywords, STORAGE.maxKeywordCount).map((keyword) => keyword.toLowerCase()),
|
|
149
219
|
scope, body, evidence, confidence: Math.min(1, raw.confidence),
|
|
150
220
|
storageHint: requestedTrack === "rule" && ruleEligible ? "rule" : "topic",
|
|
151
221
|
action: raw.action === "revise" ? "revise" : raw.action === "reinforce" ? "reinforce" : "add",
|
|
152
222
|
explicitUserDirective, durability: "stable",
|
|
153
|
-
};
|
|
223
|
+
} };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function recordSkip(result: PersistKnowledgeResult, reason: KnowledgeSkipReason): void {
|
|
227
|
+
result.skipped++;
|
|
228
|
+
result.skippedReasons ??= {};
|
|
229
|
+
result.skippedReasons[reason] = (result.skippedReasons[reason] ?? 0) + 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function writeRejectedPending(directory: string, reason: KnowledgeSkipReason, value: unknown): void {
|
|
233
|
+
let serialized: string;
|
|
234
|
+
try { serialized = JSON.stringify(value); } catch { serialized = String(value); }
|
|
235
|
+
const candidate = sanitizeKnowledgeText(serialized).slice(0, 10_000);
|
|
236
|
+
const name = `${Date.now()}-rejected-${reason}-${randomUUID().slice(0, 8)}.json`;
|
|
237
|
+
atomicWrite(join(directory, "pending", name), `${JSON.stringify({ reason, candidate }, null, 2)}\n`);
|
|
154
238
|
}
|
|
155
239
|
|
|
156
240
|
function renderKnowledgeFile(candidate: KnowledgeCandidate): string {
|
|
@@ -274,14 +358,35 @@ export function commitKnowledgeReview(
|
|
|
274
358
|
copyCurrentContent(current, temporary, home);
|
|
275
359
|
const catalog: KnowledgeCatalog = structuredClone(current.catalog);
|
|
276
360
|
const result: PersistKnowledgeResult = { saved: 0, updated: 0, pending: 0, skipped: 0 };
|
|
361
|
+
const rejected: Array<{ reason: KnowledgeSkipReason; value: unknown }> = [];
|
|
277
362
|
for (const value of values.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates)) {
|
|
278
|
-
const
|
|
279
|
-
if (!candidate) {
|
|
363
|
+
const normalized = normalizeCandidate(value, task.projectKey, `${task.context ?? ""}\n${task.delta}`);
|
|
364
|
+
if (!("candidate" in normalized)) {
|
|
365
|
+
recordSkip(result, normalized.reason);
|
|
366
|
+
rejected.push({ reason: normalized.reason, value });
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const candidate = normalized.candidate;
|
|
280
370
|
const fingerprint = normalizedFingerprint(candidate);
|
|
281
371
|
const hash = contentHash(candidate);
|
|
282
|
-
const
|
|
372
|
+
const targetIndex = candidate.targetId
|
|
373
|
+
? catalog.items.findIndex((item) => item.id === candidate.targetId && applicable(item, task.projectKey))
|
|
374
|
+
: -1;
|
|
375
|
+
if (candidate.targetId && targetIndex < 0) {
|
|
376
|
+
recordSkip(result, "unknown-target");
|
|
377
|
+
rejected.push({ reason: "unknown-target", value });
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
let existingIndex = targetIndex >= 0
|
|
381
|
+
? targetIndex
|
|
382
|
+
: catalog.items.findIndex((item) => item.fingerprint === fingerprint);
|
|
383
|
+
let semanticReinforcement = false;
|
|
384
|
+
if (existingIndex < 0) {
|
|
385
|
+
existingIndex = semanticDuplicateIndex(candidate, catalog);
|
|
386
|
+
semanticReinforcement = existingIndex >= 0;
|
|
387
|
+
}
|
|
283
388
|
const existing = existingIndex >= 0 ? catalog.items[existingIndex] : undefined;
|
|
284
|
-
if (existing
|
|
389
|
+
if (existing && (existing.contentHash === hash || candidate.action === "reinforce" || semanticReinforcement)) {
|
|
285
390
|
existing.evidenceCount += candidate.evidence.length;
|
|
286
391
|
existing.updatedAt = new Date().toISOString();
|
|
287
392
|
result.updated++;
|
|
@@ -304,14 +409,19 @@ export function commitKnowledgeReview(
|
|
|
304
409
|
const relativeFile = `${track === "rule" ? "rules" : "topics"}/${id}.md`;
|
|
305
410
|
atomicWrite(join(temporary, relativeFile), renderKnowledgeFile(candidate));
|
|
306
411
|
const entry: KnowledgeCatalogEntry = {
|
|
307
|
-
id, fingerprint
|
|
308
|
-
|
|
412
|
+
id, fingerprint: existing?.fingerprint ?? fingerprint, contentHash: hash,
|
|
413
|
+
title: candidate.title, summary: candidate.summary,
|
|
414
|
+
keywords: candidate.keywords, scope: existing?.scope ?? candidate.scope, track, file: relativeFile,
|
|
309
415
|
evidenceCount: (existing?.evidenceCount ?? 0) + candidate.evidence.length,
|
|
310
416
|
createdAt: existing?.createdAt ?? now, updatedAt: now,
|
|
311
417
|
};
|
|
312
418
|
if (existingIndex >= 0) { catalog.items[existingIndex] = entry; result.updated++; }
|
|
313
419
|
else { catalog.items.push(entry); result.saved++; }
|
|
314
420
|
}
|
|
421
|
+
if (result.skipped > 0 && result.saved === 0 && result.updated === 0 && result.pending === 0) {
|
|
422
|
+
for (const item of rejected) writeRejectedPending(temporary, item.reason, item.value);
|
|
423
|
+
result.pending += rejected.length;
|
|
424
|
+
}
|
|
315
425
|
catalog.updatedAt = new Date().toISOString();
|
|
316
426
|
const manifest: KnowledgeManifest = {
|
|
317
427
|
version: 3, generationId, createdAt: new Date().toISOString(), leaderToken, catalog,
|
|
@@ -5,6 +5,7 @@ export type KnowledgeDurability = "stable";
|
|
|
5
5
|
|
|
6
6
|
export interface KnowledgeCandidate {
|
|
7
7
|
key: string;
|
|
8
|
+
targetId?: string;
|
|
8
9
|
title: string;
|
|
9
10
|
summary: string;
|
|
10
11
|
keywords: string[];
|
|
@@ -74,8 +75,17 @@ export interface PersistKnowledgeResult {
|
|
|
74
75
|
updated: number;
|
|
75
76
|
pending: number;
|
|
76
77
|
skipped: number;
|
|
78
|
+
skippedReasons?: Partial<Record<KnowledgeSkipReason, number>>;
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
export type KnowledgeSkipReason =
|
|
82
|
+
| "invalid-schema"
|
|
83
|
+
| "unstable"
|
|
84
|
+
| "low-confidence"
|
|
85
|
+
| "missing-body-or-evidence"
|
|
86
|
+
| "contradicts-verified-failure"
|
|
87
|
+
| "unknown-target";
|
|
88
|
+
|
|
79
89
|
export interface KnowledgeReviewTask {
|
|
80
90
|
requestId: string;
|
|
81
91
|
reviewKey: string;
|
|
@@ -86,6 +96,7 @@ export interface KnowledgeReviewTask {
|
|
|
86
96
|
projectKey: string;
|
|
87
97
|
firstEntryId: string;
|
|
88
98
|
lastEntryId: string;
|
|
99
|
+
context?: string;
|
|
89
100
|
delta: string;
|
|
90
101
|
deltaDigest: string;
|
|
91
102
|
fileSize: 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" };
|
|
@@ -32,13 +32,17 @@ export const KNOWLEDGE_RUNTIME_DEFAULTS = Object.freeze({
|
|
|
32
32
|
idleMs: 60_000,
|
|
33
33
|
capabilityPollMs: 5_000,
|
|
34
34
|
modelTimeoutMs: 180_000,
|
|
35
|
-
maxDeltaChars:
|
|
36
|
-
|
|
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:
|
|
41
|
-
maxRuleFileLines:
|
|
44
|
+
maxRuleChars: 320,
|
|
45
|
+
maxRuleFileLines: 6,
|
|
42
46
|
maxRulesPromptChars: 12_000,
|
|
43
47
|
maxMemoryChars: 25_000,
|
|
44
48
|
maxMemoryLines: 200,
|
|
@@ -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
|
}
|