@tom2012/cc-web 1.5.93 → 1.5.95
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/README.md +1 -1
- package/backend/dist/memory-pool/global-pool-manager.d.ts +2 -11
- package/backend/dist/memory-pool/global-pool-manager.d.ts.map +1 -1
- package/backend/dist/memory-pool/global-pool-manager.js +126 -232
- package/backend/dist/memory-pool/global-pool-manager.js.map +1 -1
- package/backend/dist/memory-pool/pool-lock.d.ts +2 -0
- package/backend/dist/memory-pool/pool-lock.d.ts.map +1 -0
- package/backend/dist/memory-pool/pool-lock.js +31 -0
- package/backend/dist/memory-pool/pool-lock.js.map +1 -0
- package/backend/dist/memory-pool/pool-manager.d.ts +34 -0
- package/backend/dist/memory-pool/pool-manager.d.ts.map +1 -1
- package/backend/dist/memory-pool/pool-manager.js +91 -0
- package/backend/dist/memory-pool/pool-manager.js.map +1 -1
- package/backend/dist/memory-pool/templates.d.ts.map +1 -1
- package/backend/dist/memory-pool/templates.js +97 -131
- package/backend/dist/memory-pool/templates.js.map +1 -1
- package/backend/dist/memory-pool/types.d.ts +4 -8
- package/backend/dist/memory-pool/types.d.ts.map +1 -1
- package/backend/dist/routes/hooks.d.ts.map +1 -1
- package/backend/dist/routes/hooks.js +14 -0
- package/backend/dist/routes/hooks.js.map +1 -1
- package/backend/dist/routes/memory-pool.d.ts.map +1 -1
- package/backend/dist/routes/memory-pool.js +505 -95
- package/backend/dist/routes/memory-pool.js.map +1 -1
- package/frontend/dist/assets/{GraphPreview-CSZTNhQX.js → GraphPreview-BFYA_WA5.js} +1 -1
- package/frontend/dist/assets/{OfficePreview-DZmKFnER.js → OfficePreview-DN-it4Af.js} +2 -2
- package/frontend/dist/assets/{PlanPanel-DuAa0AZZ.js → PlanPanel-DYupjf3X.js} +1 -1
- package/frontend/dist/assets/{ProjectPage-BxuE0x69.js → ProjectPage-B6tVjIYr.js} +5 -5
- package/frontend/dist/assets/{SettingsPage-DSnQze4Z.js → SettingsPage-CQL_FfkR.js} +1 -1
- package/frontend/dist/assets/{ShareViewPage-Dmur70tT.js → ShareViewPage-C_x31zAw.js} +1 -1
- package/frontend/dist/assets/{SkillHubPage-DnGb0OHx.js → SkillHubPage-Cgoxl30L.js} +2 -2
- package/frontend/dist/assets/{bot-DKy4a5yK.js → bot-Dst2XqWX.js} +1 -1
- package/frontend/dist/assets/{chevron-down-B-GU2wv8.js → chevron-down-DsAldQb3.js} +1 -1
- package/frontend/dist/assets/{download-CsOUNuPy.js → download-YA6lXM4C.js} +1 -1
- package/frontend/dist/assets/index-BDYnq-Fr.css +1 -0
- package/frontend/dist/assets/{index-C202jcvT.js → index-CY9Au1bg.js} +4 -4
- package/frontend/dist/assets/{index-I4_xf4g_.js → index-_m8u9esJ.js} +1 -1
- package/frontend/dist/assets/{jszip.min-Dc0FmeMP.js → jszip.min-BC9RCrTS.js} +1 -1
- package/frontend/dist/assets/{matter-Da3rDMpe.js → matter-Mm9BJmx6.js} +1 -1
- package/frontend/dist/assets/{maximize-2-BEyzEiyH.js → maximize-2-CuzznumM.js} +1 -1
- package/frontend/dist/assets/{save-BYSx9fYK.js → save-Q2W6S7fk.js} +1 -1
- package/frontend/dist/assets/{user-D3KuBJwv.js → user-BM9GY1Xb.js} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-BiVVvGoh.css +0 -1
|
@@ -39,9 +39,13 @@ const path = __importStar(require("path"));
|
|
|
39
39
|
const config_1 = require("../config");
|
|
40
40
|
const templates_1 = require("../memory-pool/templates");
|
|
41
41
|
const pool_manager_1 = require("../memory-pool/pool-manager");
|
|
42
|
+
const buoyancy_1 = require("../memory-pool/buoyancy");
|
|
43
|
+
const pool_lock_1 = require("../memory-pool/pool-lock");
|
|
42
44
|
const global_pool_manager_1 = require("../memory-pool/global-pool-manager");
|
|
43
45
|
const router = (0, express_1.Router)();
|
|
44
46
|
const BALL_ID_RE = /^ball_\d{1,6}$/;
|
|
47
|
+
const VALID_TYPES = ['feedback', 'user', 'project', 'reference'];
|
|
48
|
+
const DEFAULT_B0 = { feedback: 9, user: 6, project: 5, reference: 3 };
|
|
45
49
|
const MEMORY_POOL_DIR = '.memory-pool';
|
|
46
50
|
const CLAUDE_MD_MARKER = '## 记忆池(Memory Pool)';
|
|
47
51
|
function resolveProjectFolder(projectId, username, res) {
|
|
@@ -57,6 +61,9 @@ function resolveProjectFolder(projectId, username, res) {
|
|
|
57
61
|
}
|
|
58
62
|
return project.folderPath;
|
|
59
63
|
}
|
|
64
|
+
function validateBallIds(ids) {
|
|
65
|
+
return ids.every((id) => typeof id === 'string' && BALL_ID_RE.test(id));
|
|
66
|
+
}
|
|
60
67
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
61
68
|
// Global Memory Pool Endpoints (MUST be before /:projectId routes)
|
|
62
69
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
@@ -81,6 +88,7 @@ router.get('/global/status', (_req, res) => {
|
|
|
81
88
|
lambda: pool.lambda,
|
|
82
89
|
alpha: pool.alpha,
|
|
83
90
|
active_capacity: pool.active_capacity,
|
|
91
|
+
surface_width: pool.surface_width ?? 10000,
|
|
84
92
|
next_id: pool.next_id,
|
|
85
93
|
pool: pool.pool,
|
|
86
94
|
initialized_at: pool.initialized_at,
|
|
@@ -97,12 +105,37 @@ router.get('/global/index', (_req, res) => {
|
|
|
97
105
|
res.status(404).json({ error: 'Global pool not initialized' });
|
|
98
106
|
return;
|
|
99
107
|
}
|
|
100
|
-
// Use fresh calendar-based t for accurate buoyancy calculation
|
|
101
108
|
pool.t = (0, global_pool_manager_1.computeGlobalT)(pool.initialized_at);
|
|
102
109
|
const balls = (0, pool_manager_1.enrichBallsWithBuoyancy)(pool);
|
|
103
110
|
res.json({ t: pool.t, updated_at: new Date().toISOString(), balls, active_capacity: pool.active_capacity });
|
|
104
111
|
});
|
|
105
|
-
// GET /api/memory-pool/global/
|
|
112
|
+
// GET /api/memory-pool/global/surface
|
|
113
|
+
router.get('/global/surface', (_req, res) => {
|
|
114
|
+
const globalDir = (0, global_pool_manager_1.getGlobalPoolDir)();
|
|
115
|
+
(0, pool_lock_1.withPoolLock)(globalDir, () => {
|
|
116
|
+
const pool = (0, global_pool_manager_1.readGlobalPool)();
|
|
117
|
+
if (!pool) {
|
|
118
|
+
res.status(404).json({ error: 'Global pool not initialized' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const freshT = (0, global_pool_manager_1.computeGlobalT)(pool.initialized_at);
|
|
122
|
+
if (pool.t !== freshT) {
|
|
123
|
+
pool.t = freshT;
|
|
124
|
+
(0, pool_manager_1.writePool)(globalDir, pool);
|
|
125
|
+
}
|
|
126
|
+
const { surfaceBalls, totalTokens } = (0, pool_manager_1.buildSurface)(globalDir);
|
|
127
|
+
res.json({
|
|
128
|
+
t: pool.t,
|
|
129
|
+
surface_width: pool.surface_width ?? 10000,
|
|
130
|
+
used_tokens: totalTokens,
|
|
131
|
+
balls: surfaceBalls,
|
|
132
|
+
});
|
|
133
|
+
}).catch((err) => {
|
|
134
|
+
if (!res.headersSent)
|
|
135
|
+
res.status(500).json({ error: err.message || err });
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
// GET /api/memory-pool/global/ball/:ballId (pure read, no side effect)
|
|
106
139
|
router.get('/global/ball/:ballId', (req, res) => {
|
|
107
140
|
const { ballId } = req.params;
|
|
108
141
|
if (!BALL_ID_RE.test(ballId)) {
|
|
@@ -116,6 +149,46 @@ router.get('/global/ball/:ballId', (req, res) => {
|
|
|
116
149
|
}
|
|
117
150
|
res.json({ id: ballId, content });
|
|
118
151
|
});
|
|
152
|
+
// POST /api/memory-pool/global/balls/:ballId/hit
|
|
153
|
+
router.post('/global/balls/:ballId/hit', (req, res) => {
|
|
154
|
+
const { ballId } = req.params;
|
|
155
|
+
if (!BALL_ID_RE.test(ballId)) {
|
|
156
|
+
res.status(400).json({ error: 'Invalid ball ID format' });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const globalDir = (0, global_pool_manager_1.getGlobalPoolDir)();
|
|
160
|
+
(0, pool_lock_1.withPoolLock)(globalDir, () => {
|
|
161
|
+
const pool = (0, global_pool_manager_1.readGlobalPool)();
|
|
162
|
+
if (!pool) {
|
|
163
|
+
res.status(404).json({ error: 'Global pool not initialized' });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
pool.t = (0, global_pool_manager_1.computeGlobalT)(pool.initialized_at);
|
|
167
|
+
const ball = pool.balls.find((b) => b.id === ballId);
|
|
168
|
+
if (!ball) {
|
|
169
|
+
res.status(404).json({ error: 'Ball not found' });
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
ball.H += 1;
|
|
173
|
+
ball.t_last = pool.t;
|
|
174
|
+
(0, pool_manager_1.writePool)(globalDir, pool);
|
|
175
|
+
const content = (0, pool_manager_1.readBallContent)(globalDir, ballId);
|
|
176
|
+
const buoy = (0, buoyancy_1.computeBuoyancy)(ball.B0, ball.H, pool.alpha, pool.lambda, pool.t, ball.t_last, ball.permanent);
|
|
177
|
+
const linkedBalls = ball.links
|
|
178
|
+
.map((lid) => pool.balls.find((b) => b.id === lid))
|
|
179
|
+
.filter((b) => !!b)
|
|
180
|
+
.map((b) => ({
|
|
181
|
+
id: b.id,
|
|
182
|
+
type: b.type,
|
|
183
|
+
summary: b.summary,
|
|
184
|
+
buoyancy: (0, buoyancy_1.computeBuoyancy)(b.B0, b.H, pool.alpha, pool.lambda, pool.t, b.t_last, b.permanent),
|
|
185
|
+
}));
|
|
186
|
+
res.json({ id: ballId, content, buoyancy: buoy, linked_balls: linkedBalls });
|
|
187
|
+
}).catch((err) => {
|
|
188
|
+
if (!res.headersSent)
|
|
189
|
+
res.status(500).json({ error: err.message || err });
|
|
190
|
+
});
|
|
191
|
+
});
|
|
119
192
|
// GET /api/memory-pool/global/sources
|
|
120
193
|
router.get('/global/sources', (_req, res) => {
|
|
121
194
|
const sources = (0, global_pool_manager_1.readSources)();
|
|
@@ -132,17 +205,16 @@ router.delete('/global/sources/:projectId', (req, res) => {
|
|
|
132
205
|
});
|
|
133
206
|
// POST /api/memory-pool/global/sync
|
|
134
207
|
router.post('/global/sync', (_req, res) => {
|
|
135
|
-
|
|
136
|
-
const result = (0, global_pool_manager_1.syncToGlobal)();
|
|
208
|
+
(0, global_pool_manager_1.syncToGlobal)().then((result) => {
|
|
137
209
|
res.json(result);
|
|
138
|
-
}
|
|
139
|
-
catch (err) {
|
|
210
|
+
}).catch((err) => {
|
|
140
211
|
if (err.message === 'SYNC_IN_PROGRESS') {
|
|
141
212
|
res.status(409).json({ error: 'Sync already in progress' });
|
|
142
213
|
return;
|
|
143
214
|
}
|
|
144
|
-
|
|
145
|
-
|
|
215
|
+
if (!res.headersSent)
|
|
216
|
+
res.status(500).json({ error: 'Sync failed: ' + (err.message || err) });
|
|
217
|
+
});
|
|
146
218
|
});
|
|
147
219
|
// ══════════════════════════════════════════════════════════════════════════════
|
|
148
220
|
// Project Memory Pool Endpoints
|
|
@@ -157,7 +229,6 @@ router.get('/:projectId/status', (req, res) => {
|
|
|
157
229
|
res.json({ initialized: false });
|
|
158
230
|
return;
|
|
159
231
|
}
|
|
160
|
-
// Try v2 format first
|
|
161
232
|
const pool = (0, pool_manager_1.readPool)(poolDir);
|
|
162
233
|
if (pool) {
|
|
163
234
|
res.json({
|
|
@@ -169,6 +240,7 @@ router.get('/:projectId/status', (req, res) => {
|
|
|
169
240
|
lambda: pool.lambda,
|
|
170
241
|
alpha: pool.alpha,
|
|
171
242
|
active_capacity: pool.active_capacity,
|
|
243
|
+
surface_width: pool.surface_width ?? 10000,
|
|
172
244
|
next_id: pool.next_id,
|
|
173
245
|
pool: pool.pool,
|
|
174
246
|
initialized_at: pool.initialized_at,
|
|
@@ -177,7 +249,6 @@ router.get('/:projectId/status', (req, res) => {
|
|
|
177
249
|
});
|
|
178
250
|
return;
|
|
179
251
|
}
|
|
180
|
-
// Fall back to v1 format
|
|
181
252
|
const stateFile = path.join(poolDir, 'state.json');
|
|
182
253
|
try {
|
|
183
254
|
const state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
|
|
@@ -199,52 +270,51 @@ router.post('/:projectId/init', (req, res) => {
|
|
|
199
270
|
if (!folder)
|
|
200
271
|
return;
|
|
201
272
|
const poolDir = path.join(folder, MEMORY_POOL_DIR);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
// Create directory structure
|
|
207
|
-
fs.mkdirSync(path.join(poolDir, 'balls'), { recursive: true });
|
|
208
|
-
// Generate documents
|
|
209
|
-
(0, config_1.atomicWriteSync)(path.join(poolDir, 'SPEC.md'), (0, templates_1.generateSpecMd)());
|
|
210
|
-
(0, config_1.atomicWriteSync)(path.join(poolDir, 'QUICK-REF.md'), (0, templates_1.generateQuickRefMd)());
|
|
211
|
-
// Create pool.json (v2 format directly)
|
|
212
|
-
const now = new Date().toISOString();
|
|
213
|
-
const pool = {
|
|
214
|
-
version: 2,
|
|
215
|
-
t: 0,
|
|
216
|
-
lambda: 0.97,
|
|
217
|
-
alpha: 1.0,
|
|
218
|
-
active_capacity: 20,
|
|
219
|
-
next_id: 1,
|
|
220
|
-
pool: 'project',
|
|
221
|
-
initialized_at: now,
|
|
222
|
-
balls: [],
|
|
223
|
-
};
|
|
224
|
-
(0, pool_manager_1.writePool)(poolDir, pool);
|
|
225
|
-
// Register with global pool
|
|
226
|
-
try {
|
|
227
|
-
const project = (0, config_1.getProject)(req.params.projectId);
|
|
228
|
-
if (project) {
|
|
229
|
-
(0, global_pool_manager_1.registerProject)(req.params.projectId, project.name, poolDir);
|
|
230
|
-
pool.global_pool_path = (0, global_pool_manager_1.getGlobalPoolDir)();
|
|
231
|
-
(0, pool_manager_1.writePool)(poolDir, pool);
|
|
273
|
+
(0, pool_lock_1.withPoolLock)(poolDir, () => {
|
|
274
|
+
if ((0, pool_manager_1.isInitialized)(poolDir)) {
|
|
275
|
+
res.status(409).json({ error: 'Memory pool already initialized' });
|
|
276
|
+
return;
|
|
232
277
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
278
|
+
fs.mkdirSync(path.join(poolDir, 'balls'), { recursive: true });
|
|
279
|
+
(0, config_1.atomicWriteSync)(path.join(poolDir, 'SPEC.md'), (0, templates_1.generateSpecMd)());
|
|
280
|
+
(0, config_1.atomicWriteSync)(path.join(poolDir, 'QUICK-REF.md'), (0, templates_1.generateQuickRefMd)());
|
|
281
|
+
const now = new Date().toISOString();
|
|
282
|
+
const pool = {
|
|
283
|
+
version: 2,
|
|
284
|
+
t: 0,
|
|
285
|
+
lambda: 0.97,
|
|
286
|
+
alpha: 1.0,
|
|
287
|
+
active_capacity: 20,
|
|
288
|
+
surface_width: 10000,
|
|
289
|
+
next_id: 1,
|
|
290
|
+
pool: 'project',
|
|
291
|
+
initialized_at: now,
|
|
292
|
+
balls: [],
|
|
293
|
+
};
|
|
294
|
+
(0, pool_manager_1.writePool)(poolDir, pool);
|
|
295
|
+
try {
|
|
296
|
+
const project = (0, config_1.getProject)(req.params.projectId);
|
|
297
|
+
if (project) {
|
|
298
|
+
(0, global_pool_manager_1.registerProject)(req.params.projectId, project.name, poolDir);
|
|
299
|
+
pool.global_pool_path = (0, global_pool_manager_1.getGlobalPoolDir)();
|
|
300
|
+
(0, pool_manager_1.writePool)(poolDir, pool);
|
|
301
|
+
}
|
|
242
302
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
303
|
+
catch { /* non-fatal */ }
|
|
304
|
+
const claudeMdPath = path.join(folder, 'CLAUDE.md');
|
|
305
|
+
try {
|
|
306
|
+
const existing = fs.existsSync(claudeMdPath) ? fs.readFileSync(claudeMdPath, 'utf-8') : '';
|
|
307
|
+
if (!existing.includes(CLAUDE_MD_MARKER)) {
|
|
308
|
+
(0, config_1.atomicWriteSync)(claudeMdPath, existing + '\n' + (0, templates_1.generateClaudeMdBlock)());
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch { /* non-fatal */ }
|
|
312
|
+
(0, pool_manager_1.buildSurface)(poolDir);
|
|
313
|
+
res.json({ success: true });
|
|
314
|
+
}).catch((err) => {
|
|
315
|
+
if (!res.headersSent)
|
|
316
|
+
res.status(500).json({ error: err.message || err });
|
|
317
|
+
});
|
|
248
318
|
});
|
|
249
319
|
// POST /api/memory-pool/:projectId/upgrade
|
|
250
320
|
router.post('/:projectId/upgrade', (req, res) => {
|
|
@@ -256,20 +326,17 @@ router.post('/:projectId/upgrade', (req, res) => {
|
|
|
256
326
|
res.status(404).json({ error: 'Memory pool not initialized' });
|
|
257
327
|
return;
|
|
258
328
|
}
|
|
259
|
-
|
|
329
|
+
(0, pool_lock_1.withPoolLock)(poolDir, () => {
|
|
260
330
|
const allChanges = [];
|
|
261
|
-
// Step 1: Migrate data format (v1 → v2)
|
|
262
331
|
if ((0, pool_manager_1.needsUpgrade)(poolDir)) {
|
|
263
332
|
const { changes } = (0, pool_manager_1.migrateV1toV2)(poolDir);
|
|
264
333
|
allChanges.push(...changes);
|
|
265
334
|
}
|
|
266
|
-
// Step 2: Regenerate documentation files (always update to latest templates)
|
|
267
335
|
const pool = (0, pool_manager_1.readPool)(poolDir);
|
|
268
336
|
if (pool) {
|
|
269
337
|
(0, config_1.atomicWriteSync)(path.join(poolDir, 'SPEC.md'), (0, templates_1.generateSpecMd)());
|
|
270
338
|
(0, config_1.atomicWriteSync)(path.join(poolDir, 'QUICK-REF.md'), (0, templates_1.generateQuickRefMd)());
|
|
271
339
|
allChanges.push('updated SPEC.md and QUICK-REF.md');
|
|
272
|
-
// Step 3: Update CLAUDE.md block
|
|
273
340
|
const claudeMdPath = path.join(folder, 'CLAUDE.md');
|
|
274
341
|
try {
|
|
275
342
|
if (fs.existsSync(claudeMdPath)) {
|
|
@@ -289,10 +356,7 @@ router.post('/:projectId/upgrade', (req, res) => {
|
|
|
289
356
|
allChanges.push('updated CLAUDE.md memory pool section');
|
|
290
357
|
}
|
|
291
358
|
}
|
|
292
|
-
catch {
|
|
293
|
-
// Non-fatal
|
|
294
|
-
}
|
|
295
|
-
// Step 4: Register with global pool
|
|
359
|
+
catch { /* non-fatal */ }
|
|
296
360
|
try {
|
|
297
361
|
const project = (0, config_1.getProject)(req.params.projectId);
|
|
298
362
|
if (project) {
|
|
@@ -303,15 +367,16 @@ router.post('/:projectId/upgrade', (req, res) => {
|
|
|
303
367
|
}
|
|
304
368
|
}
|
|
305
369
|
catch { /* non-fatal */ }
|
|
370
|
+
(0, pool_manager_1.buildSurface)(poolDir);
|
|
306
371
|
res.json({ success: true, version: pool.version, changes: allChanges });
|
|
307
372
|
}
|
|
308
373
|
else {
|
|
309
374
|
res.status(500).json({ error: 'Failed to read pool after migration' });
|
|
310
375
|
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
376
|
+
}).catch((err) => {
|
|
377
|
+
if (!res.headersSent)
|
|
378
|
+
res.status(500).json({ error: 'Upgrade failed: ' + (err.message || err) });
|
|
379
|
+
});
|
|
315
380
|
});
|
|
316
381
|
// GET /api/memory-pool/:projectId/index
|
|
317
382
|
router.get('/:projectId/index', (req, res) => {
|
|
@@ -319,14 +384,12 @@ router.get('/:projectId/index', (req, res) => {
|
|
|
319
384
|
if (!folder)
|
|
320
385
|
return;
|
|
321
386
|
const poolDir = path.join(folder, MEMORY_POOL_DIR);
|
|
322
|
-
// Try v2 format
|
|
323
387
|
const pool = (0, pool_manager_1.readPool)(poolDir);
|
|
324
388
|
if (pool) {
|
|
325
389
|
const balls = (0, pool_manager_1.enrichBallsWithBuoyancy)(pool);
|
|
326
|
-
res.json({ t: pool.t, updated_at: new Date().toISOString(), balls });
|
|
390
|
+
res.json({ t: pool.t, updated_at: new Date().toISOString(), balls, active_capacity: pool.active_capacity });
|
|
327
391
|
return;
|
|
328
392
|
}
|
|
329
|
-
// Fall back to v1 format (read index.json directly)
|
|
330
393
|
const indexFile = path.join(poolDir, 'index.json');
|
|
331
394
|
try {
|
|
332
395
|
const data = JSON.parse(fs.readFileSync(indexFile, 'utf-8'));
|
|
@@ -350,14 +413,9 @@ router.get('/:projectId/snapshot', (req, res) => {
|
|
|
350
413
|
const cap = pool.active_capacity;
|
|
351
414
|
const ballCount = pool.balls.length;
|
|
352
415
|
const snapshot = (0, pool_manager_1.generateSnapshot)(pool);
|
|
353
|
-
res.json({
|
|
354
|
-
snapshot,
|
|
355
|
-
t: pool.t,
|
|
356
|
-
activeCount: Math.min(ballCount, cap),
|
|
357
|
-
deepCount: Math.max(0, ballCount - cap),
|
|
358
|
-
});
|
|
416
|
+
res.json({ snapshot, t: pool.t, activeCount: Math.min(ballCount, cap), deepCount: Math.max(0, ballCount - cap) });
|
|
359
417
|
});
|
|
360
|
-
// GET /api/memory-pool/:projectId/ball/:ballId
|
|
418
|
+
// GET /api/memory-pool/:projectId/ball/:ballId (pure read, no side effect)
|
|
361
419
|
router.get('/:projectId/ball/:ballId', (req, res) => {
|
|
362
420
|
const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
|
|
363
421
|
if (!folder)
|
|
@@ -375,43 +433,395 @@ router.get('/:projectId/ball/:ballId', (req, res) => {
|
|
|
375
433
|
}
|
|
376
434
|
res.json({ id: ballId, content });
|
|
377
435
|
});
|
|
378
|
-
//
|
|
379
|
-
router.
|
|
436
|
+
// PUT /api/memory-pool/:projectId/surface-width
|
|
437
|
+
router.put('/:projectId/surface-width', (req, res) => {
|
|
380
438
|
const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
|
|
381
439
|
if (!folder)
|
|
382
440
|
return;
|
|
383
441
|
const poolDir = path.join(folder, MEMORY_POOL_DIR);
|
|
384
|
-
|
|
385
|
-
const
|
|
386
|
-
|
|
442
|
+
(0, pool_lock_1.withPoolLock)(poolDir, () => {
|
|
443
|
+
const pool = (0, pool_manager_1.readPool)(poolDir);
|
|
444
|
+
if (!pool) {
|
|
445
|
+
res.status(404).json({ error: 'Memory pool not initialized' });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const { surface_width } = req.body;
|
|
449
|
+
if (typeof surface_width !== 'number' || surface_width < 1000 || surface_width > 100000) {
|
|
450
|
+
res.status(400).json({ error: 'surface_width must be a number between 1000 and 100000' });
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
pool.surface_width = surface_width;
|
|
454
|
+
(0, pool_manager_1.writePool)(poolDir, pool);
|
|
455
|
+
const { surfaceBalls, totalTokens } = (0, pool_manager_1.buildSurface)(poolDir);
|
|
456
|
+
res.json({ success: true, surface_width, surface_balls: surfaceBalls.length, total_tokens: totalTokens });
|
|
457
|
+
}).catch((err) => {
|
|
458
|
+
if (!res.headersSent)
|
|
459
|
+
res.status(500).json({ error: err.message || err });
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
463
|
+
// Ball CRUD Endpoints (ccweb-managed)
|
|
464
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
465
|
+
// POST /api/memory-pool/:projectId/balls — Create ball
|
|
466
|
+
router.post('/:projectId/balls', (req, res) => {
|
|
467
|
+
const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
|
|
468
|
+
if (!folder)
|
|
469
|
+
return;
|
|
470
|
+
const poolDir = path.join(folder, MEMORY_POOL_DIR);
|
|
471
|
+
const { type, summary, content, links, b0_override } = req.body;
|
|
472
|
+
// Validate required fields
|
|
473
|
+
if (!type || !VALID_TYPES.includes(type)) {
|
|
474
|
+
res.status(400).json({ error: `type must be one of: ${VALID_TYPES.join(', ')}` });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (!summary || typeof summary !== 'string' || summary.length > 500) {
|
|
478
|
+
res.status(400).json({ error: 'summary is required and must be <= 500 characters' });
|
|
479
|
+
return;
|
|
387
480
|
}
|
|
388
|
-
|
|
389
|
-
res.status(
|
|
481
|
+
if (!content || typeof content !== 'string' || content.length > 100000) {
|
|
482
|
+
res.status(400).json({ error: 'content is required and must be <= 100KB' });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (links && (!Array.isArray(links) || !validateBallIds(links))) {
|
|
486
|
+
res.status(400).json({ error: 'links must be an array of valid ball IDs' });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
if (b0_override !== undefined && b0_override !== null &&
|
|
490
|
+
(typeof b0_override !== 'number' || b0_override < 1 || b0_override > 10)) {
|
|
491
|
+
res.status(400).json({ error: 'b0_override must be a number between 1 and 10' });
|
|
492
|
+
return;
|
|
390
493
|
}
|
|
494
|
+
(0, pool_lock_1.withPoolLock)(poolDir, () => {
|
|
495
|
+
const pool = (0, pool_manager_1.readPool)(poolDir);
|
|
496
|
+
if (!pool) {
|
|
497
|
+
res.status(404).json({ error: 'Memory pool not initialized' });
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
// M-2 fix: validate that linked ball IDs exist in pool
|
|
501
|
+
const validLinks = [];
|
|
502
|
+
if (links) {
|
|
503
|
+
const existingIds = new Set(pool.balls.map((b) => b.id));
|
|
504
|
+
for (const lid of links) {
|
|
505
|
+
if (!existingIds.has(lid)) {
|
|
506
|
+
res.status(400).json({ error: `Linked ball ${lid} does not exist` });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
validLinks.push(lid);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const ballId = `ball_${String(pool.next_id).padStart(4, '0')}`;
|
|
513
|
+
pool.next_id++;
|
|
514
|
+
const B0 = b0_override ?? DEFAULT_B0[type] ?? 5;
|
|
515
|
+
const diameter = (0, pool_manager_1.estimateTokens)(content);
|
|
516
|
+
// Write ball content first (gate pattern)
|
|
517
|
+
(0, pool_manager_1.writeBallContent)(poolDir, ballId, content);
|
|
518
|
+
const newBall = {
|
|
519
|
+
id: ballId,
|
|
520
|
+
type: type,
|
|
521
|
+
summary,
|
|
522
|
+
B0,
|
|
523
|
+
H: 0,
|
|
524
|
+
t_last: pool.t,
|
|
525
|
+
hardness: 5,
|
|
526
|
+
permanent: false,
|
|
527
|
+
links: validLinks,
|
|
528
|
+
created_at: new Date().toISOString(),
|
|
529
|
+
diameter,
|
|
530
|
+
};
|
|
531
|
+
pool.balls.push(newBall);
|
|
532
|
+
(0, pool_manager_1.writePool)(poolDir, pool);
|
|
533
|
+
(0, pool_manager_1.buildSurface)(poolDir);
|
|
534
|
+
res.json({ id: ballId, B0, diameter });
|
|
535
|
+
}).catch((err) => {
|
|
536
|
+
if (!res.headersSent)
|
|
537
|
+
res.status(500).json({ error: err.message || err });
|
|
538
|
+
});
|
|
391
539
|
});
|
|
392
|
-
//
|
|
393
|
-
router.
|
|
540
|
+
// PUT /api/memory-pool/:projectId/balls/:ballId — Update ball metadata after content edit
|
|
541
|
+
router.put('/:projectId/balls/:ballId', (req, res) => {
|
|
394
542
|
const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
|
|
395
543
|
if (!folder)
|
|
396
544
|
return;
|
|
545
|
+
const { ballId } = req.params;
|
|
546
|
+
if (!BALL_ID_RE.test(ballId)) {
|
|
547
|
+
res.status(400).json({ error: 'Invalid ball ID format' });
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
397
550
|
const poolDir = path.join(folder, MEMORY_POOL_DIR);
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
551
|
+
(0, pool_lock_1.withPoolLock)(poolDir, () => {
|
|
552
|
+
const pool = (0, pool_manager_1.readPool)(poolDir);
|
|
553
|
+
if (!pool) {
|
|
554
|
+
res.status(404).json({ error: 'Memory pool not initialized' });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const ball = pool.balls.find((b) => b.id === ballId);
|
|
558
|
+
if (!ball) {
|
|
559
|
+
res.status(404).json({ error: 'Ball not found in pool' });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
// Recalculate diameter from current content
|
|
563
|
+
const diameter = (0, pool_manager_1.computeDiameter)(poolDir, ballId);
|
|
564
|
+
ball.diameter = diameter;
|
|
565
|
+
// Update summary if provided
|
|
566
|
+
const { summary } = req.body;
|
|
567
|
+
if (summary && typeof summary === 'string') {
|
|
568
|
+
ball.summary = summary;
|
|
569
|
+
}
|
|
570
|
+
(0, pool_manager_1.writePool)(poolDir, pool);
|
|
571
|
+
(0, pool_manager_1.buildSurface)(poolDir);
|
|
572
|
+
res.json({ id: ballId, diameter, summary: ball.summary });
|
|
573
|
+
}).catch((err) => {
|
|
574
|
+
if (!res.headersSent)
|
|
575
|
+
res.status(500).json({ error: err.message || err });
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
// POST /api/memory-pool/:projectId/balls/:ballId/hit — Hit query (read + count)
|
|
579
|
+
router.post('/:projectId/balls/:ballId/hit', (req, res) => {
|
|
580
|
+
const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
|
|
581
|
+
if (!folder)
|
|
582
|
+
return;
|
|
583
|
+
const { ballId } = req.params;
|
|
584
|
+
if (!BALL_ID_RE.test(ballId)) {
|
|
585
|
+
res.status(400).json({ error: 'Invalid ball ID format' });
|
|
401
586
|
return;
|
|
402
587
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
588
|
+
const poolDir = path.join(folder, MEMORY_POOL_DIR);
|
|
589
|
+
(0, pool_lock_1.withPoolLock)(poolDir, () => {
|
|
590
|
+
const pool = (0, pool_manager_1.readPool)(poolDir);
|
|
591
|
+
if (!pool) {
|
|
592
|
+
res.status(404).json({ error: 'Memory pool not initialized' });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const ball = pool.balls.find((b) => b.id === ballId);
|
|
596
|
+
if (!ball) {
|
|
597
|
+
res.status(404).json({ error: 'Ball not found' });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
// Update hit count
|
|
601
|
+
ball.H += 1;
|
|
602
|
+
ball.t_last = pool.t;
|
|
603
|
+
(0, pool_manager_1.writePool)(poolDir, pool);
|
|
604
|
+
// Skip surface rebuild for hit — H change rarely affects surface order
|
|
605
|
+
// Read content
|
|
606
|
+
const content = (0, pool_manager_1.readBallContent)(poolDir, ballId);
|
|
607
|
+
const buoy = (0, buoyancy_1.computeBuoyancy)(ball.B0, ball.H, pool.alpha, pool.lambda, pool.t, ball.t_last, ball.permanent);
|
|
608
|
+
// Read linked balls' summaries
|
|
609
|
+
const linkedBalls = ball.links
|
|
610
|
+
.map((lid) => pool.balls.find((b) => b.id === lid))
|
|
611
|
+
.filter((b) => !!b)
|
|
612
|
+
.map((b) => ({
|
|
613
|
+
id: b.id,
|
|
614
|
+
type: b.type,
|
|
615
|
+
summary: b.summary,
|
|
616
|
+
buoyancy: (0, buoyancy_1.computeBuoyancy)(b.B0, b.H, pool.alpha, pool.lambda, pool.t, b.t_last, b.permanent),
|
|
617
|
+
}));
|
|
618
|
+
res.json({ id: ballId, content, buoyancy: buoy, linked_balls: linkedBalls });
|
|
619
|
+
}).catch((err) => {
|
|
620
|
+
if (!res.headersSent)
|
|
621
|
+
res.status(500).json({ error: err.message || err });
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
// DELETE /api/memory-pool/:projectId/balls/:ballId — Delete ball
|
|
625
|
+
router.delete('/:projectId/balls/:ballId', (req, res) => {
|
|
626
|
+
const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
|
|
627
|
+
if (!folder)
|
|
628
|
+
return;
|
|
629
|
+
const { ballId } = req.params;
|
|
630
|
+
if (!BALL_ID_RE.test(ballId)) {
|
|
631
|
+
res.status(400).json({ error: 'Invalid ball ID format' });
|
|
406
632
|
return;
|
|
407
633
|
}
|
|
408
|
-
|
|
409
|
-
|
|
634
|
+
const poolDir = path.join(folder, MEMORY_POOL_DIR);
|
|
635
|
+
(0, pool_lock_1.withPoolLock)(poolDir, () => {
|
|
636
|
+
const pool = (0, pool_manager_1.readPool)(poolDir);
|
|
637
|
+
if (!pool) {
|
|
638
|
+
res.status(404).json({ error: 'Memory pool not initialized' });
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const idx = pool.balls.findIndex((b) => b.id === ballId);
|
|
642
|
+
if (idx === -1) {
|
|
643
|
+
res.status(404).json({ error: 'Ball not found' });
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
// Remove from balls array
|
|
647
|
+
pool.balls.splice(idx, 1);
|
|
648
|
+
// Clean up links in other balls that reference this ball
|
|
649
|
+
let linksCleaned = 0;
|
|
650
|
+
for (const b of pool.balls) {
|
|
651
|
+
const before = b.links.length;
|
|
652
|
+
b.links = b.links.filter((l) => l !== ballId);
|
|
653
|
+
linksCleaned += before - b.links.length;
|
|
654
|
+
}
|
|
655
|
+
(0, pool_manager_1.writePool)(poolDir, pool);
|
|
656
|
+
// Delete ball file
|
|
657
|
+
const ballFile = path.join(poolDir, 'balls', `${ballId}.md`);
|
|
658
|
+
try {
|
|
659
|
+
fs.unlinkSync(ballFile);
|
|
660
|
+
}
|
|
661
|
+
catch { /* may not exist */ }
|
|
662
|
+
(0, pool_manager_1.buildSurface)(poolDir);
|
|
663
|
+
res.json({ id: ballId, deleted: true, links_cleaned: linksCleaned });
|
|
664
|
+
}).catch((err) => {
|
|
665
|
+
if (!res.headersSent)
|
|
666
|
+
res.status(500).json({ error: err.message || err });
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
// PATCH /api/memory-pool/:projectId/balls/:ballId/links — Manage links
|
|
670
|
+
router.patch('/:projectId/balls/:ballId/links', (req, res) => {
|
|
671
|
+
const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
|
|
672
|
+
if (!folder)
|
|
673
|
+
return;
|
|
674
|
+
const { ballId } = req.params;
|
|
675
|
+
if (!BALL_ID_RE.test(ballId)) {
|
|
676
|
+
res.status(400).json({ error: 'Invalid ball ID format' });
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
const poolDir = path.join(folder, MEMORY_POOL_DIR);
|
|
680
|
+
const { add, remove } = req.body;
|
|
681
|
+
if (add && (!Array.isArray(add) || !validateBallIds(add))) {
|
|
682
|
+
res.status(400).json({ error: 'add must be an array of valid ball IDs' });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (remove && (!Array.isArray(remove) || !validateBallIds(remove))) {
|
|
686
|
+
res.status(400).json({ error: 'remove must be an array of valid ball IDs' });
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
(0, pool_lock_1.withPoolLock)(poolDir, () => {
|
|
690
|
+
const pool = (0, pool_manager_1.readPool)(poolDir);
|
|
691
|
+
if (!pool) {
|
|
692
|
+
res.status(404).json({ error: 'Memory pool not initialized' });
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const ball = pool.balls.find((b) => b.id === ballId);
|
|
696
|
+
if (!ball) {
|
|
697
|
+
res.status(404).json({ error: 'Ball not found' });
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const existingIds = new Set(pool.balls.map((b) => b.id));
|
|
701
|
+
if (remove) {
|
|
702
|
+
const removeSet = new Set(remove);
|
|
703
|
+
ball.links = ball.links.filter((l) => !removeSet.has(l));
|
|
704
|
+
}
|
|
705
|
+
if (add) {
|
|
706
|
+
for (const lid of add) {
|
|
707
|
+
if (existingIds.has(lid) && !ball.links.includes(lid) && lid !== ballId) {
|
|
708
|
+
ball.links.push(lid);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
(0, pool_manager_1.writePool)(poolDir, pool);
|
|
713
|
+
(0, pool_manager_1.buildSurface)(poolDir);
|
|
714
|
+
res.json({ id: ballId, links: ball.links });
|
|
715
|
+
}).catch((err) => {
|
|
716
|
+
if (!res.headersSent)
|
|
717
|
+
res.status(500).json({ error: err.message || err });
|
|
718
|
+
});
|
|
719
|
+
});
|
|
720
|
+
// POST /api/memory-pool/:projectId/tick — Increment turn
|
|
721
|
+
router.post('/:projectId/tick', (req, res) => {
|
|
722
|
+
const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
|
|
723
|
+
if (!folder)
|
|
724
|
+
return;
|
|
725
|
+
const poolDir = path.join(folder, MEMORY_POOL_DIR);
|
|
726
|
+
const { session } = req.body || {};
|
|
727
|
+
(0, pool_lock_1.withPoolLock)(poolDir, () => {
|
|
728
|
+
const result = (0, pool_manager_1.tickPool)(poolDir, session);
|
|
729
|
+
if (!result) {
|
|
730
|
+
res.status(404).json({ error: 'Memory pool not initialized' });
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
410
733
|
res.json(result);
|
|
734
|
+
}).catch((err) => {
|
|
735
|
+
if (!res.headersSent)
|
|
736
|
+
res.status(500).json({ error: err.message || err });
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
// GET /api/memory-pool/:projectId/surface — Get active layer summary
|
|
740
|
+
router.get('/:projectId/surface', (req, res) => {
|
|
741
|
+
const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
|
|
742
|
+
if (!folder)
|
|
743
|
+
return;
|
|
744
|
+
const poolDir = path.join(folder, MEMORY_POOL_DIR);
|
|
745
|
+
(0, pool_lock_1.withPoolLock)(poolDir, () => {
|
|
746
|
+
const pool = (0, pool_manager_1.readPool)(poolDir);
|
|
747
|
+
if (!pool) {
|
|
748
|
+
res.status(404).json({ error: 'Memory pool not initialized' });
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
// Compensate missed tick if last_tick_at > 10 minutes ago
|
|
752
|
+
if (pool.last_tick_at) {
|
|
753
|
+
const elapsed = Date.now() - new Date(pool.last_tick_at).getTime();
|
|
754
|
+
if (elapsed > 10 * 60 * 1000) {
|
|
755
|
+
pool.t += 1;
|
|
756
|
+
pool.last_tick_at = new Date().toISOString();
|
|
757
|
+
pool.last_tick_session = undefined;
|
|
758
|
+
(0, pool_manager_1.writePool)(poolDir, pool);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
const { surfaceBalls, totalTokens } = (0, pool_manager_1.buildSurface)(poolDir);
|
|
762
|
+
res.json({
|
|
763
|
+
t: pool.t,
|
|
764
|
+
surface_width: pool.surface_width ?? 10000,
|
|
765
|
+
used_tokens: totalTokens,
|
|
766
|
+
balls: surfaceBalls,
|
|
767
|
+
});
|
|
768
|
+
}).catch((err) => {
|
|
769
|
+
if (!res.headersSent)
|
|
770
|
+
res.status(500).json({ error: err.message || err });
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
// POST /api/memory-pool/:projectId/maintenance — Maintenance suggestions + self-healing
|
|
774
|
+
router.post('/:projectId/maintenance', (req, res) => {
|
|
775
|
+
const folder = resolveProjectFolder(req.params.projectId, req.user?.username || '', res);
|
|
776
|
+
if (!folder)
|
|
777
|
+
return;
|
|
778
|
+
const poolDir = path.join(folder, MEMORY_POOL_DIR);
|
|
779
|
+
const pool = (0, pool_manager_1.readPool)(poolDir);
|
|
780
|
+
if (!pool) {
|
|
781
|
+
res.status(404).json({ error: 'Memory pool not initialized' });
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const enriched = (0, pool_manager_1.enrichBallsWithBuoyancy)(pool);
|
|
785
|
+
const cap = pool.active_capacity;
|
|
786
|
+
const activeFull = enriched.length >= cap;
|
|
787
|
+
// Split suggestions: all active layer balls, mark recommended based on hardness + size
|
|
788
|
+
const suggestions = [];
|
|
789
|
+
const activeBalls = enriched.slice(0, cap);
|
|
790
|
+
for (const b of activeBalls) {
|
|
791
|
+
const diameter = b.diameter ?? (0, pool_manager_1.computeDiameter)(poolDir, b.id);
|
|
792
|
+
if (diameter > 100) {
|
|
793
|
+
const recommended = b.hardness < 7;
|
|
794
|
+
suggestions.push({
|
|
795
|
+
action: 'split',
|
|
796
|
+
ball_id: b.id,
|
|
797
|
+
reason: `diameter=${diameter}tok, hardness=${b.hardness}${recommended ? ' — 可考虑拆分' : ' — 硬度过高,不建议拆分'}`,
|
|
798
|
+
recommended,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
411
801
|
}
|
|
412
|
-
|
|
413
|
-
|
|
802
|
+
// Self-healing: detect inconsistencies (H-4 fix)
|
|
803
|
+
const anomalies = [];
|
|
804
|
+
const ballsDir = path.join(poolDir, 'balls');
|
|
805
|
+
// Check for ghost entries (pool.json has ball but file missing)
|
|
806
|
+
for (const b of pool.balls) {
|
|
807
|
+
const file = path.join(ballsDir, `${b.id}.md`);
|
|
808
|
+
if (!fs.existsSync(file)) {
|
|
809
|
+
anomalies.push(`ghost: ${b.id} in pool.json but file missing`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
// Check for orphan files (file exists but not in pool.json)
|
|
813
|
+
try {
|
|
814
|
+
const poolIds = new Set(pool.balls.map((b) => b.id));
|
|
815
|
+
const files = fs.readdirSync(ballsDir).filter((f) => f.endsWith('.md') && f.startsWith('ball_'));
|
|
816
|
+
for (const f of files) {
|
|
817
|
+
const id = f.replace('.md', '');
|
|
818
|
+
if (!poolIds.has(id)) {
|
|
819
|
+
anomalies.push(`orphan_file: ${f} exists but not in pool.json`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
414
822
|
}
|
|
823
|
+
catch { /* balls dir may not exist */ }
|
|
824
|
+
res.json({ active_full: activeFull, suggestions, anomalies });
|
|
415
825
|
});
|
|
416
826
|
exports.default = router;
|
|
417
827
|
//# sourceMappingURL=memory-pool.js.map
|