@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
|
@@ -8,12 +8,14 @@ Persist only knowledge that is likely to be useful in future sessions:
|
|
|
8
8
|
- reusable architecture, testing, debugging, deployment, or operational knowledge.
|
|
9
9
|
Do not persist task summaries, guesses, temporary IDs, credentials, secrets, private data, or facts recoverable by simply reading the repository.
|
|
10
10
|
Do not persist mutable inventory such as current cloud resources, names, availability, status, timestamps, or account snapshots. Preserve the discovery method or selection rule instead. If useful procedure is mixed with volatile facts, remove the volatile facts.
|
|
11
|
+
Do not persist hypotheses, suspected causes, or statements marked as possible, pending validation, or unconfirmed. Omit them until direct evidence verifies them.
|
|
11
12
|
Treat failed, unsupported, invalid, or corrected operations as negative evidence. Never present an operation as valid when the supplied evidence says it failed; use only a verified alternative or record an explicit warning. Never invent commands or parameters absent from successful evidence.
|
|
12
13
|
Use storageHint "rule" only for one short imperative instruction of at most 240 characters. A rule must not contain incident narration, example resource names, IDs, timestamps, or evidence details. Use "topic" for multi-step SOPs and detailed experience.
|
|
13
14
|
Relevant existing knowledge may be supplied. If a candidate has the same intent as an existing item, do not add a translated, renamed, or reformatted duplicate. Set targetId to that exact existing ID and action to "reinforce" when the existing content remains correct, or "revise" only when the delta explicitly proves a correction. Use targetId null only for genuinely new knowledge.
|
|
14
15
|
Scope may be "global" only for an explicit user directive intended across projects; otherwise use "project".
|
|
15
16
|
The prior validation context is only for checking facts and contradictions; do not persist it again unless the new delta independently reinforces it. Prefer accumulated workflow successfulSteps and failedApproaches over assistant narration.
|
|
16
17
|
Each summary must be one complete sentence of at most 160 characters. Keep each topic body under 2200 characters. Escape line breaks and quotes inside JSON strings. Set durability to "stable" only after removing facts likely to change or be cheaply rediscovered. Return at most 3 candidates.
|
|
18
|
+
The body must not repeat the title as a Markdown H1. The storage renderer supplies the H1. Replace any necessary example resource value with an obvious placeholder instead of a real project, namespace, image, network, or resource name.
|
|
17
19
|
Return: {"candidates":[{"key":"stable semantic key","targetId":"existing-id or null","title":"...","summary":"one complete sentence","keywords":["..."],"scope":"global|project","body":"markdown body","evidence":["verified evidence"],"confidence":0.0,"storageHint":"rule|topic","action":"add|reinforce|revise","explicitUserDirective":false,"durability":"stable"}]}
|
|
18
20
|
Return {"candidates":[]} when nothing meets the threshold.`;
|
|
19
21
|
export function buildKnowledgeExtractionPrompt(projectRoot, delta, context = "", existing = []) {
|
|
@@ -104,6 +104,25 @@ function finishReview(task, outcome, detail) {
|
|
|
104
104
|
reviewAttempts.delete(task.reviewKey);
|
|
105
105
|
publishReviewStatus();
|
|
106
106
|
}
|
|
107
|
+
function deferReview(task, reason) {
|
|
108
|
+
clearTimeout(activeDeadlineTimer);
|
|
109
|
+
activeDeadlineTimer = undefined;
|
|
110
|
+
const attempts = reviewAttempts.get(task.reviewKey) ?? 1;
|
|
111
|
+
if (attempts <= 1)
|
|
112
|
+
reviewAttempts.delete(task.reviewKey);
|
|
113
|
+
else
|
|
114
|
+
reviewAttempts.set(task.reviewKey, attempts - 1);
|
|
115
|
+
if (!reviewStatus)
|
|
116
|
+
return;
|
|
117
|
+
reviewStatus.activeReview = undefined;
|
|
118
|
+
reviewStatus.lastDeferred = {
|
|
119
|
+
reviewKey: task.reviewKey,
|
|
120
|
+
sessionKey: task.sessionKey,
|
|
121
|
+
deferredAt: new Date().toISOString(),
|
|
122
|
+
reason: boundedError(reason),
|
|
123
|
+
};
|
|
124
|
+
publishReviewStatus();
|
|
125
|
+
}
|
|
107
126
|
function removeLeaderMetadata(token) {
|
|
108
127
|
try {
|
|
109
128
|
const current = JSON.parse(readFileSync(paths.knowledgeLeader, "utf8"));
|
|
@@ -355,6 +374,13 @@ function handleReviewResult(message) {
|
|
|
355
374
|
queueMicrotask(() => void scanForReview());
|
|
356
375
|
}
|
|
357
376
|
}
|
|
377
|
+
function handleReviewDeferred(message) {
|
|
378
|
+
const task = activeTask;
|
|
379
|
+
if (!task || message.requestId !== task.requestId || message.leaderToken !== leaderToken || !leaderServer)
|
|
380
|
+
return;
|
|
381
|
+
deferReview(task, message.reason);
|
|
382
|
+
activeTask = undefined;
|
|
383
|
+
}
|
|
358
384
|
const scanTimer = setInterval(() => { void scanForReview(); }, KNOWLEDGE_RUNTIME_DEFAULTS.review.intervalMs);
|
|
359
385
|
scanTimer.unref();
|
|
360
386
|
parentPort.on("message", (message) => {
|
|
@@ -373,6 +399,10 @@ parentPort.on("message", (message) => {
|
|
|
373
399
|
scheduleElection(0);
|
|
374
400
|
return;
|
|
375
401
|
}
|
|
402
|
+
if (message.type === "review_deferred") {
|
|
403
|
+
handleReviewDeferred(message);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
376
406
|
if (message.type === "review_result") {
|
|
377
407
|
handleReviewResult(message);
|
|
378
408
|
return;
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
3
|
-
import { resolve } from "node:path";
|
|
3
|
+
import { resolve, sep } from "node:path";
|
|
4
4
|
import { buildContextEntries, parseSessionEntries, } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { KNOWLEDGE_RUNTIME_DEFAULTS } from "../runtime/defaults.js";
|
|
6
|
+
import { findLatestWorkflowRoot } from "../working-directory.js";
|
|
6
7
|
import { knowledgeDeltaDigest } from "./extractor.js";
|
|
7
8
|
import { sanitizeKnowledgeText } from "./sanitize.js";
|
|
8
9
|
import { projectKnowledgeKey } from "./store.js";
|
|
9
10
|
const FAILURE_EVIDENCE_RE = /(?:\b(?:error|failed|failure|invalid|unsupported|mistyp(?:e|ed)|typo)\b|not supported|失败|错误|不支持|无效)/iu;
|
|
10
11
|
const INJECTED_SKILL_RE = /<skill\b[^>]*>[\s\S]*?<\/skill>/giu;
|
|
12
|
+
const SAFE_KNOWLEDGE_ID_RE = /^[a-z0-9][a-z0-9-]{0,79}$/u;
|
|
13
|
+
const MAX_RECALL_QUERY_CHARS = 4_000;
|
|
14
|
+
const MAX_LOADED_KNOWLEDGE_IDS = 8;
|
|
11
15
|
function hash(value, length = 64) {
|
|
12
16
|
return createHash("sha256").update(value).digest("hex").slice(0, length);
|
|
13
17
|
}
|
|
@@ -97,16 +101,131 @@ function buildValidationContext(entries) {
|
|
|
97
101
|
return selected.sort((left, right) => left.index - right.index).map((piece) => piece.text).join("\n\n")
|
|
98
102
|
.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxPriorContextChars);
|
|
99
103
|
}
|
|
104
|
+
function messageRecord(entry) {
|
|
105
|
+
return entry.type === "message" && entry.message && typeof entry.message === "object"
|
|
106
|
+
? entry.message
|
|
107
|
+
: undefined;
|
|
108
|
+
}
|
|
109
|
+
function loadedKnowledgeIds(entries) {
|
|
110
|
+
const ids = [];
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
const content = messageRecord(entry)?.content;
|
|
113
|
+
if (!Array.isArray(content))
|
|
114
|
+
continue;
|
|
115
|
+
for (const item of content) {
|
|
116
|
+
if (!item || typeof item !== "object")
|
|
117
|
+
continue;
|
|
118
|
+
const call = item;
|
|
119
|
+
if (call.type !== "toolCall" || call.name !== "hwcode_knowledge_lookup"
|
|
120
|
+
|| !call.arguments || typeof call.arguments !== "object")
|
|
121
|
+
continue;
|
|
122
|
+
const id = call.arguments.id;
|
|
123
|
+
if (typeof id !== "string" || !SAFE_KNOWLEDGE_ID_RE.test(id))
|
|
124
|
+
continue;
|
|
125
|
+
const existing = ids.indexOf(id);
|
|
126
|
+
if (existing >= 0)
|
|
127
|
+
ids.splice(existing, 1);
|
|
128
|
+
ids.push(id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return ids.slice(-MAX_LOADED_KNOWLEDGE_IDS);
|
|
132
|
+
}
|
|
133
|
+
function compactRecallValue(value, maxChars = 600) {
|
|
134
|
+
return typeof value === "string" ? sanitizeKnowledgeText(value).replace(/\s+/gu, " ").trim().slice(0, maxChars) : "";
|
|
135
|
+
}
|
|
136
|
+
function workflowRecallLines(entry) {
|
|
137
|
+
if (entry.type !== "custom" || !entry.customType.startsWith("hwcode-workflow")
|
|
138
|
+
|| !entry.data || typeof entry.data !== "object")
|
|
139
|
+
return [];
|
|
140
|
+
const data = entry.data;
|
|
141
|
+
const details = data.details && typeof data.details === "object" ? data.details : undefined;
|
|
142
|
+
const lines = [];
|
|
143
|
+
const request = compactRecallValue(details?.request ?? data.request, 800);
|
|
144
|
+
if (request)
|
|
145
|
+
lines.push(`Task objective: ${request}`);
|
|
146
|
+
const addSteps = (label, value, maxItems) => {
|
|
147
|
+
if (!Array.isArray(value))
|
|
148
|
+
return;
|
|
149
|
+
for (const raw of value.slice(-maxItems)) {
|
|
150
|
+
if (typeof raw === "string") {
|
|
151
|
+
const text = compactRecallValue(raw);
|
|
152
|
+
if (text)
|
|
153
|
+
lines.push(`${label}: ${text}`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (!raw || typeof raw !== "object")
|
|
157
|
+
continue;
|
|
158
|
+
const step = raw;
|
|
159
|
+
const parts = [step.approach, step.intent, step.reason].map((item) => compactRecallValue(item, 320)).filter(Boolean);
|
|
160
|
+
if (typeof step.command === "string")
|
|
161
|
+
parts.push(step.command);
|
|
162
|
+
if (Array.isArray(step.args)) {
|
|
163
|
+
const operation = step.args.slice(0, 2).filter((item) => typeof item === "string")
|
|
164
|
+
.map((item) => compactRecallValue(item, 100)).filter(Boolean).join(" ");
|
|
165
|
+
if (operation)
|
|
166
|
+
parts.push(operation);
|
|
167
|
+
}
|
|
168
|
+
const text = [...new Set(parts)].join("; ");
|
|
169
|
+
if (text)
|
|
170
|
+
lines.push(`${label}: ${text}`);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
addSteps("Verified success", details?.successfulSteps ?? data.successfulSteps, 12);
|
|
174
|
+
addSteps("Verified failure", details?.failedApproaches ?? data.failedApproaches, 8);
|
|
175
|
+
return lines;
|
|
176
|
+
}
|
|
177
|
+
function buildRecallQuery(entries, ids) {
|
|
178
|
+
const lines = [];
|
|
179
|
+
const workflows = entries.map(workflowRecallLines).filter((item) => item.length > 0);
|
|
180
|
+
const workflowLines = workflows.at(-1) ?? [];
|
|
181
|
+
lines.push(...workflowLines.filter((line) => line.startsWith("Task objective:")));
|
|
182
|
+
for (const entry of entries.slice().reverse()) {
|
|
183
|
+
const message = messageRecord(entry);
|
|
184
|
+
if (!message || message.role !== "user")
|
|
185
|
+
continue;
|
|
186
|
+
const text = compactRecallValue(textContent(message.content).replace(INJECTED_SKILL_RE, ""), 800);
|
|
187
|
+
if (text)
|
|
188
|
+
lines.push(`User request: ${text}`);
|
|
189
|
+
if (lines.filter((line) => line.startsWith("User request:")).length >= 2)
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
for (const entry of entries.slice().reverse()) {
|
|
193
|
+
const message = messageRecord(entry);
|
|
194
|
+
if (!message || message.role !== "assistant")
|
|
195
|
+
continue;
|
|
196
|
+
const text = compactRecallValue(textContent(message.content), 1_000);
|
|
197
|
+
if (text) {
|
|
198
|
+
lines.push(`Latest conclusion: ${text}`);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
for (const id of ids)
|
|
203
|
+
lines.push(`Loaded knowledge: ${id}`);
|
|
204
|
+
lines.push(...workflowLines.filter((line) => line.startsWith("Verified failure:")));
|
|
205
|
+
lines.push(...workflowLines.filter((line) => line.startsWith("Verified success:")));
|
|
206
|
+
return lines.join("\n").slice(0, MAX_RECALL_QUERY_CHARS);
|
|
207
|
+
}
|
|
100
208
|
function projectRootForSession(header, branch) {
|
|
209
|
+
const sessionRoot = resolve(header.cwd);
|
|
210
|
+
const workflowRootValue = findLatestWorkflowRoot(branch);
|
|
211
|
+
const workflowRoot = workflowRootValue ? resolve(workflowRootValue) : undefined;
|
|
212
|
+
let workingDirectory = sessionRoot;
|
|
101
213
|
for (const entry of branch.slice().reverse()) {
|
|
102
214
|
if (entry.type !== "custom" || entry.customType !== "hwcode-working-directory"
|
|
103
215
|
|| !entry.data || typeof entry.data !== "object")
|
|
104
216
|
continue;
|
|
105
217
|
const cwd = entry.data.cwd;
|
|
106
|
-
if (typeof cwd
|
|
107
|
-
|
|
218
|
+
if (typeof cwd !== "string" || !cwd)
|
|
219
|
+
continue;
|
|
220
|
+
workingDirectory = resolve(cwd);
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
if (workflowRoot && (workingDirectory === workflowRoot || workingDirectory.startsWith(`${workflowRoot}${sep}`))) {
|
|
224
|
+
return workflowRoot;
|
|
108
225
|
}
|
|
109
|
-
|
|
226
|
+
if (workingDirectory === sessionRoot || workingDirectory.startsWith(`${sessionRoot}${sep}`))
|
|
227
|
+
return sessionRoot;
|
|
228
|
+
return workingDirectory;
|
|
110
229
|
}
|
|
111
230
|
export function discoverSessionFiles(root) {
|
|
112
231
|
const files = [];
|
|
@@ -164,10 +283,12 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
164
283
|
if (pending.length === 0)
|
|
165
284
|
return undefined;
|
|
166
285
|
const delta = buildDelta(pending);
|
|
286
|
+
const loadedIds = loadedKnowledgeIds(pending);
|
|
287
|
+
const recallQuery = buildRecallQuery(pending, loadedIds);
|
|
167
288
|
const context = cursorIndex >= 0 ? buildValidationContext(branch.slice(0, cursorIndex + 1)) : "";
|
|
168
289
|
const firstEntryId = pending[0].id;
|
|
169
290
|
const lastEntryId = pending[pending.length - 1].id;
|
|
170
|
-
const deltaDigest = knowledgeDeltaDigest(delta || `${firstEntryId}\0${lastEntryId}`);
|
|
291
|
+
const deltaDigest = knowledgeDeltaDigest(`${delta || `${firstEntryId}\0${lastEntryId}`}\0${recallQuery}\0${loadedIds.join("\0")}`);
|
|
171
292
|
const sessionFileHash = hash(resolve(sessionFile), 16);
|
|
172
293
|
const sessionKey = hash(header.id, 24);
|
|
173
294
|
const reviewKey = hash(`${header.id}\0${firstEntryId}\0${lastEntryId}\0${deltaDigest}`);
|
|
@@ -175,7 +296,8 @@ export function readReviewTask(sessionFile, cursor, now = Date.now()) {
|
|
|
175
296
|
return {
|
|
176
297
|
requestId: randomUUID(), reviewKey, sessionId: header.id, sessionKey, sessionFileHash,
|
|
177
298
|
projectRoot, projectKey: projectKnowledgeKey(projectRoot),
|
|
178
|
-
firstEntryId, lastEntryId, context, delta,
|
|
299
|
+
firstEntryId, lastEntryId, context, delta, recallQuery, loadedKnowledgeIds: loadedIds,
|
|
300
|
+
deltaDigest, fileSize: stat.size, fileMtimeMs: stat.mtimeMs,
|
|
179
301
|
};
|
|
180
302
|
}
|
|
181
303
|
export function findNextReviewTask(sessionsRoot, reviews, now = Date.now()) {
|
|
@@ -9,6 +9,12 @@ const STORAGE = KNOWLEDGE_RUNTIME_DEFAULTS.storage;
|
|
|
9
9
|
const SAFE_ID_RE = /^[a-z0-9][a-z0-9-]{0,79}$/u;
|
|
10
10
|
const SAFE_GENERATION_RE = /^[0-9TZ-]+-[a-f0-9]{12}$/u;
|
|
11
11
|
const PROJECT_SCOPE_RE = /^project:[a-f0-9]{16}$/u;
|
|
12
|
+
const UNVERIFIED_CLAIM_RE = /(?:待验证|尚未(?:验证|确认)|未经(?:验证|确认)|未(?:经)?证实|推测|猜测|疑似|可能是|可能由于|可能导致|或许|unverified|unconfirmed|speculat(?:e|ive|ion)|hypothes(?:is|ize)|suspect(?:ed|ion)?|possibly|probably)/iu;
|
|
13
|
+
const VOLATILE_ARGUMENT_RE = /--(?:namespace|name|server_name|security_group_id|subnet_id|vpc_id|image_id|project_id)(?:\.\d+)?=(?!<[^>]+>|\$?\{)[^\s`"']+/iu;
|
|
14
|
+
const DISTINCTIVE_COMMON_TERMS = new Set([
|
|
15
|
+
"cloud", "huawei", "huaweicloud", "hcloud", "topic", "rule", "sop", "project", "region", "create",
|
|
16
|
+
"deploy", "deployment", "service", "workflow", "cn", "south",
|
|
17
|
+
]);
|
|
12
18
|
export class KnowledgeCommitBusyError extends Error {
|
|
13
19
|
}
|
|
14
20
|
function emptyCatalog() {
|
|
@@ -120,9 +126,14 @@ function semanticDuplicateScore(candidate, entry) {
|
|
|
120
126
|
return 0;
|
|
121
127
|
const titleScore = containment(comparisonTerms(candidate.title), comparisonTerms(entry.title));
|
|
122
128
|
const keywordScore = containment(comparisonTerms(candidate.keywords.join(" ")), comparisonTerms(entry.keywords.join(" ")));
|
|
123
|
-
if (titleScore
|
|
129
|
+
if (titleScore >= 0.8 && keywordScore >= 0.5)
|
|
130
|
+
return titleScore * 0.7 + keywordScore * 0.3;
|
|
131
|
+
const candidateTerms = comparisonTerms(`${candidate.title} ${candidate.keywords.join(" ")}`);
|
|
132
|
+
const entryTerms = comparisonTerms(`${entry.title} ${entry.keywords.join(" ")}`);
|
|
133
|
+
const distinctiveShared = [...candidateTerms].filter((term) => (entryTerms.has(term) && term.length >= 3 && !DISTINCTIVE_COMMON_TERMS.has(term))).length;
|
|
134
|
+
if (keywordScore < 0.5 || distinctiveShared < 3)
|
|
124
135
|
return 0;
|
|
125
|
-
return
|
|
136
|
+
return keywordScore * 0.7 + Math.min(distinctiveShared / 6, 1) * 0.3;
|
|
126
137
|
}
|
|
127
138
|
function semanticDuplicateIndex(candidate, catalog) {
|
|
128
139
|
let bestIndex = -1;
|
|
@@ -137,16 +148,22 @@ function semanticDuplicateIndex(candidate, catalog) {
|
|
|
137
148
|
return bestIndex;
|
|
138
149
|
}
|
|
139
150
|
function unsupportedOperations(value) {
|
|
140
|
-
return [...value.matchAll(/\bOperation\s+([A-Za-z][A-Za-z0-9]+)\s+is not supported\b/gu)]
|
|
141
|
-
.
|
|
151
|
+
return [...value.matchAll(/\bOperation\s+([A-Za-z][A-Za-z0-9]+)\s+is not supported\b/gu)].map((match) => {
|
|
152
|
+
const suffix = value.slice(match.index + match[0].length, match.index + match[0].length + 500);
|
|
153
|
+
const service = /\bhcloud\s+([A-Za-z][A-Za-z0-9-]*)\s+--help\b/iu.exec(suffix)?.[1]?.toLowerCase();
|
|
154
|
+
return { operation: match[1].toLowerCase(), service };
|
|
155
|
+
});
|
|
142
156
|
}
|
|
143
157
|
function contradictsVerifiedFailures(body, validationText) {
|
|
144
158
|
const normalizedBody = body.toLowerCase();
|
|
145
|
-
for (const
|
|
146
|
-
const
|
|
159
|
+
for (const unsupported of unsupportedOperations(validationText)) {
|
|
160
|
+
const operationExpression = unsupported.service
|
|
161
|
+
? new RegExp(`\\b(?:hcloud\\s+)?${unsupported.service}\\b[^\\n]{0,120}\\b${unsupported.operation}\\b`, "u")
|
|
162
|
+
: new RegExp(`\\b${unsupported.operation}\\b`, "u");
|
|
163
|
+
const index = normalizedBody.search(operationExpression);
|
|
147
164
|
if (index < 0)
|
|
148
165
|
continue;
|
|
149
|
-
const context = normalizedBody.slice(Math.max(0, index - 80), index +
|
|
166
|
+
const context = normalizedBody.slice(Math.max(0, index - 80), index + 240);
|
|
150
167
|
if (!/(?:not supported|unsupported|do not|don't|avoid|不支持|不要|避免)/u.test(context))
|
|
151
168
|
return true;
|
|
152
169
|
}
|
|
@@ -161,40 +178,77 @@ function asStringArray(value, maxItems) {
|
|
|
161
178
|
return [...new Set(value.filter((item) => typeof item === "string")
|
|
162
179
|
.map((item) => compactKnowledgeText(item, 240)).filter(Boolean))].slice(0, maxItems);
|
|
163
180
|
}
|
|
164
|
-
function
|
|
181
|
+
function withoutLeadingTitle(body) {
|
|
182
|
+
return body.replace(/^#\s+[^\n]*(?:\n+|$)/u, "").trim();
|
|
183
|
+
}
|
|
184
|
+
function containsVolatileDetail(body, projectRoot) {
|
|
185
|
+
const projectName = basename(resolve(projectRoot));
|
|
186
|
+
const escapedProjectName = projectName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
187
|
+
const namedProject = projectName.length >= 5
|
|
188
|
+
&& new RegExp(`(?:项目|project)\\s*(?:[::=]|is)?\\s*[\`'\"]?${escapedProjectName}(?![\\p{L}\\p{N}-])`, "iu").test(body);
|
|
189
|
+
return VOLATILE_ARGUMENT_RE.test(body)
|
|
190
|
+
|| namedProject
|
|
191
|
+
|| /\.hwcode\/cloud\/runs\//iu.test(body);
|
|
192
|
+
}
|
|
193
|
+
function normalizeCandidate(value, projectKey, projectRoot, validationText) {
|
|
165
194
|
if (!value || typeof value !== "object")
|
|
166
|
-
return
|
|
195
|
+
return { reason: "invalid-schema" };
|
|
167
196
|
const raw = value;
|
|
168
197
|
if (typeof raw.key !== "string" || typeof raw.title !== "string" || typeof raw.summary !== "string"
|
|
169
198
|
|| typeof raw.body !== "string" || typeof raw.confidence !== "number")
|
|
170
|
-
return
|
|
199
|
+
return { reason: "invalid-schema" };
|
|
171
200
|
if (raw.durability !== "stable")
|
|
172
|
-
return
|
|
201
|
+
return { reason: "unstable" };
|
|
173
202
|
if (raw.confidence < KNOWLEDGE_RUNTIME_DEFAULTS.review.minimumConfidence)
|
|
174
|
-
return
|
|
203
|
+
return { reason: "low-confidence" };
|
|
204
|
+
if (raw.targetId !== undefined && raw.targetId !== null
|
|
205
|
+
&& (typeof raw.targetId !== "string" || !SAFE_ID_RE.test(raw.targetId)))
|
|
206
|
+
return { reason: "unknown-target" };
|
|
175
207
|
const explicitUserDirective = raw.explicitUserDirective === true;
|
|
176
208
|
const scope = raw.scope === "global" && explicitUserDirective ? "global" : `project:${projectKey}`;
|
|
177
|
-
const body = compactKnowledgeText(raw.body, 20_000);
|
|
209
|
+
const body = withoutLeadingTitle(compactKnowledgeText(raw.body, 20_000));
|
|
178
210
|
const evidence = asStringArray(raw.evidence, 8);
|
|
179
211
|
if (!body || evidence.length === 0)
|
|
180
|
-
return
|
|
212
|
+
return { reason: "missing-body-or-evidence" };
|
|
213
|
+
const candidateClaims = `${raw.title}\n${raw.summary}\n${body}`;
|
|
214
|
+
if (!explicitUserDirective && UNVERIFIED_CLAIM_RE.test(candidateClaims))
|
|
215
|
+
return { reason: "unverified-claim" };
|
|
216
|
+
if (containsVolatileDetail(body, projectRoot))
|
|
217
|
+
return { reason: "volatile-detail" };
|
|
181
218
|
if (contradictsVerifiedFailures(body, validationText))
|
|
182
|
-
return
|
|
219
|
+
return { reason: "contradicts-verified-failure" };
|
|
183
220
|
const requestedTrack = raw.storageHint === "rule" ? "rule" : "topic";
|
|
184
221
|
const ruleEligible = body.length <= STORAGE.maxRuleChars && body.split("\n").length <= STORAGE.maxRuleFileLines
|
|
185
222
|
&& body.split(/\n\s*\n/gu).length === 1
|
|
186
223
|
&& (explicitUserDirective || raw.confidence >= 0.9);
|
|
187
224
|
const targetId = typeof raw.targetId === "string" && SAFE_ID_RE.test(raw.targetId) ? raw.targetId : undefined;
|
|
188
|
-
return {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
225
|
+
return { candidate: {
|
|
226
|
+
key: compactKnowledgeText(raw.key, 160), targetId,
|
|
227
|
+
title: compactKnowledgeText(raw.title, STORAGE.maxTitleChars),
|
|
228
|
+
summary: compactKnowledgeSummary(raw.summary, STORAGE.maxSummaryChars),
|
|
229
|
+
keywords: asStringArray(raw.keywords, STORAGE.maxKeywordCount).map((keyword) => keyword.toLowerCase()),
|
|
230
|
+
scope, body, evidence, confidence: Math.min(1, raw.confidence),
|
|
231
|
+
storageHint: requestedTrack === "rule" && ruleEligible ? "rule" : "topic",
|
|
232
|
+
action: raw.action === "revise" ? "revise" : raw.action === "reinforce" ? "reinforce" : "add",
|
|
233
|
+
explicitUserDirective, durability: "stable",
|
|
234
|
+
} };
|
|
235
|
+
}
|
|
236
|
+
function recordSkip(result, reason) {
|
|
237
|
+
result.skipped++;
|
|
238
|
+
result.skippedReasons ??= {};
|
|
239
|
+
result.skippedReasons[reason] = (result.skippedReasons[reason] ?? 0) + 1;
|
|
240
|
+
}
|
|
241
|
+
function writeRejectedPending(directory, reason, value) {
|
|
242
|
+
let serialized;
|
|
243
|
+
try {
|
|
244
|
+
serialized = JSON.stringify(value);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
serialized = String(value);
|
|
248
|
+
}
|
|
249
|
+
const candidate = sanitizeKnowledgeText(serialized).slice(0, 10_000);
|
|
250
|
+
const name = `${Date.now()}-rejected-${reason}-${randomUUID().slice(0, 8)}.json`;
|
|
251
|
+
atomicWrite(join(directory, "pending", name), `${JSON.stringify({ reason, candidate }, null, 2)}\n`);
|
|
198
252
|
}
|
|
199
253
|
function renderKnowledgeFile(candidate) {
|
|
200
254
|
if (candidate.storageHint === "rule")
|
|
@@ -324,19 +378,23 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
324
378
|
copyCurrentContent(current, temporary, home);
|
|
325
379
|
const catalog = structuredClone(current.catalog);
|
|
326
380
|
const result = { saved: 0, updated: 0, pending: 0, skipped: 0 };
|
|
381
|
+
const rejected = [];
|
|
327
382
|
for (const value of values.slice(0, KNOWLEDGE_RUNTIME_DEFAULTS.review.maxCandidates)) {
|
|
328
|
-
const
|
|
329
|
-
if (!candidate) {
|
|
330
|
-
result.
|
|
383
|
+
const normalized = normalizeCandidate(value, task.projectKey, task.projectRoot, `${task.context ?? ""}\n${task.delta}`);
|
|
384
|
+
if (!("candidate" in normalized)) {
|
|
385
|
+
recordSkip(result, normalized.reason);
|
|
386
|
+
rejected.push({ reason: normalized.reason, value });
|
|
331
387
|
continue;
|
|
332
388
|
}
|
|
389
|
+
const candidate = normalized.candidate;
|
|
333
390
|
const fingerprint = normalizedFingerprint(candidate);
|
|
334
391
|
const hash = contentHash(candidate);
|
|
335
392
|
const targetIndex = candidate.targetId
|
|
336
393
|
? catalog.items.findIndex((item) => item.id === candidate.targetId && applicable(item, task.projectKey))
|
|
337
394
|
: -1;
|
|
338
395
|
if (candidate.targetId && targetIndex < 0) {
|
|
339
|
-
result
|
|
396
|
+
recordSkip(result, "unknown-target");
|
|
397
|
+
rejected.push({ reason: "unknown-target", value });
|
|
340
398
|
continue;
|
|
341
399
|
}
|
|
342
400
|
let existingIndex = targetIndex >= 0
|
|
@@ -386,6 +444,11 @@ export function commitKnowledgeReview(values, task, leaderToken, home = homedir(
|
|
|
386
444
|
result.saved++;
|
|
387
445
|
}
|
|
388
446
|
}
|
|
447
|
+
if (result.skipped > 0 && result.saved === 0 && result.updated === 0 && result.pending === 0) {
|
|
448
|
+
for (const item of rejected)
|
|
449
|
+
writeRejectedPending(temporary, item.reason, item.value);
|
|
450
|
+
result.pending += rejected.length;
|
|
451
|
+
}
|
|
389
452
|
catalog.updatedAt = new Date().toISOString();
|
|
390
453
|
const manifest = {
|
|
391
454
|
version: 3, generationId, createdAt: new Date().toISOString(), leaderToken, catalog,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function findLatestState(entries, codec) {
|
|
2
|
+
for (let index = entries.length - 1; index >= 0; index--) {
|
|
3
|
+
const entry = entries[index];
|
|
4
|
+
if (entry.type !== "custom" || entry.customType !== codec.customType)
|
|
5
|
+
continue;
|
|
6
|
+
return { found: true, value: codec.decode(entry.data) };
|
|
7
|
+
}
|
|
8
|
+
return { found: false };
|
|
9
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { findLatestState } from "../runtime/session-state.js";
|
|
2
|
+
export const WORKFLOW_STATE_TYPE = "hwcode-workflow-state";
|
|
3
|
+
export const WORKFLOW_EXTERNAL_AUDIT_TYPE = "hwcode-workflow-external-approval";
|
|
4
|
+
function isCloudTemplateSource(value) {
|
|
5
|
+
if (!value || typeof value !== "object")
|
|
6
|
+
return false;
|
|
7
|
+
const data = value;
|
|
8
|
+
return typeof data.id === "string"
|
|
9
|
+
&& typeof data.name === "string"
|
|
10
|
+
&& typeof data.createdAt === "string"
|
|
11
|
+
&& typeof data.updatedAt === "string";
|
|
12
|
+
}
|
|
13
|
+
function isCloudRunnerSummary(value) {
|
|
14
|
+
if (!value || typeof value !== "object")
|
|
15
|
+
return false;
|
|
16
|
+
const data = value;
|
|
17
|
+
return typeof data.id === "string"
|
|
18
|
+
&& typeof data.name === "string"
|
|
19
|
+
&& typeof data.vendor === "string"
|
|
20
|
+
&& typeof data.region === "string"
|
|
21
|
+
&& typeof data.host === "string"
|
|
22
|
+
&& Number.isInteger(data.port)
|
|
23
|
+
&& typeof data.user === "string"
|
|
24
|
+
&& typeof data.remoteRoot === "string"
|
|
25
|
+
&& typeof data.hostKeyFingerprint === "string"
|
|
26
|
+
&& ["instance-role", "agency", "ssh-only"].includes(data.identityType);
|
|
27
|
+
}
|
|
28
|
+
function isTerraformRunState(value) {
|
|
29
|
+
if (!value || typeof value !== "object")
|
|
30
|
+
return false;
|
|
31
|
+
const data = value;
|
|
32
|
+
const summary = data.planSummary;
|
|
33
|
+
return typeof data.runId === "string"
|
|
34
|
+
&& typeof data.runnerId === "string"
|
|
35
|
+
&& ["synced", "validated", "planned", "applied", "failed"].includes(data.phase)
|
|
36
|
+
&& typeof data.sourceDigest === "string"
|
|
37
|
+
&& typeof data.remoteWorkspace === "string"
|
|
38
|
+
&& typeof data.startedAt === "string"
|
|
39
|
+
&& typeof data.updatedAt === "string"
|
|
40
|
+
&& (data.planDigest === undefined || typeof data.planDigest === "string")
|
|
41
|
+
&& (summary === undefined || (summary !== null && typeof summary === "object"
|
|
42
|
+
&& Object.values(summary).every((entry) => Number.isInteger(entry))));
|
|
43
|
+
}
|
|
44
|
+
function isCloudDetails(value) {
|
|
45
|
+
if (!value || typeof value !== "object")
|
|
46
|
+
return false;
|
|
47
|
+
const data = value;
|
|
48
|
+
return typeof data.vendor === "string"
|
|
49
|
+
&& typeof data.deployCurrentProject === "boolean"
|
|
50
|
+
&& typeof data.request === "string"
|
|
51
|
+
&& typeof data.allowNonDeleteChanges === "boolean"
|
|
52
|
+
&& Array.isArray(data.failedApproaches)
|
|
53
|
+
&& Array.isArray(data.successfulSteps)
|
|
54
|
+
&& (data.resources === undefined || (Array.isArray(data.resources) && data.resources.every((resource) => {
|
|
55
|
+
if (!resource || typeof resource !== "object")
|
|
56
|
+
return false;
|
|
57
|
+
const entry = resource;
|
|
58
|
+
return typeof entry.id === "string" && typeof entry.type === "string" && typeof entry.region === "string"
|
|
59
|
+
&& ["existing", "workflow-created"].includes(entry.ownership)
|
|
60
|
+
&& ["active", "deleted"].includes(entry.status)
|
|
61
|
+
&& typeof entry.updatedAt === "string";
|
|
62
|
+
})))
|
|
63
|
+
&& typeof data.terminalFailure === "boolean"
|
|
64
|
+
&& (data.artifactDirectory === undefined || typeof data.artifactDirectory === "string")
|
|
65
|
+
&& (data.templateGuidance === undefined || typeof data.templateGuidance === "string")
|
|
66
|
+
&& (data.terraformSourcePath === undefined || typeof data.terraformSourcePath === "string")
|
|
67
|
+
&& (data.sourceTemplate === undefined || isCloudTemplateSource(data.sourceTemplate))
|
|
68
|
+
&& (data.runner === undefined || isCloudRunnerSummary(data.runner))
|
|
69
|
+
&& (data.runnerPreference === undefined || ["automatic", "deferred", "existing"].includes(data.runnerPreference))
|
|
70
|
+
&& (data.terraformRun === undefined || isTerraformRunState(data.terraformRun));
|
|
71
|
+
}
|
|
72
|
+
export function decodeWorkflowState(value) {
|
|
73
|
+
if (!value || typeof value !== "object")
|
|
74
|
+
return undefined;
|
|
75
|
+
const data = value;
|
|
76
|
+
if (data.version === 2) {
|
|
77
|
+
if (!["active", "completed", "cancelled", "failed"].includes(data.status)
|
|
78
|
+
|| !["vibe", "sdd", "cloud"].includes(data.mode)
|
|
79
|
+
|| typeof data.root !== "string"
|
|
80
|
+
|| typeof data.phase !== "string"
|
|
81
|
+
|| typeof data.activatedAt !== "string"
|
|
82
|
+
|| typeof data.updatedAt !== "string")
|
|
83
|
+
return undefined;
|
|
84
|
+
if (data.mode === "cloud" && !isCloudDetails(data.details))
|
|
85
|
+
return undefined;
|
|
86
|
+
if (data.mode === "sdd" && data.sdd !== undefined) {
|
|
87
|
+
const sdd = data.sdd;
|
|
88
|
+
if (!["discovery", "requirements", "design", "test-plan", "tasks", "tests", "implementation", "verification"].includes(sdd.phase)
|
|
89
|
+
|| !Array.isArray(sdd.approvals))
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return data;
|
|
93
|
+
}
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
export const WORKFLOW_STATE_CODEC = {
|
|
97
|
+
customType: WORKFLOW_STATE_TYPE,
|
|
98
|
+
decode: decodeWorkflowState,
|
|
99
|
+
};
|
|
100
|
+
export function restoreWorkflowState(entries) {
|
|
101
|
+
return findLatestState(entries, WORKFLOW_STATE_CODEC).value;
|
|
102
|
+
}
|
|
103
|
+
export function activeWorkflow(entries) {
|
|
104
|
+
const state = restoreWorkflowState(entries);
|
|
105
|
+
return state?.status === "active" ? state : undefined;
|
|
106
|
+
}
|
|
107
|
+
export function cloudDetails(state) {
|
|
108
|
+
return state.mode === "cloud" ? state.details : undefined;
|
|
109
|
+
}
|
|
110
|
+
export function createWorkflowState(mode, root, phase = "activated") {
|
|
111
|
+
const now = new Date().toISOString();
|
|
112
|
+
return {
|
|
113
|
+
version: 2,
|
|
114
|
+
status: "active",
|
|
115
|
+
mode,
|
|
116
|
+
root,
|
|
117
|
+
phase,
|
|
118
|
+
activatedAt: now,
|
|
119
|
+
updatedAt: now,
|
|
120
|
+
...(mode === "sdd" ? { sdd: { phase: "discovery", approvals: [] } } : {}),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function createCloudWorkflowState(root, vendor, deployCurrentProject, request, template, artifactDirectory) {
|
|
124
|
+
const now = new Date().toISOString();
|
|
125
|
+
return {
|
|
126
|
+
version: 2,
|
|
127
|
+
status: "active",
|
|
128
|
+
mode: "cloud",
|
|
129
|
+
root,
|
|
130
|
+
phase: "connected",
|
|
131
|
+
activatedAt: now,
|
|
132
|
+
updatedAt: now,
|
|
133
|
+
details: {
|
|
134
|
+
vendor,
|
|
135
|
+
deployCurrentProject,
|
|
136
|
+
request,
|
|
137
|
+
allowNonDeleteChanges: false,
|
|
138
|
+
failedApproaches: [],
|
|
139
|
+
successfulSteps: [],
|
|
140
|
+
resources: [],
|
|
141
|
+
terminalFailure: false,
|
|
142
|
+
...(artifactDirectory ? { artifactDirectory } : {}),
|
|
143
|
+
...(template ? {
|
|
144
|
+
templateGuidance: template.guidance,
|
|
145
|
+
sourceTemplate: template.source,
|
|
146
|
+
} : {}),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
export function updateWorkflowState(state, changes) {
|
|
151
|
+
return { ...state, ...changes, version: 2, updatedAt: new Date().toISOString() };
|
|
152
|
+
}
|
|
153
|
+
export function workflowLabel(mode) {
|
|
154
|
+
if (mode === "vibe")
|
|
155
|
+
return "HWCode Vibe";
|
|
156
|
+
if (mode === "sdd")
|
|
157
|
+
return "HWCode SDD";
|
|
158
|
+
return "HWCode Cloud";
|
|
159
|
+
}
|