mellos-mapping 0.18.0 → 0.19.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
@@ -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
package/README.zh-CN.md CHANGED
@@ -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 需要
package/dist/server.mjs CHANGED
@@ -22141,6 +22141,57 @@ var PAGES_DIR_NAME = "mellos-mapping.pages";
22141
22141
  function pageFilePath(defaultFile, page) {
22142
22142
  return page === void 0 ? defaultFile : join(dirname(defaultFile), PAGES_DIR_NAME, `${page}.json`);
22143
22143
  }
22144
+ var CONFIG_FILE_NAME = "mellos-mapping.config.json";
22145
+ var CONFIG_FILE_VERSION = 1;
22146
+ function configFilePath(defaultFile) {
22147
+ return join(dirname(defaultFile), CONFIG_FILE_NAME);
22148
+ }
22149
+ var MAPPING_POLICIES = ["always", "complex", "on-request"];
22150
+ function makeMappingPolicy(raw) {
22151
+ return MAPPING_POLICIES.includes(raw) ? ok(raw) : err({ kind: "invalid-policy", raw, allowed: MAPPING_POLICIES });
22152
+ }
22153
+ function describeMappingPolicy(policy) {
22154
+ switch (policy) {
22155
+ case "always":
22156
+ return "map every structured task \u2014 workflows, designs, architecture, technical dependencies";
22157
+ case "complex":
22158
+ return "map only medium or complex tasks \u2014 several modules, a new subsystem, roughly an hour or more";
22159
+ case "on-request":
22160
+ return "map only when the user explicitly asks";
22161
+ }
22162
+ }
22163
+ function loadMappingPolicy(defaultFile) {
22164
+ const path = configFilePath(defaultFile);
22165
+ let text2;
22166
+ try {
22167
+ text2 = readFileSync(path, "utf8");
22168
+ } catch (e) {
22169
+ if (e.code === "ENOENT") return ok(void 0);
22170
+ throw e;
22171
+ }
22172
+ let raw;
22173
+ try {
22174
+ raw = JSON.parse(text2);
22175
+ } catch (e) {
22176
+ return err({ kind: "malformed-json", path, detail: e.message });
22177
+ }
22178
+ if (!isRecord(raw)) return err({ kind: "bad-shape", path, detail: "root is not an object" });
22179
+ if (raw["version"] !== CONFIG_FILE_VERSION) {
22180
+ return err({ kind: "bad-shape", path, detail: `version is ${String(raw["version"])}, expected ${CONFIG_FILE_VERSION}` });
22181
+ }
22182
+ const rawPolicy = raw["policy"];
22183
+ if (rawPolicy === void 0) return ok(void 0);
22184
+ if (typeof rawPolicy !== "string") return err({ kind: "bad-shape", path, detail: "policy is not a string" });
22185
+ const policy = makeMappingPolicy(rawPolicy);
22186
+ return policy.ok ? ok(policy.value) : err({ kind: "bad-shape", path, detail: `policy is "${rawPolicy}", expected one of: ${MAPPING_POLICIES.join(" | ")}` });
22187
+ }
22188
+ function saveMappingPolicy(defaultFile, policy) {
22189
+ const path = configFilePath(defaultFile);
22190
+ mkdirSync(dirname(path), { recursive: true });
22191
+ const tmp = path + ".tmp";
22192
+ writeFileSync(tmp, JSON.stringify({ version: CONFIG_FILE_VERSION, policy }, null, 2) + "\n", "utf8");
22193
+ renameSync(tmp, path);
22194
+ }
22144
22195
  function describeStoreError(e) {
22145
22196
  switch (e.kind) {
22146
22197
  case "not-found":
@@ -22515,7 +22566,7 @@ function summarize(map) {
22515
22566
 
22516
22567
  // src/server/server.ts
22517
22568
  var SERVER_NAME = "mellos-mapping";
22518
- var SERVER_VERSION = "0.18.0";
22569
+ var SERVER_VERSION = "0.19.0";
22519
22570
  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
22571
  var PAGE = ID.optional().describe(
22521
22572
  "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 +22603,13 @@ function buildServer(stateFile) {
22552
22603
  saveMapFile(file, applied.value);
22553
22604
  return text(summarize(applied.value) + (page !== void 0 ? ` [page: ${page}]` : ""));
22554
22605
  };
22606
+ const setupNudge = () => {
22607
+ const policy = loadMappingPolicy(stateFile);
22608
+ if (!policy.ok) return `
22609
+ note: ${describeStoreError(policy.error)} \u2014 fix it or rerun setup (mmap_setup).`;
22610
+ if (policy.value !== void 0) return "";
22611
+ 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.";
22612
+ };
22555
22613
  server.registerTool(
22556
22614
  "mmap_declare",
22557
22615
  {
@@ -22599,7 +22657,12 @@ function buildServer(stateFile) {
22599
22657
  edges: external_exports.array(EDGE.extend({ label: external_exports.string().max(80).optional().describe("what flows along the edge") })).optional()
22600
22658
  }
22601
22659
  },
22602
- (input) => mutate(input.page, (map) => applyDeclare(map, input))
22660
+ (input) => {
22661
+ const result = mutate(input.page, (map) => applyDeclare(map, input));
22662
+ if (result.isError === true) return result;
22663
+ const nudge = setupNudge();
22664
+ return nudge === "" ? result : text((result.content[0]?.text ?? "") + nudge);
22665
+ }
22603
22666
  );
22604
22667
  server.registerTool(
22605
22668
  "mmap_update",
@@ -22641,6 +22704,31 @@ function buildServer(stateFile) {
22641
22704
  },
22642
22705
  (input) => mutate(input.page, (map) => applyRemove(map, input))
22643
22706
  );
22707
+ server.registerTool(
22708
+ "mmap_setup",
22709
+ {
22710
+ title: "Configure when maps open",
22711
+ 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.",
22712
+ inputSchema: {
22713
+ policy: external_exports.enum(MAPPING_POLICIES).optional().describe("the user's choice to persist; omit to read the current policy")
22714
+ }
22715
+ },
22716
+ (input) => {
22717
+ if (input.policy !== void 0) {
22718
+ const policy = input.policy;
22719
+ saveMappingPolicy(stateFile, policy);
22720
+ return text(`mapping policy set: ${policy} \u2014 ${describeMappingPolicy(policy)} [${configFilePath(stateFile)}]`);
22721
+ }
22722
+ const loaded = loadMappingPolicy(stateFile);
22723
+ if (!loaded.ok) return text(describeStoreError(loaded.error), true);
22724
+ if (loaded.value === void 0) {
22725
+ return text(
22726
+ "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."
22727
+ );
22728
+ }
22729
+ return text(`mapping policy: ${loaded.value} \u2014 ${describeMappingPolicy(loaded.value)}`);
22730
+ }
22731
+ );
22644
22732
  server.registerTool(
22645
22733
  "mmap_view",
22646
22734
  {
package/dist/watch.mjs CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from 'node:module'; const require = createRequire(impor
3
3
 
4
4
  // src/watch/watch.ts
5
5
  import { realpathSync, statSync } from "node:fs";
6
- import { join as join2 } from "node:path";
6
+ import { dirname as dirname2, join as join2 } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
 
9
9
  // src/domain/types.ts
@@ -1535,19 +1535,8 @@ function nodePanel(map, focusId, unicode, width, pinned, rows = PANEL_CONTENT_RO
1535
1535
  }
1536
1536
  return lines.slice(0, rows);
1537
1537
  }
1538
- var SPLASH_FONT = {
1539
- M: ["# #", "## ##", "# # #", "# #", "# #"],
1540
- E: ["####", "#", "###", "#", "####"],
1541
- L: ["#", "#", "#", "#", "####"],
1542
- O: [" ###", "# #", "# #", "# #", " ###"],
1543
- S: [" ####", "#", " ###", " #", "####"],
1544
- A: [" ###", "# #", "#####", "# #", "# #"],
1545
- P: ["####", "# #", "####", "#", "#"],
1546
- I: ["###", " #", " #", " #", "###"],
1547
- N: ["# #", "## #", "# # #", "# ##", "# #"],
1548
- G: [" ####", "#", "# ##", "# #", " ###"]
1549
- };
1550
- var SPLASH_ROWS = 5;
1538
+ var WATER_ROWS = 7;
1539
+ var WATER_COLS_MAX = 60;
1551
1540
  var SPLASH_SHADES = {
1552
1541
  unicode: ["\u2591", "\u2591", "\u2592", "\u2592", "\u2593", "\u2593", "\u2588", "\u2588"],
1553
1542
  ascii: [".", ".", ":", ":", "=", "=", "#", "#"]
@@ -1557,23 +1546,24 @@ var SPINNER_FRAMES = {
1557
1546
  unicode: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
1558
1547
  ascii: ["|", "/", "-", "\\"]
1559
1548
  };
1560
- function wordArt(word) {
1561
- const glyphs = [...word.toUpperCase()].map((ch) => SPLASH_FONT[ch]).filter((g) => g !== void 0);
1562
- const widths = glyphs.map((g) => Math.max(...g.map((r) => r.length)));
1563
- const rows = [];
1564
- for (let r = 0; r < SPLASH_ROWS; r++) {
1565
- rows.push(glyphs.map((g, i) => (g[r] ?? "").padEnd(widths[i], " ")).join(" "));
1566
- }
1567
- return rows;
1568
- }
1569
- function splashArt() {
1570
- const words = [wordArt("MELLOS"), wordArt("MAPPING")];
1571
- const width = Math.max(...words.flat().map((r) => r.length));
1572
- const centered = words.map((rows) => {
1573
- const own = Math.max(...rows.map((r) => r.length));
1574
- return rows.map((r) => " ".repeat(Math.floor((width - own) / 2)) + r);
1575
- });
1576
- return [...centered[0], "", ...centered[1]];
1549
+ function elapsedLabel(ms) {
1550
+ const s = Math.max(0, Math.floor(ms / 1e3));
1551
+ const m = Math.floor(s / 60);
1552
+ const h = Math.floor(m / 60);
1553
+ const two = (n) => String(n).padStart(2, "0");
1554
+ return h > 0 ? `${h}:${two(m % 60)}:${two(s % 60)}` : `${m}:${two(s % 60)}`;
1555
+ }
1556
+ function waitingInfo(s, width) {
1557
+ const w = Math.max(1, width);
1558
+ const clock = s.elapsedMs !== void 0 ? ` \xB7 waiting ${elapsedLabel(s.elapsedMs)}` : "";
1559
+ const lines = [
1560
+ `watching ${s.defaultFile}`,
1561
+ ` and ${join2(s.pagesDir, "*.json")}`,
1562
+ `polling every ${s.intervalMs} ms${clock}`
1563
+ ];
1564
+ for (const b of s.broken) lines.push(`! ${b}`);
1565
+ lines.push("the map appears at the first mmap_declare");
1566
+ return lines.map((l) => fitWidth(l, w));
1577
1567
  }
1578
1568
  var WAVE_INTERVAL = 18;
1579
1569
  var WAVE_LIFETIME = 64;
@@ -1624,39 +1614,41 @@ function waveAt(ripples, x, y) {
1624
1614
  function waveLevel(value) {
1625
1615
  return Math.max(-WAVE_LEVELS, Math.min(WAVE_LEVELS, Math.round(value * WAVE_GAIN)));
1626
1616
  }
1627
- function splashFrame(notice, frame, width, height, unicode, color) {
1628
- const art = splashArt();
1629
- const artWidth = Math.max(...art.map((r) => r.length));
1630
- if (width < artWidth + 2 || height < art.length + 2) return void 0;
1617
+ function splashFrame(notice, info, frame, width, height, unicode, color) {
1618
+ const fieldW = Math.min(width - 4, WATER_COLS_MAX);
1619
+ if (fieldW < 24 || height < WATER_ROWS + info.length + 3) return void 0;
1631
1620
  const mode = unicode ? "unicode" : "ascii";
1632
1621
  const shades = SPLASH_SHADES[mode];
1633
- const solid = shades[shades.length - 1];
1634
- const indent = " ".repeat(Math.floor((width - artWidth) / 2));
1635
- const ripples = liveRipples(frame, artWidth, art.length);
1636
- const inkOf = (x, y) => {
1637
- const level = waveLevel(waveAt(ripples, x, y));
1638
- return color ? `38;5;${WAVE_RAMP[WAVE_LEVELS + level]}` : shades[Math.abs(level)];
1639
- };
1640
- const paintRow = (row, y) => {
1641
- const cells = [...row].map((ch, x) => ch === "#" ? inkOf(x, y) : void 0);
1622
+ const indent = " ".repeat(Math.max(0, Math.floor((width - fieldW) / 2)));
1623
+ const ripples = liveRipples(frame, fieldW, WATER_ROWS);
1624
+ const paintRow = (y) => {
1625
+ const levels = Array.from({ length: fieldW }, (_, x) => waveLevel(waveAt(ripples, x, y)));
1642
1626
  let out = "";
1643
- for (let i = 0; i < cells.length; ) {
1644
- const cell = cells[i];
1627
+ for (let i = 0; i < fieldW; ) {
1628
+ const level = levels[i];
1645
1629
  let j = i;
1646
- while (j < cells.length && cells[j] === cell) j++;
1647
- if (cell === void 0) out += " ".repeat(j - i);
1648
- else out += color ? `\x1B[${cell}m${solid.repeat(j - i)}${RESET}` : cell.repeat(j - i);
1630
+ while (j < fieldW && levels[j] === level) j++;
1631
+ if (level === 0) out += " ".repeat(j - i);
1632
+ else {
1633
+ const ink = shades[Math.abs(level)].repeat(j - i);
1634
+ out += color ? `\x1B[38;5;${WAVE_RAMP[WAVE_LEVELS + level]}m${ink}${RESET}` : ink;
1635
+ }
1649
1636
  i = j;
1650
1637
  }
1651
1638
  return out;
1652
1639
  };
1640
+ const dim = (s) => color ? `\x1B[90m${s}${RESET}` : s;
1653
1641
  const spinner = SPINNER_FRAMES[mode];
1654
1642
  const status = fitWidth(`${spinner[frame % spinner.length]} ${notice}`, Math.max(1, width - 2));
1655
1643
  const statusIndent = " ".repeat(Math.max(0, Math.floor((width - displayWidth(status)) / 2)));
1644
+ const infoWidth = Math.max(0, ...info.map((l) => displayWidth(l)));
1645
+ const infoIndent = " ".repeat(Math.max(0, Math.floor((width - infoWidth) / 2)));
1656
1646
  const block = [
1657
- ...art.map((row, y) => row.trim() === "" ? "" : indent + paintRow(row, y)),
1647
+ ...Array.from({ length: WATER_ROWS }, (_, y) => indent + paintRow(y)),
1658
1648
  "",
1659
- statusIndent + (color ? `\x1B[90m${status}${RESET}` : status)
1649
+ statusIndent + dim(status),
1650
+ "",
1651
+ ...info.map((l) => infoIndent + dim(l))
1660
1652
  ];
1661
1653
  return [...Array.from({ length: Math.max(0, Math.floor((height - block.length) / 2)) }, () => ""), ...block];
1662
1654
  }
@@ -1685,8 +1677,10 @@ function main() {
1685
1677
  let lastFrame = "";
1686
1678
  let spinnerFrame = 0;
1687
1679
  let splashTick = 0;
1680
+ const startedAt = Date.now();
1681
+ const standbyNotice = "waiting for the first mmap_declare ...";
1688
1682
  let map;
1689
- let notice = `waiting for ${cfg.file} ...`;
1683
+ let notice = standbyNotice;
1690
1684
  let lastCols = process.stdout.columns ?? 0;
1691
1685
  let lastRows = process.stdout.rows ?? 0;
1692
1686
  let pageFiles = [cfg.file];
@@ -1755,7 +1749,7 @@ function main() {
1755
1749
  const entry = pageData.get(file);
1756
1750
  if (entry !== void 0 && entry.fresh) pageData.set(file, { ...entry, fresh: false });
1757
1751
  map = entry?.map;
1758
- notice = map === void 0 ? `waiting for ${file} ...` : "";
1752
+ notice = map !== void 0 ? "" : entry?.error ?? (file === cfg.file ? standbyNotice : `waiting for ${file} ...`);
1759
1753
  const top = topFiles();
1760
1754
  const tabIndex = top.indexOf(file);
1761
1755
  if (tabIndex >= 0) tabScroll = tabScrollFor(pageTabsOf(top), viewWidth(), cfg.unicode, tabScroll, tabIndex);
@@ -1813,7 +1807,17 @@ function main() {
1813
1807
  lastContent = { w: windowed.contentWidth, h: windowed.contentHeight };
1814
1808
  if (offsetX !== 0 || offsetY !== 0) panned = ` (+${offsetX},+${offsetY})`;
1815
1809
  } else {
1816
- body = (interactive ? splashFrame(notice, splashTick, viewW, viewH, cfg.unicode, cfg.color) : void 0) ?? [fitWidth(notice, viewW)];
1810
+ const info = waitingInfo(
1811
+ {
1812
+ defaultFile: cfg.file,
1813
+ pagesDir: join2(dirname2(cfg.file), PAGES_DIR_NAME),
1814
+ intervalMs: cfg.intervalMs,
1815
+ elapsedMs: interactive ? Date.now() - startedAt : void 0,
1816
+ broken: [...pageData.values()].flatMap((e) => e.map === void 0 && e.error !== void 0 ? [e.error] : [])
1817
+ },
1818
+ Math.max(1, viewW - 2)
1819
+ );
1820
+ body = (interactive ? splashFrame(notice, info, splashTick, viewW, viewH, cfg.unicode, cfg.color) : void 0) ?? [fitWidth(notice, viewW), "", ...info.map((l) => fitWidth(` ${l}`, viewW))];
1817
1821
  }
1818
1822
  if (notice !== "" && map !== void 0) {
1819
1823
  body[body.length - 1] = fitWidth(` ${notice}`, viewW);
@@ -1917,8 +1921,15 @@ function main() {
1917
1921
  flash = { text: `${cfg.unicode ? "\u229E " : ""}${title} updated`, until: Date.now() + 4e3 };
1918
1922
  }
1919
1923
  } else if (loaded.error.kind === "malformed-json") {
1924
+ pageData.set(file, {
1925
+ map: entry?.map,
1926
+ mtimeMs: entry?.mtimeMs ?? -1,
1927
+ fresh: entry?.fresh ?? false,
1928
+ error: describeStoreError(loaded.error)
1929
+ });
1930
+ if (file === activeFile && entry?.map === void 0) notice = describeStoreError(loaded.error);
1920
1931
  } else {
1921
- pageData.set(file, { map: entry?.map, mtimeMs, fresh: entry?.fresh ?? false });
1932
+ pageData.set(file, { map: entry?.map, mtimeMs, fresh: entry?.fresh ?? false, error: describeStoreError(loaded.error) });
1922
1933
  if (file === activeFile) notice = describeStoreError(loaded.error);
1923
1934
  }
1924
1935
  }
@@ -2143,6 +2154,7 @@ export {
2143
2154
  clampPanelRows,
2144
2155
  diveOrigin,
2145
2156
  dividerRow,
2157
+ elapsedLabel,
2146
2158
  fitWidth,
2147
2159
  launchedAsEntry,
2148
2160
  liveRipples,
@@ -2153,14 +2165,13 @@ export {
2153
2165
  pageTabRow,
2154
2166
  panelRowsFromDividerY,
2155
2167
  parseArgs,
2156
- splashArt,
2157
2168
  splashFrame,
2158
2169
  tabScrollFor,
2159
2170
  topLevelFiles,
2160
2171
  usableColumns,
2172
+ waitingInfo,
2161
2173
  waveAt,
2162
2174
  waveHash,
2163
2175
  waveLevel,
2164
- wordArt,
2165
2176
  wrapWidth
2166
2177
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mellos-mapping",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "mcpName": "io.github.GuangminJu/mellos-mapping",
5
5
  "description": "A live layered dependency map for bottom-up development — MCP server + terminal pane. Ghost the design first, then light nodes up from the bottom as they are built and verified.",
6
6
  "type": "module",