memorix 1.1.13 → 1.2.1
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/CHANGELOG.md +33 -1
- package/README.md +6 -3
- package/README.zh-CN.md +6 -3
- package/dist/cli/index.js +36639 -31279
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +50 -30
- package/dist/index.js +6636 -1778
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +1 -1
- package/dist/maintenance-runner.js +3775 -302
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +33 -1
- package/dist/sdk.d.ts +1 -1
- package/dist/sdk.js +6634 -1776
- package/dist/sdk.js.map +1 -1
- package/docs/1.2.0-CLAIM-LEDGER.md +72 -0
- package/docs/1.2.0-CODE-STATE.md +61 -0
- package/docs/1.2.0-DEVELOPMENT-CHARTER.md +255 -0
- package/docs/1.2.0-DYNAMIC-LIFECYCLE.md +71 -0
- package/docs/1.2.0-EVALUATION-HARNESS.md +51 -0
- package/docs/1.2.0-IMPLEMENTATION-PLAN.md +554 -0
- package/docs/1.2.0-KNOWLEDGE-WORKFLOW-RESEARCH.md +205 -0
- package/docs/1.2.0-KNOWLEDGE-WORKSPACE.md +68 -0
- package/docs/1.2.0-PRODUCT-STORY.md +234 -0
- package/docs/1.2.0-PROVIDER-QUALITY.md +189 -0
- package/docs/1.2.0-WORKFLOW-INHERITANCE.md +101 -0
- package/docs/1.2.0-WORKSET-RETRIEVAL.md +80 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +6 -3
- package/docs/API_REFERENCE.md +27 -6
- package/docs/CONFIGURATION.md +21 -3
- package/docs/DEVELOPMENT.md +4 -0
- package/docs/README.md +17 -2
- package/docs/SETUP.md +7 -1
- package/docs/dev-log/progress.txt +120 -40
- package/docs/knowledge/workflows/memorix-release.md +57 -0
- package/llms-full.txt +16 -2
- package/llms.txt +9 -4
- package/package.json +3 -2
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/cli/capability-map.ts +1 -0
- package/src/cli/commands/codegraph.ts +111 -9
- package/src/cli/commands/context.ts +2 -0
- package/src/cli/commands/doctor.ts +73 -4
- package/src/cli/commands/knowledge.ts +322 -0
- package/src/cli/commands/serve-http.ts +26 -42
- package/src/cli/commands/setup.ts +9 -3
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/App.tsx +1 -1
- package/src/cli/tui/Panels.tsx +8 -8
- package/src/cli/tui/theme.ts +1 -1
- package/src/cli/tui/views/GraphView.tsx +8 -7
- package/src/cli/tui/views/KnowledgeView.tsx +9 -9
- package/src/codegraph/auto-context.ts +169 -19
- package/src/codegraph/code-state.ts +95 -0
- package/src/codegraph/context-pack.ts +82 -1
- package/src/codegraph/current-facts.ts +19 -1
- package/src/codegraph/external-provider.ts +581 -0
- package/src/codegraph/lite-provider.ts +64 -19
- package/src/codegraph/project-context.ts +9 -1
- package/src/codegraph/store.ts +154 -6
- package/src/codegraph/task-lens.ts +49 -5
- package/src/codegraph/types.ts +117 -0
- package/src/config/resolved-config.ts +28 -0
- package/src/config/toml-loader.ts +3 -0
- package/src/config/yaml-loader.ts +6 -0
- package/src/dashboard/server.ts +30 -47
- package/src/evaluation/workset-evaluation.ts +120 -0
- package/src/hooks/handler.ts +48 -6
- package/src/knowledge/claim-store.ts +267 -0
- package/src/knowledge/claims.ts +587 -0
- package/src/knowledge/markdown.ts +129 -0
- package/src/knowledge/types.ts +158 -0
- package/src/knowledge/wiki.ts +524 -0
- package/src/knowledge/workflow-store.ts +168 -0
- package/src/knowledge/workflow-types.ts +95 -0
- package/src/knowledge/workflows.ts +774 -0
- package/src/knowledge/workset.ts +515 -0
- package/src/knowledge/workspace-store.ts +220 -0
- package/src/knowledge/workspace-types.ts +106 -0
- package/src/knowledge/workspace.ts +220 -0
- package/src/memory/auto-relations.ts +21 -0
- package/src/memory/graph-scope.ts +46 -0
- package/src/memory/observations.ts +19 -0
- package/src/orchestrate/verify-gate.ts +33 -10
- package/src/runtime/control-plane-maintenance.ts +5 -0
- package/src/runtime/isolated-maintenance.ts +5 -0
- package/src/runtime/lifecycle-status.ts +102 -0
- package/src/runtime/lifecycle.ts +107 -0
- package/src/runtime/maintenance-jobs.ts +5 -0
- package/src/runtime/project-maintenance.ts +190 -0
- package/src/server/tool-profile.ts +3 -2
- package/src/server.ts +424 -22
- package/src/store/file-lock.ts +24 -4
- package/src/store/sqlite-db.ts +307 -0
- package/src/wiki/generator.ts +4 -2
- package/src/wiki/knowledge-graph.ts +7 -4
- package/src/wiki/types.ts +16 -4
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type { CodeGraphStore } from '../codegraph/store.js';
|
|
5
|
+
import { withFileLock } from '../store/file-lock.js';
|
|
6
|
+
import { ClaimStore } from './claim-store.js';
|
|
7
|
+
import { extractInternalMarkdownLinks, pageContentHash, readKnowledgePage, renderKnowledgePage, resolvePageLink, writeKnowledgePage } from './markdown.js';
|
|
8
|
+
import { KnowledgeWorkspaceStore } from './workspace-store.js';
|
|
9
|
+
import { getKnowledgeWorkspacePaths, resolveKnowledgeWorkspaceFile } from './workspace.js';
|
|
10
|
+
import type {
|
|
11
|
+
KnowledgePage,
|
|
12
|
+
KnowledgePageFrontmatter,
|
|
13
|
+
KnowledgePageRecord,
|
|
14
|
+
KnowledgeProposal,
|
|
15
|
+
KnowledgeProposalReason,
|
|
16
|
+
KnowledgeWorkspace,
|
|
17
|
+
WikiLintIssue,
|
|
18
|
+
WikiLintResult,
|
|
19
|
+
} from './workspace-types.js';
|
|
20
|
+
|
|
21
|
+
export { readKnowledgePage } from './markdown.js';
|
|
22
|
+
|
|
23
|
+
export interface CompileKnowledgeWorkspaceResult {
|
|
24
|
+
proposals: KnowledgeProposal[];
|
|
25
|
+
published: KnowledgePageRecord[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function hash(value: string): string {
|
|
29
|
+
return createHash('sha256').update(value).digest('hex');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function now(): string {
|
|
33
|
+
return new Date().toISOString();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function storeFor(workspace: KnowledgeWorkspace): Promise<KnowledgeWorkspaceStore> {
|
|
37
|
+
if (!workspace.dataDir) throw new Error('Knowledge workspace has no operational data directory');
|
|
38
|
+
const store = new KnowledgeWorkspaceStore();
|
|
39
|
+
await store.init(workspace.dataDir);
|
|
40
|
+
return store;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function topicSlug(subject: string): string {
|
|
44
|
+
const safe = subject
|
|
45
|
+
.toLocaleLowerCase('en-US')
|
|
46
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
47
|
+
.replace(/^-+|-+$/g, '')
|
|
48
|
+
.slice(0, 48);
|
|
49
|
+
return (safe || 'topic') + '-' + hash(subject).slice(0, 8);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function groupClaims(claims: ReturnType<ClaimStore['listClaims']>): Map<string, ReturnType<ClaimStore['listClaims']>> {
|
|
53
|
+
const groups = new Map<string, ReturnType<ClaimStore['listClaims']>>();
|
|
54
|
+
for (const claim of claims) {
|
|
55
|
+
const group = groups.get(claim.subject) ?? [];
|
|
56
|
+
group.push(claim);
|
|
57
|
+
groups.set(claim.subject, group);
|
|
58
|
+
}
|
|
59
|
+
return groups;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function selectPublishableClaims(claims: ClaimStore, projectId: string) {
|
|
63
|
+
return claims.listClaims(projectId, { statuses: ['active'], limit: 1_000 })
|
|
64
|
+
.filter(claim => claim.reviewState === 'approved');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function evidenceRefs(claims: ReturnType<ClaimStore['listClaims']>, store: ClaimStore): string[] {
|
|
68
|
+
const refs = new Set<string>();
|
|
69
|
+
for (const claim of claims) {
|
|
70
|
+
for (const evidence of store.listEvidence(claim.id)) {
|
|
71
|
+
refs.add('claim:' + claim.id + ':evidence:' + evidence.id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return [...refs].sort();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function snapshotIdForClaims(claims: ReturnType<ClaimStore['listClaims']>, store: ClaimStore): string | undefined {
|
|
78
|
+
const ids = new Set<string>();
|
|
79
|
+
for (const claim of claims) {
|
|
80
|
+
for (const evidence of store.listEvidence(claim.id)) {
|
|
81
|
+
if (evidence.snapshotId) ids.add(evidence.snapshotId);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return ids.size === 1 ? [...ids][0] : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function sourceHashForClaims(claims: ReturnType<ClaimStore['listClaims']>, store: ClaimStore): string {
|
|
88
|
+
const source = claims.map(claim => ({
|
|
89
|
+
id: claim.id,
|
|
90
|
+
status: claim.status,
|
|
91
|
+
confidence: claim.confidence,
|
|
92
|
+
evidence: store.listEvidence(claim.id).map(item => ({
|
|
93
|
+
id: item.id,
|
|
94
|
+
kind: item.evidenceKind,
|
|
95
|
+
evidenceId: item.evidenceId,
|
|
96
|
+
relation: item.relation,
|
|
97
|
+
snapshotId: item.snapshotId ?? '',
|
|
98
|
+
capturedHash: item.capturedHash ?? '',
|
|
99
|
+
})),
|
|
100
|
+
}));
|
|
101
|
+
return hash(JSON.stringify(source));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderTopicBody(title: string, claims: ReturnType<ClaimStore['listClaims']>, store: ClaimStore): string {
|
|
105
|
+
const lines = [
|
|
106
|
+
'# ' + title,
|
|
107
|
+
'',
|
|
108
|
+
'This page is compiled from approved source-qualified claims.',
|
|
109
|
+
'',
|
|
110
|
+
'## Current claims',
|
|
111
|
+
'',
|
|
112
|
+
];
|
|
113
|
+
for (const claim of claims) {
|
|
114
|
+
lines.push('- ' + claim.predicate + ': ' + claim.objectValue + ' (claim ' + claim.id + ')');
|
|
115
|
+
}
|
|
116
|
+
lines.push('');
|
|
117
|
+
lines.push('## Evidence');
|
|
118
|
+
lines.push('');
|
|
119
|
+
for (const claim of claims) {
|
|
120
|
+
lines.push('### Claim ' + claim.id);
|
|
121
|
+
const evidence = store.listEvidence(claim.id);
|
|
122
|
+
if (evidence.length === 0) {
|
|
123
|
+
lines.push('- Missing evidence.');
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
for (const item of evidence) {
|
|
127
|
+
const location = item.locator ? ' at ' + item.locator : '';
|
|
128
|
+
const snapshot = item.snapshotId ? ' (snapshot ' + item.snapshotId + ')' : '';
|
|
129
|
+
lines.push('- ' + item.evidenceKind + ': ' + item.evidenceId + location + snapshot);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
lines.push('');
|
|
133
|
+
return lines.join('\n');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function candidatePage(
|
|
137
|
+
workspace: KnowledgeWorkspace,
|
|
138
|
+
title: string,
|
|
139
|
+
claims: ReturnType<ClaimStore['listClaims']>,
|
|
140
|
+
store: ClaimStore,
|
|
141
|
+
): { page: KnowledgePage; content: string; targetRelativePath: string; proposalRelativePath: string } {
|
|
142
|
+
const sourceHash = sourceHashForClaims(claims, store);
|
|
143
|
+
const generatedAt = now();
|
|
144
|
+
const slug = topicSlug(title);
|
|
145
|
+
const targetRelativePath = 'pages/' + slug + '.md';
|
|
146
|
+
const proposalRelativePath = 'proposals/' + slug + '.md';
|
|
147
|
+
const frontmatter: KnowledgePageFrontmatter = {
|
|
148
|
+
id: 'page:' + hash(workspace.projectId + ':' + title).slice(0, 24),
|
|
149
|
+
title,
|
|
150
|
+
kind: 'topic',
|
|
151
|
+
status: 'proposed',
|
|
152
|
+
reviewState: 'needs-review',
|
|
153
|
+
claimIds: claims.map(claim => claim.id).sort(),
|
|
154
|
+
evidenceRefs: evidenceRefs(claims, store),
|
|
155
|
+
...(snapshotIdForClaims(claims, store) ? { snapshotId: snapshotIdForClaims(claims, store) } : {}),
|
|
156
|
+
tags: ['generated', 'topic'],
|
|
157
|
+
sourceHash,
|
|
158
|
+
generatedAt,
|
|
159
|
+
updatedAt: generatedAt,
|
|
160
|
+
};
|
|
161
|
+
const content = renderKnowledgePage(frontmatter, renderTopicBody(title, claims, store));
|
|
162
|
+
const proposalPath = resolveKnowledgeWorkspaceFile(workspace, proposalRelativePath);
|
|
163
|
+
return {
|
|
164
|
+
page: {
|
|
165
|
+
absolutePath: proposalPath,
|
|
166
|
+
relativePath: proposalRelativePath,
|
|
167
|
+
frontmatter,
|
|
168
|
+
body: renderTopicBody(title, claims, store),
|
|
169
|
+
contentHash: pageContentHash(content),
|
|
170
|
+
links: [],
|
|
171
|
+
},
|
|
172
|
+
content,
|
|
173
|
+
targetRelativePath,
|
|
174
|
+
proposalRelativePath,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function fileHashIfPresent(filePath: string): Promise<string | undefined> {
|
|
179
|
+
try {
|
|
180
|
+
return pageContentHash(await fs.readFile(filePath, 'utf8'));
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const code = error && typeof error === 'object' && 'code' in error
|
|
183
|
+
? (error as { code?: string }).code
|
|
184
|
+
: undefined;
|
|
185
|
+
if (code === 'ENOENT') return undefined;
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function writeIndex(workspace: KnowledgeWorkspace, store: KnowledgeWorkspaceStore): Promise<void> {
|
|
191
|
+
const paths = getKnowledgeWorkspacePaths(workspace);
|
|
192
|
+
const published = store.listPages(workspace.id)
|
|
193
|
+
.filter(page => page.status === 'active')
|
|
194
|
+
.sort((left, right) => left.title.localeCompare(right.title));
|
|
195
|
+
const pending = store.listProposals(workspace.id, 'pending')
|
|
196
|
+
.sort((left, right) => left.proposalPath.localeCompare(right.proposalPath));
|
|
197
|
+
const lines = ['# Knowledge Workspace', '', '## Published pages', ''];
|
|
198
|
+
if (published.length === 0) {
|
|
199
|
+
lines.push('No published pages yet.');
|
|
200
|
+
} else {
|
|
201
|
+
for (const page of published) lines.push('- [' + page.title + '](' + page.relativePath + ')');
|
|
202
|
+
}
|
|
203
|
+
lines.push('');
|
|
204
|
+
lines.push('## Review queue');
|
|
205
|
+
lines.push('');
|
|
206
|
+
if (pending.length === 0) {
|
|
207
|
+
lines.push('No pending proposals.');
|
|
208
|
+
} else {
|
|
209
|
+
for (const proposal of pending) {
|
|
210
|
+
const relative = path.relative(paths.root, proposal.proposalPath).split(path.sep).join('/');
|
|
211
|
+
lines.push('- [' + path.basename(proposal.proposalPath, '.md') + '](' + relative + ')');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
lines.push('');
|
|
215
|
+
await writeKnowledgePage(paths.index, lines.join('\n'));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function appendLog(workspace: KnowledgeWorkspace, line: string): Promise<void> {
|
|
219
|
+
const logPath = getKnowledgeWorkspacePaths(workspace).log;
|
|
220
|
+
await fs.appendFile(logPath, '- ' + now() + ' ' + line + '\n', 'utf8');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function toPageRecord(
|
|
224
|
+
workspace: KnowledgeWorkspace,
|
|
225
|
+
page: KnowledgePage,
|
|
226
|
+
status: KnowledgePageRecord['status'],
|
|
227
|
+
contentHash: string,
|
|
228
|
+
manualContentHash?: string,
|
|
229
|
+
): KnowledgePageRecord {
|
|
230
|
+
return {
|
|
231
|
+
id: page.frontmatter.id,
|
|
232
|
+
workspaceId: workspace.id,
|
|
233
|
+
relativePath: page.relativePath.replace(/^proposals\//, 'pages/'),
|
|
234
|
+
title: page.frontmatter.title,
|
|
235
|
+
kind: page.frontmatter.kind,
|
|
236
|
+
status,
|
|
237
|
+
reviewState: page.frontmatter.reviewState,
|
|
238
|
+
contentHash,
|
|
239
|
+
sourceHash: page.frontmatter.sourceHash,
|
|
240
|
+
claimIds: page.frontmatter.claimIds,
|
|
241
|
+
...(page.frontmatter.snapshotId ? { snapshotId: page.frontmatter.snapshotId } : {}),
|
|
242
|
+
tags: page.frontmatter.tags,
|
|
243
|
+
generatedAt: page.frontmatter.generatedAt,
|
|
244
|
+
updatedAt: page.frontmatter.updatedAt,
|
|
245
|
+
...(manualContentHash ? { manualContentHash } : {}),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export async function compileKnowledgeWorkspace(input: {
|
|
250
|
+
workspace: KnowledgeWorkspace;
|
|
251
|
+
claims: ClaimStore;
|
|
252
|
+
}): Promise<CompileKnowledgeWorkspaceResult> {
|
|
253
|
+
const store = await storeFor(input.workspace);
|
|
254
|
+
const published: KnowledgePageRecord[] = [];
|
|
255
|
+
const proposals: KnowledgeProposal[] = [];
|
|
256
|
+
const claims = selectPublishableClaims(input.claims, input.workspace.projectId);
|
|
257
|
+
|
|
258
|
+
await withFileLock(getKnowledgeWorkspacePaths(input.workspace).root, async () => {
|
|
259
|
+
for (const [title, group] of groupClaims(claims)) {
|
|
260
|
+
const candidate = candidatePage(input.workspace, title, group, input.claims);
|
|
261
|
+
const targetPath = resolveKnowledgeWorkspaceFile(input.workspace, candidate.targetRelativePath);
|
|
262
|
+
const proposalPath = resolveKnowledgeWorkspaceFile(input.workspace, candidate.proposalRelativePath);
|
|
263
|
+
let existing = store.getPage(input.workspace.id, candidate.targetRelativePath);
|
|
264
|
+
const actualTargetHash = await fileHashIfPresent(targetPath);
|
|
265
|
+
if (actualTargetHash && !existing) {
|
|
266
|
+
const manualRecord = {
|
|
267
|
+
...toPageRecord(input.workspace, candidate.page, 'active', actualTargetHash, actualTargetHash),
|
|
268
|
+
reviewState: 'needs-review' as const,
|
|
269
|
+
sourceHash: 'manual:' + actualTargetHash,
|
|
270
|
+
};
|
|
271
|
+
store.upsertPage(manualRecord);
|
|
272
|
+
existing = manualRecord;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (
|
|
276
|
+
actualTargetHash
|
|
277
|
+
&& existing
|
|
278
|
+
&& existing.status === 'active'
|
|
279
|
+
&& !existing.manualContentHash
|
|
280
|
+
&& actualTargetHash === existing.contentHash
|
|
281
|
+
&& existing.sourceHash === candidate.page.frontmatter.sourceHash
|
|
282
|
+
) {
|
|
283
|
+
published.push(existing);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
let reason: KnowledgeProposalReason = 'new-page';
|
|
288
|
+
let baseContentHash: string | undefined;
|
|
289
|
+
if (actualTargetHash) {
|
|
290
|
+
baseContentHash = existing?.contentHash;
|
|
291
|
+
if (
|
|
292
|
+
!existing
|
|
293
|
+
|| existing.status !== 'active'
|
|
294
|
+
|| !!existing.manualContentHash
|
|
295
|
+
|| actualTargetHash !== existing.contentHash
|
|
296
|
+
) {
|
|
297
|
+
reason = 'manual-edit-protected';
|
|
298
|
+
} else {
|
|
299
|
+
reason = 'source-changed';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!actualTargetHash) {
|
|
304
|
+
store.upsertPage(toPageRecord(input.workspace, candidate.page, 'proposed', candidate.page.contentHash));
|
|
305
|
+
}
|
|
306
|
+
await writeKnowledgePage(proposalPath, candidate.content);
|
|
307
|
+
const proposal = store.createProposal({
|
|
308
|
+
workspaceId: input.workspace.id,
|
|
309
|
+
pageId: candidate.page.frontmatter.id,
|
|
310
|
+
targetPath,
|
|
311
|
+
proposalPath,
|
|
312
|
+
...(baseContentHash ? { baseContentHash } : {}),
|
|
313
|
+
sourceHash: candidate.page.frontmatter.sourceHash,
|
|
314
|
+
reason,
|
|
315
|
+
});
|
|
316
|
+
proposals.push(proposal);
|
|
317
|
+
await appendLog(input.workspace, 'proposed ' + candidate.targetRelativePath + ' (' + reason + ').');
|
|
318
|
+
}
|
|
319
|
+
store.markWorkspaceCompiled(input.workspace.id);
|
|
320
|
+
await writeIndex(input.workspace, store);
|
|
321
|
+
});
|
|
322
|
+
return { proposals, published };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export async function applyKnowledgeProposal(input: {
|
|
326
|
+
workspace: KnowledgeWorkspace;
|
|
327
|
+
proposalId: string;
|
|
328
|
+
/** Explicit review acknowledgement before replacing a manually edited page. */
|
|
329
|
+
allowManualOverwrite?: boolean;
|
|
330
|
+
}): Promise<{ proposal: KnowledgeProposal; targetPath: string }> {
|
|
331
|
+
const store = await storeFor(input.workspace);
|
|
332
|
+
const proposal = store.getProposal(input.proposalId);
|
|
333
|
+
if (!proposal || proposal.workspaceId !== input.workspace.id) {
|
|
334
|
+
throw new Error('Knowledge proposal was not found for this workspace');
|
|
335
|
+
}
|
|
336
|
+
if (proposal.status !== 'pending') throw new Error('Knowledge proposal is not pending review');
|
|
337
|
+
const root = getKnowledgeWorkspacePaths(input.workspace).root;
|
|
338
|
+
const targetPath = path.resolve(proposal.targetPath);
|
|
339
|
+
const proposalPath = path.resolve(proposal.proposalPath);
|
|
340
|
+
if (!targetPath.startsWith(root + path.sep) || !proposalPath.startsWith(root + path.sep)) {
|
|
341
|
+
throw new Error('Knowledge proposal path escapes its workspace');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
await withFileLock(root, async () => {
|
|
345
|
+
const actualTargetHash = await fileHashIfPresent(targetPath);
|
|
346
|
+
if (
|
|
347
|
+
proposal.baseContentHash
|
|
348
|
+
&& actualTargetHash !== proposal.baseContentHash
|
|
349
|
+
&& !input.allowManualOverwrite
|
|
350
|
+
) {
|
|
351
|
+
throw new Error('The target page has manual changes; review the proposal before applying it');
|
|
352
|
+
}
|
|
353
|
+
if (!proposal.baseContentHash && actualTargetHash && !input.allowManualOverwrite) {
|
|
354
|
+
throw new Error('The target page already exists and has no known safe base revision');
|
|
355
|
+
}
|
|
356
|
+
const proposalPage = await readKnowledgePage(proposalPath, root);
|
|
357
|
+
const activeFrontmatter: KnowledgePageFrontmatter = {
|
|
358
|
+
...proposalPage.frontmatter,
|
|
359
|
+
status: 'active',
|
|
360
|
+
reviewState: 'approved',
|
|
361
|
+
updatedAt: now(),
|
|
362
|
+
};
|
|
363
|
+
const content = renderKnowledgePage(activeFrontmatter, proposalPage.body);
|
|
364
|
+
const contentHash = await writeKnowledgePage(targetPath, content);
|
|
365
|
+
const activePage = await readKnowledgePage(targetPath, root);
|
|
366
|
+
const record = toPageRecord(input.workspace, activePage, 'active', contentHash);
|
|
367
|
+
store.upsertPage(record);
|
|
368
|
+
store.replacePageClaims(record.id, record.claimIds);
|
|
369
|
+
store.replacePageLinks(record.id, activePage.links.map(link => resolvePageLink(record.relativePath, link)).filter((link): link is string => !!link));
|
|
370
|
+
store.markProposalApplied(proposal.id);
|
|
371
|
+
await appendLog(
|
|
372
|
+
input.workspace,
|
|
373
|
+
'applied ' + record.relativePath + (input.allowManualOverwrite ? ' after explicit manual overwrite review.' : '.'),
|
|
374
|
+
);
|
|
375
|
+
await writeIndex(input.workspace, store);
|
|
376
|
+
});
|
|
377
|
+
return { proposal: store.getProposal(proposal.id)!, targetPath };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function markdownFiles(directory: string): Promise<string[]> {
|
|
381
|
+
const files: string[] = [];
|
|
382
|
+
let entries: Array<import('node:fs').Dirent>;
|
|
383
|
+
try {
|
|
384
|
+
entries = await fs.readdir(directory, { withFileTypes: true });
|
|
385
|
+
} catch {
|
|
386
|
+
return files;
|
|
387
|
+
}
|
|
388
|
+
for (const entry of entries) {
|
|
389
|
+
const filePath = path.join(directory, entry.name);
|
|
390
|
+
if (entry.isDirectory()) {
|
|
391
|
+
files.push(...await markdownFiles(filePath));
|
|
392
|
+
} else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) {
|
|
393
|
+
files.push(filePath);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return files;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export async function lintKnowledgeWorkspace(input: {
|
|
400
|
+
workspace: KnowledgeWorkspace;
|
|
401
|
+
claims: ClaimStore;
|
|
402
|
+
codeStore?: CodeGraphStore;
|
|
403
|
+
}): Promise<WikiLintResult> {
|
|
404
|
+
const store = await storeFor(input.workspace);
|
|
405
|
+
const paths = getKnowledgeWorkspacePaths(input.workspace);
|
|
406
|
+
const issues: WikiLintIssue[] = [];
|
|
407
|
+
const validPages: KnowledgePage[] = [];
|
|
408
|
+
const files = await markdownFiles(paths.pages);
|
|
409
|
+
|
|
410
|
+
for (const filePath of files) {
|
|
411
|
+
try {
|
|
412
|
+
validPages.push(await readKnowledgePage(filePath, paths.root));
|
|
413
|
+
} catch (error) {
|
|
414
|
+
issues.push({
|
|
415
|
+
kind: 'malformed-frontmatter',
|
|
416
|
+
severity: 'error',
|
|
417
|
+
message: error instanceof Error ? error.message : 'Malformed knowledge page.',
|
|
418
|
+
relativePath: path.relative(paths.root, filePath).split(path.sep).join('/'),
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const indexed = new Set<string>();
|
|
424
|
+
try {
|
|
425
|
+
const index = await fs.readFile(paths.index, 'utf8');
|
|
426
|
+
for (const link of extractInternalMarkdownLinks(index)) {
|
|
427
|
+
const resolved = resolvePageLink('index.md', link);
|
|
428
|
+
if (resolved) indexed.add(resolved);
|
|
429
|
+
}
|
|
430
|
+
} catch {
|
|
431
|
+
issues.push({
|
|
432
|
+
kind: 'broken-link',
|
|
433
|
+
severity: 'error',
|
|
434
|
+
message: 'Knowledge index is missing.',
|
|
435
|
+
relativePath: 'index.md',
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const latestSnapshotId = input.codeStore?.latestSnapshot(input.workspace.projectId)?.id;
|
|
440
|
+
for (const page of validPages) {
|
|
441
|
+
if (!indexed.has(page.relativePath)) {
|
|
442
|
+
issues.push({
|
|
443
|
+
kind: 'orphan-page',
|
|
444
|
+
severity: 'warning',
|
|
445
|
+
message: 'Page is not linked from index.md.',
|
|
446
|
+
relativePath: page.relativePath,
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
for (const link of page.links) {
|
|
450
|
+
const target = resolvePageLink(page.relativePath, link);
|
|
451
|
+
if (!target || !await fileHashIfPresent(resolveKnowledgeWorkspaceFile(input.workspace, target))) {
|
|
452
|
+
issues.push({
|
|
453
|
+
kind: 'broken-link',
|
|
454
|
+
severity: 'error',
|
|
455
|
+
message: 'Internal Markdown link does not resolve to a workspace page.',
|
|
456
|
+
relativePath: page.relativePath,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for (const claimId of page.frontmatter.claimIds) {
|
|
461
|
+
const claim = input.claims.getClaim(claimId);
|
|
462
|
+
if (!claim) {
|
|
463
|
+
issues.push({
|
|
464
|
+
kind: 'missing-claim',
|
|
465
|
+
severity: 'error',
|
|
466
|
+
message: 'Page references a claim that is not present in the ledger.',
|
|
467
|
+
relativePath: page.relativePath,
|
|
468
|
+
claimId,
|
|
469
|
+
});
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (input.claims.listEvidence(claim.id).length === 0) {
|
|
473
|
+
issues.push({
|
|
474
|
+
kind: 'missing-evidence',
|
|
475
|
+
severity: 'error',
|
|
476
|
+
message: 'Page claim has no source evidence.',
|
|
477
|
+
relativePath: page.relativePath,
|
|
478
|
+
claimId,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
if (claim.status === 'superseded') {
|
|
482
|
+
issues.push({
|
|
483
|
+
kind: 'superseded-claim',
|
|
484
|
+
severity: 'error',
|
|
485
|
+
message: 'Page still presents a superseded primary claim.',
|
|
486
|
+
relativePath: page.relativePath,
|
|
487
|
+
claimId,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
if (claim.status === 'disputed') {
|
|
491
|
+
issues.push({
|
|
492
|
+
kind: 'unresolved-conflict',
|
|
493
|
+
severity: 'error',
|
|
494
|
+
message: 'Page claim has an unresolved competing assertion.',
|
|
495
|
+
relativePath: page.relativePath,
|
|
496
|
+
claimId,
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (latestSnapshotId && page.frontmatter.snapshotId && page.frontmatter.snapshotId !== latestSnapshotId) {
|
|
501
|
+
issues.push({
|
|
502
|
+
kind: 'stale-snapshot',
|
|
503
|
+
severity: 'warning',
|
|
504
|
+
message: 'Page was compiled against an older code snapshot.',
|
|
505
|
+
relativePath: page.relativePath,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
for (const proposal of store.listProposals(input.workspace.id, 'pending')) {
|
|
511
|
+
if (proposal.reason === 'manual-edit-protected') {
|
|
512
|
+
issues.push({
|
|
513
|
+
kind: 'manual-edit-protected',
|
|
514
|
+
severity: 'warning',
|
|
515
|
+
message: 'A newer proposal was held because the target page has manual edits.',
|
|
516
|
+
relativePath: path.relative(paths.root, proposal.targetPath).split(path.sep).join('/'),
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const valid = !issues.some(issue => issue.severity === 'error');
|
|
522
|
+
store.markWorkspaceLinted(input.workspace.id, valid ? 'ready' : 'needs-review');
|
|
523
|
+
return { valid, pagesScanned: validPages.length, issues };
|
|
524
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { getDatabase } from '../store/sqlite-db.js';
|
|
3
|
+
import type {
|
|
4
|
+
WorkflowRun,
|
|
5
|
+
WorkflowRunInput,
|
|
6
|
+
WorkflowRunOutcome,
|
|
7
|
+
WorkflowSpec,
|
|
8
|
+
WorkflowVerificationVerdict,
|
|
9
|
+
} from './workflow-types.js';
|
|
10
|
+
|
|
11
|
+
function parseJson<T>(value: unknown, fallback: T): T {
|
|
12
|
+
if (typeof value !== 'string' || !value) return fallback;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(value) as T;
|
|
15
|
+
} catch {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function optionalText(value: unknown): string | undefined {
|
|
21
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function rowToWorkflow(row: any): WorkflowSpec {
|
|
25
|
+
return parseJson<WorkflowSpec>(row.specJson, {
|
|
26
|
+
id: row.id,
|
|
27
|
+
workspaceId: row.workspaceId,
|
|
28
|
+
title: row.title,
|
|
29
|
+
description: '',
|
|
30
|
+
status: row.status,
|
|
31
|
+
version: row.version,
|
|
32
|
+
taskLenses: [],
|
|
33
|
+
triggers: [],
|
|
34
|
+
assumptions: [],
|
|
35
|
+
requiredContext: [],
|
|
36
|
+
guardrails: [],
|
|
37
|
+
allowedTools: [],
|
|
38
|
+
phases: [],
|
|
39
|
+
verificationGates: [],
|
|
40
|
+
claimIds: [],
|
|
41
|
+
evidenceRefs: [],
|
|
42
|
+
codeRefs: [],
|
|
43
|
+
compatibleAgents: [],
|
|
44
|
+
body: '',
|
|
45
|
+
sourcePath: row.sourcePath,
|
|
46
|
+
sourceHash: row.sourceHash,
|
|
47
|
+
contentHash: row.contentHash,
|
|
48
|
+
createdAt: row.createdAt,
|
|
49
|
+
updatedAt: row.updatedAt,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function rowToRun(row: any): WorkflowRun {
|
|
54
|
+
return {
|
|
55
|
+
id: row.id,
|
|
56
|
+
workflowId: row.workflowId,
|
|
57
|
+
projectId: row.projectId,
|
|
58
|
+
task: row.task,
|
|
59
|
+
...(optionalText(row.startingSnapshotId) ? { startingSnapshotId: row.startingSnapshotId } : {}),
|
|
60
|
+
selectedEvidence: parseJson<string[]>(row.selectedEvidenceJson, []),
|
|
61
|
+
phaseState: parseJson<WorkflowRun['phaseState']>(row.phaseStateJson, {}),
|
|
62
|
+
outcome: row.outcome as WorkflowRunOutcome,
|
|
63
|
+
verificationVerdict: row.verificationVerdict as WorkflowVerificationVerdict,
|
|
64
|
+
...(optionalText(row.failureReason) ? { failureReason: row.failureReason } : {}),
|
|
65
|
+
startedAt: row.startedAt,
|
|
66
|
+
...(optionalText(row.completedAt) ? { completedAt: row.completedAt } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class WorkflowStore {
|
|
71
|
+
private db: any = null;
|
|
72
|
+
|
|
73
|
+
async init(dataDir: string): Promise<void> {
|
|
74
|
+
this.db = getDatabase(dataDir);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
upsertWorkflow(workflow: WorkflowSpec): WorkflowSpec {
|
|
78
|
+
this.db.prepare(
|
|
79
|
+
'INSERT INTO knowledge_workflows (id, workspaceId, sourcePath, title, status, version, sourceHash, contentHash, specJson, importedFrom, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(workspaceId, sourcePath) DO UPDATE SET id = excluded.id, title = excluded.title, status = excluded.status, version = excluded.version, sourceHash = excluded.sourceHash, contentHash = excluded.contentHash, specJson = excluded.specJson, importedFrom = excluded.importedFrom, updatedAt = excluded.updatedAt',
|
|
80
|
+
).run(
|
|
81
|
+
workflow.id,
|
|
82
|
+
workflow.workspaceId,
|
|
83
|
+
workflow.sourcePath,
|
|
84
|
+
workflow.title,
|
|
85
|
+
workflow.status,
|
|
86
|
+
workflow.version,
|
|
87
|
+
workflow.sourceHash,
|
|
88
|
+
workflow.contentHash,
|
|
89
|
+
JSON.stringify(workflow),
|
|
90
|
+
workflow.importedFrom ?? null,
|
|
91
|
+
workflow.createdAt,
|
|
92
|
+
workflow.updatedAt,
|
|
93
|
+
);
|
|
94
|
+
return this.getWorkflowByPath(workflow.workspaceId, workflow.sourcePath)!;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
getWorkflow(id: string): WorkflowSpec | undefined {
|
|
98
|
+
const row = this.db.prepare('SELECT * FROM knowledge_workflows WHERE id = ?').get(id);
|
|
99
|
+
return row ? rowToWorkflow(row) : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getWorkflowByPath(workspaceId: string, sourcePath: string): WorkflowSpec | undefined {
|
|
103
|
+
const row = this.db.prepare('SELECT * FROM knowledge_workflows WHERE workspaceId = ? AND sourcePath = ?').get(workspaceId, sourcePath);
|
|
104
|
+
return row ? rowToWorkflow(row) : undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
listWorkflows(workspaceId: string, status?: WorkflowSpec['status']): WorkflowSpec[] {
|
|
108
|
+
const rows = status
|
|
109
|
+
? this.db.prepare('SELECT * FROM knowledge_workflows WHERE workspaceId = ? AND status = ? ORDER BY title, id').all(workspaceId, status)
|
|
110
|
+
: this.db.prepare('SELECT * FROM knowledge_workflows WHERE workspaceId = ? ORDER BY title, id').all(workspaceId);
|
|
111
|
+
return rows.map(rowToWorkflow);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
recordRun(input: WorkflowRunInput): WorkflowRun {
|
|
115
|
+
const run: WorkflowRun = {
|
|
116
|
+
id: randomUUID(),
|
|
117
|
+
workflowId: input.workflowId,
|
|
118
|
+
projectId: input.projectId,
|
|
119
|
+
task: input.task.trim(),
|
|
120
|
+
...(input.startingSnapshotId ? { startingSnapshotId: input.startingSnapshotId } : {}),
|
|
121
|
+
selectedEvidence: [...new Set(input.selectedEvidence ?? [])],
|
|
122
|
+
phaseState: input.phaseState ?? {},
|
|
123
|
+
outcome: input.outcome,
|
|
124
|
+
verificationVerdict: input.verificationVerdict ?? (input.outcome === 'failed' ? 'failed' : 'not-run'),
|
|
125
|
+
...(input.failureReason?.trim() ? { failureReason: input.failureReason.trim() } : {}),
|
|
126
|
+
startedAt: input.startedAt ?? new Date().toISOString(),
|
|
127
|
+
...(input.completedAt ? { completedAt: input.completedAt } : input.outcome !== 'in-progress' ? { completedAt: new Date().toISOString() } : {}),
|
|
128
|
+
};
|
|
129
|
+
this.db.prepare(
|
|
130
|
+
'INSERT INTO knowledge_workflow_runs (id, workflowId, projectId, task, startingSnapshotId, selectedEvidenceJson, phaseStateJson, outcome, verificationVerdict, failureReason, startedAt, completedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
|
131
|
+
).run(
|
|
132
|
+
run.id,
|
|
133
|
+
run.workflowId,
|
|
134
|
+
run.projectId,
|
|
135
|
+
run.task,
|
|
136
|
+
run.startingSnapshotId ?? null,
|
|
137
|
+
JSON.stringify(run.selectedEvidence),
|
|
138
|
+
JSON.stringify(run.phaseState),
|
|
139
|
+
run.outcome,
|
|
140
|
+
run.verificationVerdict,
|
|
141
|
+
run.failureReason ?? null,
|
|
142
|
+
run.startedAt,
|
|
143
|
+
run.completedAt ?? null,
|
|
144
|
+
);
|
|
145
|
+
return this.getRun(run.id)!;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
getRun(id: string): WorkflowRun | undefined {
|
|
149
|
+
const row = this.db.prepare('SELECT * FROM knowledge_workflow_runs WHERE id = ?').get(id);
|
|
150
|
+
return row ? rowToRun(row) : undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
listRuns(projectId: string, workflowId?: string, limit = 20): WorkflowRun[] {
|
|
154
|
+
const boundedLimit = Math.max(1, Math.min(Math.floor(limit), 100));
|
|
155
|
+
const rows = workflowId
|
|
156
|
+
? this.db.prepare('SELECT * FROM knowledge_workflow_runs WHERE projectId = ? AND workflowId = ? ORDER BY startedAt DESC LIMIT ?').all(projectId, workflowId, boundedLimit)
|
|
157
|
+
: this.db.prepare('SELECT * FROM knowledge_workflow_runs WHERE projectId = ? ORDER BY startedAt DESC LIMIT ?').all(projectId, boundedLimit);
|
|
158
|
+
return rows.map(rowToRun);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
recentFailureCautions(projectId: string, workflowId: string, limit = 2): string[] {
|
|
162
|
+
return this.listRuns(projectId, workflowId, limit)
|
|
163
|
+
.filter(run => run.outcome === 'failed' || run.verificationVerdict === 'failed')
|
|
164
|
+
.map(run => run.failureReason
|
|
165
|
+
? 'Previous run failed: ' + run.failureReason
|
|
166
|
+
: 'Previous run has a failed verification gate.');
|
|
167
|
+
}
|
|
168
|
+
}
|