mellos-mapping 0.22.1 → 0.23.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
@@ -62,7 +62,7 @@ Clone the branch for your host, then run one command. No build is required.
62
62
  | `chatgpt-app` | ChatGPT desktop, Codex mode | `node install.mjs` |
63
63
 
64
64
  Requires Node.js 18+ and the corresponding host CLI on PATH. The installer checks
65
- release integrity and all six MCP tools, retains the runtime outside the clone,
65
+ release integrity and all eight MCP tools, retains the runtime outside the clone,
66
66
  and configures the host. Start a new conversation after installation.
67
67
  See [release and branch instructions](docs/releasing.md).
68
68
 
@@ -158,7 +158,7 @@ directory they resolved at startup.
158
158
 
159
159
  This is Codex mode in the ChatGPT desktop app (also called Codex App).
160
160
  From the source branch run the following command; on `chatgpt-app`, omit the
161
- host argument. It configures the desktop skill, marketplace and six MCP tools.
161
+ host argument. It configures the desktop skill, marketplace and eight MCP tools.
162
162
 
163
163
  ```
164
164
  node install.mjs chatgpt-app
@@ -239,7 +239,7 @@ servers), then the server process's own working directory. Set
239
239
  somewhere other than the project you are working in.
240
240
 
241
241
  The skill/discipline layer is Claude Code + Codex specific; other clients
242
- get the six `mmap_*` tools and the pane, and bring their own prompting.
242
+ get the eight `mmap_*` tools and the pane, and bring their own prompting.
243
243
 
244
244
  ## Use
245
245
 
@@ -494,12 +494,19 @@ When a hidden sub-map changes in the background, the footer says so.
494
494
 
495
495
  ## MCP tools
496
496
 
497
+ Start with `mmap_read` to resume an existing page; a new conversation is not a
498
+ new effort. Use stable IDs, filtered reads and revision-checked writes. The
499
+ [persistent-map API guide](docs/map-api.md) covers complete CRUD, atomic mixed
500
+ batches, context checkpoints, source hashes and worktree boundaries.
501
+
497
502
  | Tool | Purpose |
498
503
  | --- | --- |
499
504
  | `mmap_declare` | Grow the map: title (`null` removes it), diagram kind, layer bands, lanes, groups (subsystems), nodes — with `status`, `evidence`, `detail`, `kind`, `group`, `lane`, `submap` — and edges, optionally labeled (all-or-nothing batch) |
500
505
  | `mmap_update` | Record progress **and revise**: status (`planned → in-progress → done` +evidence, `regressed`), relabel a node, move it to another band (`layer`), join/leave a group or lane, set a node kind or a `submap`; rename and re-rank bands (`layers`), relabel groups (`groups`) and lanes (`lanes`); `null` clears any clearable field |
501
506
  | `mmap_remove` | Revise: drop edges, nodes, groups, lanes, empty bands — and, with `pages`, whole pages, file and all (permanent; applied after this call's map edits) |
502
507
  | `mmap_view` | Render the current map as text inline (optional `zoom`, `-4`…`2`), ending with a `pages:` line naming every page the project has and which one you are looking at |
508
+ | `mmap_read` | Structured page discovery, precise IDs, filtered/paginated reads, context and source changes |
509
+ | `mmap_batch` | One-page mixed create/update/remove transaction with a revision check |
503
510
  | `mmap_setup` | Get/set the project's mapping policy — when maps open |
504
511
  | `mmap_open` | Put the map on your screen: open the pane, or retarget an open one to a `page` (`window: true` for the dedicated window). It answers with whether a pane actually reported in afterwards, not merely that a command ran — and it can never close one |
505
512
 
@@ -624,7 +631,7 @@ The repo is itself layered bottom-up, and each layer has its spec:
624
631
  | 1 store | `src/store/store.ts` | `store.test.ts`, `atomic-save.test.ts` | atomic state-file persistence on Node |
625
632
  | 1 semantics | `src/semantics/` | `semantics.test.ts` | medium-neutral view semantics: zoom ladder, group aggregation, page-set rules, sequence flip, the shared glyph vocabulary |
626
633
  | 2 apply | `src/server/apply.ts` | `apply.test.ts` | tool inputs → transactional op sequences |
627
- | 3 server | `src/server/server.ts` | `server.test.ts`, `save-failure.test.ts` | the six MCP tools over stdio |
634
+ | 3 server | `src/server/server.ts` | `server.test.ts`, `save-failure.test.ts` | the eight MCP tools over stdio |
628
635
  | 4 render | `src/render/` | `render.test.ts`, `routing.test.ts` | the ASCII renderer and its wire routing |
629
636
  | 4 pane | `src/watch/` | `watch.test.ts`, `pane-state.test.ts`, `input.test.ts` | the polling pane: page set, input parsing, panel and chrome |
630
637
  | — launchers | `scripts/` | `open-pane.test.mjs`, `codex-register.test.mjs` | plain-node entry points |
package/README.zh-CN.md CHANGED
@@ -55,7 +55,7 @@ Claude 为你构建系统时,对话旁边的分屏实时显示这个系统的*
55
55
  | `chatgpt-app` | ChatGPT 桌面 App 的 Codex 模式 | `node install.mjs` |
56
56
 
57
57
  前置要求为 Node.js 18+ 和对应宿主的 CLI,并确保命令在 PATH 中。安装器检查
58
- 发行文件和六个 MCP 工具,将运行时保留到克隆目录之外,完成宿主配置。
58
+ 发行文件和八个 MCP 工具,将运行时保留到克隆目录之外,完成宿主配置。
59
59
  安装后开启新对话。详见[发布与分支说明](docs/releasing.md)。
60
60
 
61
61
  Claude Code 也可以通过插件市场安装:
@@ -141,7 +141,7 @@ the move.`):
141
141
 
142
142
  本版用于 ChatGPT 桌面 App 的 Codex 模式(也称 Codex App)。在源码分支运行
143
143
  以下命令;在 `chatgpt-app` 分支运行时省略宿主参数。它会一次配置桌面专用技能、
144
- 插件市场与六个 MCP 工具。
144
+ 插件市场与八个 MCP 工具。
145
145
 
146
146
  ```
147
147
  node install.mjs chatgpt-app
@@ -223,7 +223,7 @@ npx -y -p mellos-mapping mellos-mapping-watch
223
223
  插件 MCP 服务器设置的约定),最后才是服务器进程自己的工作目录。如果你的
224
224
  客户端会在你实际工作的项目之外启动服务器,就设 `MELLOS_MAPPING_CWD`。
225
225
 
226
- 技能/纪律层是 Claude Code 与 Codex 专属的;其他客户端获得六个 `mmap_*`
226
+ 技能/纪律层是 Claude Code 与 Codex 专属的;其他客户端获得八个 `mmap_*`
227
227
  工具和面板,提示词自备。
228
228
 
229
229
  ## 使用
@@ -448,12 +448,18 @@ Claude 会话的正确姿势。
448
448
 
449
449
  ## MCP 工具
450
450
 
451
+ 先用 `mmap_read` 查找并恢复已有页面;新对话不等于新任务。通过稳定 ID、过滤查询
452
+ 和版本检查做增量修改。[持久化地图接口说明](docs/map-api.md)包含完整增删改查、
453
+ 混合事务、恢复摘要、源码哈希和 worktree 边界。
454
+
451
455
  | 工具 | 用途 |
452
456
  | --- | --- |
453
457
  | `mmap_declare` | 生长地图:标题(传 `null` 删掉)、图种、层级横条、泳道、分组(子系统)、节点(可带 `status`、`evidence`、`detail`、`kind`、`group`、`lane`、`submap`)、边(可带标签);批量,全有或全无 |
454
458
  | `mmap_update` | 记录进度**并修订**:状态(`planned → in-progress → done` 附证据、`regressed`)、改节点标签、把节点搬到另一层(`layer`)、加入/退出分组或泳道、设节点 kind 或 `submap`;给层改名和改 rank(`layers`)、给分组改标签(`groups`)、给泳道改标签(`lanes`);任何可清空的字段传 `null` 即清空 |
455
459
  | `mmap_remove` | 修订:删除边、节点、分组、泳道、空层——以及用 `pages` 删掉整页,连文件一起(永久;在本次调用的地图修改之后执行) |
456
460
  | `mmap_view` | 把当前地图渲染成文本,直接在对话里看(可选 `zoom`,`-4`…`2`);每次响应结尾都有一行 `pages:`,列出本项目有哪些页、以及你正在看哪一页 |
461
+ | `mmap_read` | 结构化页面发现、稳定 ID、过滤分页查询、恢复摘要与源码变化检查 |
462
+ | `mmap_batch` | 单页增删改混合事务与版本冲突检查 |
457
463
  | `mmap_setup` | 查/设本项目的建图策略——什么时候开地图 |
458
464
  | `mmap_open` | 把地图放到你屏幕上:开面板,或把已开的面板切到某一 `page`(`window: true` 用独立窗口)。它回答的是"事后有没有面板真的报到",而不只是"命令跑过了"——而且它永远关不掉面板 |
459
465
 
@@ -561,7 +567,7 @@ npm run verify
561
567
  | 1 store | `src/store/store.ts` | `store.test.ts`、`atomic-save.test.ts` | Node 上的原子化状态文件持久化 |
562
568
  | 1 semantics | `src/semantics/` | `semantics.test.ts` | 媒介无关的视图语义:缩放阶梯、分组聚合、页集规则、时序翻转、共享字形词汇表 |
563
569
  | 2 apply | `src/server/apply.ts` | `apply.test.ts` | 工具输入 → 事务性操作序列 |
564
- | 3 server | `src/server/server.ts` | `server.test.ts`、`save-failure.test.ts` | stdio 上的六个 MCP 工具 |
570
+ | 3 server | `src/server/server.ts` | `server.test.ts`、`save-failure.test.ts` | stdio 上的八个 MCP 工具 |
565
571
  | 4 render | `src/render/` | `render.test.ts`、`routing.test.ts` | ASCII 渲染器与它的走线 |
566
572
  | 4 pane | `src/watch/` | `watch.test.ts`、`pane-state.test.ts`、`input.test.ts` | 轮询面板:页集、输入解析、详情面板与外框 |
567
573
  | — 启动脚本 | `scripts/` | `open-pane.test.mjs`、`codex-register.test.mjs` | 纯 node 的入口 |
@@ -113,7 +113,8 @@ function sessionStartContext(input) {
113
113
  `mellos-mapping policy: ${policy} \u2014 ${describeMappingPolicy(policy)}.`,
114
114
  scope,
115
115
  " 1. Load the mellos-mapping skill and follow it.",
116
- " 2. Declare the whole ghost design with mmap_declare BEFORE writing code.",
116
+ " 2. Read existing pages and context with mmap_read; a new conversation is not a new effort.",
117
+ " Reuse the matching page and verified nodes. Use mmap_declare only for missing structure.",
117
118
  " 3. Open the map pane WITHOUT asking \u2014 the recorded policy is the user's standing consent:",
118
119
  ' mmap_open {page: "<the page this effort lives on>"}',
119
120
  " It opens the pane beside this conversation, or retargets one that is already open.",
@@ -121,6 +122,7 @@ function sessionStartContext(input) {
121
122
  " looking. `pane: CLOSED` means the user cannot see this map \u2014 call mmap_open then too.",
122
123
  " 5. Keep the map current as the work proceeds: in-progress when a node is started,",
123
124
  " done WITH EVIDENCE when its verification passes, regressed when something breaks.",
125
+ " Save a concise context.next checkpoint and use expectedRevision for incremental writes.",
124
126
  "An explicit request from the user always outranks this."
125
127
  ].join("\n");
126
128
  }
package/dist/mmap.mjs CHANGED
@@ -418,7 +418,7 @@ async function main() {
418
418
  console.error(parsed.error);
419
419
  process.exit(1);
420
420
  }
421
- const candidates = storeSearchPath(process.cwd());
421
+ const candidates = [store.resolveProjectDirectory(process.cwd())];
422
422
  const marker = storeMarkerOf(store.STATE_FILE_RELATIVE_PATH);
423
423
  const project = nearestProject(candidates, candidates.map((dir) => existsSync2(join2(dir, marker))));
424
424
  const cfg = { ...parsed.value, projectDir: project.root };
package/dist/preview.mjs CHANGED
@@ -222,12 +222,37 @@ function updateNode(map, input) {
222
222
  return ok({ ...map, nodes: map.nodes.map((n) => n.id === input.id ? updated : n) });
223
223
  }
224
224
 
225
+ // src/domain/context.ts
226
+ function sourceError(raw) {
227
+ if (!Array.isArray(raw) || raw.length > 100) return "sources must be an array of at most 100 file references";
228
+ for (const item of raw) {
229
+ if (!item || typeof item !== "object" || Array.isArray(item)) return "source must be an object";
230
+ const s = item;
231
+ if (Object.keys(s).some((k) => k !== "path" && k !== "sha256")) return "unknown source field";
232
+ if (typeof s.path !== "string" || s.path.length > 1024 || !s.path || /[\u0000-\u001f\u007f-\u009f\\:]/.test(s.path) || s.path.startsWith("/") || s.path.split("/").some((p) => !p || p === "." || p === "..")) return "source path must be relative to the project, with forward slashes and no traversal";
233
+ if (s.sha256 !== void 0 && (typeof s.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(s.sha256))) return "source sha256 must be a lowercase SHA256 hash";
234
+ }
235
+ return void 0;
236
+ }
237
+ function contextError(raw) {
238
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return "context must be an object";
239
+ for (const [key, value] of Object.entries(raw)) {
240
+ if (key !== "summary" && key !== "next") return "unknown context field";
241
+ if (typeof value !== "string" || value.length > 2e3 || /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/.test(value)) return "context fields must be text of at most 2000 characters";
242
+ }
243
+ return void 0;
244
+ }
245
+
225
246
  // src/domain/text.ts
226
247
  var NO_CONTROLS = /^[^\u0000-\u001f\u007f-\u009f]*$/;
227
248
  var NO_CONTROLS_TEXT = "one line of text; control characters (ESC, newline, tab) are not allowed";
228
249
  var NO_CONTROLS_BUT_BREAKS = /^[^\u0000-\u0008\u000b-\u001f\u007f-\u009f]*$/;
229
250
  var NO_CONTROLS_BUT_BREAKS_TEXT = "text with optional newlines (\\n) and tabs; other control characters (ESC, BEL, CR) are not allowed";
230
251
  function mapTextError(map) {
252
+ if (map.context !== void 0) {
253
+ const error2 = contextError(map.context);
254
+ if (error2) return error2;
255
+ }
231
256
  const check = (field, value, multiline = false) => value === void 0 || (multiline ? NO_CONTROLS_BUT_BREAKS : NO_CONTROLS).test(value) ? void 0 : `${field}: ${multiline ? NO_CONTROLS_BUT_BREAKS_TEXT : NO_CONTROLS_TEXT}`;
232
257
  let error = check("title", map.title);
233
258
  if (error) return error;
@@ -242,6 +267,10 @@ function mapTextError(map) {
242
267
  }
243
268
  }
244
269
  for (const [i, node] of map.nodes.entries()) {
270
+ if (node.sources !== void 0) {
271
+ const error2 = sourceError(node.sources);
272
+ if (error2) return `nodes[${i}]: ${error2}`;
273
+ }
245
274
  for (const name of ["label", "evidence", "detail"]) {
246
275
  error = check(`nodes[${i}].${name}`, node[name], name !== "label");
247
276
  if (error) return error;
@@ -307,8 +336,8 @@ function optionalString(rec, key, where, path) {
307
336
  }
308
337
  function parseMap(raw, path) {
309
338
  if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
310
- if (raw["version"] !== STATE_FILE_VERSION) {
311
- return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION}` });
339
+ if (raw["version"] !== STATE_FILE_VERSION && raw["version"] !== 2) {
340
+ return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${STATE_FILE_VERSION} or 2` });
312
341
  }
313
342
  const layers = arrayField(raw, "layers", path, "required");
314
343
  if (!layers.ok) return layers;
@@ -321,6 +350,11 @@ function parseMap(raw, path) {
321
350
  const groups = arrayField(raw, "groups", path, "optional");
322
351
  if (!groups.ok) return groups;
323
352
  let map = EMPTY_MAP;
353
+ if (raw["context"] !== void 0) {
354
+ const error = contextError(raw["context"]);
355
+ if (error) return err({ kind: "bad-shape", path, detail: error });
356
+ map = { ...map, context: raw["context"] };
357
+ }
324
358
  const title = optionalString(raw, "title", "map", path);
325
359
  if (!title.ok) return title;
326
360
  if (title.value !== void 0) map = setTitle(map, title.value);
@@ -449,6 +483,11 @@ function parseMap(raw, path) {
449
483
  if (!updated.ok) return err({ kind: "invariant-violation", path, violation: updated.error });
450
484
  map = updated.value;
451
485
  }
486
+ if (rawNode["sources"] !== void 0) {
487
+ const error = sourceError(rawNode["sources"]);
488
+ if (error) return err({ kind: "bad-shape", path, detail: `${where}: ${error}` });
489
+ map = { ...map, nodes: map.nodes.map((n) => n.id === id.value ? { ...n, sources: rawNode["sources"] } : n) };
490
+ }
452
491
  }
453
492
  for (const [i, rawEdge] of edges.value.entries()) {
454
493
  const where = `edges[${i}]`;