mellos-mapping 0.18.0 → 0.20.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.
package/README.md CHANGED
@@ -147,7 +147,7 @@ npx -y mellos-mapping
147
147
  ```
148
148
 
149
149
  The map file lands in the client session's working directory
150
- (`.claude/mellos-mapping.json`). Open the live pane from the same project:
150
+ (`.mellos/map.json`). Open the live pane from the same project:
151
151
 
152
152
  ```
153
153
  npx -y -p mellos-mapping mellos-mapping-watch
@@ -224,8 +224,8 @@ its own pan, zoom and pinned node. Because every page is its own file, two
224
224
  Claude sessions writing two pages can never clobber each other — this is
225
225
  also the answer to running several Claude sessions in one project.
226
226
 
227
- State lives in `.claude/mellos-mapping.json` (the default page) plus
228
- `.claude/mellos-mapping.pages/<page>.json` for named pages — plain JSON,
227
+ State lives in `.mellos/map.json` (the default page) plus
228
+ `.mellos/pages/<page>.json` for named pages — plain JSON,
229
229
  safe to commit if you want the maps' history in git.
230
230
 
231
231
  ### Diagram kinds
@@ -265,6 +265,21 @@ When a hidden sub-map changes in the background, the footer says so.
265
265
  | `mmap_update` | Record progress: `planned → in-progress → done` (+evidence), `regressed`, group/lane membership, node kind |
266
266
  | `mmap_remove` | Revise: drop edges, nodes, groups, lanes, empty bands |
267
267
  | `mmap_view` | Render the current map as text inline (optional `zoom`) |
268
+ | `mmap_setup` | Get/set the project's mapping policy — when maps open |
269
+
270
+ ### Setup: choose when maps open
271
+
272
+ Each project chooses how eager mapping is, once, via `/mmap setup` (or the
273
+ first time the assistant declares a map — the reply nudges it to ask you):
274
+
275
+ - `always` — map every structured task: workflows, designs, architecture,
276
+ technical dependencies.
277
+ - `complex` — map only medium or complex tasks (the default until configured).
278
+ - `on-request` — map only when you explicitly ask.
279
+
280
+ The choice is stored in `.claude/mellos-mapping.config.json` and guides the
281
+ assistant; it never blocks the tools, and asking for a map explicitly always
282
+ works under any policy.
268
283
 
269
284
  Structural invariants enforced by the tools: layers form a total order by
270
285
  rank; every node lives in exactly one layer; edges point **strictly
@@ -284,7 +299,9 @@ The repo is itself layered bottom-up, and each layer has its spec:
284
299
  | Layer | Code | Owns |
285
300
  | --- | --- | --- |
286
301
  | 0 domain | `src/domain/` | the map value, structural invariants, pure ops |
287
- | 1 store | `src/store/` | atomic state-file persistence, boundary validation |
302
+ | 1 format | `src/store/format.ts` | the state-file format: replay-validated parse, serialize I/O-free |
303
+ | 1 store | `src/store/store.ts` | atomic state-file persistence on Node |
304
+ | 1 semantics | `src/semantics/` | medium-neutral view semantics: zoom ladder, group aggregation, sequence flip |
288
305
  | 2 apply | `src/server/apply.ts` | tool inputs → transactional op sequences |
289
306
  | 3 server | `src/server/server.ts` | the four MCP tools over stdio |
290
307
  | 4 render | `src/render/`, `src/watch/` | ASCII renderer and the polling pane |
@@ -292,6 +309,17 @@ The repo is itself layered bottom-up, and each layer has its spec:
292
309
  `dist/` is committed deliberately: plugin installation clones this repo and
293
310
  runs nothing, so entry points ship bundled.
294
311
 
312
+ ### Library
313
+
314
+ The lower layers are also a library (`npm run build` emits `lib/` with type
315
+ declarations; npm packs it). Subpath exports mirror the source:
316
+ `mellos-mapping/domain/types`, `/domain/ops`, `/format`, `/semantics` are
317
+ **browser-safe** — no Node builtins in their import closure, gated by a test —
318
+ so a graphical client (a web panel, an editor view) can parse state files and
319
+ reuse the exact aggregation and zoom semantics the terminal pane draws with.
320
+ `/store` (Node filesystem persistence) and `/render` (the terminal renderer)
321
+ complete the surface for Node hosts.
322
+
295
323
  ## License
296
324
 
297
325
  MIT
package/README.zh-CN.md CHANGED
@@ -134,7 +134,7 @@ CLI……)都能用标准 stdio 条目接入:
134
134
  npx -y mellos-mapping
135
135
  ```
136
136
 
137
- 地图文件落在客户端会话的工作目录(`.claude/mellos-mapping.json`)。在同
137
+ 地图文件落在客户端会话的工作目录(`.mellos/map.json`)。在同
138
138
  一项目里打开实况面板:
139
139
 
140
140
  ```
@@ -204,8 +204,8 @@ npx -y -p mellos-mapping mellos-mapping-watch
204
204
  每页就是一个独立文件,两个 Claude 会话各写各页永远不会互相覆盖——这也是
205
205
  同一项目里跑多个 Claude 会话的正确姿势。
206
206
 
207
- 地图状态存在 `.claude/mellos-mapping.json`(默认页)和
208
- `.claude/mellos-mapping.pages/<页名>.json`(命名页)——纯 JSON,想在 git
207
+ 地图状态存在 `.mellos/map.json`(默认页)和
208
+ `.mellos/pages/<页名>.json`(命名页)——纯 JSON,想在 git
209
209
  里留下地图的历史就把它们提交进去。
210
210
 
211
211
  ### 图种
@@ -242,6 +242,19 @@ npx -y -p mellos-mapping mellos-mapping-watch
242
242
  | `mmap_update` | 记录进度:`planned → in-progress → done`(附证据)、`regressed`、分组/泳道归属、节点 kind |
243
243
  | `mmap_remove` | 修订:删除边、节点、分组、泳道、空层 |
244
244
  | `mmap_view` | 把当前地图渲染成文本,直接在对话里看(可选 `zoom` 参数) |
245
+ | `mmap_setup` | 查/设本项目的建图策略——什么时候开地图 |
246
+
247
+ ### Setup:选择什么时候建图
248
+
249
+ 每个项目选一次建图的积极程度,用 `/mmap setup`(或 AI 第一次 declare 时,
250
+ 工具响应会引导它来问你):
251
+
252
+ - `always` —— 任何有结构的任务都建图:流程、设计、架构、技术依赖。
253
+ - `complex` —— 只在中等或复杂任务时建图(未配置时的默认行为)。
254
+ - `on-request` —— 只在你明确要求时建图。
255
+
256
+ 选择保存在 `.claude/mellos-mapping.config.json`,用于引导 AI;它从不阻止
257
+ 工具本身——无论什么策略,明确要求建图永远有效。
245
258
 
246
259
  工具强制的结构不变量:层按 rank 构成全序;每个节点恰好属于一层;边**严格
247
260
  向下**——因此图从构造上就是无环的;节点不能依赖同层兄弟(如果 A 需要
@@ -259,7 +272,9 @@ npm run verify # 类型检查 + 测试 + 打包
259
272
  | 层 | 代码 | 职责 |
260
273
  | --- | --- | --- |
261
274
  | 0 domain | `src/domain/` | 地图值、结构不变量、纯操作 |
262
- | 1 store | `src/store/` | 原子化状态文件持久化、边界校验 |
275
+ | 1 format | `src/store/format.ts` | 状态文件格式:重放校验的解析与序列化,零 I/O |
276
+ | 1 store | `src/store/store.ts` | Node 上的原子化状态文件持久化 |
277
+ | 1 semantics | `src/semantics/` | 媒介无关的视图语义:缩放阶梯、分组聚合、时序翻转 |
263
278
  | 2 apply | `src/server/apply.ts` | 工具输入 → 事务性操作序列 |
264
279
  | 3 server | `src/server/server.ts` | stdio 上的四个 MCP 工具 |
265
280
  | 4 render | `src/render/`、`src/watch/` | ASCII 渲染器和轮询面板 |
@@ -267,6 +282,16 @@ npm run verify # 类型检查 + 测试 + 打包
267
282
  `dist/` 是刻意提交的:插件安装就是克隆本仓库、不运行任何东西,所以入口
268
283
  文件以打包形式随仓库分发。
269
284
 
285
+ ### 库
286
+
287
+ 底部各层同时是一个库(`npm run build` 产出带类型声明的 `lib/`,npm 打包
288
+ 收录)。子路径导出与源码结构一一对应:`mellos-mapping/domain/types`、
289
+ `/domain/ops`、`/format`、`/semantics` 是**浏览器安全**的——import 闭包中
290
+ 没有任何 Node 内建模块,由测试门禁守护——图形客户端(web 面板、编辑器
291
+ 视图)可以直接解析状态文件,并复用与终端面板完全一致的聚合与缩放语义。
292
+ `/store`(Node 文件系统持久化)与 `/render`(终端渲染器)补齐 Node 宿主
293
+ 所需的完整表面。
294
+
270
295
  ## 许可证
271
296
 
272
297
  MIT
package/dist/server.mjs CHANGED
@@ -21388,13 +21388,66 @@ function removeLayer(map, id) {
21388
21388
  return ok({ ...map, layers: map.layers.filter((l) => l.id !== id) });
21389
21389
  }
21390
21390
 
21391
- // src/render/render.ts
21391
+ // src/semantics/semantics.ts
21392
21392
  var ZOOM_MIN = -4;
21393
21393
  var ZOOM_MAX = 2;
21394
21394
  var ZOOM_DEFAULT = 0;
21395
21395
  function clampZoom(n) {
21396
21396
  return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, Math.round(n)));
21397
21397
  }
21398
+ function zoomMode(zoom) {
21399
+ if (zoom >= 1) return "detail";
21400
+ if (zoom <= -4) return "overview";
21401
+ return "boxes";
21402
+ }
21403
+ function isNeutralKind(map) {
21404
+ return map.kind !== void 0 && map.kind !== "dev";
21405
+ }
21406
+ function aggregateMap(map) {
21407
+ if (map.groups.length === 0) return void 0;
21408
+ const representative = /* @__PURE__ */ new Map();
21409
+ for (const n of map.nodes) representative.set(n.id, n.group ?? n.id);
21410
+ const nodes = map.groups.map((g) => {
21411
+ const members = map.nodes.filter((n) => n.group === g.id);
21412
+ const done = members.filter((n) => n.status === "done").length;
21413
+ return {
21414
+ id: g.id,
21415
+ // neutral kinds document structure, not progress — no member counts
21416
+ label: isNeutralKind(map) ? g.label : `${g.label} ${done}/${members.length}`,
21417
+ layer: g.layer,
21418
+ status: groupStatus(map, g.id)
21419
+ };
21420
+ });
21421
+ for (const n of map.nodes) if (n.group === void 0) nodes.push(n);
21422
+ const seen = /* @__PURE__ */ new Set();
21423
+ const edges = [];
21424
+ for (const e of map.edges) {
21425
+ const from = representative.get(e.from);
21426
+ const to = representative.get(e.to);
21427
+ if (from === to || seen.has(`${from}->${to}`)) continue;
21428
+ seen.add(`${from}->${to}`);
21429
+ edges.push({ from, to });
21430
+ }
21431
+ return {
21432
+ ...map.title !== void 0 ? { title: map.title } : {},
21433
+ ...map.kind !== void 0 ? { kind: map.kind } : {},
21434
+ layers: map.layers,
21435
+ groups: [],
21436
+ lanes: map.lanes,
21437
+ nodes,
21438
+ edges
21439
+ };
21440
+ }
21441
+ function flipForSequence(map) {
21442
+ if (map.kind !== "sequence") return map;
21443
+ return {
21444
+ ...map,
21445
+ layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
21446
+ edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
21447
+ };
21448
+ }
21449
+
21450
+ // src/render/render.ts
21398
21451
  var WIDE_RANGES = [
21399
21452
  [4352, 4447],
21400
21453
  // Hangul Jamo
@@ -21659,9 +21712,6 @@ function kindGlyph(kind, unicode) {
21659
21712
  const pair = NODE_KIND_GLYPHS[kind];
21660
21713
  return pair === void 0 ? void 0 : unicode ? pair[0] : pair[1];
21661
21714
  }
21662
- function isNeutralKind(map) {
21663
- return map.kind !== void 0 && map.kind !== "dev";
21664
- }
21665
21715
  function neutralSkin(unicode) {
21666
21716
  return unicode ? { h: "\u2500", v: "\u2502", corners: ["\u256D", "\u256E", "\u2570", "\u256F"], style: "none" } : { h: "-", v: "|", corners: ["+", "+", "+", "+"], style: "none" };
21667
21717
  }
@@ -21671,21 +21721,23 @@ var LEFT_MARGIN = 2;
21671
21721
  var DETAIL_BUDGET = { innerMin: 22, innerMax: 32, noteRows: 3 };
21672
21722
  var DETAIL_PLUS_BUDGET = { innerMin: 30, innerMax: 48, noteRows: 12 };
21673
21723
  function zoomGeometry(zoom) {
21724
+ const m = zoomMode(zoom);
21725
+ const mode = m === "overview" ? "constellation" : m;
21674
21726
  switch (zoom) {
21675
21727
  case 2:
21676
- return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
21728
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_PLUS_BUDGET };
21677
21729
  case 1:
21678
- return { mode: "detail", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
21730
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false, detail: DETAIL_BUDGET };
21679
21731
  case 0:
21680
- return { mode: "boxes", scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
21732
+ return { mode, scale: 1, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
21681
21733
  case -1:
21682
- return { mode: "boxes", scale: 0.85, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
21734
+ return { mode, scale: 0.85, pad: 1, boxGap: BOX_GAP, breathe: 1, titleGap: 1, barGap: 1, bandCounts: false };
21683
21735
  case -2:
21684
- return { mode: "boxes", scale: 0.7, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: false };
21736
+ return { mode, scale: 0.7, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: false };
21685
21737
  case -3:
21686
- return { mode: "boxes", scale: 0.55, pad: 0, boxGap: 1, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
21738
+ return { mode, scale: 0.55, pad: 0, boxGap: 1, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
21687
21739
  case -4:
21688
- return { mode: "constellation", scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
21740
+ return { mode, scale: 0, pad: 0, boxGap: BOX_GAP, breathe: 0, titleGap: 0, barGap: 1, bandCounts: true };
21689
21741
  }
21690
21742
  }
21691
21743
  var LABEL_BUDGET_MIN = 4;
@@ -21733,41 +21785,6 @@ function renderMap(map, opts) {
21733
21785
  const built = buildCanvas(map, opts);
21734
21786
  return built.canvas.emit(opts);
21735
21787
  }
21736
- function aggregateMap(map) {
21737
- if (map.groups.length === 0) return void 0;
21738
- const representative = /* @__PURE__ */ new Map();
21739
- for (const n of map.nodes) representative.set(n.id, n.group ?? n.id);
21740
- const nodes = map.groups.map((g) => {
21741
- const members = map.nodes.filter((n) => n.group === g.id);
21742
- const done = members.filter((n) => n.status === "done").length;
21743
- return {
21744
- id: g.id,
21745
- // neutral kinds document structure, not progress — no member counts
21746
- label: isNeutralKind(map) ? g.label : `${g.label} ${done}/${members.length}`,
21747
- layer: g.layer,
21748
- status: groupStatus(map, g.id)
21749
- };
21750
- });
21751
- for (const n of map.nodes) if (n.group === void 0) nodes.push(n);
21752
- const seen = /* @__PURE__ */ new Set();
21753
- const edges = [];
21754
- for (const e of map.edges) {
21755
- const from = representative.get(e.from);
21756
- const to = representative.get(e.to);
21757
- if (from === to || seen.has(`${from}->${to}`)) continue;
21758
- seen.add(`${from}->${to}`);
21759
- edges.push({ from, to });
21760
- }
21761
- return {
21762
- ...map.title !== void 0 ? { title: map.title } : {},
21763
- ...map.kind !== void 0 ? { kind: map.kind } : {},
21764
- layers: map.layers,
21765
- groups: [],
21766
- lanes: map.lanes,
21767
- nodes,
21768
- edges
21769
- };
21770
- }
21771
21788
  var AGGREGATE_GEO = {
21772
21789
  mode: "boxes",
21773
21790
  scale: 1,
@@ -21778,14 +21795,6 @@ var AGGREGATE_GEO = {
21778
21795
  barGap: 1,
21779
21796
  bandCounts: false
21780
21797
  };
21781
- function flipForSequence(map) {
21782
- if (map.kind !== "sequence") return map;
21783
- return {
21784
- ...map,
21785
- layers: map.layers.map((l) => ({ ...l, rank: -l.rank })),
21786
- edges: map.edges.map((e) => ({ from: e.to, to: e.from, ...e.label !== void 0 ? { label: e.label } : {} }))
21787
- };
21788
- }
21789
21798
  function buildCanvas(map, opts) {
21790
21799
  const oriented = flipForSequence(map);
21791
21800
  const plainGeo = zoomGeometry(opts.zoom ?? ZOOM_DEFAULT);
@@ -22135,12 +22144,9 @@ function drawBox(canvas, box, opts, neutral, focused = false) {
22135
22144
  // src/store/store.ts
22136
22145
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
22137
22146
  import { basename, dirname, join } from "node:path";
22147
+
22148
+ // src/store/format.ts
22138
22149
  var STATE_FILE_VERSION = 1;
22139
- var STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
22140
- var PAGES_DIR_NAME = "mellos-mapping.pages";
22141
- function pageFilePath(defaultFile, page) {
22142
- return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
22143
- }
22144
22150
  function describeStoreError(e) {
22145
22151
  switch (e.kind) {
22146
22152
  case "not-found":
@@ -22295,6 +22301,84 @@ function serializeMap(map) {
22295
22301
  };
22296
22302
  return JSON.stringify(body, null, 2) + "\n";
22297
22303
  }
22304
+
22305
+ // src/store/store.ts
22306
+ var STATE_FILE_RELATIVE_PATH = join(".mellos", "map.json");
22307
+ var PAGES_DIR_NAME = "pages";
22308
+ function pageFilePath(defaultFile, page) {
22309
+ return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
22310
+ }
22311
+ var CONFIG_FILE_NAME = "config.json";
22312
+ var CONFIG_FILE_VERSION = 1;
22313
+ function configFilePath(defaultFile) {
22314
+ return join(dirname(defaultFile), CONFIG_FILE_NAME);
22315
+ }
22316
+ var MAPPING_POLICIES = ["always", "complex", "on-request"];
22317
+ function makeMappingPolicy(raw) {
22318
+ return MAPPING_POLICIES.includes(raw) ? ok(raw) : err({ kind: "invalid-policy", raw, allowed: MAPPING_POLICIES });
22319
+ }
22320
+ function describeMappingPolicy(policy) {
22321
+ switch (policy) {
22322
+ case "always":
22323
+ return "map every structured task \u2014 workflows, designs, architecture, technical dependencies";
22324
+ case "complex":
22325
+ return "map only medium or complex tasks \u2014 several modules, a new subsystem, roughly an hour or more";
22326
+ case "on-request":
22327
+ return "map only when the user explicitly asks";
22328
+ }
22329
+ }
22330
+ function isRecord2(v) {
22331
+ return typeof v === "object" && v !== null && !Array.isArray(v);
22332
+ }
22333
+ function loadMappingPolicy(defaultFile) {
22334
+ const path = configFilePath(defaultFile);
22335
+ let text2;
22336
+ try {
22337
+ text2 = readFileSync(path, "utf8");
22338
+ } catch (e) {
22339
+ if (e.code === "ENOENT") return ok(void 0);
22340
+ throw e;
22341
+ }
22342
+ let raw;
22343
+ try {
22344
+ raw = JSON.parse(text2);
22345
+ } catch (e) {
22346
+ return err({ kind: "malformed-json", path, detail: e.message });
22347
+ }
22348
+ if (!isRecord2(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
22349
+ if (raw["version"] !== CONFIG_FILE_VERSION) {
22350
+ return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${CONFIG_FILE_VERSION}` });
22351
+ }
22352
+ const rawPolicy = raw["policy"];
22353
+ if (rawPolicy === void 0) return ok(void 0);
22354
+ if (typeof rawPolicy !== "string") return err({ kind: "bad-shape", path, detail: "policy is not a string" });
22355
+ const policy = makeMappingPolicy(rawPolicy);
22356
+ return policy.ok ? ok(policy.value) : err({ kind: "bad-shape", path, detail: `policy is "${rawPolicy}", expected one of: ${MAPPING_POLICIES.join(" | ")}` });
22357
+ }
22358
+ function saveMappingPolicy(defaultFile, policy) {
22359
+ const path = configFilePath(defaultFile);
22360
+ mkdirSync(dirname(path), { recursive: true });
22361
+ const tmp = path + ".tmp";
22362
+ writeFileSync(tmp, JSON.stringify({ version: CONFIG_FILE_VERSION, policy }, null, 2) + "\n", "utf8");
22363
+ renameSync(tmp, path);
22364
+ }
22365
+ var LEGACY_STATE_FILE_RELATIVE_PATH = join(".claude", "mellos-mapping.json");
22366
+ var LEGACY_PAGES_DIR_NAME = "mellos-mapping.pages";
22367
+ var LEGACY_CONFIG_FILE_NAME = "mellos-mapping.config.json";
22368
+ function migrateLegacyStore(defaultFile) {
22369
+ const projectRoot = dirname(dirname(defaultFile));
22370
+ const legacyDefault = join(projectRoot, LEGACY_STATE_FILE_RELATIVE_PATH);
22371
+ const legacyPages = join(dirname(legacyDefault), LEGACY_PAGES_DIR_NAME);
22372
+ const legacyConfig = join(dirname(legacyDefault), LEGACY_CONFIG_FILE_NAME);
22373
+ const hasLegacy = existsSync(legacyDefault) || existsSync(legacyPages) || existsSync(legacyConfig);
22374
+ const hasCurrent = existsSync(defaultFile) || existsSync(join(dirname(defaultFile), PAGES_DIR_NAME)) || existsSync(configFilePath(defaultFile));
22375
+ if (!hasLegacy || hasCurrent) return false;
22376
+ mkdirSync(dirname(defaultFile), { recursive: true });
22377
+ if (existsSync(legacyDefault)) renameSync(legacyDefault, defaultFile);
22378
+ if (existsSync(legacyPages)) renameSync(legacyPages, join(dirname(defaultFile), PAGES_DIR_NAME));
22379
+ if (existsSync(legacyConfig)) renameSync(legacyConfig, configFilePath(defaultFile));
22380
+ return true;
22381
+ }
22298
22382
  function loadMapFile(path) {
22299
22383
  let text2;
22300
22384
  try {
@@ -22515,7 +22599,7 @@ function summarize(map) {
22515
22599
 
22516
22600
  // src/server/server.ts
22517
22601
  var SERVER_NAME = "mellos-mapping";
22518
- var SERVER_VERSION = "0.18.0";
22602
+ var SERVER_VERSION = "0.20.0";
22519
22603
  var ID = external_exports.string().regex(/^[a-z0-9][a-z0-9-]{0,63}$/, "lowercase letters, digits and dashes, 1-64 chars").describe("stable kebab-case identifier");
22520
22604
  var PAGE = ID.optional().describe(
22521
22605
  "page (parallel map) this call targets; omit for the default page. One effort = one page: start a NEW effort on its own page named after the effort, so concurrent sessions never write over each other and the pane can switch between pages."
@@ -22552,6 +22636,13 @@ function buildServer(stateFile) {
22552
22636
  saveMapFile(file, applied.value);
22553
22637
  return text(summarize(applied.value) + (page !== void 0 ? ` [page: ${page}]` : ""));
22554
22638
  };
22639
+ const setupNudge = () => {
22640
+ const policy = loadMappingPolicy(stateFile);
22641
+ if (!policy.ok) return `
22642
+ note: ${describeStoreError(policy.error)} \u2014 fix it or rerun setup (mmap_setup).`;
22643
+ if (policy.value !== void 0) return "";
22644
+ return "\nnote: mapping policy not set for this project. Ask the user when maps should open \u2014 " + MAPPING_POLICIES.map((p) => `${p} (${describeMappingPolicy(p)})`).join("; ") + " \u2014 then record the answer with mmap_setup.";
22645
+ };
22555
22646
  server.registerTool(
22556
22647
  "mmap_declare",
22557
22648
  {
@@ -22599,7 +22690,12 @@ function buildServer(stateFile) {
22599
22690
  edges: external_exports.array(EDGE.extend({ label: external_exports.string().max(80).optional().describe("what flows along the edge") })).optional()
22600
22691
  }
22601
22692
  },
22602
- (input) => mutate(input.page, (map) => applyDeclare(map, input))
22693
+ (input) => {
22694
+ const result = mutate(input.page, (map) => applyDeclare(map, input));
22695
+ if (result.isError === true) return result;
22696
+ const nudge = setupNudge();
22697
+ return nudge === "" ? result : text((result.content[0]?.text ?? "") + nudge);
22698
+ }
22603
22699
  );
22604
22700
  server.registerTool(
22605
22701
  "mmap_update",
@@ -22641,6 +22737,31 @@ function buildServer(stateFile) {
22641
22737
  },
22642
22738
  (input) => mutate(input.page, (map) => applyRemove(map, input))
22643
22739
  );
22740
+ server.registerTool(
22741
+ "mmap_setup",
22742
+ {
22743
+ title: "Configure when maps open",
22744
+ description: `Get or set this project's mapping policy \u2014 WHEN the assistant opens a Mellos map. Call with no arguments to read it. If it reports "not set", ask the USER to choose (never pick for them): always = ` + describeMappingPolicy("always") + "; complex = " + describeMappingPolicy("complex") + "; on-request = " + describeMappingPolicy("on-request") + ". Then call again with their choice to persist it. The policy guides you; it never blocks the tools, and an explicit user request for a map always wins.",
22745
+ inputSchema: {
22746
+ policy: external_exports.enum(MAPPING_POLICIES).optional().describe("the user's choice to persist; omit to read the current policy")
22747
+ }
22748
+ },
22749
+ (input) => {
22750
+ if (input.policy !== void 0) {
22751
+ const policy = input.policy;
22752
+ saveMappingPolicy(stateFile, policy);
22753
+ return text(`mapping policy set: ${policy} \u2014 ${describeMappingPolicy(policy)} [${configFilePath(stateFile)}]`);
22754
+ }
22755
+ const loaded = loadMappingPolicy(stateFile);
22756
+ if (!loaded.ok) return text(describeStoreError(loaded.error), true);
22757
+ if (loaded.value === void 0) {
22758
+ return text(
22759
+ "mapping policy not set. Ask the user to choose one of: " + MAPPING_POLICIES.map((p) => `${p} (${describeMappingPolicy(p)})`).join("; ") + " \u2014 then call mmap_setup with their choice. Until then act as complex."
22760
+ );
22761
+ }
22762
+ return text(`mapping policy: ${loaded.value} \u2014 ${describeMappingPolicy(loaded.value)}`);
22763
+ }
22764
+ );
22644
22765
  server.registerTool(
22645
22766
  "mmap_view",
22646
22767
  {
@@ -22665,7 +22786,9 @@ function resolveStateFile(env, cwd) {
22665
22786
  return join2(projectDir, STATE_FILE_RELATIVE_PATH);
22666
22787
  }
22667
22788
  async function main() {
22668
- const server = buildServer(resolveStateFile(process.env, process.cwd()));
22789
+ const stateFile = resolveStateFile(process.env, process.cwd());
22790
+ migrateLegacyStore(stateFile);
22791
+ const server = buildServer(stateFile);
22669
22792
  await server.connect(new StdioServerTransport());
22670
22793
  }
22671
22794
  function launchedAsEntry(argv1, moduleUrl) {