pi-harness-runtime 1.1.324 → 1.1.326
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/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/dist/policy-loader.d.ts +8 -1
- package/packages/agent-policy/dist/policy-loader.d.ts.map +1 -1
- package/packages/agent-policy/dist/policy-loader.js +23 -5
- package/packages/agent-policy/dist/policy-loader.js.map +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/agent-policy/src/policy-loader.ts +28 -7
- package/packages/agent-policy/test/policy-retention.test.ts +43 -1
- package/packages/agent-policy/test/policy.test.ts +7 -3
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/dist/index.d.ts +132 -0
- package/packages/wiki/dist/index.d.ts.map +1 -0
- package/packages/wiki/dist/index.js +448 -0
- package/packages/wiki/dist/index.js.map +1 -0
- package/packages/wiki/dist/storage.d.ts +142 -0
- package/packages/wiki/dist/storage.d.ts.map +1 -0
- package/packages/wiki/dist/storage.js +603 -0
- package/packages/wiki/dist/storage.js.map +1 -0
- package/packages/wiki/dist/types.d.ts +189 -0
- package/packages/wiki/dist/types.d.ts.map +1 -0
- package/packages/wiki/dist/types.js +30 -0
- package/packages/wiki/dist/types.js.map +1 -0
- package/packages/wiki/package.json +22 -0
- package/packages/wiki/src/index.ts +578 -0
- package/packages/wiki/src/storage.ts +696 -0
- package/packages/wiki/src/types.ts +263 -0
- package/packages/wiki/test/basic.test.ts +242 -0
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wiki Storage - File-based persistence for wiki pages
|
|
3
|
+
*
|
|
4
|
+
* Handles atomic writes, transactions, and consistency.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
existsSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
unlinkSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
statSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { join, dirname, relative } from "node:path";
|
|
18
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
19
|
+
import type {
|
|
20
|
+
WikiConfig,
|
|
21
|
+
WikiPage,
|
|
22
|
+
WikiPageFrontmatter,
|
|
23
|
+
SourceSnapshot,
|
|
24
|
+
WikiOperation,
|
|
25
|
+
PageIndexEntry,
|
|
26
|
+
WikiInitResult,
|
|
27
|
+
WikiOperationType,
|
|
28
|
+
WikiSearchFilters,
|
|
29
|
+
WikiSearchResult,
|
|
30
|
+
LintFinding,
|
|
31
|
+
StagedChange,
|
|
32
|
+
WIKI_DIRS,
|
|
33
|
+
} from "./types.js";
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// Utilities
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compute SHA-256 hash of content
|
|
41
|
+
*/
|
|
42
|
+
export function computeHash(content: string): string {
|
|
43
|
+
return createHash("sha256").update(content, "utf8").digest("hex");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get current ISO timestamp
|
|
48
|
+
*/
|
|
49
|
+
export function now(): string {
|
|
50
|
+
return new Date().toISOString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse YAML frontmatter from markdown content
|
|
55
|
+
*/
|
|
56
|
+
export function parseFrontmatter(
|
|
57
|
+
content: string
|
|
58
|
+
): { frontmatter: WikiPageFrontmatter | null; body: string; raw: string } {
|
|
59
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
60
|
+
|
|
61
|
+
if (!match) {
|
|
62
|
+
return { frontmatter: null, body: content, raw: "" };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const [, yamlStr, body] = match;
|
|
66
|
+
const frontmatter = parseYaml(yamlStr);
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
frontmatter: frontmatter as unknown as WikiPageFrontmatter | null,
|
|
70
|
+
body: body.trim(),
|
|
71
|
+
raw: yamlStr,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Simple YAML parser for frontmatter
|
|
77
|
+
* Handles: strings, booleans, numbers, arrays, nested objects
|
|
78
|
+
*/
|
|
79
|
+
function parseYaml(yamlStr: string): Record<string, unknown> {
|
|
80
|
+
const result: Record<string, unknown> = {};
|
|
81
|
+
const lines = yamlStr.split("\n");
|
|
82
|
+
let currentKey = "";
|
|
83
|
+
let currentIndent = 0;
|
|
84
|
+
const stack: Array<{ obj: Record<string, unknown>; indent: number }> = [];
|
|
85
|
+
|
|
86
|
+
for (const line of lines) {
|
|
87
|
+
const trimmed = line.trim();
|
|
88
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
89
|
+
|
|
90
|
+
const indent = line.search(/\S/);
|
|
91
|
+
const colonIdx = trimmed.indexOf(":");
|
|
92
|
+
|
|
93
|
+
if (colonIdx === -1) continue;
|
|
94
|
+
|
|
95
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
96
|
+
let value: unknown = trimmed.slice(colonIdx + 1).trim();
|
|
97
|
+
|
|
98
|
+
// Pop stack while indent decreases
|
|
99
|
+
while (stack.length > 0 && indent <= stack[stack.length - 1].indent) {
|
|
100
|
+
stack.pop();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parent = stack.length > 0 ? stack[stack.length - 1].obj : result;
|
|
104
|
+
|
|
105
|
+
// Parse value
|
|
106
|
+
if (value === "" || value === "null" || value === "~") {
|
|
107
|
+
value = null;
|
|
108
|
+
} else if (value === "true") {
|
|
109
|
+
value = true;
|
|
110
|
+
} else if (value === "false") {
|
|
111
|
+
value = false;
|
|
112
|
+
} else if (/^-?\d+(\.\d+)?$/.test(value as string)) {
|
|
113
|
+
value = parseFloat(value as string);
|
|
114
|
+
} else if ((value as string).startsWith("[") && (value as string).endsWith("]")) {
|
|
115
|
+
// Array
|
|
116
|
+
const arrContent = (value as string).slice(1, -1);
|
|
117
|
+
value = arrContent
|
|
118
|
+
? arrContent.split(",").map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
|
119
|
+
: [];
|
|
120
|
+
} else {
|
|
121
|
+
// String - remove quotes
|
|
122
|
+
value = (value as string).replace(/^["']|["']$/g, "");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
parent[key] = value;
|
|
126
|
+
currentKey = key;
|
|
127
|
+
currentIndent = indent;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Serialize frontmatter to YAML string
|
|
135
|
+
*/
|
|
136
|
+
export function serializeFrontmatter(
|
|
137
|
+
frontmatter: WikiPageFrontmatter
|
|
138
|
+
): string {
|
|
139
|
+
const lines: string[] = [];
|
|
140
|
+
|
|
141
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
142
|
+
if (value === undefined || value === null) continue;
|
|
143
|
+
|
|
144
|
+
if (Array.isArray(value)) {
|
|
145
|
+
if (value.length === 0) {
|
|
146
|
+
lines.push(`${key}: []`);
|
|
147
|
+
} else if (value.every((v) => typeof v === "object")) {
|
|
148
|
+
// Array of objects
|
|
149
|
+
lines.push(`${key}:`);
|
|
150
|
+
for (const item of value) {
|
|
151
|
+
lines.push(` - ${JSON.stringify(item)}`);
|
|
152
|
+
}
|
|
153
|
+
} else {
|
|
154
|
+
lines.push(`${key}: [${value.join(", ")}]`);
|
|
155
|
+
}
|
|
156
|
+
} else if (typeof value === "object") {
|
|
157
|
+
// Nested object
|
|
158
|
+
lines.push(`${key}:`);
|
|
159
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
160
|
+
lines.push(` ${k}: ${JSON.stringify(v)}`);
|
|
161
|
+
}
|
|
162
|
+
} else if (typeof value === "boolean" || typeof value === "number") {
|
|
163
|
+
lines.push(`${key}: ${value}`);
|
|
164
|
+
} else {
|
|
165
|
+
// String - quote if needed
|
|
166
|
+
const str = String(value);
|
|
167
|
+
if (str.includes(":") || str.includes("#") || str.includes("\n")) {
|
|
168
|
+
lines.push(`${key}: "${str.replace(/"/g, '\\"')}"`);
|
|
169
|
+
} else {
|
|
170
|
+
lines.push(`${key}: ${str}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return lines.join("\n");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Combine frontmatter and body into full page content
|
|
180
|
+
*/
|
|
181
|
+
export function combinePage(
|
|
182
|
+
frontmatter: WikiPageFrontmatter,
|
|
183
|
+
body: string
|
|
184
|
+
): string {
|
|
185
|
+
return `---\n${serializeFrontmatter(frontmatter)}\n---\n\n${body}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Storage Implementation
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Wiki storage manager
|
|
194
|
+
*/
|
|
195
|
+
export class WikiStorage {
|
|
196
|
+
private config: WikiConfig;
|
|
197
|
+
private index: Map<string, PageIndexEntry> = new Map();
|
|
198
|
+
private operations: WikiOperation[] = [];
|
|
199
|
+
private stagedChanges: Map<string, StagedChange> = new Map();
|
|
200
|
+
private dirty = false;
|
|
201
|
+
|
|
202
|
+
constructor(config: WikiConfig) {
|
|
203
|
+
this.config = config;
|
|
204
|
+
this.ensureStructure();
|
|
205
|
+
this.loadIndex();
|
|
206
|
+
this.loadOperations();
|
|
207
|
+
this.loadStaged();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get wiki configuration
|
|
212
|
+
*/
|
|
213
|
+
getConfig(): WikiConfig {
|
|
214
|
+
return this.config;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Initialize wiki structure
|
|
219
|
+
*/
|
|
220
|
+
initialize(): WikiInitResult {
|
|
221
|
+
const existing = existsSync(this.config.wikiRoot);
|
|
222
|
+
if (!existing) {
|
|
223
|
+
mkdirSync(this.config.wikiRoot, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Create subdirectories
|
|
227
|
+
for (const dir of [
|
|
228
|
+
this.config.rawDir,
|
|
229
|
+
this.config.knowledgeDir,
|
|
230
|
+
this.config.harnessDir,
|
|
231
|
+
]) {
|
|
232
|
+
if (!existsSync(dir)) {
|
|
233
|
+
mkdirSync(dir, { recursive: true });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Create schema file if not exists
|
|
238
|
+
if (!existsSync(this.config.schemaFile)) {
|
|
239
|
+
writeFileSync(
|
|
240
|
+
this.config.schemaFile,
|
|
241
|
+
`# Wiki Maintenance Conventions
|
|
242
|
+
|
|
243
|
+
## Page Structure
|
|
244
|
+
|
|
245
|
+
Every wiki page must have:
|
|
246
|
+
|
|
247
|
+
\`\`\`yaml
|
|
248
|
+
---
|
|
249
|
+
id: <uuid>
|
|
250
|
+
title: <human-readable title>
|
|
251
|
+
kind: <concept|decision|incident|plan|verification|reference>
|
|
252
|
+
projectId: <canonical project identifier>
|
|
253
|
+
status: <proposed|implemented|verified|stale|superseded>
|
|
254
|
+
createdAt: <ISO timestamp>
|
|
255
|
+
updatedAt: <ISO timestamp>
|
|
256
|
+
sources: []
|
|
257
|
+
relatedPages: []
|
|
258
|
+
---
|
|
259
|
+
\`\`\`
|
|
260
|
+
|
|
261
|
+
## Status Lifecycle
|
|
262
|
+
|
|
263
|
+
- \`proposed\`: Awaiting synthesis/verification
|
|
264
|
+
- \`implemented\`: Applied but not yet verified
|
|
265
|
+
- \`verified\`: Test/inspection evidence recorded
|
|
266
|
+
- \`stale\`: Source changed, needs review
|
|
267
|
+
- \`superseded\`: Replaced by another page
|
|
268
|
+
|
|
269
|
+
## Source References
|
|
270
|
+
|
|
271
|
+
Always cite immutable snapshots, never live files:
|
|
272
|
+
|
|
273
|
+
\`\`\`yaml
|
|
274
|
+
sources:
|
|
275
|
+
- sourceId: <snapshot-id>
|
|
276
|
+
path: <original path>
|
|
277
|
+
contentHash: <sha256>
|
|
278
|
+
capturedAt: <ISO timestamp>
|
|
279
|
+
redacted: <true|false>
|
|
280
|
+
\`\`\`
|
|
281
|
+
|
|
282
|
+
## Verification
|
|
283
|
+
|
|
284
|
+
A page is \`verified\` only with recorded evidence:
|
|
285
|
+
|
|
286
|
+
\`\`\`yaml
|
|
287
|
+
verification:
|
|
288
|
+
evidence: <test name or inspection reference>
|
|
289
|
+
scope: <what was verified>
|
|
290
|
+
version: <code version>
|
|
291
|
+
\`\`\`
|
|
292
|
+
`,
|
|
293
|
+
"utf8"
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
created: !existing,
|
|
299
|
+
existing,
|
|
300
|
+
config: this.config,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Ensure directory structure exists
|
|
306
|
+
*/
|
|
307
|
+
private ensureStructure(): void {
|
|
308
|
+
if (!existsSync(this.config.wikiRoot)) {
|
|
309
|
+
mkdirSync(this.config.wikiRoot, { recursive: true });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Load page index from disk
|
|
315
|
+
*/
|
|
316
|
+
private loadIndex(): void {
|
|
317
|
+
this.index.clear();
|
|
318
|
+
if (!existsSync(this.config.indexFile)) return;
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const content = readFileSync(this.config.indexFile, "utf8");
|
|
322
|
+
const entries: PageIndexEntry[] = JSON.parse(content);
|
|
323
|
+
for (const entry of entries) {
|
|
324
|
+
this.index.set(entry.id, entry);
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
// Invalid index, rebuild
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Save page index to disk (atomic)
|
|
333
|
+
*/
|
|
334
|
+
private saveIndex(): void {
|
|
335
|
+
const entries = Array.from(this.index.values());
|
|
336
|
+
const content = JSON.stringify(entries, null, 2);
|
|
337
|
+
const tmpFile = this.config.indexFile + ".tmp";
|
|
338
|
+
writeFileSync(tmpFile, content, "utf8");
|
|
339
|
+
renameSync(tmpFile, this.config.indexFile);
|
|
340
|
+
this.dirty = false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Load operations from log
|
|
345
|
+
*/
|
|
346
|
+
private loadOperations(): void {
|
|
347
|
+
this.operations = [];
|
|
348
|
+
if (!existsSync(this.config.logFile)) return;
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const content = readFileSync(this.config.logFile, "utf8");
|
|
352
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
353
|
+
for (const line of lines) {
|
|
354
|
+
try {
|
|
355
|
+
this.operations.push(JSON.parse(line));
|
|
356
|
+
} catch {
|
|
357
|
+
// Skip invalid lines
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} catch {
|
|
361
|
+
// No log yet
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Append operation to log
|
|
367
|
+
*/
|
|
368
|
+
private appendOperation(op: WikiOperation): void {
|
|
369
|
+
const line = JSON.stringify(op) + "\n";
|
|
370
|
+
writeFileSync(this.config.logFile, line, { flag: "a", encoding: "utf8" });
|
|
371
|
+
this.operations.push(op);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Load staged changes
|
|
376
|
+
*/
|
|
377
|
+
private loadStaged(): void {
|
|
378
|
+
this.stagedChanges.clear();
|
|
379
|
+
const stagedFile = join(this.config.harnessDir, "staged.json");
|
|
380
|
+
if (!existsSync(stagedFile)) return;
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const content = readFileSync(stagedFile, "utf8");
|
|
384
|
+
const changes: StagedChange[] = JSON.parse(content);
|
|
385
|
+
for (const change of changes) {
|
|
386
|
+
this.stagedChanges.set(change.pageId, change);
|
|
387
|
+
}
|
|
388
|
+
} catch {
|
|
389
|
+
// Invalid staged file
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Save staged changes (atomic)
|
|
395
|
+
*/
|
|
396
|
+
private saveStaged(): void {
|
|
397
|
+
const changes = Array.from(this.stagedChanges.values());
|
|
398
|
+
const stagedFile = join(this.config.harnessDir, "staged.json");
|
|
399
|
+
const content = JSON.stringify(changes, null, 2);
|
|
400
|
+
const tmpFile = stagedFile + ".tmp";
|
|
401
|
+
writeFileSync(tmpFile, content, "utf8");
|
|
402
|
+
renameSync(tmpFile, stagedFile);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ========================================================================
|
|
406
|
+
// Source Snapshot Operations
|
|
407
|
+
// ========================================================================
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Create immutable source snapshot
|
|
411
|
+
*/
|
|
412
|
+
createSnapshot(
|
|
413
|
+
originalPath: string,
|
|
414
|
+
content: string,
|
|
415
|
+
operationId: string
|
|
416
|
+
): SourceSnapshot {
|
|
417
|
+
const id = `snap-${randomUUID().slice(0, 8)}`;
|
|
418
|
+
const snapshot: SourceSnapshot = {
|
|
419
|
+
id,
|
|
420
|
+
originalPath,
|
|
421
|
+
contentHash: computeHash(content),
|
|
422
|
+
capturedAt: now(),
|
|
423
|
+
content,
|
|
424
|
+
redacted: false,
|
|
425
|
+
operationId,
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// Save snapshot file
|
|
429
|
+
const snapFile = join(this.config.rawDir, `${id}.json`);
|
|
430
|
+
writeFileSync(snapFile, JSON.stringify(snapshot, null, 2), "utf8");
|
|
431
|
+
|
|
432
|
+
return snapshot;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Get source snapshot by ID
|
|
437
|
+
*/
|
|
438
|
+
getSnapshot(snapshotId: string): SourceSnapshot | null {
|
|
439
|
+
const snapFile = join(this.config.rawDir, `${snapshotId}.json`);
|
|
440
|
+
if (!existsSync(snapFile)) return null;
|
|
441
|
+
|
|
442
|
+
try {
|
|
443
|
+
const content = readFileSync(snapFile, "utf8");
|
|
444
|
+
return JSON.parse(content);
|
|
445
|
+
} catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ========================================================================
|
|
451
|
+
// Page Operations
|
|
452
|
+
// ========================================================================
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Save a wiki page
|
|
456
|
+
*/
|
|
457
|
+
savePage(page: WikiPage): void {
|
|
458
|
+
const content = combinePage(page.frontmatter, page.body);
|
|
459
|
+
const pageFile = join(this.config.knowledgeDir, `${page.frontmatter.id}.md`);
|
|
460
|
+
const tmpFile = pageFile + ".tmp";
|
|
461
|
+
writeFileSync(tmpFile, content, "utf8");
|
|
462
|
+
renameSync(tmpFile, pageFile);
|
|
463
|
+
|
|
464
|
+
// Update index
|
|
465
|
+
const entry: PageIndexEntry = {
|
|
466
|
+
id: page.frontmatter.id,
|
|
467
|
+
title: page.frontmatter.title,
|
|
468
|
+
kind: page.frontmatter.kind,
|
|
469
|
+
status: page.frontmatter.status,
|
|
470
|
+
tags: [], // Extracted from body
|
|
471
|
+
summary: page.body.slice(0, 200),
|
|
472
|
+
updatedAt: page.frontmatter.updatedAt,
|
|
473
|
+
codeVersion: page.frontmatter.codeVersion,
|
|
474
|
+
};
|
|
475
|
+
this.index.set(entry.id, entry);
|
|
476
|
+
this.saveIndex();
|
|
477
|
+
this.dirty = true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Load a wiki page
|
|
482
|
+
*/
|
|
483
|
+
loadPage(pageId: string): WikiPage | null {
|
|
484
|
+
const pageFile = join(this.config.knowledgeDir, `${pageId}.md`);
|
|
485
|
+
if (!existsSync(pageFile)) return null;
|
|
486
|
+
|
|
487
|
+
const content = readFileSync(pageFile, "utf8");
|
|
488
|
+
const { frontmatter, body, raw } = parseFrontmatter(content);
|
|
489
|
+
|
|
490
|
+
if (!frontmatter) return null;
|
|
491
|
+
|
|
492
|
+
return { frontmatter, body, rawFrontmatter: raw };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Delete a wiki page
|
|
497
|
+
*/
|
|
498
|
+
deletePage(pageId: string): void {
|
|
499
|
+
const pageFile = join(this.config.knowledgeDir, `${pageId}.md`);
|
|
500
|
+
if (existsSync(pageFile)) {
|
|
501
|
+
unlinkSync(pageFile);
|
|
502
|
+
}
|
|
503
|
+
this.index.delete(pageId);
|
|
504
|
+
this.saveIndex();
|
|
505
|
+
this.stagedChanges.delete(pageId);
|
|
506
|
+
this.saveStaged();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Get all pages
|
|
511
|
+
*/
|
|
512
|
+
getAllPages(): WikiPage[] {
|
|
513
|
+
const pages: WikiPage[] = [];
|
|
514
|
+
if (!existsSync(this.config.knowledgeDir)) return pages;
|
|
515
|
+
|
|
516
|
+
const files = readdirSync(this.config.knowledgeDir);
|
|
517
|
+
for (const file of files) {
|
|
518
|
+
if (!file.endsWith(".md")) continue;
|
|
519
|
+
const pageId = file.replace(".md", "");
|
|
520
|
+
const page = this.loadPage(pageId);
|
|
521
|
+
if (page) pages.push(page);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return pages;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ========================================================================
|
|
528
|
+
// Staged Changes Operations
|
|
529
|
+
// ========================================================================
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Stage a change for later application
|
|
533
|
+
*/
|
|
534
|
+
stageChange(change: StagedChange): void {
|
|
535
|
+
this.stagedChanges.set(change.pageId, change);
|
|
536
|
+
this.saveStaged();
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Get staged change for a page
|
|
541
|
+
*/
|
|
542
|
+
getStagedChange(pageId: string): StagedChange | undefined {
|
|
543
|
+
return this.stagedChanges.get(pageId);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Clear staged change
|
|
548
|
+
*/
|
|
549
|
+
clearStagedChange(pageId: string): void {
|
|
550
|
+
this.stagedChanges.delete(pageId);
|
|
551
|
+
this.saveStaged();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Get all staged changes
|
|
556
|
+
*/
|
|
557
|
+
getAllStagedChanges(): StagedChange[] {
|
|
558
|
+
return Array.from(this.stagedChanges.values());
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ========================================================================
|
|
562
|
+
// Operation Logging
|
|
563
|
+
// ========================================================================
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Log an operation
|
|
567
|
+
*/
|
|
568
|
+
logOperation(
|
|
569
|
+
type: WikiOperationType,
|
|
570
|
+
details: Record<string, unknown>,
|
|
571
|
+
requestId?: string
|
|
572
|
+
): WikiOperation {
|
|
573
|
+
const op: WikiOperation = {
|
|
574
|
+
id: `op-${randomUUID().slice(0, 8)}`,
|
|
575
|
+
type,
|
|
576
|
+
status: "pending",
|
|
577
|
+
startedAt: now(),
|
|
578
|
+
requestId,
|
|
579
|
+
details,
|
|
580
|
+
};
|
|
581
|
+
this.appendOperation(op);
|
|
582
|
+
return op;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Update operation status
|
|
587
|
+
*/
|
|
588
|
+
completeOperation(opId: string, status: WikiOperation["status"], error?: string): void {
|
|
589
|
+
const opIndex = this.operations.findIndex((o) => o.id === opId);
|
|
590
|
+
if (opIndex === -1) return;
|
|
591
|
+
|
|
592
|
+
this.operations[opIndex] = {
|
|
593
|
+
...this.operations[opIndex],
|
|
594
|
+
status,
|
|
595
|
+
completedAt: now(),
|
|
596
|
+
error,
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
// Rewrite log with updated operation
|
|
600
|
+
const tmpFile = this.config.logFile + ".tmp";
|
|
601
|
+
const content = this.operations.map((o) => JSON.stringify(o)).join("\n") + "\n";
|
|
602
|
+
writeFileSync(tmpFile, content, "utf8");
|
|
603
|
+
renameSync(tmpFile, this.config.logFile);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Get recent operations
|
|
608
|
+
*/
|
|
609
|
+
getRecentOperations(limit = 50): WikiOperation[] {
|
|
610
|
+
return this.operations.slice(-limit);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ========================================================================
|
|
614
|
+
// Search
|
|
615
|
+
// ========================================================================
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Search wiki pages
|
|
619
|
+
*/
|
|
620
|
+
search(query: string, filters?: WikiSearchFilters, limit = 10): WikiSearchResult[] {
|
|
621
|
+
const queryLower = query.toLowerCase();
|
|
622
|
+
const results: WikiSearchResult[] = [];
|
|
623
|
+
|
|
624
|
+
for (const [id, entry] of this.index) {
|
|
625
|
+
// Apply filters
|
|
626
|
+
if (filters?.kinds?.length && !filters.kinds.includes(entry.kind)) continue;
|
|
627
|
+
if (filters?.statuses?.length && !filters.statuses.includes(entry.status)) continue;
|
|
628
|
+
if (filters?.updatedAfter && entry.updatedAt < filters.updatedAfter) continue;
|
|
629
|
+
if (filters?.updatedBefore && entry.updatedAt > filters.updatedBefore) continue;
|
|
630
|
+
|
|
631
|
+
// Calculate relevance
|
|
632
|
+
let relevance = 0;
|
|
633
|
+
const matchedOn: WikiSearchResult["matchedOn"] = [];
|
|
634
|
+
|
|
635
|
+
if (entry.title.toLowerCase().includes(queryLower)) {
|
|
636
|
+
relevance += 0.5;
|
|
637
|
+
matchedOn.push("title");
|
|
638
|
+
}
|
|
639
|
+
if (entry.summary.toLowerCase().includes(queryLower)) {
|
|
640
|
+
relevance += 0.3;
|
|
641
|
+
matchedOn.push("body");
|
|
642
|
+
}
|
|
643
|
+
if (entry.tags.some((t) => t.toLowerCase().includes(queryLower))) {
|
|
644
|
+
relevance += 0.2;
|
|
645
|
+
matchedOn.push("tags");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (relevance > 0) {
|
|
649
|
+
results.push({
|
|
650
|
+
page: entry,
|
|
651
|
+
relevance,
|
|
652
|
+
matchedOn,
|
|
653
|
+
snippets: [entry.summary],
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Sort by relevance and limit
|
|
659
|
+
results.sort((a, b) => b.relevance - a.relevance);
|
|
660
|
+
return results.slice(0, limit);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Get page index
|
|
665
|
+
*/
|
|
666
|
+
getIndex(): PageIndexEntry[] {
|
|
667
|
+
return Array.from(this.index.values());
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ============================================================================
|
|
672
|
+
// Factory
|
|
673
|
+
// ============================================================================
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Create wiki storage for a workspace
|
|
677
|
+
*/
|
|
678
|
+
export function createWikiStorage(
|
|
679
|
+
workspaceRoot: string,
|
|
680
|
+
projectId: string
|
|
681
|
+
): WikiStorage {
|
|
682
|
+
const wikiRoot = join(workspaceRoot, "wiki");
|
|
683
|
+
|
|
684
|
+
const config: WikiConfig = {
|
|
685
|
+
wikiRoot,
|
|
686
|
+
rawDir: join(wikiRoot, "raw"),
|
|
687
|
+
knowledgeDir: join(wikiRoot, "knowledge"),
|
|
688
|
+
harnessDir: join(wikiRoot, ".harness"),
|
|
689
|
+
indexFile: join(wikiRoot, ".harness", "index.json"),
|
|
690
|
+
logFile: join(wikiRoot, ".harness", "log.jsonl"),
|
|
691
|
+
schemaFile: join(wikiRoot, "SCHEMA.md"),
|
|
692
|
+
projectId,
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
return new WikiStorage(config);
|
|
696
|
+
}
|