mmm-mcp-server 0.1.1 → 0.1.3
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.en.md +228 -0
- package/README.md +2 -0
- package/dist/src/domain/appendForest.js +0 -50
- package/dist/src/domain/levels.js +3 -180
- package/dist/src/domain/levelsOps.js +10 -2
- package/dist/src/domain/mapResolver.js +1 -4
- package/dist/src/domain/markdownParser.js +20 -21
- package/dist/src/firestore/restClient.js +0 -39
- package/dist/src/server.js +1 -1
- package/dist/src/tools/autoStructureThought.js +9 -48
- package/dist/src/tools/createMap.js +3 -7
- package/dist/src/tools/deleteElements.js +17 -87
- package/dist/src/tools/editElement.js +11 -44
- package/dist/src/tools/importMarkdownContext.js +9 -53
- package/dist/src/tools/listElements.js +2 -2
- package/dist/src/tools/moveElement.js +8 -46
- package/dist/src/tools/updateTaskStatus.js +15 -59
- package/package.json +3 -3
- package/dist/src/domain/body.js +0 -127
- package/dist/src/domain/deleteElements.js +0 -85
- package/dist/src/domain/levelsMode.js +0 -24
- package/dist/src/domain/moveElement.js +0 -93
package/dist/src/domain/body.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
export function bodyFromFirestore(id, data, updateTime) {
|
|
2
|
-
return {
|
|
3
|
-
id,
|
|
4
|
-
detail: String(data.detail ?? ""),
|
|
5
|
-
prev: data.prev ?? null,
|
|
6
|
-
next: data.next ?? null,
|
|
7
|
-
parent: data.parent ?? null,
|
|
8
|
-
child: data.child ?? null,
|
|
9
|
-
done: Boolean(data.done ?? false),
|
|
10
|
-
updateTime,
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* 壊れている(prev/nextが循環・欠落している)データでも、要素を1件も
|
|
15
|
-
* 落とさずに返す。壊れ方の修復はアプリ本体(MatryoshkaModel.repair)の
|
|
16
|
-
* 役目であり、ここでは読み取り専用なので直さず、可能な範囲で順序を
|
|
17
|
-
* 復元しつつ全件を出す。
|
|
18
|
-
*/
|
|
19
|
-
export function buildTree(bodies) {
|
|
20
|
-
const globalVisited = new Set();
|
|
21
|
-
function orderedChildrenOf(parentId) {
|
|
22
|
-
const siblings = bodies.filter((b) => b.parent === parentId);
|
|
23
|
-
if (siblings.length === 0)
|
|
24
|
-
return [];
|
|
25
|
-
const bySelfId = new Map(siblings.map((b) => [b.id, b]));
|
|
26
|
-
const head = siblings.find((b) => b.prev === null || !bySelfId.has(b.prev));
|
|
27
|
-
const ordered = [];
|
|
28
|
-
const seen = new Set();
|
|
29
|
-
let current = head?.id ?? null;
|
|
30
|
-
while (current !== null && !seen.has(current)) {
|
|
31
|
-
const node = bySelfId.get(current);
|
|
32
|
-
if (!node)
|
|
33
|
-
break;
|
|
34
|
-
seen.add(current);
|
|
35
|
-
ordered.push(node);
|
|
36
|
-
current = node.next;
|
|
37
|
-
}
|
|
38
|
-
// next/prevの循環や分断で辿れなかった残りも、無くさず末尾に足す。
|
|
39
|
-
for (const s of siblings) {
|
|
40
|
-
if (!seen.has(s.id))
|
|
41
|
-
ordered.push(s);
|
|
42
|
-
}
|
|
43
|
-
return ordered;
|
|
44
|
-
}
|
|
45
|
-
function walk(parentId) {
|
|
46
|
-
const result = [];
|
|
47
|
-
for (const node of orderedChildrenOf(parentId)) {
|
|
48
|
-
if (globalVisited.has(node.id))
|
|
49
|
-
continue; // 循環参照からの二重取り込みを防ぐ
|
|
50
|
-
globalVisited.add(node.id);
|
|
51
|
-
result.push({
|
|
52
|
-
id: node.id,
|
|
53
|
-
detail: node.detail,
|
|
54
|
-
done: node.done,
|
|
55
|
-
parentId: node.parent,
|
|
56
|
-
children: walk(node.id),
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
return result;
|
|
60
|
-
}
|
|
61
|
-
const tree = walk(null);
|
|
62
|
-
// parentが存在しない要素を指している等で上のwalkから漏れた要素も、
|
|
63
|
-
// 最上位に足しておく(取りこぼしが無いことを優先する)。
|
|
64
|
-
for (const b of bodies) {
|
|
65
|
-
if (!globalVisited.has(b.id)) {
|
|
66
|
-
globalVisited.add(b.id);
|
|
67
|
-
tree.push({ id: b.id, detail: b.detail, done: b.done, parentId: b.parent, children: [] });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return tree;
|
|
71
|
-
}
|
|
72
|
-
/** ルートから[elementId]までの祖先(自分自身を含む)のdetailを、根から順に並べる。 */
|
|
73
|
-
export function ancestorDetails(bodies, elementId) {
|
|
74
|
-
if (elementId === null)
|
|
75
|
-
return [];
|
|
76
|
-
const byId = new Map(bodies.map((b) => [b.id, b]));
|
|
77
|
-
const chain = [];
|
|
78
|
-
let current = elementId;
|
|
79
|
-
const seen = new Set();
|
|
80
|
-
while (current !== null && byId.has(current) && !seen.has(current)) {
|
|
81
|
-
seen.add(current);
|
|
82
|
-
const node = byId.get(current);
|
|
83
|
-
chain.push(node.detail);
|
|
84
|
-
current = node.parent;
|
|
85
|
-
}
|
|
86
|
-
return chain.reverse();
|
|
87
|
-
}
|
|
88
|
-
/** [parentId]の子の連結リストの末尾のIDを求める。子が無ければnull。 */
|
|
89
|
-
export function lastChildOf(bodies, parentId) {
|
|
90
|
-
const siblings = bodies.filter((b) => b.parent === parentId);
|
|
91
|
-
if (siblings.length === 0)
|
|
92
|
-
return null;
|
|
93
|
-
const byId = new Map(siblings.map((b) => [b.id, b]));
|
|
94
|
-
let current = siblings.find((b) => b.prev === null || !byId.has(b.prev)) ?? siblings[0];
|
|
95
|
-
const seen = new Set([current.id]);
|
|
96
|
-
while (current.next !== null && byId.has(current.next) && !seen.has(current.next)) {
|
|
97
|
-
current = byId.get(current.next);
|
|
98
|
-
seen.add(current.id);
|
|
99
|
-
}
|
|
100
|
-
return current.id;
|
|
101
|
-
}
|
|
102
|
-
/**
|
|
103
|
-
* [rootId]自身とその配下(子孫)全ての id を集める。move/deleteのように
|
|
104
|
-
* 「要素ごと配下を丸ごと動かす・消す」操作の対象範囲を求めるために使う。
|
|
105
|
-
* 循環参照があっても無限ループしない。
|
|
106
|
-
*/
|
|
107
|
-
export function collectSubtreeIds(bodies, rootId) {
|
|
108
|
-
const byParent = new Map();
|
|
109
|
-
for (const b of bodies) {
|
|
110
|
-
const list = byParent.get(b.parent) ?? [];
|
|
111
|
-
list.push(b);
|
|
112
|
-
byParent.set(b.parent, list);
|
|
113
|
-
}
|
|
114
|
-
const result = [];
|
|
115
|
-
const seen = new Set();
|
|
116
|
-
function walk(id) {
|
|
117
|
-
if (seen.has(id))
|
|
118
|
-
return;
|
|
119
|
-
seen.add(id);
|
|
120
|
-
result.push(id);
|
|
121
|
-
for (const child of byParent.get(id) ?? []) {
|
|
122
|
-
walk(child.id);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
walk(rootId);
|
|
126
|
-
return result;
|
|
127
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { collectSubtreeIds } from "./body.js";
|
|
2
|
-
import { buildRemoveChildrenWrite } from "./levels.js";
|
|
3
|
-
import { mergeWritesByPath } from "../firestore/restClient.js";
|
|
4
|
-
/**
|
|
5
|
-
* [elementIds](配下ごと)を削除するための書き込みを組み立てる。tools/deleteElements.tsから
|
|
6
|
-
* 切り出した純粋寄りの関数(client.getDocumentだけ呼ぶ)。
|
|
7
|
-
*
|
|
8
|
-
* 1周目で消える要素を全て確定させてから2周目で繋ぎ直すのは、隣り合う要素を
|
|
9
|
-
* 同時に消すとき、1件ずつその場で繋ぎ直すと消える予定の相手を指すポインタを
|
|
10
|
-
* 書いてしまい、存在しないIDを指したまま残る不具合(実データで発生)を防ぐため。
|
|
11
|
-
*/
|
|
12
|
-
export async function buildDeleteElementWrites(client, bodiesPath, levelsPath, currentBodies, elementIds) {
|
|
13
|
-
const currentById = new Map(currentBodies.map((b) => [b.id, b]));
|
|
14
|
-
const writes = [];
|
|
15
|
-
const levelRemovals = new Map();
|
|
16
|
-
const allIds = new Set();
|
|
17
|
-
// 1周目: 消える要素を全て確定させる。繋ぎ直しは2周目で行う。
|
|
18
|
-
for (const elementId of elementIds) {
|
|
19
|
-
const body = currentById.get(elementId);
|
|
20
|
-
if (!body)
|
|
21
|
-
continue; // 既に削除済み(リトライ時など)
|
|
22
|
-
const subtree = collectSubtreeIds(currentBodies, elementId);
|
|
23
|
-
for (const id of subtree)
|
|
24
|
-
allIds.add(id);
|
|
25
|
-
}
|
|
26
|
-
/** [startId]から[key]方向へ、消えない要素に当たるまで辿る。 */
|
|
27
|
-
const survivingNeighbor = (startId, key) => {
|
|
28
|
-
let cursor = startId;
|
|
29
|
-
const seen = new Set();
|
|
30
|
-
while (cursor !== null && allIds.has(cursor) && !seen.has(cursor)) {
|
|
31
|
-
seen.add(cursor);
|
|
32
|
-
cursor = currentById.get(cursor)?.[key] ?? null;
|
|
33
|
-
}
|
|
34
|
-
return cursor;
|
|
35
|
-
};
|
|
36
|
-
// 2周目: 生き残る要素どうしを繋ぎ直す。
|
|
37
|
-
for (const elementId of elementIds) {
|
|
38
|
-
const body = currentById.get(elementId);
|
|
39
|
-
if (!body)
|
|
40
|
-
continue;
|
|
41
|
-
const newPrev = survivingNeighbor(body.prev, "prev");
|
|
42
|
-
const newNext = survivingNeighbor(body.next, "next");
|
|
43
|
-
if (newPrev !== null) {
|
|
44
|
-
writes.push({
|
|
45
|
-
path: `${bodiesPath}/${newPrev}`,
|
|
46
|
-
fields: { next: newNext },
|
|
47
|
-
requireUpdateTime: currentById.get(newPrev)?.updateTime,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
else if (body.parent !== null && !allIds.has(body.parent)) {
|
|
51
|
-
writes.push({
|
|
52
|
-
path: `${bodiesPath}/${body.parent}`,
|
|
53
|
-
fields: { child: newNext },
|
|
54
|
-
requireUpdateTime: currentById.get(body.parent)?.updateTime,
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
if (newNext !== null) {
|
|
58
|
-
writes.push({
|
|
59
|
-
path: `${bodiesPath}/${newNext}`,
|
|
60
|
-
fields: { prev: newPrev },
|
|
61
|
-
requireUpdateTime: currentById.get(newNext)?.updateTime,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
const levelId = body.parent ?? "root";
|
|
65
|
-
const list = levelRemovals.get(levelId) ?? [];
|
|
66
|
-
list.push(elementId);
|
|
67
|
-
levelRemovals.set(levelId, list);
|
|
68
|
-
}
|
|
69
|
-
for (const id of allIds) {
|
|
70
|
-
writes.push({ path: `${bodiesPath}/${id}`, delete: true });
|
|
71
|
-
// 配下の要素が子を持っていた場合、そのlevelsドキュメント自体も消す。
|
|
72
|
-
// 存在しないドキュメントへのdeleteはFirestoreでは無害。
|
|
73
|
-
writes.push({ path: `${levelsPath}/${id}`, delete: true });
|
|
74
|
-
}
|
|
75
|
-
for (const [levelId, removeIds] of levelRemovals) {
|
|
76
|
-
// 親自身も消えるなら、levels/{親} は上のdeleteで消える。
|
|
77
|
-
// ここで書き戻すと、消したドキュメントが中身入りで復活する。
|
|
78
|
-
if (allIds.has(levelId))
|
|
79
|
-
continue;
|
|
80
|
-
const write = await buildRemoveChildrenWrite(client, levelsPath, levelId, removeIds);
|
|
81
|
-
if (write)
|
|
82
|
-
writes.push(write);
|
|
83
|
-
}
|
|
84
|
-
return { writes: mergeWritesByPath(writes), deletedIds: allIds };
|
|
85
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// levels移行の切替フラグ。アプリと同じく Firestore の config/app.migrationEnabled を
|
|
2
|
-
// 見て、明示的にfalseの間だけ従来どおりbodiesとlevelsの両方へ書く。それ以外
|
|
3
|
-
// (true・フィールドが無い・ドキュメントが無い)はbodiesを一切使わずlevelsだけで
|
|
4
|
-
// 読み書きする(levelsOps.ts)。bodies削除後にフラグを消しても、公開済みの古い版が
|
|
5
|
-
// bodiesへ書きに行かないよう、未設定はlevels側に倒す。
|
|
6
|
-
//
|
|
7
|
-
// フラグを見ずにlevelsだけへ書くと、切替前はアプリ(bodiesを読んで書く)の
|
|
8
|
-
// 書き込みがMCPの変更を上書きしてしまう。逆にbodiesを使い続けると、切替後は
|
|
9
|
-
// アプリで足した要素が見つからず、bodiesを読み取り専用にするルールの適用後は
|
|
10
|
-
// コミットごと失敗する。呼び出しのたびに読み直すので、MCPサーバーを
|
|
11
|
-
// 再起動しなくても切替に追従する。
|
|
12
|
-
import { LevelsNotInitializedError } from "./levels.js";
|
|
13
|
-
import { levelsSnapshotFromDocs } from "./levelsOps.js";
|
|
14
|
-
export async function isLevelsOnly(client) {
|
|
15
|
-
const config = await client.getDocument("config/app");
|
|
16
|
-
return config?.data.migrationEnabled !== false;
|
|
17
|
-
}
|
|
18
|
-
/** levels全件を読む。levels/rootが無ければ、アプリで開いてもらうよう案内して止める。 */
|
|
19
|
-
export async function readLevelsSnapshot(client, levelsPath, mapTitle) {
|
|
20
|
-
const snapshot = levelsSnapshotFromDocs(await client.listDocuments(levelsPath));
|
|
21
|
-
if (!snapshot.has("root"))
|
|
22
|
-
throw new LevelsNotInitializedError(mapTitle);
|
|
23
|
-
return snapshot;
|
|
24
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import { buildInsertChildWrite, buildReorderChildWrite, buildRemoveChildrenWrite } from "./levels.js";
|
|
2
|
-
import { mergeWritesByPath } from "../firestore/restClient.js";
|
|
3
|
-
/**
|
|
4
|
-
* 要素1件(配下ごと)を新しい場所へ移動するための書き込みを組み立てる。
|
|
5
|
-
* tools/moveElement.tsから切り出した純粋寄りの関数(client.getDocumentだけ呼ぶ)。
|
|
6
|
-
* 呼び出し側で自己参照・循環などのバリデーションを済ませ、[elementId]が
|
|
7
|
-
* [existingBodies]に存在することを保証しておくこと。
|
|
8
|
-
*/
|
|
9
|
-
export async function buildMoveElementWrites(client, bodiesPath, levelsPath, existingBodies, elementId, newParentElementId, afterElementId) {
|
|
10
|
-
const byId = new Map(existingBodies.map((b) => [b.id, b]));
|
|
11
|
-
const body = byId.get(elementId);
|
|
12
|
-
if (!body) {
|
|
13
|
-
throw new Error(`elementId(${elementId})がこのマップに見つかりません。`);
|
|
14
|
-
}
|
|
15
|
-
const writes = [];
|
|
16
|
-
// 1) 元の場所の連結リストを繋ぎ直す
|
|
17
|
-
if (body.prev !== null) {
|
|
18
|
-
writes.push({
|
|
19
|
-
path: `${bodiesPath}/${body.prev}`,
|
|
20
|
-
fields: { next: body.next },
|
|
21
|
-
requireUpdateTime: byId.get(body.prev)?.updateTime,
|
|
22
|
-
});
|
|
23
|
-
}
|
|
24
|
-
else if (body.parent !== null) {
|
|
25
|
-
writes.push({
|
|
26
|
-
path: `${bodiesPath}/${body.parent}`,
|
|
27
|
-
fields: { child: body.next },
|
|
28
|
-
requireUpdateTime: byId.get(body.parent)?.updateTime,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
if (body.next !== null) {
|
|
32
|
-
writes.push({
|
|
33
|
-
path: `${bodiesPath}/${body.next}`,
|
|
34
|
-
fields: { prev: body.prev },
|
|
35
|
-
requireUpdateTime: byId.get(body.next)?.updateTime,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
// 2) 新しい場所へ挿入する位置を求める(afterElementIdが実際にnewParentElementId
|
|
39
|
-
// の子でなければ、見つからない扱い=末尾として無視する)。
|
|
40
|
-
const newSiblingsExcludingSelf = existingBodies.filter((b) => b.parent === newParentElementId && b.id !== elementId);
|
|
41
|
-
const afterBody = afterElementId !== null
|
|
42
|
-
? newSiblingsExcludingSelf.find((b) => b.id === afterElementId) ?? null
|
|
43
|
-
: null;
|
|
44
|
-
const newPrevId = afterElementId === null ? null : afterBody?.id ?? null;
|
|
45
|
-
const newNextId = newPrevId === null
|
|
46
|
-
? newSiblingsExcludingSelf.find((b) => b.prev === null)?.id ?? null
|
|
47
|
-
: newSiblingsExcludingSelf.find((b) => b.prev === newPrevId)?.id ?? null;
|
|
48
|
-
writes.push({
|
|
49
|
-
path: `${bodiesPath}/${elementId}`,
|
|
50
|
-
fields: { parent: newParentElementId, prev: newPrevId, next: newNextId },
|
|
51
|
-
requireUpdateTime: body.updateTime,
|
|
52
|
-
});
|
|
53
|
-
if (newPrevId !== null) {
|
|
54
|
-
writes.push({
|
|
55
|
-
path: `${bodiesPath}/${newPrevId}`,
|
|
56
|
-
fields: { next: elementId },
|
|
57
|
-
requireUpdateTime: byId.get(newPrevId)?.updateTime,
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
else if (newParentElementId !== null) {
|
|
61
|
-
writes.push({
|
|
62
|
-
path: `${bodiesPath}/${newParentElementId}`,
|
|
63
|
-
fields: { child: elementId },
|
|
64
|
-
requireUpdateTime: byId.get(newParentElementId)?.updateTime,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
if (newNextId !== null) {
|
|
68
|
-
writes.push({
|
|
69
|
-
path: `${bodiesPath}/${newNextId}`,
|
|
70
|
-
fields: { prev: elementId },
|
|
71
|
-
requireUpdateTime: byId.get(newNextId)?.updateTime,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
// 3) levels側: 元の親から取り除き、新しい親へ挿入する
|
|
75
|
-
const oldLevelId = body.parent ?? "root";
|
|
76
|
-
const newLevelId = newParentElementId ?? "root";
|
|
77
|
-
const entry = { id: elementId, detail: body.detail, done: body.done };
|
|
78
|
-
if (oldLevelId === newLevelId) {
|
|
79
|
-
// **同じ親の中での並べ替えは1つの書き込みにまとめること。**
|
|
80
|
-
// remove用とinsert用を別々に作ると、どちらも「元のchildren」を
|
|
81
|
-
// 読んでから組み立てるため、同じドキュメントへの書き込みが2つでき、
|
|
82
|
-
// 後勝ちのinsert側だけが残る。insert側は対象がまだ入ったままの配列に
|
|
83
|
-
// もう1つ足したものなので、要素が重複する(実データで踏んだ)。
|
|
84
|
-
writes.push(await buildReorderChildWrite(client, levelsPath, oldLevelId, entry, newPrevId));
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
const removeWrite = await buildRemoveChildrenWrite(client, levelsPath, oldLevelId, [elementId]);
|
|
88
|
-
if (removeWrite)
|
|
89
|
-
writes.push(removeWrite);
|
|
90
|
-
writes.push(await buildInsertChildWrite(client, levelsPath, newLevelId, entry, newPrevId));
|
|
91
|
-
}
|
|
92
|
-
return mergeWritesByPath(writes);
|
|
93
|
-
}
|