@michelj/context-guard 0.4.4 → 0.6.2
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/Coordinator.md +88 -0
- package/Executor.md +53 -0
- package/README.md +72 -102
- package/README.zh-CN.md +72 -102
- package/SKILL.md +26 -33
- package/THIRD_PARTY_NOTICES.md +47 -0
- package/Tester.md +53 -0
- package/bin/build-runtime.mjs +96 -0
- package/bin/context-guard-skill.js +287 -69
- package/bin/postinstall.js +1 -1
- package/hooks.json +80 -4
- package/licenses/JSONParse-MIT.txt +24 -0
- package/licenses/Marked-MIT.txt +44 -0
- package/licenses/Portless-Apache-2.0.txt +201 -0
- package/package.json +31 -5
- package/prototype/LICENSES/Marked-MIT.txt +44 -0
- package/prototype/LICENSES/Ready-redistribution.txt +14 -0
- package/prototype/attachments.mjs +75 -0
- package/prototype/coordinator-markdown.mjs +283 -0
- package/prototype/coordinator-working-blot.mjs +124 -0
- package/prototype/vendor/marked.mjs +2189 -0
- package/prototype/workbench-app.js +5197 -0
- package/prototype/workbench-data.js +33 -0
- package/prototype/workbench-sync.mjs +898 -0
- package/prototype/workbench.css +1050 -0
- package/prototype/workbench.html +139 -4861
- package/prototype/working-blot-atlas.png +0 -0
- package/references/agent-handoff.md +40 -0
- package/references/claude-runtime.md +120 -0
- package/references/cloud-sync-interface.md +66 -0
- package/references/design-current.md +14 -0
- package/references/map-mount.md +41 -0
- package/references/map-read.md +50 -0
- package/references/memory-definition.md +120 -0
- package/references/memory-filesystem-v2/Bug.en.md +162 -0
- package/references/memory-filesystem-v2/Bug.md +162 -0
- package/references/memory-filesystem-v2/Bug_Coordinater.md +8 -0
- package/references/memory-filesystem-v2/Bug_Executor.md +8 -0
- package/references/memory-filesystem-v2/Bug_Tester.md +7 -0
- package/references/memory-filesystem-v2/Idea.en.md +36 -0
- package/references/memory-filesystem-v2/Idea.md +36 -0
- package/references/memory-filesystem-v2/Node_Module_Index.en.md +88 -0
- package/references/memory-filesystem-v2/Node_Module_Index.md +88 -0
- package/references/memory-filesystem-v2/README.md +60 -0
- package/references/memory-filesystem-v2/Todo.en.md +137 -0
- package/references/memory-filesystem-v2/Todo.md +137 -0
- package/references/memory-filesystem-v2/Todo_Coordinater.md +7 -0
- package/references/memory-filesystem-v2/Todo_Executor.md +7 -0
- package/references/memory-filesystem-v2/Todo_Tester.md +7 -0
- package/references/named-workbench.md +124 -0
- package/references/plan-review.md +12 -0
- package/references/server-memory.md +276 -0
- package/references/test-check.md +7 -0
- package/references/user-reply.md +38 -0
- package/references/workbench-interface.md +531 -0
- package/roles.md +13 -0
- package/scripts/context_guard.py +1163 -321
- package/scripts/context_guard_hook.py +1864 -63
- package/scripts/map_owns.py +68 -138
- package/scripts/shared/LICENSES/JSONParse-MIT.txt +24 -0
- package/scripts/shared/filesystem-v2.mjs +430 -0
- package/scripts/shared/io.mjs +117 -0
- package/scripts/shared/map-model.mjs +506 -0
- package/scripts/shared/memory-schema.mjs +13 -0
- package/scripts/shared/protocol-blobs.mjs +112 -0
- package/scripts/shared/protocol-map.mjs +146 -0
- package/scripts/shared/protocol-snapshots.mjs +84 -0
- package/scripts/shared/protocol-store.mjs +624 -0
- package/scripts/shared/protocol-workflow.mjs +226 -0
- package/scripts/shared/protocol.mjs +125 -0
- package/scripts/shared/vendor/jsonparse.cjs +413 -0
- package/scripts/workbench/access.mjs +496 -0
- package/scripts/workbench/attachments.mjs +92 -0
- package/scripts/workbench/browser-login.mjs +78 -0
- package/scripts/workbench/claude-runtime.mjs +372 -0
- package/scripts/workbench/cli.mjs +980 -0
- package/scripts/workbench/device-heartbeat.mjs +72 -0
- package/scripts/workbench/hook-status.mjs +38 -0
- package/scripts/workbench/inbox.mjs +155 -0
- package/scripts/workbench/journal.mjs +56 -0
- package/scripts/workbench/memory-merge.mjs +65 -0
- package/scripts/workbench/memory.mjs +252 -0
- package/scripts/workbench/named-proxy.mjs +108 -0
- package/scripts/workbench/named.mjs +152 -0
- package/scripts/workbench/portless-routes.mjs +51 -0
- package/scripts/workbench/project.mjs +327 -0
- package/scripts/workbench/projections.mjs +68 -0
- package/scripts/workbench/protocol-client.mjs +165 -0
- package/scripts/workbench/protocol-delivery.mjs +133 -0
- package/scripts/workbench/protocol-device.mjs +316 -0
- package/scripts/workbench/protocol-events.mjs +53 -0
- package/scripts/workbench/protocol-repository.mjs +58 -0
- package/scripts/workbench/reconcile.mjs +244 -0
- package/scripts/workbench/registry.mjs +111 -0
- package/scripts/workbench/runtime.mjs +54 -0
- package/scripts/workbench/server.mjs +1171 -0
- package/scripts/workbench/store.mjs +243 -0
- package/scripts/workbench/sync-coordinator.mjs +518 -0
- package/scripts/workbench/sync.mjs +86 -0
- package/references/bug-record-template.md +0 -37
- package/references/context-template.md +0 -19
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { entries } from './map-model.mjs';
|
|
6
|
+
|
|
7
|
+
const BUG_STATUS = {
|
|
8
|
+
open: 'Open',
|
|
9
|
+
handling: 'InProgress',
|
|
10
|
+
inprogress: 'InProgress',
|
|
11
|
+
pending: 'Pending',
|
|
12
|
+
fixed: 'Pending',
|
|
13
|
+
resolved: 'Resolved',
|
|
14
|
+
recurred: 'InProgress',
|
|
15
|
+
unfixable: 'Unfixable',
|
|
16
|
+
};
|
|
17
|
+
const DROP_BUG_STATUS = new Set(['wontfix']);
|
|
18
|
+
const END_LEGACY_BUG_STATUS = { deferred: 'Unfixable' };
|
|
19
|
+
|
|
20
|
+
const TODO_STATUS = { pending: 'Open', processing: 'InProgress', done: 'Done' };
|
|
21
|
+
|
|
22
|
+
function cleanSegment(value) {
|
|
23
|
+
return String(value || '')
|
|
24
|
+
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '-')
|
|
25
|
+
.replace(/[. ]+$/g, '')
|
|
26
|
+
.trim() || 'unnamed';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function firstParagraph(value) {
|
|
30
|
+
const text = String(value || '').trim();
|
|
31
|
+
if (!text) return 'NULL';
|
|
32
|
+
return text.split(/\r?\n\s*\r?\n/)[0].trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const textOrNull = value => String(value || '').trim() || 'NULL';
|
|
36
|
+
const linesOrNull = lines => lines.length ? lines.join('\n\n') : 'NULL';
|
|
37
|
+
|
|
38
|
+
function codeIndex(attempt) {
|
|
39
|
+
return attempt.codeIndex?.length ? attempt.codeIndex.map(entry => `- \`${entry.path}\`\n ${entry.summary}`).join('\n') : 'NULL';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function attemptEvent(attempt, number) {
|
|
43
|
+
return attempt.event ? `- A${number} / ${attempt.eventSource || 'Agent'}:${attempt.event}` : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function attemptStatus(attempt) {
|
|
47
|
+
return `Status: ${attempt.status}${attempt.status === 'Refuted' ? `\nRefutedBy: ${attempt.refutedBy}\nReason: ${attempt.reason}` : ''}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function extract(text, label) {
|
|
51
|
+
return text.match(new RegExp(`^- ${label}:\\s*(.*)$`, 'm'))?.[1]?.trim() || '';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function section(text, title) {
|
|
55
|
+
return text.match(new RegExp(`^## ${title}\\r?\\n([\\s\\S]*?)(?=^## |$(?![\\s\\S]))`, 'm'))?.[1]?.trim() || '';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isMeaningful(value) {
|
|
59
|
+
const text = String(value || '').trim();
|
|
60
|
+
return text && text !== 'NULL' && !/^(待定位|待补充|未修|未知)(?:[;;,,。.]|$)/.test(text);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function relative(from, to) {
|
|
64
|
+
const target = path.posix.relative(path.posix.dirname(from), to) || path.posix.basename(to);
|
|
65
|
+
return target.split('/').map((part) => ['.', '..'].includes(part) ? part : encodeURIComponent(part)
|
|
66
|
+
.replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`)).join('/');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function groupBy(items, keyOf) {
|
|
70
|
+
const groups = new Map();
|
|
71
|
+
for (const item of items) {
|
|
72
|
+
const key = keyOf(item);
|
|
73
|
+
const group = groups.get(key) || [];
|
|
74
|
+
group.push(item);
|
|
75
|
+
groups.set(key, group);
|
|
76
|
+
}
|
|
77
|
+
return groups;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function validateGeneratedLinks(files, links) {
|
|
81
|
+
for (const { from, to } of links) assert(files.has(to), `${from}: missing linked file ${to}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function buildFilesystemV2(snapshot) {
|
|
85
|
+
assert(snapshot?.memory?.map?.root, 'Snapshot is missing memory.map.root');
|
|
86
|
+
const document = snapshot.memory.map;
|
|
87
|
+
const records = snapshot.memory.records || {};
|
|
88
|
+
const nodeEntries = [...entries(document.root).values()];
|
|
89
|
+
const byId = new Map(nodeEntries.map((entry) => [entry.node.id, entry]));
|
|
90
|
+
const warnings = [];
|
|
91
|
+
const files = new Map();
|
|
92
|
+
const generatedLinks = [];
|
|
93
|
+
const link = (from, to) => { generatedLinks.push({ from, to }); return relative(from, to); };
|
|
94
|
+
const put = (name, content) => {
|
|
95
|
+
assert(!files.has(name), `Duplicate generated path: ${name}`);
|
|
96
|
+
files.set(name, `${content.trim()}\n`);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const siblings = new Map();
|
|
100
|
+
for (const { node, parent } of nodeEntries) {
|
|
101
|
+
const key = parent?.id || '__root__';
|
|
102
|
+
const list = siblings.get(key) || [];
|
|
103
|
+
list.push({
|
|
104
|
+
id: node.id,
|
|
105
|
+
stem: `${cleanSegment(node.title)}-${node.kind === 'module' ? 'module' : 'node'}`,
|
|
106
|
+
});
|
|
107
|
+
siblings.set(key, list);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const segment = new Map();
|
|
111
|
+
for (const list of siblings.values()) {
|
|
112
|
+
for (const group of groupBy(list, (item) => item.stem.toLowerCase()).values()) {
|
|
113
|
+
for (const item of group) {
|
|
114
|
+
segment.set(item.id, group.length === 1 ? item.stem : `${item.stem}--${item.id}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const nodeDir = new Map();
|
|
120
|
+
function assign(entry, parentDir = 'nodes') {
|
|
121
|
+
assert(entry, 'Map root or child is missing from the flattened node index');
|
|
122
|
+
const current = `${parentDir}/${segment.get(entry.node.id)}`;
|
|
123
|
+
nodeDir.set(entry.node.id, current);
|
|
124
|
+
for (const child of [...(entry.node.children || []), ...(entry.node._inbox || [])]) {
|
|
125
|
+
assign(byId.get(child.id), current);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
assign(byId.get(document.root.id));
|
|
129
|
+
|
|
130
|
+
for (const { node } of nodeEntries) {
|
|
131
|
+
if (!String(node.memoryDocument || '').trim()) continue;
|
|
132
|
+
const file = node.id === document.root.id ? 'memory.md' : `${nodeDir.get(node.id)}/memory.md`;
|
|
133
|
+
put(file, node.memoryDocument);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const related = new Map(nodeEntries.map(({ node }) => [node.id, new Set()]));
|
|
137
|
+
for (const flow of [...(document.flows || []), ...(document.root.flows || [])]) {
|
|
138
|
+
if (!byId.has(flow.from) || !byId.has(flow.to)) {
|
|
139
|
+
warnings.push({ code: 'BROKEN_FLOW', from: flow.from, to: flow.to });
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
related.get(flow.from).add(flow.to);
|
|
143
|
+
related.get(flow.to).add(flow.from);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const bugsByNode = new Map();
|
|
147
|
+
const unassignedBugs = [];
|
|
148
|
+
const bugRecords = Object.entries(records).filter(([name]) => /^bugs\/[^/]+\.md$/.test(name));
|
|
149
|
+
const mapBugs = new Map();
|
|
150
|
+
for (const { node } of nodeEntries) for (const item of node.bugs || []) {
|
|
151
|
+
if (!item.id) continue;
|
|
152
|
+
if (mapBugs.has(item.id)) warnings.push({ code: 'DUPLICATE_BUG_ID', id: item.id });
|
|
153
|
+
else mapBugs.set(item.id, { item, nodeId: node.id });
|
|
154
|
+
}
|
|
155
|
+
for (const item of document.unassigned_bugs || []) {
|
|
156
|
+
if (!item.id) continue;
|
|
157
|
+
if (mapBugs.has(item.id)) warnings.push({ code: 'DUPLICATE_BUG_ID', id: item.id });
|
|
158
|
+
else mapBugs.set(item.id, { item, nodeId: '' });
|
|
159
|
+
}
|
|
160
|
+
const legacyBugs = new Map(bugRecords.map(([name, raw]) => [path.posix.basename(name, '.md'), raw]));
|
|
161
|
+
for (const id of new Set([...legacyBugs.keys(), ...mapBugs.keys()])) {
|
|
162
|
+
const raw = legacyBugs.get(id) || '';
|
|
163
|
+
const mapped = mapBugs.get(id), item = mapped?.item;
|
|
164
|
+
const title = textOrNull(item?.title || raw.match(/^#\s+(.+)$/m)?.[1] || id);
|
|
165
|
+
const heading = title === id || title.startsWith(`${id} `) ? title : `${id} ${title}`;
|
|
166
|
+
const nodeId = mapped ? mapped.nodeId : extract(raw, 'node');
|
|
167
|
+
const phenomenon = textOrNull(item?.phenomenon || item?.description || item?.desc || extract(raw, '现象'));
|
|
168
|
+
const rawStatus = String(item?.status || extract(raw, 'status')).toLowerCase();
|
|
169
|
+
if (DROP_BUG_STATUS.has(rawStatus)) {
|
|
170
|
+
warnings.push({ code: 'DROPPED_WONTFIX', id, nodeId });
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const status = BUG_STATUS[rawStatus] || END_LEGACY_BUG_STATUS[rawStatus];
|
|
174
|
+
if (!status) {
|
|
175
|
+
warnings.push({ code: 'UNKNOWN_BUG_STATUS', id, nodeId, status: rawStatus || null });
|
|
176
|
+
}
|
|
177
|
+
const fileStatus = status || 'Open';
|
|
178
|
+
if (END_LEGACY_BUG_STATUS[rawStatus]) {
|
|
179
|
+
warnings.push({ code: 'HISTORICAL_DEFERRED', id, nodeId, projected: fileStatus });
|
|
180
|
+
}
|
|
181
|
+
const assigned = byId.has(nodeId);
|
|
182
|
+
if (!assigned) warnings.push({ code: 'ORPHAN_BUG', id, nodeId });
|
|
183
|
+
|
|
184
|
+
const fixRaw = records[`fixes/${id}.md`] || '';
|
|
185
|
+
const cause = section(fixRaw, '根因') || 'NULL';
|
|
186
|
+
const method = section(fixRaw, '怎么修') || 'NULL';
|
|
187
|
+
const trigger = section(fixRaw, '触发') || 'NULL';
|
|
188
|
+
const code = section(fixRaw, '代码') || 'NULL';
|
|
189
|
+
const evidence = section(fixRaw, '证据') || 'NULL';
|
|
190
|
+
const dir = assigned ? nodeDir.get(nodeId) : 'unassigned';
|
|
191
|
+
const file = `${dir}/bugs/${id}.md`;
|
|
192
|
+
const sessions = extract(raw, 'sessions').split(/[,,]\s*/).filter(Boolean);
|
|
193
|
+
const attempts = Array.isArray(item?.attempts) && item.attempts.length ? item.attempts : null;
|
|
194
|
+
const attributionBlock = attempts ? attempts.map((attempt, index) => `### A${index + 1}
|
|
195
|
+
${attemptStatus(attempt)}
|
|
196
|
+
|
|
197
|
+
原因:${textOrNull(attempt.cause)}
|
|
198
|
+
|
|
199
|
+
#### code index
|
|
200
|
+
${codeIndex(attempt)}`).join('\n\n') : isMeaningful(cause) ? `### A1
|
|
201
|
+
Status: Confirmed
|
|
202
|
+
|
|
203
|
+
原因:${cause}
|
|
204
|
+
|
|
205
|
+
#### code index
|
|
206
|
+
${code}` : 'NULL';
|
|
207
|
+
const rounds = attempts || [{ reproduction: trigger, test: { summary: evidence, content: evidence }, sessionIds: sessions }];
|
|
208
|
+
for (const [index, attempt] of rounds.entries()) {
|
|
209
|
+
const round = `A${index + 1}`;
|
|
210
|
+
put(`${dir}/bugs/tests/${id}-${round}.md`, `# ${id} ${round}\n\n${textOrNull(attempt.test?.content)}`);
|
|
211
|
+
put(`${dir}/bugs/traces/${id}-${round}.md`, `# ${id} ${round}\n\n${attempt.sessionIds?.length ? attempt.sessionIds.map(value => `- sessions/${value}.md`).join('\n') : 'NULL'}`);
|
|
212
|
+
}
|
|
213
|
+
const effective = attempts ? attempts.filter(attempt => attempt.status === 'Confirmed').at(-1) : null;
|
|
214
|
+
put(file, `# ${heading}
|
|
215
|
+
|
|
216
|
+
Reporter: ${['Human', 'Agent'].includes(item?.reporter) ? item.reporter : 'NULL'}
|
|
217
|
+
Status: ${fileStatus}
|
|
218
|
+
CurrentAttempt: A${rounds.length}
|
|
219
|
+
|
|
220
|
+
## 1. 现象
|
|
221
|
+
|
|
222
|
+
${phenomenon}
|
|
223
|
+
|
|
224
|
+
## 2. 后续纠正事件
|
|
225
|
+
|
|
226
|
+
${attempts ? linesOrNull(attempts.map((attempt, index) => attemptEvent(attempt, index + 1)).filter(Boolean)) : 'NULL'}
|
|
227
|
+
|
|
228
|
+
## 3. 复现
|
|
229
|
+
|
|
230
|
+
${rounds.map((attempt, index) => `### A${index + 1}\n${textOrNull(attempt.reproduction)}`).join('\n\n')}
|
|
231
|
+
|
|
232
|
+
## 4. 原因与代码改动
|
|
233
|
+
|
|
234
|
+
### 当前有效结论
|
|
235
|
+
${attempts ? textOrNull(effective?.resolution || effective?.cause) : method}
|
|
236
|
+
|
|
237
|
+
${attributionBlock}
|
|
238
|
+
|
|
239
|
+
## 5. 测试
|
|
240
|
+
|
|
241
|
+
${rounds.map((attempt, index) => `- [A${index + 1}](${link(file, `${dir}/bugs/tests/${id}-A${index + 1}.md`)}):${textOrNull(attempt.test?.summary)}`).join('\n')}
|
|
242
|
+
|
|
243
|
+
## 6. 修复 Session 索引
|
|
244
|
+
|
|
245
|
+
${rounds.map((_, index) => `- [A${index + 1}](${link(file, `${dir}/bugs/traces/${id}-A${index + 1}.md`)})`).join('\n')}`);
|
|
246
|
+
|
|
247
|
+
const projected = { id, title: heading, phenomenon, status: fileStatus, file, legacyNode: nodeId };
|
|
248
|
+
if (assigned) {
|
|
249
|
+
const list = bugsByNode.get(nodeId) || [];
|
|
250
|
+
list.push(projected);
|
|
251
|
+
bugsByNode.set(nodeId, list);
|
|
252
|
+
} else {
|
|
253
|
+
unassignedBugs.push(projected);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (unassignedBugs.length) {
|
|
258
|
+
put('unassigned/index.md', `# Unassigned migration records
|
|
259
|
+
|
|
260
|
+
${unassignedBugs.map((bug) => `### [${bug.title}](${link('unassigned/index.md', bug.file)})
|
|
261
|
+
${bug.phenomenon}
|
|
262
|
+
|
|
263
|
+
Status: ${bug.status}
|
|
264
|
+
LegacyNode: ${bug.legacyNode || 'NULL'}`).join('\n\n')}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const todosByNode = new Map();
|
|
268
|
+
const ideasByNode = new Map();
|
|
269
|
+
for (const { node } of nodeEntries) {
|
|
270
|
+
let index = 0;
|
|
271
|
+
for (const todo of node.todos || []) {
|
|
272
|
+
index += 1;
|
|
273
|
+
const id = todo.id || `T-${node.id}-${index}`;
|
|
274
|
+
const title = String(todo.title || todo.desc || todo.description || id).trim() || id;
|
|
275
|
+
const description = String(todo.description || todo.desc || todo.title || '').trim() || 'NULL';
|
|
276
|
+
const status = TODO_STATUS[todo.status] || 'Open';
|
|
277
|
+
const file = `${nodeDir.get(node.id)}/todos/${id}.md`;
|
|
278
|
+
const attempts = Array.isArray(todo.attempts) && todo.attempts.length ? todo.attempts : null;
|
|
279
|
+
const rounds = attempts || [{ acceptance: 'NULL', solution: 'NULL', test: { summary: 'NULL', content: 'NULL' } }];
|
|
280
|
+
if (!attempts) warnings.push({ code: 'TODO_ATTEMPT_UNCLASSIFIED', id, nodeId: node.id });
|
|
281
|
+
for (const [roundIndex, attempt] of rounds.entries()) {
|
|
282
|
+
const round = `A${roundIndex + 1}`;
|
|
283
|
+
put(`${nodeDir.get(node.id)}/todos/tests/${id}-${round}.md`, `# ${id} ${round}\n\n${textOrNull(attempt.test?.content)}`);
|
|
284
|
+
put(`${nodeDir.get(node.id)}/todos/traces/${id}-${round}.md`, `# ${id} ${round}\n\n${attempt.sessionIds?.length ? attempt.sessionIds.map(value => `- sessions/${value}.md`).join('\n') : 'NULL'}`);
|
|
285
|
+
}
|
|
286
|
+
const effective = attempts?.filter(attempt => attempt.status === 'Confirmed').at(-1);
|
|
287
|
+
put(file, `# ${id} ${title}
|
|
288
|
+
|
|
289
|
+
Reporter: ${['Human', 'Agent'].includes(todo.reporter) ? todo.reporter : 'NULL'}
|
|
290
|
+
Status: ${status}
|
|
291
|
+
CurrentAttempt: A${rounds.length}
|
|
292
|
+
|
|
293
|
+
## 1. 需求
|
|
294
|
+
|
|
295
|
+
${description}
|
|
296
|
+
|
|
297
|
+
## 2. 后续调整事件
|
|
298
|
+
|
|
299
|
+
${attempts ? linesOrNull(attempts.map((attempt, roundIndex) => attemptEvent(attempt, roundIndex + 1)).filter(Boolean)) : 'NULL'}
|
|
300
|
+
|
|
301
|
+
## 3. 验收标准
|
|
302
|
+
|
|
303
|
+
${rounds.map((attempt, roundIndex) => `### A${roundIndex + 1}\n${textOrNull(attempt.acceptance)}`).join('\n\n')}
|
|
304
|
+
|
|
305
|
+
## 4. 方案与代码改动
|
|
306
|
+
|
|
307
|
+
### 当前有效方案
|
|
308
|
+
${attempts ? textOrNull(effective?.solution) : 'NULL'}
|
|
309
|
+
|
|
310
|
+
${rounds.map((attempt, roundIndex) => `### A${roundIndex + 1}\n${attempts ? `${attemptStatus(attempt)}\n` : ''}\n方案:${textOrNull(attempt.solution)}\n\n#### code index\n${codeIndex(attempt)}`).join('\n\n')}
|
|
311
|
+
|
|
312
|
+
## 5. 测试
|
|
313
|
+
|
|
314
|
+
${rounds.map((attempt, roundIndex) => `- [A${roundIndex + 1}](${link(file, `${nodeDir.get(node.id)}/todos/tests/${id}-A${roundIndex + 1}.md`)}):${textOrNull(attempt.test?.summary)}`).join('\n')}
|
|
315
|
+
|
|
316
|
+
## 6. 实现 Session 索引
|
|
317
|
+
|
|
318
|
+
- ${rounds.map((_, roundIndex) => `[A${roundIndex + 1}](${link(file, `${nodeDir.get(node.id)}/todos/traces/${id}-A${roundIndex + 1}.md`)})`).join('\n- ')}`);
|
|
319
|
+
const list = todosByNode.get(node.id) || [];
|
|
320
|
+
list.push({ id, title, description, status, file });
|
|
321
|
+
todosByNode.set(node.id, list);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
index = 0;
|
|
325
|
+
for (const idea of node.ideas || []) {
|
|
326
|
+
index += 1;
|
|
327
|
+
const id = idea.id || `I-${node.id}-${index}`;
|
|
328
|
+
const text = String(idea.text || '').trim() || 'NULL';
|
|
329
|
+
const title = text === 'NULL' ? id : text.split(/\r?\n/)[0].trim();
|
|
330
|
+
const status = idea.state === 'success' ? 'Accepted' : 'Proposed';
|
|
331
|
+
const file = `${nodeDir.get(node.id)}/ideas/${id}.md`;
|
|
332
|
+
put(file, `# ${id} ${title}\nStatus: ${status}\n\n## 1. 想法\n\n${text}\n\n## 2. 讨论与结论\n\nNULL`);
|
|
333
|
+
const list = ideasByNode.get(node.id) || [];
|
|
334
|
+
list.push({ id, title, text, status, file });
|
|
335
|
+
ideasByNode.set(node.id, list);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const linkBlock = (from, ids) => ids.length ? ids.map((id) => {
|
|
340
|
+
const targetNode = byId.get(id).node;
|
|
341
|
+
return `#### [${targetNode.title}](${link(from, `${nodeDir.get(id)}/index.md`)})\n${firstParagraph(targetNode.purpose)}`;
|
|
342
|
+
}).join('\n\n') : 'NULL';
|
|
343
|
+
|
|
344
|
+
for (const { node } of nodeEntries) {
|
|
345
|
+
const file = `${nodeDir.get(node.id)}/index.md`;
|
|
346
|
+
const memoryFile = node.id === document.root.id ? 'memory.md' : `${nodeDir.get(node.id)}/memory.md`;
|
|
347
|
+
const memoryLink = files.has(memoryFile) ? `\n\n## 记忆\n\n[阅读记忆](${link(file, memoryFile)})` : '';
|
|
348
|
+
const children = [...(node.children || []), ...(node._inbox || [])].map((child) => child.id);
|
|
349
|
+
const bugs = bugsByNode.get(node.id) || [];
|
|
350
|
+
const todos = todosByNode.get(node.id) || [];
|
|
351
|
+
const ideas = ideasByNode.get(node.id) || [];
|
|
352
|
+
const bugBlock = bugs.length ? bugs.map((bug) => `### [${bug.title}](${link(file, bug.file)})\n${firstParagraph(bug.phenomenon)}\n\nStatus: ${bug.status}`).join('\n\n') : 'NULL';
|
|
353
|
+
const todoBlock = todos.length ? todos.map((todo) => `### [${todo.title}](${link(file, todo.file)})\n${firstParagraph(todo.description)}\n\nStatus: ${todo.status}`).join('\n\n') : 'NULL';
|
|
354
|
+
const ideaBlock = ideas.length ? ideas.map((idea) => `### [${idea.title}](${link(file, idea.file)})\n${firstParagraph(idea.text)}\n\nStatus: ${idea.status}`).join('\n\n') : 'NULL';
|
|
355
|
+
put(file, `# ${node.title}
|
|
356
|
+
|
|
357
|
+
${firstParagraph(node.purpose)}
|
|
358
|
+
|
|
359
|
+
## 关联模块与节点
|
|
360
|
+
|
|
361
|
+
### Related
|
|
362
|
+
|
|
363
|
+
${linkBlock(file, [...related.get(node.id)])}
|
|
364
|
+
|
|
365
|
+
### Sub
|
|
366
|
+
|
|
367
|
+
${linkBlock(file, children)}
|
|
368
|
+
|
|
369
|
+
## Bug
|
|
370
|
+
|
|
371
|
+
${bugBlock}
|
|
372
|
+
|
|
373
|
+
## Todo
|
|
374
|
+
|
|
375
|
+
${todoBlock}
|
|
376
|
+
|
|
377
|
+
## Idea
|
|
378
|
+
|
|
379
|
+
${ideaBlock}${memoryLink}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const map = {
|
|
383
|
+
schema_version: 1,
|
|
384
|
+
generated_from: snapshot.version,
|
|
385
|
+
root: document.root.id,
|
|
386
|
+
nodes: nodeEntries.map(({ node, parent }) => ({
|
|
387
|
+
id: node.id,
|
|
388
|
+
type: node.kind === 'module' ? 'module' : 'node',
|
|
389
|
+
parent: parent?.id || null,
|
|
390
|
+
path: `${nodeDir.get(node.id)}/index.md`,
|
|
391
|
+
related: [...related.get(node.id)],
|
|
392
|
+
})),
|
|
393
|
+
};
|
|
394
|
+
put('map.json', JSON.stringify(map, null, 2));
|
|
395
|
+
|
|
396
|
+
const report = {
|
|
397
|
+
sourceVersion: snapshot.version,
|
|
398
|
+
sourceMainSha: snapshot.mainSha,
|
|
399
|
+
sourcePublishedAt: snapshot.publishedAt,
|
|
400
|
+
nodes: nodeEntries.length,
|
|
401
|
+
files: files.size + 2,
|
|
402
|
+
bugs: {
|
|
403
|
+
source: bugRecords.length,
|
|
404
|
+
migrated: [...bugsByNode.values()].flat().length + unassignedBugs.length,
|
|
405
|
+
unassigned: unassignedBugs.length,
|
|
406
|
+
},
|
|
407
|
+
todos: [...todosByNode.values()].flat().length,
|
|
408
|
+
ideas: [...ideasByNode.values()].flat().length,
|
|
409
|
+
deferred: {
|
|
410
|
+
memories: nodeEntries.reduce((count, { node }) => count + (node.memories?.length || 0), 0),
|
|
411
|
+
sessions: Object.keys(records).filter((name) => name.startsWith('sessions/')).length,
|
|
412
|
+
legacyRecords: Object.keys(records).length,
|
|
413
|
+
},
|
|
414
|
+
lossy: {
|
|
415
|
+
briefsTruncated: 0,
|
|
416
|
+
briefsMissing: nodeEntries.filter(({ node }) => !String(node.purpose || '').trim()).length,
|
|
417
|
+
reporterUnknown: bugRecords.length + [...todosByNode.values()].flat().length,
|
|
418
|
+
},
|
|
419
|
+
warnings,
|
|
420
|
+
};
|
|
421
|
+
put('migration-report.json', JSON.stringify(report, null, 2));
|
|
422
|
+
validateGeneratedLinks(files, generatedLinks);
|
|
423
|
+
|
|
424
|
+
const hashes = [...files].map(([name, content]) => ({
|
|
425
|
+
path: name,
|
|
426
|
+
sha256: crypto.createHash('sha256').update(content).digest('hex'),
|
|
427
|
+
}));
|
|
428
|
+
put('manifest.json', JSON.stringify({ sourceVersion: snapshot.version, files: hashes }, null, 2));
|
|
429
|
+
return { files, report };
|
|
430
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
6
|
+
import { performance } from 'node:perf_hooks';
|
|
7
|
+
import JSONParser from './vendor/jsonparse.cjs';
|
|
8
|
+
export const hash = text => createHash('sha256').update(text).digest('hex');
|
|
9
|
+
export const encode = value => JSON.stringify(value, null, 2) + '\n';
|
|
10
|
+
export const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
11
|
+
export async function withFileLock(file, action, { reclaimGuard = false } = {}) {
|
|
12
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
13
|
+
const deadline = Date.now() + 5000;
|
|
14
|
+
let handle;
|
|
15
|
+
while (!handle) {
|
|
16
|
+
try {
|
|
17
|
+
handle = await fs.open(file, 'wx', 0o600);
|
|
18
|
+
await handle.writeFile(encode({ pid: process.pid, host: os.hostname(), createdAt: new Date().toISOString() }));
|
|
19
|
+
await handle.sync();
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// Windows can report delete-pending contention as EPERM rather than EEXIST.
|
|
23
|
+
// Retry only acquisition; an error after opening our own lock must fail.
|
|
24
|
+
if (!handle && process.platform === 'win32' && ['EPERM', 'EACCES', 'EBUSY'].includes(error.code) && Date.now() < deadline) {
|
|
25
|
+
await pause(25); continue;
|
|
26
|
+
}
|
|
27
|
+
if (error.code !== 'EEXIST') {
|
|
28
|
+
if (handle) { await handle.close().catch(() => {}); handle = null; await fs.unlink(file).catch(() => {}); }
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
const owner = await readJSON(file, null).catch(() => null);
|
|
32
|
+
let dead = owner?.host === os.hostname() && Number.isSafeInteger(owner?.pid) && owner.pid > 0;
|
|
33
|
+
if (dead) {
|
|
34
|
+
try { process.kill(owner.pid, 0); dead = false; }
|
|
35
|
+
catch (cause) { dead = cause.code === 'ESRCH'; }
|
|
36
|
+
}
|
|
37
|
+
if (dead) {
|
|
38
|
+
if (reclaimGuard) throw Object.assign(new Error('Interrupted lock recovery needs explicit repair; preserve the recovery guard'), { code: 'STATE_BUSY' });
|
|
39
|
+
// Serialize stale recovery and reread the owner inside that guard. A
|
|
40
|
+
// contender must never remove a new live lock using an old PID snapshot.
|
|
41
|
+
await withFileLock(`${file}.reclaim`, async () => {
|
|
42
|
+
const current = await readJSON(file, null).catch(() => null);
|
|
43
|
+
if (!current || current.host !== os.hostname() || !Number.isSafeInteger(current.pid) || current.pid <= 0) return;
|
|
44
|
+
try { process.kill(current.pid, 0); return; }
|
|
45
|
+
catch (cause) { if (cause.code !== 'ESRCH') return; }
|
|
46
|
+
await fs.unlink(file).catch(cause => { if (cause.code !== 'ENOENT') throw cause; });
|
|
47
|
+
}, { reclaimGuard: true });
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (Date.now() >= deadline) throw Object.assign(new Error('Shared state is busy; preserve lock and retry'), { code: 'STATE_BUSY' });
|
|
51
|
+
await pause(25);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
try { return await action(); }
|
|
55
|
+
finally { await handle.close(); await fs.unlink(file); }
|
|
56
|
+
}
|
|
57
|
+
async function parseJSONFile(file, maxReadFileBytes) {
|
|
58
|
+
const info = await fs.stat(file);
|
|
59
|
+
if (info.size <= maxReadFileBytes) return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const parser = new JSONParser();
|
|
62
|
+
const stream = createReadStream(file);
|
|
63
|
+
let complete = false;
|
|
64
|
+
let value;
|
|
65
|
+
let settled = false;
|
|
66
|
+
const fail = error => {
|
|
67
|
+
if (settled) return;
|
|
68
|
+
settled = true;
|
|
69
|
+
reject(error);
|
|
70
|
+
};
|
|
71
|
+
parser.onValue = function onValue(parsed) {
|
|
72
|
+
if (this.stack.length === 0) {
|
|
73
|
+
complete = true;
|
|
74
|
+
value = parsed;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
stream.on('data', chunk => {
|
|
78
|
+
if (settled) return;
|
|
79
|
+
try { parser.write(chunk); }
|
|
80
|
+
catch (error) { fail(error); stream.destroy(); }
|
|
81
|
+
});
|
|
82
|
+
stream.on('error', fail);
|
|
83
|
+
stream.on('end', () => {
|
|
84
|
+
if (settled) return;
|
|
85
|
+
if (!complete) return fail(new SyntaxError('Unexpected end of JSON input'));
|
|
86
|
+
settled = true;
|
|
87
|
+
resolve(value);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
export async function readJSON(file, fallback, { maxReadFileBytes = 128 * 1024 * 1024 } = {}) {
|
|
92
|
+
try { return await parseJSONFile(file, maxReadFileBytes); }
|
|
93
|
+
catch (e) { if (e.code === 'ENOENT' && fallback !== undefined) return fallback; throw e; }
|
|
94
|
+
}
|
|
95
|
+
export async function atomicWrite(file, content, { deadlineMs = 350, beforeReplace = async () => {} } = {}) {
|
|
96
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
97
|
+
const temp = `${file}.${randomUUID()}.tmp`;
|
|
98
|
+
let handle;
|
|
99
|
+
try {
|
|
100
|
+
handle = await fs.open(temp, 'wx', 0o600);
|
|
101
|
+
await handle.writeFile(content); await handle.sync(); await handle.close(); handle = null;
|
|
102
|
+
const deadline = performance.now() + deadlineMs;
|
|
103
|
+
for (;;) {
|
|
104
|
+
await beforeReplace();
|
|
105
|
+
try { await fs.rename(temp, file); break; }
|
|
106
|
+
catch (e) {
|
|
107
|
+
if (!['EPERM', 'EACCES', 'EBUSY'].includes(e.code) || performance.now() >= deadline) throw e;
|
|
108
|
+
await pause(Math.min(10, Math.max(0, deadline - performance.now())));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Directory fsync is supported on Unix, not Windows. File contents were flushed.
|
|
112
|
+
if (process.platform !== 'win32') {
|
|
113
|
+
const dir = await fs.open(path.dirname(file), 'r');
|
|
114
|
+
try { await dir.sync(); } finally { await dir.close(); }
|
|
115
|
+
}
|
|
116
|
+
} finally { if (handle) await handle.close(); await fs.unlink(temp).catch(() => {}); }
|
|
117
|
+
}
|