storymapper 0.1.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 (61) hide show
  1. package/ARCHITECTURE.md +334 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +239 -0
  5. package/frontend/css/storymap.css +4637 -0
  6. package/frontend/js/adapters.js +423 -0
  7. package/frontend/js/card-animate.js +384 -0
  8. package/frontend/js/core/graph.js +825 -0
  9. package/frontend/js/core.js +3908 -0
  10. package/frontend/js/dialog-ticket-import.js +506 -0
  11. package/frontend/js/dnd.js +322 -0
  12. package/frontend/js/filter.js +215 -0
  13. package/frontend/js/main.js +2499 -0
  14. package/frontend/js/project-io.js +109 -0
  15. package/frontend/js/query-autocomplete.js +196 -0
  16. package/frontend/js/query-engine.js +339 -0
  17. package/frontend/js/query.js +280 -0
  18. package/frontend/js/renderer-card.js +639 -0
  19. package/frontend/js/renderer-dependencies.js +974 -0
  20. package/frontend/js/renderer-kanban.js +505 -0
  21. package/frontend/js/renderer-storymap.js +2530 -0
  22. package/frontend/js/renderer-ticket-editor.js +455 -0
  23. package/frontend/js/renderer-ticket-modal.js +758 -0
  24. package/frontend/js/save-pipeline.js +170 -0
  25. package/frontend/js/smartbar-autocomplete.js +162 -0
  26. package/frontend/js/store.js +197 -0
  27. package/frontend/js/ticket-editor-boot.js +24 -0
  28. package/frontend/js/ticket-form.js +2095 -0
  29. package/frontend/js/ticket-import.js +477 -0
  30. package/frontend/js/ui-shell.js +441 -0
  31. package/frontend/js/view-process-steps.js +233 -0
  32. package/frontend/js/view-requirements.js +361 -0
  33. package/frontend/js/view-settings.js +1864 -0
  34. package/frontend/js/view-table.js +659 -0
  35. package/frontend/js/wheel-pan.js +65 -0
  36. package/frontend/storymap.html +87 -0
  37. package/frontend/ticket-editor.html +29 -0
  38. package/package.json +76 -0
  39. package/server/bus.js +16 -0
  40. package/server/core/graph.js +10 -0
  41. package/server/core.js +10 -0
  42. package/server/identity.js +134 -0
  43. package/server/index.js +283 -0
  44. package/server/ingest.js +212 -0
  45. package/server/mcp.js +2510 -0
  46. package/server/server.js +1599 -0
  47. package/server/slice.js +103 -0
  48. package/server/storage.js +571 -0
  49. package/server/validation.js +225 -0
  50. package/shared/core/graph.js +825 -0
  51. package/shared/core.js +3908 -0
  52. package/shared/project-io.js +109 -0
  53. package/shared/query-autocomplete.js +196 -0
  54. package/shared/query-engine.js +339 -0
  55. package/shared/query.js +280 -0
  56. package/shared/ticket-import.js +477 -0
  57. package/skill/SKILL.md +458 -0
  58. package/skill/reference/anatomy.md +196 -0
  59. package/skill/reference/spec-evolution.md +52 -0
  60. package/skill/reference/tools.md +156 -0
  61. package/skill/reference/workflows.md +203 -0
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Shift+wheel → horizontal pan (SM-281).
3
+ *
4
+ * A mouse with only a vertical scroll wheel produces no deltaX, ever. On
5
+ * macOS the overlay scrollbars only flash for the axis being scrolled, so
6
+ * such users never see (let alone grab) the horizontal bar — the map's
7
+ * off-screen process steps become unreachable. Chrome translates
8
+ * shift+wheel into horizontal scrolling natively; Safari does not.
9
+ *
10
+ * This module installs that translation on the view scroller: while shift
11
+ * is held, a vertical wheel delta pans horizontally instead. Events that
12
+ * already carry a horizontal delta (Chrome's native translation, trackpad
13
+ * swipes) pass through untouched — no double-scroll.
14
+ *
15
+ * UMD-wrapped — usable in browser and Node tests.
16
+ */
17
+ (function (root, factory) {
18
+ if (typeof module === "object" && module.exports) {
19
+ module.exports = factory();
20
+ } else {
21
+ (root.STORYMAP = root.STORYMAP || {}).wheelPan = factory();
22
+ }
23
+ }(typeof self !== "undefined" ? self : this, function () {
24
+ "use strict";
25
+
26
+ const WHEEL_PAN = {
27
+ // Pixels per line for deltaMode DOM_DELTA_LINE (Firefox wheel events).
28
+ LINE_HEIGHT_PX: 16,
29
+ };
30
+
31
+ const DOM_DELTA_LINE = 1;
32
+
33
+ /**
34
+ * Horizontal delta (px) a wheel event should pan, or 0 when the event
35
+ * must not be translated (no shift, or the engine already produced a
36
+ * horizontal delta itself).
37
+ */
38
+ function translateShiftWheel(ev) {
39
+ if (!ev.shiftKey) return 0;
40
+ if (ev.deltaX) return 0; // engine translated already (Chrome) or trackpad swipe
41
+ const dy = ev.deltaY || 0;
42
+ return ev.deltaMode === DOM_DELTA_LINE ? dy * WHEEL_PAN.LINE_HEIGHT_PX : dy;
43
+ }
44
+
45
+ /**
46
+ * Install the shift+wheel pan on a scroll container. Returns an
47
+ * uninstall function. The listener is non-passive on purpose — the
48
+ * translated event must be consumed or the page would ALSO scroll
49
+ * vertically.
50
+ */
51
+ function installShiftWheelPan(el) {
52
+ function onWheel(ev) {
53
+ const dx = translateShiftWheel(ev);
54
+ if (!dx) return;
55
+ el.scrollLeft += dx;
56
+ ev.preventDefault();
57
+ }
58
+ el.addEventListener("wheel", onWheel, { passive: false });
59
+ return function uninstall() {
60
+ el.removeEventListener("wheel", onWheel);
61
+ };
62
+ }
63
+
64
+ return { WHEEL_PAN, translateShiftWheel, installShiftWheelPan };
65
+ }));
@@ -0,0 +1,87 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Story Mapper</title>
7
+ <link rel="stylesheet" href="./css/storymap.css">
8
+ </head>
9
+ <body>
10
+ <header class="app-header">
11
+ <!-- Row 1: Brand + Menu-Bar + Current-Project-Indicator. Menus only. -->
12
+ <div class="toolbar toolbar-menu">
13
+ <span class="brand"><b>Story</b><em>Mapper</em></span>
14
+ <div class="sep" aria-hidden="true"></div>
15
+ <div id="menu-bar" class="menu-bar"></div>
16
+ <div class="toolbar-spacer"></div>
17
+ <span id="current-project" class="current-project" title="Open project"></span>
18
+ </div>
19
+ <!-- Row 2: Search + Filter. View switching lives in the View menu (SM-205);
20
+ every other action moved to the menus per E22. -->
21
+ <div class="toolbar toolbar-action">
22
+ <div class="toolbar-spacer"></div>
23
+ <input id="ticket-search" class="ticket-search" type="search"
24
+ placeholder="Search… or a query: status = ready AND type = bug"
25
+ aria-label="Search or query tickets" autocomplete="off" spellcheck="false">
26
+ <span id="smartbar-status" class="smartbar-status" aria-live="polite"></span>
27
+ <button id="btn-filter" class="filter-btn" title="Filter tickets" aria-haspopup="true" aria-expanded="false">
28
+ <span class="filter-btn-label">Filter</span>
29
+ <span id="filter-badge" class="filter-badge" hidden>0</span>
30
+ </button>
31
+ </div>
32
+ </header>
33
+
34
+ <main id="main">
35
+ <section id="view-host">
36
+ <div id="story-map-host"></div>
37
+ <div id="kanban-host" hidden></div>
38
+ <div id="deps-host" hidden></div>
39
+ <div id="requirements-view-host" hidden></div>
40
+ <div id="process-steps-host" hidden></div>
41
+ <div id="table-host" hidden></div>
42
+ </section>
43
+ <!-- E23.B — Settings as overlay over Map/Kanban (not a third view). -->
44
+ <div id="settings-host" class="vs-overlay" hidden role="dialog" aria-label="Project settings"></div>
45
+ </main>
46
+
47
+ <footer class="statusbar">
48
+ <span id="adapter-badge" class="badge" title="Active storage backend"></span>
49
+ <span id="build-tag" class="build-tag" title="Loaded build (Git-Commit, kurz)"></span>
50
+ <span class="statusbar-spacer"></span>
51
+ </footer>
52
+
53
+ <!-- Single modal host (cmapper-Pattern): at most one modal at a time. -->
54
+ <div id="modal-host"></div>
55
+
56
+ <!-- Module load order: core → store → adapters → ui-shell → renderer-storymap → renderer-ticket-modal → main.
57
+ Kanban-Renderer folgt in E11. -->
58
+ <script defer src="./js/core.js"></script>
59
+ <script defer src="./js/core/graph.js"></script>
60
+ <script defer src="./js/store.js"></script>
61
+ <script defer src="./js/save-pipeline.js"></script>
62
+ <script defer src="./js/adapters.js"></script>
63
+ <script defer src="./js/ui-shell.js"></script>
64
+ <script defer src="./js/dnd.js"></script>
65
+ <script defer src="./js/wheel-pan.js"></script>
66
+ <script defer src="./js/card-animate.js"></script>
67
+ <script defer src="./js/renderer-card.js"></script>
68
+ <script defer src="./js/filter.js"></script>
69
+ <script defer src="./js/query.js"></script>
70
+ <script defer src="./js/query-engine.js"></script>
71
+ <script defer src="./js/query-autocomplete.js"></script>
72
+ <script defer src="./js/smartbar-autocomplete.js"></script>
73
+ <script defer src="./js/renderer-storymap.js"></script>
74
+ <script defer src="./js/renderer-kanban.js"></script>
75
+ <script defer src="./js/renderer-dependencies.js"></script>
76
+ <script defer src="./js/ticket-form.js"></script>
77
+ <script defer src="./js/renderer-ticket-modal.js"></script>
78
+ <script defer src="./js/view-settings.js"></script>
79
+ <script defer src="./js/view-requirements.js"></script>
80
+ <script defer src="./js/view-process-steps.js"></script>
81
+ <script defer src="./js/view-table.js"></script>
82
+ <script defer src="./js/ticket-import.js"></script>
83
+ <script defer src="./js/project-io.js"></script>
84
+ <script defer src="./js/dialog-ticket-import.js"></script>
85
+ <script defer src="./js/main.js"></script>
86
+ </body>
87
+ </html>
@@ -0,0 +1,29 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Ticket Editor · Story Mapper</title>
7
+ <link rel="stylesheet" href="./css/storymap.css">
8
+ </head>
9
+ <body class="te-page">
10
+ <!-- Full-page ticket editor (Epic B / B2, SM-117). Bookmarkable at
11
+ /editor?projectId=&ticketId= — loads the project into a ProjectStore and
12
+ renders the one ticket type-aware with the SHARED ticket-form builders.
13
+ Content-first layout lands in B3; in-place persistence in B5. -->
14
+ <main id="editor-host" class="te-host"></main>
15
+ <div id="modal-host"></div>
16
+
17
+ <!-- Module load order: core → store → adapters → ui-shell → dnd →
18
+ ticket-form (shared builders) → renderer-ticket-editor. -->
19
+ <script defer src="./js/core.js"></script>
20
+ <script defer src="./js/store.js"></script>
21
+ <script defer src="./js/save-pipeline.js"></script>
22
+ <script defer src="./js/adapters.js"></script>
23
+ <script defer src="./js/ui-shell.js"></script>
24
+ <script defer src="./js/dnd.js"></script>
25
+ <script defer src="./js/ticket-form.js"></script>
26
+ <script defer src="./js/renderer-ticket-editor.js"></script>
27
+ <script defer src="./js/ticket-editor-boot.js"></script>
28
+ </body>
29
+ </html>
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "storymapper",
3
+ "version": "0.1.0",
4
+ "description": "User Story Map + Kanban board with a live HTTP/WebSocket UI and an MCP server, so AI agents and humans plan the same backlog together.",
5
+ "keywords": [
6
+ "story-map",
7
+ "user-story-map",
8
+ "kanban",
9
+ "agile",
10
+ "backlog",
11
+ "mcp",
12
+ "model-context-protocol",
13
+ "claude",
14
+ "ai-agent",
15
+ "sqlite",
16
+ "project-planning"
17
+ ],
18
+ "author": "Thomas Schwenger",
19
+ "license": "Apache-2.0",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/TomAtAP/storymapper.git"
23
+ },
24
+ "homepage": "https://github.com/TomAtAP/storymapper#readme",
25
+ "bugs": {
26
+ "url": "https://github.com/TomAtAP/storymapper/issues"
27
+ },
28
+ "main": "server/index.js",
29
+ "type": "commonjs",
30
+ "mcpName": "io.github.tomatap/storymapper",
31
+ "scripts": {
32
+ "start": "node server/index.js server",
33
+ "mcp": "node server/index.js mcp",
34
+ "install-code": "node scripts/install.js claude-code",
35
+ "install-desktop": "node scripts/install.js claude-desktop",
36
+ "install-skill": "node scripts/install-skill.js",
37
+ "package-skill": "node scripts/install-skill.js --zip",
38
+ "prepack": "node scripts/pack-dereference.js apply",
39
+ "postpack": "node scripts/pack-dereference.js restore",
40
+ "test": "node tests/run.js",
41
+ "walkthrough": "node tests/mcp-walkthrough.js",
42
+ "anim-walkthrough": "node tests/mcp-animation-walkthrough.js",
43
+ "prd-demo": "node tests/mcp-prd-slicing-demo.js",
44
+ "query-demo": "node tests/mcp-query-demo.js",
45
+ "perf": "node tests/perf-baseline.js"
46
+ },
47
+ "bin": {
48
+ "storymapper": "./server/index.js"
49
+ },
50
+ "files": [
51
+ "server/",
52
+ "shared/",
53
+ "frontend/",
54
+ "skill/",
55
+ "README.md",
56
+ "ARCHITECTURE.md",
57
+ "LICENSE",
58
+ "NOTICE"
59
+ ],
60
+ "dependencies": {
61
+ "better-sqlite3": "^11.5.0",
62
+ "fflate": "0.8.3",
63
+ "pdf2json": "4.0.3",
64
+ "ws": "^8.21.0",
65
+ "zod": "^4.4.3"
66
+ },
67
+ "devDependencies": {
68
+ "jsdom": "^29.1.1"
69
+ },
70
+ "overrides": {
71
+ "undici": "^7.28.0"
72
+ },
73
+ "engines": {
74
+ "node": ">=20"
75
+ }
76
+ }
package/server/bus.js ADDED
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Process-wide EventEmitter for storage-change notifications.
5
+ * Storage emits 'change' after every successful save / delete / restore.
6
+ * The WebSocket layer subscribes and forwards to browser clients.
7
+ *
8
+ * Event shape: { projectId, revision, savedAt, op, actor, originId? }
9
+ */
10
+
11
+ const { EventEmitter } = require("events");
12
+
13
+ const bus = new EventEmitter();
14
+ bus.setMaxListeners(0); // no warning when many WS clients subscribe
15
+
16
+ module.exports = bus;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * storymap — graph algorithms.
5
+ *
6
+ * SINGLE SOURCE OF TRUTH lives in shared/core/graph.js (UMD). Thin re-export
7
+ * so server-side requires keep working. Edit shared/core/graph.js, never this
8
+ * file. (SM-139)
9
+ */
10
+ module.exports = require("../../shared/core/graph.js");
package/server/core.js ADDED
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * storymap — core data model.
5
+ *
6
+ * SINGLE SOURCE OF TRUTH lives in shared/core.js (UMD). This is a thin
7
+ * re-export so every server-side require("./core.js") / require("../core.js")
8
+ * keeps working unchanged. Edit shared/core.js, never this file. (SM-139)
9
+ */
10
+ module.exports = require("../shared/core.js");
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * SM-129 (A6) — Identity/Actor seam + authorization choke-point.
5
+ *
6
+ * One place that decides "who is acting" for every write path (REST + MCP),
7
+ * and ONE pluggable `authorize(actor, op, entity)` hook that every persist
8
+ * passes through. Today the hook is a no-op (allow everything) and identity
9
+ * is header/convention-based — but the seam exists so:
10
+ *
11
+ * - no write path ever invents an ad-hoc `{ id: "unknown" }` actor again
12
+ * (the only place "unknown" appears is core.normalizeActor's defensive
13
+ * fallback for legacy/stored data, never freshly minted on a write);
14
+ * - real token-verified identity (later) plugs into `resolveActor`;
15
+ * - real RBAC (the SM-130 outlook epic) plugs into `setAuthorizePolicy`
16
+ * without touching a single route or tool.
17
+ *
18
+ * Server-only module (REST + MCP). Not mirrored to the frontend.
19
+ *
20
+ * TRUST MODEL (SM-211): this is a LOCAL SINGLE-USER tool. The X-Actor-*
21
+ * headers are attribution (who shows up in the revision history), NOT
22
+ * authentication — nothing verifies them. That is acceptable because the
23
+ * server binds to loopback and CORS rejects foreign browser origins
24
+ * (server.js applyRequestSecurity). Never bind to a non-loopback interface
25
+ * as-is. Verified identity + token gate + RBAC are the deferred multi-user
26
+ * stage (epic SM-130) and plug into resolveActor / setAuthorizePolicy here.
27
+ */
28
+
29
+ const core = require("./core.js");
30
+
31
+ // Default actor for an HTTP client that does not identify itself via the
32
+ // X-Actor-* header convention. NOT "unknown" — an anonymous-but-present HTTP
33
+ // caller is still a known transport ("http"). Frozen so callers can't mutate
34
+ // the shared default.
35
+ const DEFAULT_HTTP_ACTOR = Object.freeze({ type: "human", id: "http", name: "HTTP User" });
36
+
37
+ // Default actor for MCP tool calls (the coding agent driving the stdio server).
38
+ const AI_ACTOR = Object.freeze({ type: "ai", id: "claude", name: "Claude" });
39
+
40
+ // Header convention for identifying a REST caller. Node lower-cases all
41
+ // incoming header names, so we read the lower-case keys.
42
+ const HDR = Object.freeze({
43
+ TYPE: "x-actor-type",
44
+ ID: "x-actor-id",
45
+ NAME: "x-actor-name",
46
+ SESSION: "x-actor-session"
47
+ });
48
+
49
+ /**
50
+ * Resolve the actor for a REST request from the X-Actor-* header convention.
51
+ *
52
+ * - `X-Actor-Id` present → identified client; build the actor from the
53
+ * headers (id is mandatory, type defaults to "human", name defaults to id).
54
+ * Token verification is a later concern — for now the headers ARE the
55
+ * identity. An identified client never degrades to "unknown".
56
+ * - no `X-Actor-Id` → anonymous HTTP caller → DEFAULT_HTTP_ACTOR.
57
+ *
58
+ * Passing no request (or a request without headers) yields the default,
59
+ * which is what server-internal callers want.
60
+ */
61
+ function resolveActor(req) {
62
+ const headers = (req && req.headers) || null;
63
+ if (headers) {
64
+ const get = (h) => {
65
+ const v = headers[h];
66
+ return (typeof v === "string" && v.trim()) ? v.trim() : null;
67
+ };
68
+ const id = get(HDR.ID);
69
+ if (id) {
70
+ const raw = { type: get(HDR.TYPE) || "human", id: id, name: get(HDR.NAME) || id };
71
+ const session = get(HDR.SESSION);
72
+ if (session) raw.sessionId = session;
73
+ return core.normalizeActor(raw);
74
+ }
75
+ }
76
+ return Object.assign({}, DEFAULT_HTTP_ACTOR);
77
+ }
78
+
79
+ /**
80
+ * Resolve the actor for an MCP tool call. Honours an explicit `args.actor`
81
+ * override (normalized) and otherwise attributes the write to the AI agent.
82
+ * Replaces the per-module AI_ACTOR literal + actorFromArgs in mcp.js so REST
83
+ * and MCP share one identity module.
84
+ */
85
+ function resolveAiActor(args) {
86
+ if (args && args.actor && typeof args.actor === "object") {
87
+ return core.normalizeActor(args.actor);
88
+ }
89
+ return Object.assign({}, AI_ACTOR);
90
+ }
91
+
92
+ // --- Authorization choke-point -------------------------------------------
93
+
94
+ // The default policy: allow every write. RBAC plugs in via setAuthorizePolicy.
95
+ function allowAll(/* actor, op, entity */) { return true; }
96
+
97
+ let _policy = allowAll;
98
+
99
+ /**
100
+ * The single write-authorization choke-point. Every persist (REST + MCP)
101
+ * passes through here via storage.saveProject / storage.deleteProject.
102
+ *
103
+ * The policy returns truthy to allow, falsy to deny. A denial throws a
104
+ * `{ statusCode: 403, kind: "AUTHZ" }` error so the REST router maps it to
105
+ * HTTP 403 and the MCP layer surfaces it as a structured tool error — the
106
+ * same translation path validation errors already use.
107
+ */
108
+ function authorize(actor, op, entity) {
109
+ const allowed = _policy(actor, op, entity);
110
+ if (!allowed) {
111
+ throw Object.assign(new Error("not authorized: " + op), { statusCode: 403, kind: "AUTHZ" });
112
+ }
113
+ return true;
114
+ }
115
+
116
+ /** Install an RBAC policy. Pass nothing / a non-function to reset to allow-all. */
117
+ function setAuthorizePolicy(fn) {
118
+ _policy = (typeof fn === "function") ? fn : allowAll;
119
+ }
120
+
121
+ /** Restore the default allow-all policy (mainly for tests). */
122
+ function resetAuthorizePolicy() {
123
+ _policy = allowAll;
124
+ }
125
+
126
+ module.exports = {
127
+ DEFAULT_HTTP_ACTOR,
128
+ AI_ACTOR,
129
+ resolveActor,
130
+ resolveAiActor,
131
+ authorize,
132
+ setAuthorizePolicy,
133
+ resetAuthorizePolicy
134
+ };