mellos-mapping 0.20.3 → 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.
Files changed (75) hide show
  1. package/README.md +130 -69
  2. package/README.zh-CN.md +126 -65
  3. package/dist/hook-session-start.mjs +28 -20
  4. package/dist/mmap.mjs +303 -171
  5. package/dist/preview.mjs +1457 -0
  6. package/dist/server.mjs +1984 -741
  7. package/dist/store-paths.mjs +69 -22
  8. package/dist/terminal-worker.mjs +3331 -0
  9. package/dist/watch.mjs +957 -571
  10. package/dist/web/TERMINAL-LICENSES.txt +70 -0
  11. package/dist/web/app.css +967 -0
  12. package/dist/web/app.js +1356 -0
  13. package/dist/web/index.html +9 -0
  14. package/dist/web/terminal.css +9 -0
  15. package/dist/web/terminal.html +7 -0
  16. package/dist/web/terminal.js +9293 -0
  17. package/dist/web/xterm.css +285 -0
  18. package/dist/web.mjs +5646 -0
  19. package/docs/codex.md +189 -0
  20. package/docs/map-api.md +148 -0
  21. package/lib/domain/context.d.ts +11 -0
  22. package/lib/domain/context.js +27 -0
  23. package/lib/domain/text.d.ts +9 -0
  24. package/lib/domain/text.js +54 -0
  25. package/lib/domain/types.d.ts +3 -0
  26. package/lib/preview/index.d.ts +3 -0
  27. package/lib/preview/index.js +3 -0
  28. package/lib/preview/markdown.d.ts +8 -0
  29. package/lib/preview/markdown.js +54 -0
  30. package/lib/preview/presentation.d.ts +6 -0
  31. package/lib/preview/presentation.js +14 -0
  32. package/lib/preview/publisher.d.ts +23 -0
  33. package/lib/preview/publisher.js +143 -0
  34. package/lib/preview/svg.d.ts +3 -0
  35. package/lib/preview/svg.js +74 -0
  36. package/lib/preview/text.d.ts +4 -0
  37. package/lib/preview/text.js +13 -0
  38. package/lib/render/canvas.d.ts +1 -1
  39. package/lib/render/canvas.js +4 -2
  40. package/lib/render/draw.js +9 -6
  41. package/lib/render/render.d.ts +6 -0
  42. package/lib/render/render.js +50 -18
  43. package/lib/render/width.js +3 -1
  44. package/lib/store/atomic.d.ts +18 -0
  45. package/lib/store/atomic.js +86 -0
  46. package/lib/store/channels.d.ts +40 -0
  47. package/lib/store/channels.js +135 -0
  48. package/lib/store/format.js +21 -4
  49. package/lib/store/json-text.d.ts +9 -0
  50. package/lib/store/json-text.js +16 -0
  51. package/lib/store/maps.d.ts +12 -0
  52. package/lib/store/maps.js +42 -0
  53. package/lib/store/migration.d.ts +12 -0
  54. package/lib/store/migration.js +46 -0
  55. package/lib/store/pages.d.ts +46 -0
  56. package/lib/store/pages.js +89 -0
  57. package/lib/store/policy.d.ts +74 -0
  58. package/lib/store/policy.js +144 -0
  59. package/lib/store/project.d.ts +2 -0
  60. package/lib/store/project.js +29 -0
  61. package/lib/store/store.d.ts +15 -257
  62. package/lib/store/store.js +16 -695
  63. package/lib/store/transaction.d.ts +12 -0
  64. package/lib/store/transaction.js +91 -0
  65. package/lib/store/viewers.d.ts +81 -0
  66. package/lib/store/viewers.js +186 -0
  67. package/package.json +27 -5
  68. package/scripts/codex-cli.mjs +42 -0
  69. package/scripts/codex-register.mjs +27 -94
  70. package/scripts/mmap.mjs +27 -17
  71. package/scripts/open-pane.mjs +37 -18
  72. package/scripts/pane-core.mjs +57 -220
  73. package/scripts/terminal-session.mjs +137 -0
  74. package/scripts/tmux-session.mjs +90 -0
  75. package/scripts/watcher-command.mjs +16 -0
package/docs/codex.md ADDED
@@ -0,0 +1,189 @@
1
+ # Mellos Mapping · ChatGPT App (Codex mode)
2
+
3
+ This package targets Codex mode in the ChatGPT desktop app, also called Codex App.
4
+ The two editions share the map model, store and renderers. The desktop edition
5
+ has its own skill and includes no Claude SessionStart hook or MCP configuration.
6
+
7
+ ## Install and update
8
+
9
+ Requires Node.js 18+ and a Codex CLI with plugin commands (tested with 0.153.4).
10
+ From the `chatgpt-app` branch run `node install.mjs`; from `main` run
11
+ `node install.mjs chatgpt-app`. Committed bundles need no build or npm dependencies.
12
+
13
+ The installer checks file hashes and a real MCP handshake, copies the runtime to
14
+ `~/.mellos/installations/chatgpt-app/`, registers the `mellos-mapping-codex`
15
+ marketplace, installs the skill, and registers the eight MCP tools at user scope.
16
+ The runtime uses an absolute Node executable and leaves its working directory
17
+ unset so each conversation writes to its own project. The clone can be deleted
18
+ or moved after installation. Other plugins, maps and mapping policies are preserved.
19
+
20
+ Start a new conversation after installing or updating. Ask:
21
+ “用梅勒斯地图制定计划,并在当前对话右侧终端展示,持续更新验证进度。”
22
+ Run the installer again from a newer release to update. In an edition clone,
23
+ `node install.mjs --check` checks prerequisites, file integrity and MCP without
24
+ changing host configuration. Existing watchers retain old code until restarted.
25
+
26
+ If migrating from the earlier personal-marketplace version, remove its plugin
27
+ with Codex first to avoid two copies of the skill. The registration helper
28
+ `scripts/codex-register.mjs` remains available to maintain those existing installs.
29
+ It replaces only the named MCP entry; custom transport options may need reapplying.
30
+
31
+ To uninstall the release edition:
32
+
33
+ ```sh
34
+ codex plugin remove mellos-mapping@mellos-mapping-codex
35
+ codex mcp remove mellos-mapping
36
+ codex plugin marketplace remove mellos-mapping-codex
37
+ ```
38
+
39
+ Project maps and mapping preferences remain. GitHub distribution does not itself
40
+ publish the package into OpenAI's public plugin directory.
41
+
42
+ ## Automatic web terminal
43
+
44
+ For saved-map discovery, structured CRUD, checkpoints and concurrent updates,
45
+ see [the persistent-map API guide](map-api.md). Start by reading existing pages;
46
+ a new conversation does not require a new map. Restart older MCP/native watchers
47
+ before writing maps with format-2 context or source references. Reopening a Web
48
+ surface replaces services without format-2 support and returns a new URL.
49
+
50
+ Call `mmap_open {surface: "web-terminal", page: "<slug>"}` and pass the returned
51
+ `hostOpen` object to `open_in_codex`. It uses `placement: "right"` and a browser
52
+ target in the current conversation. The local service starts mmap when the
53
+ browser connects; the user does not paste a startup command. No Computer Use
54
+ or desktop keyboard automation is involved. With an older MCP schema:
55
+
56
+ ```sh
57
+ node "<plugin root>/dist/web.mjs" "<project>" --terminal --page <slug>
58
+ ```
59
+
60
+ Open the printed JSON `url`. The page provides its own font-size selector,
61
+ help, reconnection and a link to the same map in graphical SVG mode. Font
62
+ changes remeasure terminal cells rather than stretching an image, and do not
63
+ change the host's global terminal font. The terminal uses xterm.js cell rendering;
64
+ the graphical mode remains native SVG and supports SVG export.
65
+
66
+ Both modes use the same project maps. Terminal input/output goes over a local
67
+ WebSocket to a dedicated mmap worker. Each connected browser owns its own
68
+ page/zoom/selection state and renderer; there is no shared shell. Closing the
69
+ tab or stopping the service ends its worker. `q` closes the map until Reconnect.
70
+ Network interruptions get three retry attempts; reopen via MCP if the service
71
+ has exited. A reconnect starts a fresh view on the current page.
72
+
73
+ All assets and the worker ship prebuilt. No node-pty, native compiler, npm install,
74
+ external terminal, remote hosting or account is required. The service accepts
75
+ only bounded map input/resize messages from its own origin and secret URL,
76
+ limits sessions to eight, and keeps one output chunk in flight until the browser
77
+ renders it. Slow clients cannot accumulate unlimited rendered map frames.
78
+
79
+ An already-running service from before this feature is stopped and restarted
80
+ on a web-terminal request. That restart disconnects its existing graphical tabs;
81
+ reopen them with the new URL. For later runtime updates, use `--stop` first.
82
+
83
+ ## Optional native desktop terminal
84
+
85
+ Call `mmap_open {surface: "codex-terminal", page: "<slug>"}`. This prepares the
86
+ actual Node executable and absolute watcher/map paths, with PowerShell and POSIX
87
+ commands. It starts no program or window. Use the host's `open_in_codex` tool with
88
+ `placement: "right"` and `target: {type: "terminal"}` in the current conversation.
89
+
90
+ If a supported host tool executes commands in this user terminal, use it.
91
+ If only opening/reading tools are exposed, paste the returned command once.
92
+ That interface cannot provide fully automatic first startup. Agent command PTYs
93
+ are separate; stringifying their numeric session IDs cannot attach them to the
94
+ user terminal. No shell profile modification or external-window workaround is used.
95
+
96
+ Check the map title and controls with `read_thread_terminal` after startup.
97
+ `queued` means a pending panel request; a shell prompt means the map has not
98
+ started. A viewer heartbeat could come from another window. Reuse an existing
99
+ viewer and preserve a manually pinned page. Map writes refresh the watcher.
100
+
101
+ The equivalent manual command, with paths appropriate to the current install:
102
+
103
+ ```sh
104
+ node "<plugin root>/dist/watch.mjs" --file "<project>/.mellos/map.json" --page <slug>
105
+ ```
106
+
107
+ Wheel / `+` / `-` changes semantic zoom; drag pans; click pins details;
108
+ double-click enters a submap; `0` resets and `q` exits. `--no-mouse` leaves
109
+ mouse events to the host. Font size follows the app's code/terminal settings.
110
+ After an update, press `q` and rerun the command to load the new watcher.
111
+
112
+ The native watcher uses an alternate screen and reasserts mouse reporting every
113
+ second to recover panel remounts. It outputs changed rows, coalesces mouse-motion
114
+ paints within 16 ms and keeps only the latest pending frame under backpressure.
115
+ A full snapshot every second repairs truncated terminal replay. Viewer-presence
116
+ writes yield on locked files; authoritative map saves retain atomic retries.
117
+ Hover changes border and dependency colors without toggling font weight.
118
+
119
+ Plain `mmap_open {surface: "terminal"}` is the external Windows Terminal launcher
120
+ for Claude Code and Codex CLI. It does not open the desktop integrated terminal.
121
+
122
+ ## Desktop right-side map
123
+
124
+ The existing document surface remains available. Call
125
+ `mmap_open {surface: "markdown", page: "<slug>"}`, then open the returned absolute
126
+ Markdown path with the host's file target and `placement: "right"`.
127
+
128
+ The runtime generates `.mellos/previews/page-<slug>.md` (or `map.md` for the
129
+ default page), `index.md`, and content-hashed SVG images. JSON is authoritative.
130
+ The document includes dependencies, status, details, evidence and child-page links.
131
+ Vectors stay sharp when enlarged. Image nodes do not support dragging, hovering,
132
+ animated spinners or double-clicking. Use the document links to visit child maps.
133
+
134
+ Opening this surface enables automatic exports after successful MCP map writes,
135
+ including after server restart. Direct JSON edits or older clients need regeneration:
136
+
137
+ ```sh
138
+ node "<plugin root>/dist/preview.mjs" "<project>" --page <slug>
139
+ ```
140
+
141
+ File generation does not prove visibility or automatic host refresh. Regenerate
142
+ and reopen if stale. `preview: STALE` means the map was saved but exporting failed;
143
+ fix the export without repeating the mutation. Previous SVG images remain for
144
+ already-open documents. `.mellos/previews/` is disposable; removing it disables
145
+ auto-export until the next open. Remove a stale `.publish-lock` only after its
146
+ old exporter has stopped, then regenerate.
147
+
148
+ ## Optional interactive web viewer
149
+
150
+ Call `mmap_open {surface: "web", page: "<slug>"}` and open the returned local URL
151
+ with the host's browser target on the right. Or run:
152
+
153
+ ```sh
154
+ node "<plugin root>/dist/web.mjs" "<project>" --page <slug>
155
+ ```
156
+
157
+ The viewer supports vector pan/zoom, hover and pinned details, dependency
158
+ highlighting, search, status filters, page switching, submaps, breadcrumbs,
159
+ semantic group aggregation below 55%, lanes/sequences, SVG export, themes and
160
+ confirmed page deletion. Dragging pans the canvas; nodes retain automatic layout.
161
+ Narrow panels place details beneath a draggable divider. Map edits use MCP tools.
162
+
163
+ Cards, labels and connections are native SVG. Zoom changes the SVG `viewBox`.
164
+ The local service binds to `127.0.0.1`, uses an unpredictable per-launch URL,
165
+ checks Host/Origin and serves bundled assets and project maps only. It polls every
166
+ 800 ms. Broken pages show errors without hiding other pages; connection failures
167
+ label the last available data. Manual page choice pins that page. Existing
168
+ Markdown exports and terminal preferences remain available.
169
+
170
+ The detached service survives its MCP parent and stops after five minutes without
171
+ requests. Stop it before reopening after a runtime update:
172
+
173
+ ```sh
174
+ node "<plugin root>/dist/web.mjs" "<project>" --stop
175
+ ```
176
+
177
+ A queued browser-open result does not prove that the viewer is visible.
178
+
179
+ ## Developer verification
180
+
181
+ Use Node.js 22.12+ for source development. Run `npm ci` then `npm run verify`.
182
+ `npm run package:codex`
183
+ produces the flat plugin; `npm run package:release` produces both installable host
184
+ editions. Runtime tests use actual stdio in separate temporary projects; installation
185
+ checks must use isolated host configuration. No developer path belongs in a release.
186
+
187
+ The [official plugin documentation](https://developers.openai.com/plugins/build/plugins)
188
+ describes local marketplaces and Git distribution. See the
189
+ [plugin usage guide](https://learn.chatgpt.com/docs/plugins) for host installation behavior.
@@ -0,0 +1,148 @@
1
+ # Persistent maps and the MCP API
2
+
3
+ Maps survive process restarts and new conversations. Begin with `mmap_read
4
+ {resource: "pages"}`, match the effort to a saved page, then read only relevant
5
+ records. A conversation is not a page identity. `mmap_view` remains the visual
6
+ representation; `mmap_read` is the editable data contract.
7
+
8
+ ## Read
9
+
10
+ `mmap_read` returns JSON text and the same object in `structuredContent`:
11
+ `resource`, `project`, `page`, `revision`, `total`, `items`, `nextCursor`.
12
+ The default page is represented by a null `page` and the record ID `_default`.
13
+ Named page slugs and resource IDs remain stable; change display titles/labels
14
+ instead of renaming identity. Edge IDs are `from->to`.
15
+
16
+ | resource | Records |
17
+ | --- | --- |
18
+ | pages (default) | Page IDs, titles, kind, counts, context and each page's revision; a broken page is listed with its error |
19
+ | map | One page's metadata/context/counts; it does not embed the entire graph |
20
+ | nodes | Node IDs, labels, status and membership; request detail, evidence and sources through fields |
21
+ | edges | ID, from, to and optional label |
22
+ | layers / groups / lanes | The stored editable records |
23
+ | neighborhood | Related nodes selected by id/ids, direction (dependencies/consumers/both) and depth (0–4) |
24
+ | changes | Source-file verification state for selected nodes and up to 100 affected consumer IDs |
25
+
26
+ Use `id` for exact lookup (missing is `NOT_FOUND`), `ids` for a selection,
27
+ `query` for text matching, and status/layer/group/lane for node filtering.
28
+ `fields` projects records while always retaining identity and edge endpoints.
29
+ The default limit is 30, maximum 100. Repeat the same query with nextCursor;
30
+ if the graph changes, the cursor returns `CONFLICT` instead of skipping records.
31
+ An absent page is `NOT_FOUND`; an empty list is successful. The legacy view's
32
+ empty-map behavior is retained for compatibility.
33
+
34
+ Use a page or record response's revision for that page's next write. The top-level
35
+ revision of a pages listing describes the listing, not any individual page.
36
+ `ifRevision` can avoid resending unchanged graph data. It does not suppress
37
+ source checks: files may change without a graph edit. Pagination checks graph
38
+ revisions, not a filesystem snapshot of source files changing during the query.
39
+
40
+ ```json
41
+ {"resource":"nodes","page":"payments","status":"in-progress","fields":["label","detail","evidence"],"limit":20}
42
+ ```
43
+
44
+ ## Create, update and delete
45
+
46
+ The existing tools and fields remain supported. New writes return a revision
47
+ and structured outcome in addition to the existing summary.
48
+
49
+ - `mmap_declare`: create pages, layers, groups, lanes, nodes and edges. Existing
50
+ IDs are refused. Pass expectedRevision: "absent" to require a new page.
51
+ - `mmap_update`: existing node fields, layer names/ranks, group labels/layers,
52
+ lane labels, complete laneOrder, map title/kind/context and edge patches.
53
+ An edge patch identifies from/to, with optional label, newFrom and newTo.
54
+ A null label clears it. A laneOrder contains every lane exactly once.
55
+ - `mmap_remove`: existing per-resource deletion and cascades. Use deletePage:
56
+ true to delete the targeted page, including the default page. Inbound submap
57
+ references are refused unless references: "keep" is explicit. Unlink nodes
58
+ first when the references should disappear. This form cannot include edits
59
+ or legacy pages batches.
60
+
61
+ Optional fields are unchanged when omitted and removed when set to null where
62
+ the schema permits. context and sources are replaced as whole values. Moving
63
+ a group does not silently move members: include their intended node moves in
64
+ the same update or transaction. Normal single-field changes keep the old graph
65
+ constraints; use a batch for changes that require a coordinated final graph.
66
+
67
+ Every graph writer in the current MCP, HTTP viewer and watcher uses a cooperative
68
+ cross-process project lock. MCP expectedRevision is compared inside that lock
69
+ before loading the proposed changes into the saved map. `CONFLICT` requires a
70
+ fresh read and reconsideration of the edit. `BUSY` means another transaction is
71
+ active; retry after it completes. There is no background lock polling. Locks are
72
+ released on normal completion/error; a confirmed dead PID can be recovered.
73
+ An incomplete owner file after an abrupt crash is refused for manual inspection,
74
+ not guessed stale from its age.
75
+
76
+ Low-level library saveMapFile, hand edits, and older running processes do not
77
+ participate in this contract. Restart MCP processes and native watchers after
78
+ upgrading. Opening a Web surface upgrades services that lack format-2 support;
79
+ existing tabs must reconnect with the newly returned URL. Atomic file
80
+ replacement alone does not make a caller's stale read/modify/write safe.
81
+
82
+ Legacy `mmap_remove {pages:[...]}` remains a separately documented batch of file
83
+ deletions, with explicit partial success and historical reference behavior. It
84
+ does not accept expectedRevision; use per-page deletePage for checked deletion.
85
+ It is not a multi-page transaction.
86
+
87
+ ## One-page mixed transactions
88
+
89
+ `mmap_batch` accepts page, expectedRevision and 1–100 ordered operations. Each
90
+ operation is `{op: "declare" | "update" | "remove", data: {...}}`, with the
91
+ same per-page fields as that tool. Per-operation page, expectedRevision and
92
+ page deletion are excluded. Changes are drafted, the final graph is validated,
93
+ and the page is saved once; a refusal preserves the original file bytes.
94
+
95
+ ```json
96
+ {
97
+ "page":"payments",
98
+ "expectedRevision":"<revision returned by mmap_read>",
99
+ "operations":[
100
+ {"op":"remove","data":{"edges":[{"from":"api","to":"old-store"}]}},
101
+ {"op":"declare","data":{"nodes":[{"id":"new-store","label":"Store","layer":"base"}]}},
102
+ {"op":"declare","data":{"edges":[{"from":"api","to":"new-store"}]}},
103
+ {"op":"update","data":{"context":{"summary":"Payment services","next":"Verify the new store contract"}}}
104
+ ]
105
+ }
106
+ ```
107
+
108
+ Machine-readable error codes include NOT_FOUND, INVALID_STORE, REFUSED,
109
+ CONFLICT, BUSY, INVALID_CURSOR, INVALID_ARGUMENT, REFERENCED and SAVE_FAILED.
110
+ Malformed schema inputs remain MCP invalid-argument errors. Read calls do not
111
+ open panels or create page files.
112
+
113
+ ## Checkpoints and source changes
114
+
115
+ Map context has optional summary and next fields (up to 2,000 characters each).
116
+ Save concise decisions and next actions, rather than copying the conversation.
117
+ Nodes can hold up to 100 sources: `{path: "src/store.ts", sha256: "..."}`.
118
+ Paths are project-relative, forward-slash paths with no traversal. SHA256 is
119
+ optional so sources can be linked before verification.
120
+
121
+ `mmap_read {resource:"changes", page, id}` hashes only the selected node's
122
+ listed files, reports currentSha256 and unchanged/changed/unknown node state,
123
+ and identifies affected consumers. Missing files count as changed. Missing
124
+ baselines, inaccessible files, files over 8 MiB and paths resolving outside
125
+ the project remain unverified. There is no whole-repository source scan and
126
+ no automatic status change. After verification, copy the current hashes into
127
+ sources[].sha256 with a revision-checked update.
128
+
129
+ Classic maps continue to serialize as format 1. Maps containing context or
130
+ source references serialize as format 2; this runtime reads both. Older runtimes
131
+ must refuse format 2 instead of silently dropping its new fields. Removing all
132
+ extension fields allows serialization as format 1 again. Both formats preserve
133
+ the same graph, statuses and existing evidence.
134
+
135
+ ## Project identity
136
+
137
+ Without an explicit MELLOS_MAPPING_CWD or CLAUDE_PROJECT_DIR, the server walks
138
+ up from cwd to the nearest existing map store or Git root. An explicit override
139
+ stays explicit. The mmap command and watcher use the same resolver. Nested
140
+ repositories and worktrees are boundaries; separate branches are not silently
141
+ redirected to a shared writable graph. Carry maps through Git or deliberate
142
+ workspace setup when a new worktree needs them, and recheck source baselines.
143
+
144
+ Codex CLI and App skills both start by reading existing pages. MCP initialization
145
+ also advertises the restore workflow, including for MCP-only installations.
146
+ Viewer selection follows host capabilities; the CLI does not try to use a
147
+ desktop panel tool. After context compaction, repeat the lightweight page/context
148
+ read, then load only the affected resources.
@@ -0,0 +1,11 @@
1
+ /** Optional portable provenance; no host paths or I/O in the map format. */
2
+ export interface SourceRef {
3
+ readonly path: string;
4
+ readonly sha256?: string | undefined;
5
+ }
6
+ export interface MapContext {
7
+ readonly summary?: string | undefined;
8
+ readonly next?: string | undefined;
9
+ }
10
+ export declare function sourceError(raw: unknown): string | undefined;
11
+ export declare function contextError(raw: unknown): string | undefined;
@@ -0,0 +1,27 @@
1
+ export function sourceError(raw) {
2
+ if (!Array.isArray(raw) || raw.length > 100)
3
+ return 'sources must be an array of at most 100 file references';
4
+ for (const item of raw) {
5
+ if (!item || typeof item !== 'object' || Array.isArray(item))
6
+ return 'source must be an object';
7
+ const s = item;
8
+ if (Object.keys(s).some(k => k !== 'path' && k !== 'sha256'))
9
+ return 'unknown source field';
10
+ 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 === '..'))
11
+ return 'source path must be relative to the project, with forward slashes and no traversal';
12
+ if (s.sha256 !== undefined && (typeof s.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(s.sha256)))
13
+ return 'source sha256 must be a lowercase SHA256 hash';
14
+ }
15
+ return undefined;
16
+ }
17
+ export function contextError(raw) {
18
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
19
+ return 'context must be an object';
20
+ for (const [key, value] of Object.entries(raw)) {
21
+ if (key !== 'summary' && key !== 'next')
22
+ return 'unknown context field';
23
+ if (typeof value !== 'string' || value.length > 2000 || /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/.test(value))
24
+ return 'context fields must be text of at most 2000 characters';
25
+ }
26
+ return undefined;
27
+ }
@@ -0,0 +1,9 @@
1
+ /** Text remains data at every input and display boundary. No host dependencies. */
2
+ import type { MellosMap } from './types.js';
3
+ export declare const NO_CONTROLS: RegExp;
4
+ export declare const NO_CONTROLS_TEXT = "one line of text; control characters (ESC, newline, tab) are not allowed";
5
+ export declare const NO_CONTROLS_BUT_BREAKS: RegExp;
6
+ export declare const NO_CONTROLS_BUT_BREAKS_TEXT = "text with optional newlines (\\n) and tabs; other control characters (ESC, BEL, CR) are not allowed";
7
+ export declare function mapTextError(map: MellosMap): string | undefined;
8
+ /** Defensive rendering for maps constructed directly by library consumers. */
9
+ export declare function terminalText(text: string, multiline?: boolean): string;
@@ -0,0 +1,54 @@
1
+ import { contextError, sourceError } from './context.js';
2
+ // Explicit ranges also work in the JSON Schema published by the MCP adapter.
3
+ export const NO_CONTROLS = /^[^\u0000-\u001f\u007f-\u009f]*$/;
4
+ export const NO_CONTROLS_TEXT = 'one line of text; control characters (ESC, newline, tab) are not allowed';
5
+ export const NO_CONTROLS_BUT_BREAKS = /^[^\u0000-\u0008\u000b-\u001f\u007f-\u009f]*$/;
6
+ export const NO_CONTROLS_BUT_BREAKS_TEXT = 'text with optional newlines (\\n) and tabs; other control characters (ESC, BEL, CR) are not allowed';
7
+ export function mapTextError(map) {
8
+ if (map.context !== undefined) {
9
+ const error = contextError(map.context);
10
+ if (error)
11
+ return error;
12
+ }
13
+ const check = (field, value, multiline = false) => value === undefined || (multiline ? NO_CONTROLS_BUT_BREAKS : NO_CONTROLS).test(value)
14
+ ? undefined : `${field}: ${multiline ? NO_CONTROLS_BUT_BREAKS_TEXT : NO_CONTROLS_TEXT}`;
15
+ let error = check('title', map.title);
16
+ if (error)
17
+ return error;
18
+ for (const [i, layer] of map.layers.entries()) {
19
+ error = check(`layers[${i}].name`, layer.name);
20
+ if (error)
21
+ return error;
22
+ }
23
+ for (const name of ['lanes', 'groups']) {
24
+ for (const [i, item] of map[name].entries()) {
25
+ error = check(`${name}[${i}].label`, item.label);
26
+ if (error)
27
+ return error;
28
+ }
29
+ }
30
+ for (const [i, node] of map.nodes.entries()) {
31
+ if (node.sources !== undefined) {
32
+ const error = sourceError(node.sources);
33
+ if (error)
34
+ return `nodes[${i}]: ${error}`;
35
+ }
36
+ for (const name of ['label', 'evidence', 'detail']) {
37
+ // Version-1 files and document exports also support multiline evidence.
38
+ // MCP keeps its narrower one-line evidence input for concise updates.
39
+ error = check(`nodes[${i}].${name}`, node[name], name !== 'label');
40
+ if (error)
41
+ return error;
42
+ }
43
+ }
44
+ for (const [i, edge] of map.edges.entries()) {
45
+ error = check(`edges[${i}].label`, edge.label);
46
+ if (error)
47
+ return error;
48
+ }
49
+ return undefined;
50
+ }
51
+ /** Defensive rendering for maps constructed directly by library consumers. */
52
+ export function terminalText(text, multiline = false) {
53
+ return text.replace(multiline ? /[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/g : /[\u0000-\u001f\u007f-\u009f]/g, '?');
54
+ }
@@ -171,7 +171,9 @@ export interface MapLane {
171
171
  readonly label: string;
172
172
  }
173
173
  /** A unit of work living in exactly one band. */
174
+ export type { SourceRef, MapContext } from './context.js';
174
175
  export interface MapNode {
176
+ readonly sources?: readonly import('./context.js').SourceRef[];
175
177
  readonly id: NodeId;
176
178
  readonly label: string;
177
179
  readonly layer: LayerId;
@@ -198,6 +200,7 @@ export interface DepEdge {
198
200
  }
199
201
  /** The whole map. A plain immutable value — operations return new maps. */
200
202
  export interface MellosMap {
203
+ readonly context?: import('./context.js').MapContext;
201
204
  readonly title?: string;
202
205
  /** Presentation intent; absent means 'dev' (the progress ledger). */
203
206
  readonly kind?: MapKind;
@@ -0,0 +1,3 @@
1
+ /** Pure presentation API; persistence and host integration are separate adapters. */
2
+ export { renderMapSvg } from './svg.js';
3
+ export { renderMapMarkdown, renderPreviewIndex, type PreviewPage } from './markdown.js';
@@ -0,0 +1,3 @@
1
+ /** Pure presentation API; persistence and host integration are separate adapters. */
2
+ export { renderMapSvg } from './svg.js';
3
+ export { renderMapMarkdown, renderPreviewIndex } from './markdown.js';
@@ -0,0 +1,8 @@
1
+ /** Pure document projection. JSON remains the sole editable map source. */
2
+ import type { MellosMap } from '../domain/types.js';
3
+ export interface PreviewPage {
4
+ readonly page: string | undefined;
5
+ readonly map: MellosMap;
6
+ }
7
+ export declare function renderMapMarkdown(map: MellosMap, image: string, pages: readonly PreviewPage[]): string;
8
+ export declare function renderPreviewIndex(pages: readonly PreviewPage[]): string;
@@ -0,0 +1,54 @@
1
+ import { isNeutralKind } from '../semantics/semantics.js';
2
+ import { documentName, isVerified, statusText } from './presentation.js';
3
+ import { cell, markdown as md } from './text.js';
4
+ export function renderMapMarkdown(map, image, pages) {
5
+ const neutral = isNeutralKind(map);
6
+ const rows = [`# ${md(map.title ?? '梅勒斯地图')}`, '', '[所有地图](index.md)', '',
7
+ '> 自动生成的地图预览;修改地图数据后重新生成。', ''];
8
+ if (!neutral) {
9
+ const active = map.nodes.filter(n => n.status === 'in-progress');
10
+ rows.push(`**当前:** ${active.length ? active.map(n => md(n.label)).join('、') : '暂无进行中的模块'}`, '', `已验证 **${map.nodes.filter(isVerified).length} / ${map.nodes.length}** | 回归 **${map.nodes.filter(n => n.status === 'regressed').length}**`, '');
11
+ }
12
+ rows.push('## 分层依赖', '', `![分层依赖地图](${image})`, '');
13
+ if (!neutral)
14
+ rows.push('· 待开发 ⠿ 开发中 ■ 已验证 ✗ 出现回归 □ 完成但缺少证据', '');
15
+ rows.push(map.kind === 'sequence' ? '时间从上向下推进;箭头保留地图中的依赖方向。' : '箭头由使用方指向它依赖的模块;基础层位于下方。', '', '## 模块详情', '');
16
+ const nodes = new Map(map.nodes.map(n => [n.id, n]));
17
+ const layers = [...map.layers].sort((a, b) => map.kind === 'sequence' ? a.rank - b.rank : b.rank - a.rank);
18
+ for (const layer of layers) {
19
+ rows.push(`### ${md(layer.name)}`, '');
20
+ const members = map.nodes.filter(n => n.layer === layer.id);
21
+ if (!members.length)
22
+ rows.push('尚未声明模块。', '');
23
+ for (const node of members) {
24
+ rows.push(`#### ${md(node.label)}${neutral ? '' : ` ${statusText(node)}`}`, '');
25
+ if (node.detail !== undefined)
26
+ rows.push(md(node.detail), '');
27
+ const used = map.edges.filter(e => e.from === node.id);
28
+ rows.push(`**依赖:** ${used.length ? used.map(e => `${md(nodes.get(e.to).label)}${e.label !== undefined ? `(${md(e.label)})` : ''}`).join('、') : '无'}`, '');
29
+ const meta = [node.group === undefined ? undefined : map.groups.find(g => g.id === node.group)?.label,
30
+ node.lane === undefined ? undefined : map.lanes.find(l => l.id === node.lane)?.label,
31
+ node.kind].filter((v) => v !== undefined);
32
+ if (meta.length)
33
+ rows.push(`**归属 / 类型:** ${meta.map(md).join(' · ')}`, '');
34
+ if (node.evidence !== undefined)
35
+ rows.push(`**验证记录:** ${md(node.evidence)}`, '');
36
+ if (node.submap !== undefined)
37
+ rows.push(pages.some(p => p.page === node.submap)
38
+ ? `[打开子图:${md(node.submap)}](${documentName(node.submap)})`
39
+ : `子图尚未创建:${md(node.submap)}`, '');
40
+ }
41
+ }
42
+ if (!neutral) {
43
+ rows.push('## 验证记录', '', '| 模块 | 状态 | 最近证据 |', '| --- | --- | --- |');
44
+ for (const node of map.nodes)
45
+ rows.push(`| ${cell(node.label)} | ${statusText(node)} | ${node.evidence === undefined ? '尚未记录' : cell(node.evidence)} |`);
46
+ rows.push('');
47
+ }
48
+ rows.push('---', '', '静态文档:更新时重新生成地图图片与文字。图中节点不支持拖拽、悬停展开或动画。', '');
49
+ return rows.join('\n');
50
+ }
51
+ export function renderPreviewIndex(pages) {
52
+ return ['# 梅勒斯地图 · 页面目录', '', ...pages.map(p => `- [${md(p.map.title ?? p.page ?? '默认地图')}](${documentName(p.page)})`), '',
53
+ '地图预览由项目内的地图数据生成。', ''].join('\n');
54
+ }
@@ -0,0 +1,6 @@
1
+ /** Preview vocabulary; shares status glyphs with every existing map surface. */
2
+ import type { MapNode } from '../domain/types.js';
3
+ export declare function isVerified(node: MapNode): boolean;
4
+ export declare function statusText(node: MapNode): string;
5
+ /** Default and named pages never collide, including a named page called map. */
6
+ export declare function documentName(page: string | undefined): string;
@@ -0,0 +1,14 @@
1
+ import { statusGlyph, unverifiedDoneGlyph } from '../semantics/semantics.js';
2
+ const LABELS = { planned: '待开发', 'in-progress': '开发中', done: '已验证', regressed: '出现回归' };
3
+ export function isVerified(node) {
4
+ return node.status === 'done' && node.evidence !== undefined;
5
+ }
6
+ export function statusText(node) {
7
+ return node.status === 'done' && !isVerified(node)
8
+ ? `${unverifiedDoneGlyph(true)} 完成但缺少证据`
9
+ : `${statusGlyph(node.status, true)} ${LABELS[node.status]}`;
10
+ }
11
+ /** Default and named pages never collide, including a named page called map. */
12
+ export function documentName(page) {
13
+ return page === undefined ? 'map.md' : `page-${page}.md`;
14
+ }
@@ -0,0 +1,23 @@
1
+ import { type Result } from '../domain/types.js';
2
+ import { type PageId } from '../store/store.js';
3
+ export declare const PREVIEW_DIR_NAME = "previews";
4
+ export interface PublishedPreview {
5
+ readonly path: string;
6
+ readonly index: string;
7
+ readonly pages: number;
8
+ }
9
+ export declare function previewDirectory(defaultFile: string): string;
10
+ export declare function previewFile(defaultFile: string, page?: PageId): string;
11
+ /**
12
+ * Activation is project-local and survives MCP restarts. Each publisher reads
13
+ * it again, so concurrent clients cooperate without a shared server process.
14
+ * SVG names are content-addressed: a document always points at a complete
15
+ * image and file-preview caches cannot reuse the previous state's picture.
16
+ * Old images stay available to open documents; the preview directory is a
17
+ * disposable cache, never source history.
18
+ */
19
+ export declare function createPreviewPublisher(defaultFile: string): {
20
+ enabled: () => boolean;
21
+ refresh: (page?: PageId) => Result<PublishedPreview, string>;
22
+ activate: (page?: PageId) => Result<PublishedPreview, string>;
23
+ };