taskover-mcp 1.1.0 → 1.2.0

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.
@@ -0,0 +1,1264 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ // Data lives next to the app
5
+ const DATA_DIR = path.join(__dirname, "..", "data");
6
+ const DATA_FILE = path.join(DATA_DIR, "tracker.json");
7
+
8
+ const EMPTY = {
9
+ projects: [],
10
+ tasks: [],
11
+ systems: [],
12
+ sessions: [],
13
+ changelog: [],
14
+ bugs: [],
15
+ decisions: [],
16
+ blueprints: [],
17
+ notes: [],
18
+ backups: [],
19
+ levels: [],
20
+ plugins: [],
21
+ buildErrors: [],
22
+ optimizeItems: [],
23
+ perfBudget: [],
24
+ playtests: [],
25
+ milestones: [],
26
+ iterations: [],
27
+ dialogues: [],
28
+ sounds: [],
29
+ controls: [],
30
+ assets: [],
31
+ refs: [],
32
+ marketingItems: [],
33
+ wikiPages: [],
34
+ shipChecked: [],
35
+ openQuestions: [],
36
+ };
37
+
38
+ // ===== CORE I/O =====
39
+
40
+ function ensureDir() {
41
+ if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
42
+ }
43
+
44
+ function load() {
45
+ ensureDir();
46
+ if (!fs.existsSync(DATA_FILE)) {
47
+ fs.writeFileSync(DATA_FILE, JSON.stringify(EMPTY, null, 2));
48
+ return JSON.parse(JSON.stringify(EMPTY));
49
+ }
50
+ try {
51
+ const raw = fs.readFileSync(DATA_FILE, "utf-8");
52
+ const d = JSON.parse(raw);
53
+ // Ensure every key exists
54
+ for (const k of Object.keys(EMPTY)) {
55
+ if (!d[k]) d[k] = [];
56
+ }
57
+ return d;
58
+ } catch (e) {
59
+ console.error("Data load error:", e.message);
60
+ return JSON.parse(JSON.stringify(EMPTY));
61
+ }
62
+ }
63
+
64
+ function save(data) {
65
+ ensureDir();
66
+ fs.writeFileSync(DATA_FILE, JSON.stringify(data, null, 2));
67
+ }
68
+
69
+ function id() {
70
+ return Date.now().toString(36) + Math.random().toString(36).substr(2, 6);
71
+ }
72
+ function now() { return new Date().toISOString(); }
73
+ function today() { return now().split("T")[0]; }
74
+
75
+ // ===== PROJECTS =====
76
+
77
+ function listProjects() {
78
+ return load().projects;
79
+ }
80
+
81
+ function getProject(pid) {
82
+ return load().projects.find(p => p.id === pid) || null;
83
+ }
84
+
85
+ function addProject({ name, description, trelloBoardId, status, ...extra }) {
86
+ const d = load();
87
+ const p = {
88
+ id: name.toLowerCase().replace(/[^a-z0-9]+/g, "-") + "-" + Date.now().toString(36),
89
+ name, description: description || "", trelloBoardId: trelloBoardId || "",
90
+ status: status || "active", createdAt: now(), lastUpdated: now(),
91
+ currentFocus: "", deadline: "", engine: "", icon: "", color: "#6EE7A0",
92
+ useMcp: true, guideLevel: 3, onboardDone: [], activityLog: [],
93
+ ...extra,
94
+ };
95
+ d.projects.push(p);
96
+ save(d);
97
+ return p;
98
+ }
99
+
100
+ function updateProject(pid, updates) {
101
+ const d = load();
102
+ const i = d.projects.findIndex(p => p.id === pid);
103
+ if (i === -1) return null;
104
+ d.projects[i] = { ...d.projects[i], ...updates, lastUpdated: now() };
105
+ save(d);
106
+ return d.projects[i];
107
+ }
108
+
109
+ // ===== TASKS (replaces To Do / Doing / Done / Scrapped) =====
110
+
111
+ function getTasks(pid, statusFilter, phaseFilter) {
112
+ const d = load();
113
+ let tasks = d.tasks.filter(t => t.projectId === pid);
114
+ if (statusFilter) tasks = tasks.filter(t => t.status === statusFilter);
115
+ if (phaseFilter) tasks = tasks.filter(t => t.phase === phaseFilter);
116
+ return tasks;
117
+ }
118
+
119
+ function getTask(taskId) {
120
+ return load().tasks.find(t => t.id === taskId) || null;
121
+ }
122
+
123
+ function addTask({ projectId, title, description, status, phase, priority, tags, checklist }) {
124
+ const d = load();
125
+ const t = {
126
+ id: id(), projectId, title,
127
+ description: description || "",
128
+ status: status || "todo", // todo, doing, done, scrapped
129
+ phase: phase || "", // "Phase 1 — Foundation", etc.
130
+ priority: priority || "medium",
131
+ tags: tags || [],
132
+ checklist: checklist || [], // [{text, done}]
133
+ comments: [],
134
+ createdAt: now(), lastUpdated: now(),
135
+ };
136
+ d.tasks.push(t);
137
+ save(d);
138
+ return t;
139
+ }
140
+
141
+ function updateTask(taskId, updates) {
142
+ const d = load();
143
+ const i = d.tasks.findIndex(t => t.id === taskId);
144
+ if (i === -1) return null;
145
+ d.tasks[i] = { ...d.tasks[i], ...updates, lastUpdated: now() };
146
+ save(d);
147
+ return d.tasks[i];
148
+ }
149
+
150
+ function moveTask(taskId, newStatus) {
151
+ return updateTask(taskId, { status: newStatus });
152
+ }
153
+
154
+ function addTaskComment(taskId, text) {
155
+ const d = load();
156
+ const i = d.tasks.findIndex(t => t.id === taskId);
157
+ if (i === -1) return null;
158
+ const comment = { id: id(), text, createdAt: now() };
159
+ d.tasks[i].comments.push(comment);
160
+ d.tasks[i].lastUpdated = now();
161
+ save(d);
162
+ return comment;
163
+ }
164
+
165
+ function updateChecklist(taskId, checklist) {
166
+ return updateTask(taskId, { checklist });
167
+ }
168
+
169
+ // ===== SYSTEMS (replaces Claude Memory documentation cards) =====
170
+
171
+ function getSystems(pid) {
172
+ return load().systems.filter(s => s.projectId === pid);
173
+ }
174
+
175
+ function getSystem(sid) {
176
+ return load().systems.find(s => s.id === sid) || null;
177
+ }
178
+
179
+ function addSystem({ projectId, name, description, status, keyFiles, notes, criticalRules }) {
180
+ const d = load();
181
+ const s = {
182
+ id: id(), projectId, name,
183
+ description: description || "",
184
+ status: status || "planned", // working, wip, broken, planned
185
+ keyFiles: keyFiles || "",
186
+ notes: notes || "",
187
+ criticalRules: criticalRules || "",
188
+ createdAt: now(), lastUpdated: today(),
189
+ };
190
+ d.systems.push(s);
191
+ save(d);
192
+ return s;
193
+ }
194
+
195
+ function updateSystem(sid, updates) {
196
+ const d = load();
197
+ const i = d.systems.findIndex(s => s.id === sid);
198
+ if (i === -1) return null;
199
+ d.systems[i] = { ...d.systems[i], ...updates, lastUpdated: today() };
200
+ save(d);
201
+ return d.systems[i];
202
+ }
203
+
204
+ function deleteSystem(sid) {
205
+ const d = load();
206
+ d.systems = d.systems.filter(s => s.id !== sid);
207
+ save(d);
208
+ return true;
209
+ }
210
+
211
+ // ===== SESSIONS (replaces Claude Chat Memory) =====
212
+
213
+ function getSessions(pid, limit) {
214
+ const d = load();
215
+ let s = d.sessions.filter(s => s.projectId === pid).sort((a, b) => b.date.localeCompare(a.date));
216
+ if (limit) s = s.slice(0, limit);
217
+ return s;
218
+ }
219
+
220
+ function getLastSession(pid) {
221
+ return getSessions(pid, 1)[0] || null;
222
+ }
223
+
224
+ function logSession({ projectId, title, summary, whatWeDid, whereWeLeftOff, nextSteps, systemsWorkedOn }) {
225
+ const d = load();
226
+ const s = {
227
+ id: id(), projectId, date: today(), timestamp: now(),
228
+ title, summary: summary || "",
229
+ whatWeDid: whatWeDid || [],
230
+ whereWeLeftOff: whereWeLeftOff || "",
231
+ nextSteps: nextSteps || [],
232
+ systemsWorkedOn: systemsWorkedOn || [],
233
+ };
234
+ d.sessions.push(s);
235
+ save(d);
236
+ return s;
237
+ }
238
+
239
+ function updateSession(sid, updates) {
240
+ const d = load();
241
+ const i = d.sessions.findIndex(s => s.id === sid);
242
+ if (i === -1) return null;
243
+ d.sessions[i] = { ...d.sessions[i], ...updates };
244
+ save(d);
245
+ return d.sessions[i];
246
+ }
247
+
248
+ // ===== CHANGELOG =====
249
+
250
+ function getChangelog(pid, limit) {
251
+ const d = load();
252
+ let c = d.changelog.filter(c => c.projectId === pid).sort((a, b) => b.date.localeCompare(a.date));
253
+ if (limit) c = c.slice(0, limit);
254
+ return c;
255
+ }
256
+
257
+ function addChangelog({ projectId, title, changes, systemsAffected }) {
258
+ const d = load();
259
+ const c = {
260
+ id: id(), projectId, date: today(), timestamp: now(),
261
+ title, changes: changes || [], systemsAffected: systemsAffected || [],
262
+ };
263
+ d.changelog.push(c);
264
+ save(d);
265
+ return c;
266
+ }
267
+
268
+ // ===== BUGS =====
269
+
270
+ function getBugs(pid, statusFilter) {
271
+ const d = load();
272
+ let bugs = d.bugs.filter(b => b.projectId === pid);
273
+ if (statusFilter) bugs = bugs.filter(b => b.status === statusFilter);
274
+ return bugs;
275
+ }
276
+
277
+ function addBug({ projectId, systemId, title, description, priority, stepsToReproduce }) {
278
+ const d = load();
279
+ const b = {
280
+ id: id(), projectId, systemId: systemId || "",
281
+ title, description: description || "",
282
+ priority: priority || "medium", status: "open",
283
+ stepsToReproduce: stepsToReproduce || "",
284
+ dateFound: today(), dateFixed: null, fixDescription: "",
285
+ };
286
+ d.bugs.push(b);
287
+ save(d);
288
+ return b;
289
+ }
290
+
291
+ function updateBug(bid, updates) {
292
+ const d = load();
293
+ const i = d.bugs.findIndex(b => b.id === bid);
294
+ if (i === -1) return null;
295
+ if (updates.status === "fixed" && !d.bugs[i].dateFixed) updates.dateFixed = today();
296
+ d.bugs[i] = { ...d.bugs[i], ...updates };
297
+ save(d);
298
+ return d.bugs[i];
299
+ }
300
+
301
+ function fixBug(bid, fixDescription) {
302
+ return updateBug(bid, { status: "fixed", fixDescription, dateFixed: today() });
303
+ }
304
+
305
+ // ===== DECISIONS =====
306
+
307
+ function getDecisions(pid) {
308
+ return load().decisions.filter(d => d.projectId === pid).sort((a, b) => b.date.localeCompare(a.date));
309
+ }
310
+
311
+ function logDecision({ projectId, decision, context, systemId, alternatives }) {
312
+ const d = load();
313
+ const dec = {
314
+ id: id(), projectId, date: today(), timestamp: now(),
315
+ decision, context: context || "", systemId: systemId || "", alternatives: alternatives || "",
316
+ };
317
+ d.decisions.push(dec);
318
+ save(d);
319
+ return dec;
320
+ }
321
+
322
+ // ===== BLUEPRINTS =====
323
+
324
+ function getBlueprints(pid) {
325
+ return load().blueprints.filter(b => b.projectId === pid);
326
+ }
327
+
328
+ function addBlueprint({ projectId, name, path, parentClass, systemId, description, variables, keyFunctions }) {
329
+ const d = load();
330
+ const bp = {
331
+ id: id(), projectId, name, path: path || "", parentClass: parentClass || "",
332
+ systemId: systemId || "", description: description || "",
333
+ variables: variables || [], keyFunctions: keyFunctions || [],
334
+ lastUpdated: today(),
335
+ };
336
+ d.blueprints.push(bp);
337
+ save(d);
338
+ return bp;
339
+ }
340
+
341
+ function updateBlueprint(bpid, updates) {
342
+ const d = load();
343
+ const i = d.blueprints.findIndex(b => b.id === bpid);
344
+ if (i === -1) return null;
345
+ d.blueprints[i] = { ...d.blueprints[i], ...updates, lastUpdated: today() };
346
+ save(d);
347
+ return d.blueprints[i];
348
+ }
349
+
350
+ // ===== BLUEPRINT GRAPHS =====
351
+
352
+ function getBlueprintGraphs(bpid) {
353
+ const d = load();
354
+ const bp = d.blueprints.find(b => b.id === bpid);
355
+ if (!bp) return null;
356
+ return bp.graphs || [];
357
+ }
358
+
359
+ function setBlueprintGraph(bpid, { name, type, nodes, connections }) {
360
+ const d = load();
361
+ const i = d.blueprints.findIndex(b => b.id === bpid);
362
+ if (i === -1) return null;
363
+ if (!d.blueprints[i].graphs) d.blueprints[i].graphs = [];
364
+ const gIdx = d.blueprints[i].graphs.findIndex(g => g.name === name);
365
+ const graph = {
366
+ id: gIdx >= 0 ? d.blueprints[i].graphs[gIdx].id : id(),
367
+ name,
368
+ type: type || "event",
369
+ nodes: nodes || [],
370
+ connections: connections || [],
371
+ viewport: gIdx >= 0 ? d.blueprints[i].graphs[gIdx].viewport : { x: 0, y: 0, zoom: 1 },
372
+ };
373
+ if (gIdx >= 0) {
374
+ d.blueprints[i].graphs[gIdx] = graph;
375
+ } else {
376
+ d.blueprints[i].graphs.push(graph);
377
+ }
378
+ d.blueprints[i].lastUpdated = today();
379
+ save(d);
380
+ return graph;
381
+ }
382
+
383
+ function addGraphNodes(bpid, graphName, { nodes, connections }) {
384
+ const d = load();
385
+ const i = d.blueprints.findIndex(b => b.id === bpid);
386
+ if (i === -1) return null;
387
+ if (!d.blueprints[i].graphs) d.blueprints[i].graphs = [];
388
+ const gIdx = d.blueprints[i].graphs.findIndex(g => g.name === graphName);
389
+ if (gIdx === -1) return null;
390
+ if (nodes && nodes.length > 0) d.blueprints[i].graphs[gIdx].nodes.push(...nodes);
391
+ if (connections && connections.length > 0) d.blueprints[i].graphs[gIdx].connections.push(...connections);
392
+ d.blueprints[i].lastUpdated = today();
393
+ save(d);
394
+ return d.blueprints[i].graphs[gIdx];
395
+ }
396
+
397
+ // ===== NOTES (replaces Trello comments / general docs) =====
398
+
399
+ function getNotes(pid, parentType, parentId) {
400
+ const d = load();
401
+ let notes = d.notes.filter(n => n.projectId === pid);
402
+ if (parentType) notes = notes.filter(n => n.parentType === parentType);
403
+ if (parentId) notes = notes.filter(n => n.parentId === parentId);
404
+ return notes.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
405
+ }
406
+
407
+ function addNote({ projectId, parentType, parentId, title, content }) {
408
+ const d = load();
409
+ const n = {
410
+ id: id(), projectId,
411
+ parentType: parentType || "project", // project, system, task, blueprint
412
+ parentId: parentId || projectId,
413
+ title: title || "", content: content || "",
414
+ createdAt: now(),
415
+ };
416
+ d.notes.push(n);
417
+ save(d);
418
+ return n;
419
+ }
420
+
421
+ // ===== BACKUPS =====
422
+
423
+ function getBackups(pid) {
424
+ return load().backups.filter(b => b.projectId === pid).sort((a, b) => b.date.localeCompare(a.date));
425
+ }
426
+
427
+ function logBackup({ projectId, title, description }) {
428
+ const d = load();
429
+ const b = {
430
+ id: id(), projectId, date: today(), timestamp: now(),
431
+ title: title || `Backup — ${today()}`,
432
+ description: description || "",
433
+ };
434
+ d.backups.push(b);
435
+ save(d);
436
+ return b;
437
+ }
438
+
439
+ // ===== LEVELS =====
440
+
441
+ function getLevels(pid) {
442
+ return load().levels.filter(l => l.projectId === pid);
443
+ }
444
+
445
+ function addLevel({ projectId, name, map, actorCount, status }) {
446
+ const d = load();
447
+ const l = {
448
+ id: id(), projectId, name,
449
+ map: map || "",
450
+ actorCount: actorCount || 0,
451
+ status: status || "planned", // wip, done, planned
452
+ actors: [],
453
+ createdAt: now(), lastUpdated: now(),
454
+ };
455
+ d.levels.push(l);
456
+ save(d);
457
+ return l;
458
+ }
459
+
460
+ function updateLevel(lid, updates) {
461
+ const d = load();
462
+ const i = d.levels.findIndex(l => l.id === lid);
463
+ if (i === -1) return null;
464
+ d.levels[i] = { ...d.levels[i], ...updates, lastUpdated: now() };
465
+ save(d);
466
+ return d.levels[i];
467
+ }
468
+
469
+ function addActor(lid, { name, x, y, z, notes }) {
470
+ const d = load();
471
+ const i = d.levels.findIndex(l => l.id === lid);
472
+ if (i === -1) return null;
473
+ const actor = { name, x: x || 0, y: y || 0, z: z || 0, notes: notes || "" };
474
+ d.levels[i].actors.push(actor);
475
+ d.levels[i].lastUpdated = now();
476
+ save(d);
477
+ return actor;
478
+ }
479
+
480
+ function removeActor(lid, actorIndex) {
481
+ const d = load();
482
+ const i = d.levels.findIndex(l => l.id === lid);
483
+ if (i === -1) return null;
484
+ if (actorIndex < 0 || actorIndex >= d.levels[i].actors.length) return null;
485
+ const removed = d.levels[i].actors.splice(actorIndex, 1)[0];
486
+ d.levels[i].lastUpdated = now();
487
+ save(d);
488
+ return removed;
489
+ }
490
+
491
+ // ===== PLUGINS =====
492
+
493
+ function getPlugins(pid) {
494
+ return load().plugins.filter(p => p.projectId === pid);
495
+ }
496
+
497
+ function addPlugin({ projectId, name, version, source, dependents }) {
498
+ const d = load();
499
+ const pl = {
500
+ id: id(), projectId, name,
501
+ version: version || "",
502
+ source: source || "",
503
+ dependents: dependents || [],
504
+ createdAt: now(), lastUpdated: now(),
505
+ };
506
+ d.plugins.push(pl);
507
+ save(d);
508
+ return pl;
509
+ }
510
+
511
+ function updatePlugin(plid, updates) {
512
+ const d = load();
513
+ const i = d.plugins.findIndex(p => p.id === plid);
514
+ if (i === -1) return null;
515
+ d.plugins[i] = { ...d.plugins[i], ...updates, lastUpdated: now() };
516
+ save(d);
517
+ return d.plugins[i];
518
+ }
519
+
520
+ function deletePlugin(plid) {
521
+ const d = load();
522
+ d.plugins = d.plugins.filter(p => p.id !== plid);
523
+ save(d);
524
+ return true;
525
+ }
526
+
527
+ // ===== BUILD ERRORS =====
528
+
529
+ function getBuildErrors(pid, statusFilter) {
530
+ const d = load();
531
+ let errors = d.buildErrors.filter(e => e.projectId === pid);
532
+ if (statusFilter) errors = errors.filter(e => e.status === statusFilter);
533
+ return errors;
534
+ }
535
+
536
+ function addBuildError({ projectId, error, bp }) {
537
+ const d = load();
538
+ const e = {
539
+ id: id(), projectId,
540
+ error: error || "",
541
+ bp: bp || "",
542
+ status: "open", // open, fixed
543
+ fix: "",
544
+ date: today(),
545
+ createdAt: now(),
546
+ };
547
+ d.buildErrors.push(e);
548
+ save(d);
549
+ return e;
550
+ }
551
+
552
+ function fixBuildError(eid, fix) {
553
+ const d = load();
554
+ const i = d.buildErrors.findIndex(e => e.id === eid);
555
+ if (i === -1) return null;
556
+ d.buildErrors[i].status = "fixed";
557
+ d.buildErrors[i].fix = fix || "";
558
+ save(d);
559
+ return d.buildErrors[i];
560
+ }
561
+
562
+ // ===== OPTIMIZE ITEMS =====
563
+
564
+ function getOptimizeItems(pid) {
565
+ return load().optimizeItems.filter(o => o.projectId === pid);
566
+ }
567
+
568
+ function addOptimizeItem({ projectId, title, category, status, savings }) {
569
+ const d = load();
570
+ const o = {
571
+ id: id(), projectId, title,
572
+ category: category || "",
573
+ status: status || "open", // open, fixed, ok
574
+ savings: savings || "",
575
+ date: today(),
576
+ createdAt: now(),
577
+ };
578
+ d.optimizeItems.push(o);
579
+ save(d);
580
+ return o;
581
+ }
582
+
583
+ function updateOptimizeItem(oid, updates) {
584
+ const d = load();
585
+ const i = d.optimizeItems.findIndex(o => o.id === oid);
586
+ if (i === -1) return null;
587
+ d.optimizeItems[i] = { ...d.optimizeItems[i], ...updates };
588
+ save(d);
589
+ return d.optimizeItems[i];
590
+ }
591
+
592
+ // ===== PERF BUDGET =====
593
+
594
+ function getPerfBudget(pid) {
595
+ return load().perfBudget.filter(p => p.projectId === pid);
596
+ }
597
+
598
+ function addPerfBudget({ projectId, scene, fps, gpu, memory, notes }) {
599
+ const d = load();
600
+ const pb = {
601
+ id: id(), projectId,
602
+ scene: scene || "",
603
+ fps: fps || 0,
604
+ gpu: gpu || 0,
605
+ memory: memory || 0,
606
+ date: today(),
607
+ notes: notes || "",
608
+ createdAt: now(),
609
+ };
610
+ d.perfBudget.push(pb);
611
+ save(d);
612
+ return pb;
613
+ }
614
+
615
+ function updatePerfBudget(pbid, updates) {
616
+ const d = load();
617
+ const i = d.perfBudget.findIndex(p => p.id === pbid);
618
+ if (i === -1) return null;
619
+ d.perfBudget[i] = { ...d.perfBudget[i], ...updates };
620
+ save(d);
621
+ return d.perfBudget[i];
622
+ }
623
+
624
+ // ===== PLAYTESTS =====
625
+
626
+ function getPlaytests(pid) {
627
+ return load().playtests.filter(p => p.projectId === pid);
628
+ }
629
+
630
+ function addPlaytest({ projectId, duration, issues, rating, notes, tester, buildVersion }) {
631
+ const d = load();
632
+ const pt = {
633
+ id: id(), projectId,
634
+ date: today(),
635
+ duration: duration || "",
636
+ issues: issues || [],
637
+ rating: rating || 0,
638
+ notes: notes || "",
639
+ tester: tester || "",
640
+ buildVersion: buildVersion || "",
641
+ createdAt: now(),
642
+ };
643
+ d.playtests.push(pt);
644
+ save(d);
645
+ return pt;
646
+ }
647
+
648
+ // ===== MILESTONES =====
649
+
650
+ function getMilestones(pid) {
651
+ return load().milestones.filter(m => m.projectId === pid);
652
+ }
653
+
654
+ function addMilestone({ projectId, title, date, status, notes }) {
655
+ const d = load();
656
+ const m = {
657
+ id: id(), projectId, title,
658
+ date: date || today(),
659
+ status: status || "upcoming", // upcoming, active, done, missed
660
+ notes: notes || "",
661
+ createdAt: now(),
662
+ };
663
+ d.milestones.push(m);
664
+ save(d);
665
+ return m;
666
+ }
667
+
668
+ function updateMilestone(mid, updates) {
669
+ const d = load();
670
+ const i = d.milestones.findIndex(m => m.id === mid);
671
+ if (i === -1) return null;
672
+ d.milestones[i] = { ...d.milestones[i], ...updates };
673
+ save(d);
674
+ return d.milestones[i];
675
+ }
676
+
677
+ // ===== ITERATIONS =====
678
+
679
+ function getIterations(pid) {
680
+ return load().iterations.filter(it => it.projectId === pid);
681
+ }
682
+
683
+ function addIteration({ projectId, feature, version, change, reason }) {
684
+ const d = load();
685
+ const it = {
686
+ id: id(), projectId,
687
+ feature: feature || "",
688
+ version: version || "",
689
+ change: change || "",
690
+ reason: reason || "",
691
+ date: today(),
692
+ createdAt: now(),
693
+ };
694
+ d.iterations.push(it);
695
+ save(d);
696
+ return it;
697
+ }
698
+
699
+ // ===== DIALOGUES =====
700
+
701
+ function getDialogues(pid) {
702
+ return load().dialogues.filter(dl => dl.projectId === pid);
703
+ }
704
+
705
+ function addDialogue({ projectId, npc, line, choices, nextNode, notes }) {
706
+ const d = load();
707
+ const dl = {
708
+ id: id(), projectId,
709
+ npc: npc || "",
710
+ line: line || "",
711
+ choices: choices || [],
712
+ nextNode: nextNode || "",
713
+ notes: notes || "",
714
+ createdAt: now(),
715
+ };
716
+ d.dialogues.push(dl);
717
+ save(d);
718
+ return dl;
719
+ }
720
+
721
+ function updateDialogue(did, updates) {
722
+ const d = load();
723
+ const i = d.dialogues.findIndex(dl => dl.id === did);
724
+ if (i === -1) return null;
725
+ d.dialogues[i] = { ...d.dialogues[i], ...updates };
726
+ save(d);
727
+ return d.dialogues[i];
728
+ }
729
+
730
+ // ===== SOUNDS =====
731
+
732
+ function getSounds(pid) {
733
+ return load().sounds.filter(s => s.projectId === pid);
734
+ }
735
+
736
+ function addSound({ projectId, name, actor, trigger, spatial, attenuation, notes }) {
737
+ const d = load();
738
+ const s = {
739
+ id: id(), projectId, name,
740
+ actor: actor || "",
741
+ trigger: trigger || "",
742
+ spatial: spatial || false,
743
+ attenuation: attenuation || "",
744
+ notes: notes || "",
745
+ createdAt: now(),
746
+ };
747
+ d.sounds.push(s);
748
+ save(d);
749
+ return s;
750
+ }
751
+
752
+ function updateSound(sid, updates) {
753
+ const d = load();
754
+ const i = d.sounds.findIndex(s => s.id === sid);
755
+ if (i === -1) return null;
756
+ d.sounds[i] = { ...d.sounds[i], ...updates };
757
+ save(d);
758
+ return d.sounds[i];
759
+ }
760
+
761
+ // ===== CONTROLS =====
762
+
763
+ function getControls(pid) {
764
+ return load().controls.filter(c => c.projectId === pid);
765
+ }
766
+
767
+ function addControl({ projectId, key, action, context, condition }) {
768
+ const d = load();
769
+ const c = {
770
+ id: id(), projectId,
771
+ key: key || "",
772
+ action: action || "",
773
+ context: context || "",
774
+ condition: condition || "",
775
+ createdAt: now(),
776
+ };
777
+ d.controls.push(c);
778
+ save(d);
779
+ return c;
780
+ }
781
+
782
+ function updateControl(cid, updates) {
783
+ const d = load();
784
+ const i = d.controls.findIndex(c => c.id === cid);
785
+ if (i === -1) return null;
786
+ d.controls[i] = { ...d.controls[i], ...updates };
787
+ save(d);
788
+ return d.controls[i];
789
+ }
790
+
791
+ // ===== ASSETS =====
792
+
793
+ function getAssets(pid) {
794
+ return load().assets.filter(a => a.projectId === pid);
795
+ }
796
+
797
+ function addAsset({ projectId, name, type, path, size, status, usedBy }) {
798
+ const d = load();
799
+ const a = {
800
+ id: id(), projectId, name,
801
+ type: type || "",
802
+ path: path || "",
803
+ size: size || "",
804
+ status: status || "concept", // concept, imported, integrated, tested, final
805
+ usedBy: usedBy || "",
806
+ createdAt: now(), lastUpdated: now(),
807
+ };
808
+ d.assets.push(a);
809
+ save(d);
810
+ return a;
811
+ }
812
+
813
+ function updateAsset(aid, updates) {
814
+ const d = load();
815
+ const i = d.assets.findIndex(a => a.id === aid);
816
+ if (i === -1) return null;
817
+ d.assets[i] = { ...d.assets[i], ...updates, lastUpdated: now() };
818
+ save(d);
819
+ return d.assets[i];
820
+ }
821
+
822
+ // ===== REFERENCES =====
823
+
824
+ function getRefs(pid) {
825
+ return load().refs.filter(r => r.projectId === pid);
826
+ }
827
+
828
+ function addRef({ projectId, title, type, url, notes }) {
829
+ const d = load();
830
+ const r = {
831
+ id: id(), projectId,
832
+ title: title || "",
833
+ type: type || "Article", // Game, Art, Video, Article
834
+ url: url || "",
835
+ notes: notes || "",
836
+ createdAt: now(),
837
+ };
838
+ d.refs.push(r);
839
+ save(d);
840
+ return r;
841
+ }
842
+
843
+ // ===== MARKETING ITEMS =====
844
+
845
+ function getMarketingItems(pid) {
846
+ return load().marketingItems.filter(m => m.projectId === pid);
847
+ }
848
+
849
+ function addMarketingItem({ projectId, item, category, status, notes }) {
850
+ const d = load();
851
+ const m = {
852
+ id: id(), projectId,
853
+ item: item || "",
854
+ category: category || "Steam", // Steam, Trailer, Social, Press
855
+ status: status || "todo", // todo, doing, done
856
+ notes: notes || "",
857
+ createdAt: now(),
858
+ };
859
+ d.marketingItems.push(m);
860
+ save(d);
861
+ return m;
862
+ }
863
+
864
+ function updateMarketingItem(mid, updates) {
865
+ const d = load();
866
+ const i = d.marketingItems.findIndex(m => m.id === mid);
867
+ if (i === -1) return null;
868
+ d.marketingItems[i] = { ...d.marketingItems[i], ...updates };
869
+ save(d);
870
+ return d.marketingItems[i];
871
+ }
872
+
873
+ // ===== WIKI PAGES =====
874
+
875
+ function getWikiPages(pid) {
876
+ return load().wikiPages.filter(w => w.projectId === pid);
877
+ }
878
+
879
+ function addWikiPage({ projectId, title, category, content }) {
880
+ const d = load();
881
+ const w = {
882
+ id: id(), projectId,
883
+ title: title || "",
884
+ category: category || "Reference", // Systems, Workflows, Guides, Reference
885
+ content: content || "",
886
+ createdAt: now(), lastUpdated: now(),
887
+ };
888
+ d.wikiPages.push(w);
889
+ save(d);
890
+ return w;
891
+ }
892
+
893
+ function updateWikiPage(wid, updates) {
894
+ const d = load();
895
+ const i = d.wikiPages.findIndex(w => w.id === wid);
896
+ if (i === -1) return null;
897
+ d.wikiPages[i] = { ...d.wikiPages[i], ...updates, lastUpdated: now() };
898
+ save(d);
899
+ return d.wikiPages[i];
900
+ }
901
+
902
+ // ===== SHIP CHECKED =====
903
+
904
+ function getShipChecked(pid) {
905
+ return load().shipChecked.filter(s => s.projectId === pid).map(s => s.stepId);
906
+ }
907
+
908
+ function toggleShipCheck(pid, stepId) {
909
+ const d = load();
910
+ const i = d.shipChecked.findIndex(s => s.projectId === pid && s.stepId === stepId);
911
+ if (i >= 0) {
912
+ d.shipChecked.splice(i, 1);
913
+ save(d);
914
+ return { checked: false, stepId };
915
+ } else {
916
+ d.shipChecked.push({ projectId: pid, stepId });
917
+ save(d);
918
+ return { checked: true, stepId };
919
+ }
920
+ }
921
+
922
+ // ===== OPEN QUESTIONS =====
923
+
924
+ function getOpenQuestions(pid) {
925
+ return load().openQuestions.filter(q => q.projectId === pid);
926
+ }
927
+
928
+ function addOpenQuestion({ projectId, text }) {
929
+ const d = load();
930
+ const q = {
931
+ id: id(), projectId,
932
+ text: text || "",
933
+ resolved: false,
934
+ date: today(),
935
+ createdAt: now(),
936
+ };
937
+ d.openQuestions.push(q);
938
+ save(d);
939
+ return q;
940
+ }
941
+
942
+ function toggleQuestion(qid) {
943
+ const d = load();
944
+ const i = d.openQuestions.findIndex(q => q.id === qid);
945
+ if (i === -1) return null;
946
+ d.openQuestions[i].resolved = !d.openQuestions[i].resolved;
947
+ save(d);
948
+ return d.openQuestions[i];
949
+ }
950
+
951
+ // ===== HELPER FUNCTIONS =====
952
+
953
+ function updateProjectField(pid, field, value) {
954
+ const d = load();
955
+ const i = d.projects.findIndex(p => p.id === pid);
956
+ if (i === -1) return null;
957
+ d.projects[i][field] = value;
958
+ d.projects[i].lastUpdated = now();
959
+ save(d);
960
+ return d.projects[i];
961
+ }
962
+
963
+ function logActivity(pid, action, detail) {
964
+ const d = load();
965
+ const i = d.projects.findIndex(p => p.id === pid);
966
+ if (i === -1) return null;
967
+ if (!d.projects[i].activityLog) d.projects[i].activityLog = [];
968
+ const entry = { action, detail: detail || "", timestamp: now() };
969
+ d.projects[i].activityLog.push(entry);
970
+ d.projects[i].lastUpdated = now();
971
+ save(d);
972
+ return entry;
973
+ }
974
+
975
+ // ===== SEARCH =====
976
+
977
+ function search(pid, query) {
978
+ const d = load();
979
+ const q = query.toLowerCase();
980
+ const results = [];
981
+
982
+ const match = (text) => text && text.toLowerCase().includes(q);
983
+
984
+ d.tasks.filter(t => t.projectId === pid).forEach(t => {
985
+ if (match(t.title) || match(t.description)) results.push({ type: "task", id: t.id, title: t.title, status: t.status });
986
+ });
987
+ d.systems.filter(s => s.projectId === pid).forEach(s => {
988
+ if (match(s.name) || match(s.description) || match(s.notes) || match(s.criticalRules))
989
+ results.push({ type: "system", id: s.id, title: s.name, status: s.status });
990
+ });
991
+ d.sessions.filter(s => s.projectId === pid).forEach(s => {
992
+ if (match(s.title) || match(s.summary) || match(s.whereWeLeftOff))
993
+ results.push({ type: "session", id: s.id, title: s.title, date: s.date });
994
+ });
995
+ d.bugs.filter(b => b.projectId === pid).forEach(b => {
996
+ if (match(b.title) || match(b.description))
997
+ results.push({ type: "bug", id: b.id, title: b.title, status: b.status });
998
+ });
999
+ d.blueprints.filter(b => b.projectId === pid).forEach(b => {
1000
+ if (match(b.name) || match(b.description) || match(b.path))
1001
+ results.push({ type: "blueprint", id: b.id, title: b.name, path: b.path });
1002
+ });
1003
+ d.notes.filter(n => n.projectId === pid).forEach(n => {
1004
+ if (match(n.title) || match(n.content))
1005
+ results.push({ type: "note", id: n.id, title: n.title || "(untitled)" });
1006
+ });
1007
+
1008
+ // Search across new collections
1009
+ d.levels.filter(l => l.projectId === pid).forEach(l => {
1010
+ if (match(l.name) || match(l.map))
1011
+ results.push({ type: "level", id: l.id, title: l.name, status: l.status });
1012
+ });
1013
+ d.plugins.filter(p => p.projectId === pid).forEach(p => {
1014
+ if (match(p.name) || match(p.source))
1015
+ results.push({ type: "plugin", id: p.id, title: p.name, version: p.version });
1016
+ });
1017
+ d.buildErrors.filter(e => e.projectId === pid).forEach(e => {
1018
+ if (match(e.error) || match(e.bp) || match(e.fix))
1019
+ results.push({ type: "buildError", id: e.id, title: e.error, status: e.status });
1020
+ });
1021
+ d.optimizeItems.filter(o => o.projectId === pid).forEach(o => {
1022
+ if (match(o.title) || match(o.category) || match(o.savings))
1023
+ results.push({ type: "optimizeItem", id: o.id, title: o.title, status: o.status });
1024
+ });
1025
+ d.perfBudget.filter(p => p.projectId === pid).forEach(p => {
1026
+ if (match(p.scene) || match(p.notes))
1027
+ results.push({ type: "perfBudget", id: p.id, title: p.scene, date: p.date });
1028
+ });
1029
+ d.playtests.filter(p => p.projectId === pid).forEach(p => {
1030
+ if (match(p.notes) || match(p.tester) || match(p.buildVersion))
1031
+ results.push({ type: "playtest", id: p.id, title: `Playtest ${p.date}`, date: p.date });
1032
+ });
1033
+ d.milestones.filter(m => m.projectId === pid).forEach(m => {
1034
+ if (match(m.title) || match(m.notes))
1035
+ results.push({ type: "milestone", id: m.id, title: m.title, status: m.status });
1036
+ });
1037
+ d.iterations.filter(it => it.projectId === pid).forEach(it => {
1038
+ if (match(it.feature) || match(it.change) || match(it.reason))
1039
+ results.push({ type: "iteration", id: it.id, title: it.feature, version: it.version });
1040
+ });
1041
+ d.dialogues.filter(dl => dl.projectId === pid).forEach(dl => {
1042
+ if (match(dl.npc) || match(dl.line) || match(dl.notes))
1043
+ results.push({ type: "dialogue", id: dl.id, title: dl.npc + ": " + (dl.line || "").substring(0, 50) });
1044
+ });
1045
+ d.sounds.filter(s => s.projectId === pid).forEach(s => {
1046
+ if (match(s.name) || match(s.actor) || match(s.trigger) || match(s.notes))
1047
+ results.push({ type: "sound", id: s.id, title: s.name });
1048
+ });
1049
+ d.controls.filter(c => c.projectId === pid).forEach(c => {
1050
+ if (match(c.key) || match(c.action) || match(c.context))
1051
+ results.push({ type: "control", id: c.id, title: c.key + " → " + c.action });
1052
+ });
1053
+ d.assets.filter(a => a.projectId === pid).forEach(a => {
1054
+ if (match(a.name) || match(a.type) || match(a.path))
1055
+ results.push({ type: "asset", id: a.id, title: a.name, status: a.status });
1056
+ });
1057
+ d.refs.filter(r => r.projectId === pid).forEach(r => {
1058
+ if (match(r.title) || match(r.url) || match(r.notes))
1059
+ results.push({ type: "ref", id: r.id, title: r.title, type: r.type });
1060
+ });
1061
+ d.marketingItems.filter(m => m.projectId === pid).forEach(m => {
1062
+ if (match(m.item) || match(m.category) || match(m.notes))
1063
+ results.push({ type: "marketingItem", id: m.id, title: m.item, status: m.status });
1064
+ });
1065
+ d.wikiPages.filter(w => w.projectId === pid).forEach(w => {
1066
+ if (match(w.title) || match(w.content))
1067
+ results.push({ type: "wikiPage", id: w.id, title: w.title, category: w.category });
1068
+ });
1069
+ d.openQuestions.filter(oq => oq.projectId === pid).forEach(oq => {
1070
+ if (match(oq.text))
1071
+ results.push({ type: "openQuestion", id: oq.id, title: oq.text, resolved: oq.resolved });
1072
+ });
1073
+
1074
+ return results;
1075
+ }
1076
+
1077
+ // ===== CONTEXT EXPORT =====
1078
+
1079
+ function contextExport(pid) {
1080
+ const d = load();
1081
+ const proj = d.projects.find(p => p.id === pid);
1082
+ if (!proj) return "Project not found.";
1083
+
1084
+ const systems = d.systems.filter(s => s.projectId === pid);
1085
+ const openBugs = d.bugs.filter(b => b.projectId === pid && b.status === "open");
1086
+ const sessions = d.sessions.filter(s => s.projectId === pid).sort((a, b) => b.date.localeCompare(a.date));
1087
+ const decisions = d.decisions.filter(dd => dd.projectId === pid);
1088
+ const bps = d.blueprints.filter(b => b.projectId === pid);
1089
+ const doingTasks = d.tasks.filter(t => t.projectId === pid && t.status === "doing");
1090
+ const last = sessions[0];
1091
+
1092
+ const levels = d.levels.filter(l => l.projectId === pid);
1093
+ const plugins = d.plugins.filter(p => p.projectId === pid);
1094
+ const openQuestions = d.openQuestions.filter(q => q.projectId === pid && !q.resolved);
1095
+ const optimizeItems = d.optimizeItems.filter(o => o.projectId === pid && o.status === "open");
1096
+
1097
+ let out = `PROJECT: ${proj.name} — ${proj.description}\n`;
1098
+ if (proj.trelloBoardId) out += `Trello Board ID: ${proj.trelloBoardId}\n`;
1099
+ if (proj.engine) out += `Engine: ${proj.engine}\n`;
1100
+ if (proj.currentFocus) out += `Current Focus: ${proj.currentFocus}\n`;
1101
+ if (proj.deadline) out += `Deadline: ${proj.deadline}\n`;
1102
+
1103
+ if (doingTasks.length > 0) {
1104
+ out += `\n=== CURRENTLY WORKING ON ===\n`;
1105
+ doingTasks.forEach(t => { out += `• ${t.title}${t.description ? " — " + t.description : ""}\n`; });
1106
+ }
1107
+
1108
+ out += `\n=== SYSTEMS (${systems.length}) ===\n`;
1109
+ systems.forEach(s => {
1110
+ out += `[${(s.status || "unknown").toUpperCase()}] ${s.name} — ${s.description || ""}\n`;
1111
+ if (s.keyFiles) out += ` Files: ${s.keyFiles}\n`;
1112
+ if (s.criticalRules) out += ` RULES: ${s.criticalRules}\n`;
1113
+ if (s.notes) out += ` Notes: ${s.notes}\n`;
1114
+ });
1115
+
1116
+ if (bps.length > 0) {
1117
+ out += `\n=== BLUEPRINTS (${bps.length}) ===\n`;
1118
+ bps.forEach(b => {
1119
+ out += `${b.name}${b.path ? " — " + b.path : ""}\n`;
1120
+ if (b.parentClass) out += ` Parent: ${b.parentClass}\n`;
1121
+ if (b.description) out += ` ${b.description}\n`;
1122
+ });
1123
+ }
1124
+
1125
+ if (levels.length > 0) {
1126
+ out += `\n=== LEVELS (${levels.length}) ===\n`;
1127
+ levels.forEach(l => {
1128
+ out += `[${(l.status || "planned").toUpperCase()}] ${l.name}${l.map ? " — " + l.map : ""} (${l.actorCount || 0} actors)\n`;
1129
+ });
1130
+ }
1131
+
1132
+ if (plugins.length > 0) {
1133
+ out += `\n=== PLUGINS (${plugins.length}) ===\n`;
1134
+ plugins.forEach(p => {
1135
+ out += `${p.name} v${p.version}${p.source ? " (" + p.source + ")" : ""}\n`;
1136
+ });
1137
+ }
1138
+
1139
+ if (openBugs.length > 0) {
1140
+ out += `\n=== OPEN BUGS (${openBugs.length}) ===\n`;
1141
+ openBugs.forEach(b => {
1142
+ const sys = systems.find(s => s.id === b.systemId);
1143
+ out += `[${b.priority.toUpperCase()}] ${b.title}${sys ? " (" + sys.name + ")" : ""}\n`;
1144
+ });
1145
+ }
1146
+
1147
+ if (optimizeItems.length > 0) {
1148
+ out += `\n=== OPEN OPTIMIZE ITEMS (${optimizeItems.length}) ===\n`;
1149
+ optimizeItems.forEach(o => {
1150
+ out += `[${o.category || "general"}] ${o.title}${o.savings ? " — savings: " + o.savings : ""}\n`;
1151
+ });
1152
+ }
1153
+
1154
+ if (openQuestions.length > 0) {
1155
+ out += `\n=== OPEN QUESTIONS (${openQuestions.length}) ===\n`;
1156
+ openQuestions.forEach(q => {
1157
+ out += `• ${q.text}\n`;
1158
+ });
1159
+ }
1160
+
1161
+ if (last) {
1162
+ out += `\n=== LAST SESSION (${last.date}) ===\n`;
1163
+ out += `${last.title}\n`;
1164
+ if (last.summary) out += `${last.summary}\n`;
1165
+ if (last.whereWeLeftOff) out += `LEFT OFF: ${last.whereWeLeftOff}\n`;
1166
+ if (last.nextSteps?.length) {
1167
+ out += `NEXT:\n`;
1168
+ last.nextSteps.forEach(s => out += ` - ${s}\n`);
1169
+ }
1170
+ }
1171
+
1172
+ if (decisions.length > 0) {
1173
+ out += `\n=== DECISIONS (${decisions.length}) ===\n`;
1174
+ decisions.slice(0, 10).forEach(dd => {
1175
+ out += `[${dd.date}] ${dd.decision}${dd.context ? " — " + dd.context : ""}\n`;
1176
+ });
1177
+ }
1178
+
1179
+ return out;
1180
+ }
1181
+
1182
+ // ===== DASHBOARD =====
1183
+
1184
+ function dashboard(pid) {
1185
+ const d = load();
1186
+ const proj = d.projects.find(p => p.id === pid);
1187
+ if (!proj) return null;
1188
+
1189
+ const systems = d.systems.filter(s => s.projectId === pid);
1190
+ const tasks = d.tasks.filter(t => t.projectId === pid);
1191
+ const openBugs = d.bugs.filter(b => b.projectId === pid && b.status === "open");
1192
+ const sessions = d.sessions.filter(s => s.projectId === pid).sort((a, b) => b.date.localeCompare(a.date));
1193
+ const last = sessions[0];
1194
+
1195
+ const sc = { working: 0, wip: 0, broken: 0, planned: 0 };
1196
+ systems.forEach(s => { if (sc[s.status] !== undefined) sc[s.status]++; });
1197
+
1198
+ const tc = { todo: 0, doing: 0, done: 0, scrapped: 0 };
1199
+ tasks.forEach(t => { if (tc[t.status] !== undefined) tc[t.status]++; });
1200
+
1201
+ const levels = d.levels.filter(l => l.projectId === pid);
1202
+ const plugins = d.plugins.filter(p => p.projectId === pid);
1203
+ const buildErrors = d.buildErrors.filter(e => e.projectId === pid);
1204
+ const optimizeItems = d.optimizeItems.filter(o => o.projectId === pid);
1205
+ const playtests = d.playtests.filter(p => p.projectId === pid);
1206
+ const milestones = d.milestones.filter(m => m.projectId === pid);
1207
+ const openQuestions = d.openQuestions.filter(q => q.projectId === pid && !q.resolved);
1208
+ const shipChecked = d.shipChecked.filter(s => s.projectId === pid);
1209
+
1210
+ return {
1211
+ project: proj,
1212
+ systems: { total: systems.length, ...sc },
1213
+ tasks: { total: tasks.length, ...tc },
1214
+ openBugs: openBugs.length,
1215
+ sessions: sessions.length,
1216
+ lastSession: last || null,
1217
+ openBugsList: openBugs,
1218
+ doingTasks: tasks.filter(t => t.status === "doing"),
1219
+ levels: levels.length,
1220
+ plugins: plugins.length,
1221
+ buildErrors: { total: buildErrors.length, open: buildErrors.filter(e => e.status === "open").length },
1222
+ optimizeItems: { total: optimizeItems.length, open: optimizeItems.filter(o => o.status === "open").length },
1223
+ playtests: playtests.length,
1224
+ milestones: { total: milestones.length, active: milestones.filter(m => m.status === "active").length, upcoming: milestones.filter(m => m.status === "upcoming").length },
1225
+ openQuestions: openQuestions.length,
1226
+ shipChecked: shipChecked.length,
1227
+ };
1228
+ }
1229
+
1230
+ module.exports = {
1231
+ load, save,
1232
+ listProjects, getProject, addProject, updateProject,
1233
+ getTasks, getTask, addTask, updateTask, moveTask, addTaskComment, updateChecklist,
1234
+ getSystems, getSystem, addSystem, updateSystem, deleteSystem,
1235
+ getSessions, getLastSession, logSession, updateSession,
1236
+ getChangelog, addChangelog,
1237
+ getBugs, addBug, updateBug, fixBug,
1238
+ getDecisions, logDecision,
1239
+ getBlueprints, addBlueprint, updateBlueprint, getBlueprintGraphs, setBlueprintGraph, addGraphNodes,
1240
+ getNotes, addNote,
1241
+ getBackups, logBackup,
1242
+ // New collection functions
1243
+ getLevels, addLevel, updateLevel, addActor, removeActor,
1244
+ getPlugins, addPlugin, updatePlugin, deletePlugin,
1245
+ getBuildErrors, addBuildError, fixBuildError,
1246
+ getOptimizeItems, addOptimizeItem, updateOptimizeItem,
1247
+ getPerfBudget, addPerfBudget, updatePerfBudget,
1248
+ getPlaytests, addPlaytest,
1249
+ getMilestones, addMilestone, updateMilestone,
1250
+ getIterations, addIteration,
1251
+ getDialogues, addDialogue, updateDialogue,
1252
+ getSounds, addSound, updateSound,
1253
+ getControls, addControl, updateControl,
1254
+ getAssets, addAsset, updateAsset,
1255
+ getRefs, addRef,
1256
+ getMarketingItems, addMarketingItem, updateMarketingItem,
1257
+ getWikiPages, addWikiPage, updateWikiPage,
1258
+ getShipChecked, toggleShipCheck,
1259
+ getOpenQuestions, addOpenQuestion, toggleQuestion,
1260
+ // Helper functions
1261
+ updateProjectField, logActivity,
1262
+ // Aggregation functions
1263
+ search, contextExport, dashboard,
1264
+ };