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,225 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Input validators. Throw { statusCode, message, ... } on violation.
5
+ * HTTP and MCP layers map statusCode → 400/422/413 etc.
6
+ *
7
+ * Status transitions enforce DoR (backlog→ready and any forward jump
8
+ * that crosses → ready) and DoD (any forward jump that crosses → done).
9
+ * Reopens (done → anything backwards) bypass DoR/DoD.
10
+ */
11
+
12
+ const core = require("./core.js");
13
+
14
+ function err(statusCode, message, extra) {
15
+ const e = new Error(message);
16
+ e.statusCode = statusCode;
17
+ if (extra) Object.assign(e, extra);
18
+ return e;
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // project / ticket / release / process-step input
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const TICKET_PREFIX_RE = /^[A-Z0-9]{1,10}$/;
26
+ const ID_RE = /^[a-zA-Z0-9_-]{1,100}$/;
27
+
28
+ function projectInput(p) {
29
+ if (!p || typeof p !== "object") throw err(400, "project: object required");
30
+ if (typeof p.name !== "string" || p.name.trim().length === 0) {
31
+ throw err(400, "project.name is required");
32
+ }
33
+ if (p.name.length > 200) throw err(400, "project.name too long (max 200)");
34
+ if (p.description != null && (typeof p.description !== "string" ||
35
+ p.description.length > core.LIMITS.maxDescriptionLength)) {
36
+ throw err(400, "project.description invalid or too long");
37
+ }
38
+ if (p.ticketPrefix != null) {
39
+ if (typeof p.ticketPrefix !== "string" || !TICKET_PREFIX_RE.test(p.ticketPrefix)) {
40
+ throw err(400, "project.ticketPrefix must match /^[A-Z0-9]{1,10}$/");
41
+ }
42
+ }
43
+ if (p.definitions) definitionsInput(p.definitions);
44
+ }
45
+
46
+ function ticketInput(t, project) {
47
+ if (!t || typeof t !== "object") throw err(400, "ticket: object required");
48
+ if (typeof t.title !== "string" || t.title.trim().length === 0) {
49
+ throw err(400, "ticket.title is required");
50
+ }
51
+ if (t.title.length > core.LIMITS.maxTitleLength) {
52
+ throw err(400, "ticket.title too long (max " + core.LIMITS.maxTitleLength + ")");
53
+ }
54
+ if (t.description != null) {
55
+ if (typeof t.description !== "string") throw err(400, "ticket.description must be a string");
56
+ if (t.description.length > core.LIMITS.maxDescriptionLength) {
57
+ throw err(400, "ticket.description too long (max " + core.LIMITS.maxDescriptionLength + ")");
58
+ }
59
+ }
60
+ if (t.type != null) {
61
+ if (typeof t.type !== "string") throw err(400, "ticket.type must be a string");
62
+ if (project && Array.isArray(project.ticketTypes) && project.ticketTypes.length > 0) {
63
+ if (project.ticketTypes.indexOf(t.type) < 0) {
64
+ throw err(400, "ticket.type not in project.ticketTypes: " + t.type);
65
+ }
66
+ }
67
+ }
68
+ }
69
+
70
+ function releaseInput(r) {
71
+ if (!r || typeof r !== "object") throw err(400, "release: object required");
72
+ if (typeof r.name !== "string" || r.name.trim().length === 0) {
73
+ throw err(400, "release.name is required");
74
+ }
75
+ if (r.name.length > 200) throw err(400, "release.name too long (max 200)");
76
+ if (r.status != null && core.DEFAULT_RELEASE_STATUSES.indexOf(r.status) < 0) {
77
+ throw err(400, "release.status invalid: " + r.status);
78
+ }
79
+ }
80
+
81
+ // SM-255: the last remaining (non-deleted) release may not be deleted — a
82
+ // project always keeps ≥1 release (paired with SM-254's create-time seed, the
83
+ // invariant holds end to end). Throws 422 kind=LAST_RELEASE. A no-op (the id
84
+ // isn't a live release) is left to the delete op itself.
85
+ function releaseDelete(snapshot, releaseId) {
86
+ const live = ((snapshot && snapshot.releases) || []).filter(r => r && !r.isDeleted);
87
+ const target = live.find(r => r.id === releaseId);
88
+ if (target && live.length <= 1) {
89
+ throw err(422, "cannot delete the last release — a project needs at least one",
90
+ { kind: "LAST_RELEASE" });
91
+ }
92
+ }
93
+
94
+ function processStepInput(s) {
95
+ if (!s || typeof s !== "object") throw err(400, "processStep: object required");
96
+ if (typeof s.name !== "string" || s.name.trim().length === 0) {
97
+ throw err(400, "processStep.name is required");
98
+ }
99
+ if (s.name.length > 200) throw err(400, "processStep.name too long (max 200)");
100
+ }
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // definitions input (DoR/DoD config on the project level)
104
+ // ---------------------------------------------------------------------------
105
+
106
+ function validateDefinitionItem(item, kind) {
107
+ if (!item || typeof item !== "object") throw err(400, kind + ": each item must be an object");
108
+ if (typeof item.id !== "string" || item.id.length === 0) {
109
+ throw err(400, kind + ": item.id is required");
110
+ }
111
+ if (typeof item.label !== "string" || item.label.length === 0) {
112
+ throw err(400, kind + ": item.label is required");
113
+ }
114
+ if (item.required != null && typeof item.required !== "boolean") {
115
+ throw err(400, kind + ": item.required must be boolean");
116
+ }
117
+ }
118
+
119
+ function validateBlock(block, kind) {
120
+ if (!block || typeof block !== "object") {
121
+ throw err(400, "definitions." + kind + " block missing");
122
+ }
123
+ if (block.global != null) {
124
+ if (!Array.isArray(block.global)) throw err(400, "definitions." + kind + ".global must be array");
125
+ if (block.global.length > core.LIMITS.maxChecklistItems) {
126
+ throw err(400, "definitions." + kind + ".global exceeds limit");
127
+ }
128
+ for (const item of block.global) validateDefinitionItem(item, kind + ".global");
129
+ }
130
+ if (block.byType != null) {
131
+ if (typeof block.byType !== "object" || Array.isArray(block.byType)) {
132
+ throw err(400, "definitions." + kind + ".byType must be object");
133
+ }
134
+ for (const t of Object.keys(block.byType)) {
135
+ const entry = block.byType[t];
136
+ // Guard a null / non-object entry — otherwise `entry.appended` throws a
137
+ // raw TypeError that surfaces as a 500 instead of this structured 400.
138
+ // Consistent with validateDefinitionItem's object check. (SM-151)
139
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
140
+ throw err(400, "definitions." + kind + ".byType." + t + " must be an object");
141
+ }
142
+ if (entry.appended != null) {
143
+ if (!Array.isArray(entry.appended)) throw err(400, kind + ".byType." + t + ".appended must be array");
144
+ if (entry.appended.length > core.LIMITS.maxChecklistItems) {
145
+ throw err(400, kind + ".byType." + t + ".appended exceeds limit");
146
+ }
147
+ for (const it of entry.appended) validateDefinitionItem(it, kind + ".byType." + t + ".appended");
148
+ }
149
+ if (entry.overridden != null) {
150
+ if (!Array.isArray(entry.overridden)) throw err(400, kind + ".byType." + t + ".overridden must be array");
151
+ if (entry.overridden.length > core.LIMITS.maxChecklistItems) {
152
+ throw err(400, kind + ".byType." + t + ".overridden exceeds limit");
153
+ }
154
+ for (const it of entry.overridden) validateDefinitionItem(it, kind + ".byType." + t + ".overridden");
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ function definitionsInput(defs) {
161
+ if (!defs || typeof defs !== "object") throw err(400, "definitions object required");
162
+ if (!defs.ready) throw err(400, "definitions.ready block missing");
163
+ if (!defs.done) throw err(400, "definitions.done block missing");
164
+ validateBlock(defs.ready, "ready");
165
+ validateBlock(defs.done, "done");
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // changeStatusTransition — DoR / DoD enforcement
170
+ // ---------------------------------------------------------------------------
171
+ //
172
+ // Status-Sequenz + Gate-Regeln kommen jetzt aus dem per-Projekt + per-Type
173
+ // resolvbaren Workflow (E13.C). Pro Vorwärts-Schritt im Workflow wird
174
+ // geprüft, ob das Ziel ein `requireGate` hat (DoR/DoD/null); jeder
175
+ // überquerte Status mit Gate wird einzeln validiert (Sprung-Übergänge
176
+ // greifen alle dazwischenliegenden Gates).
177
+
178
+ // E18.D: Implementierung liegt jetzt in `core.validateStatusTransition` —
179
+ // dieselbe Logik wird vom Frontend-Store vor dem Commit aufgerufen, damit
180
+ // die Gate-Validierung im snapshot-PUT-Pfad nicht verloren geht. Hier nur
181
+ // noch ein Delegate für Backward-Compat der bestehenden REST-Endpoints.
182
+ function changeStatusTransition(ticket, newStatus, project) {
183
+ return core.validateStatusTransition(ticket, newStatus, project);
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // snapshotLimits — guard against oversized snapshots before saving
188
+ // ---------------------------------------------------------------------------
189
+
190
+ function snapshotLimits(snap) {
191
+ const t = (snap.tickets || []).length;
192
+ if (t > core.LIMITS.maxTicketsPerProject) {
193
+ throw err(400, "too many tickets: " + t + " (limit " + core.LIMITS.maxTicketsPerProject + ")");
194
+ }
195
+ const r = (snap.releases || []).length;
196
+ if (r > core.LIMITS.maxReleases) {
197
+ throw err(400, "too many releases: " + r + " (limit " + core.LIMITS.maxReleases + ")");
198
+ }
199
+ const ps = (snap.processSteps || []).length;
200
+ if (ps > core.LIMITS.maxProcessSteps) {
201
+ throw err(400, "too many processSteps: " + ps + " (limit " + core.LIMITS.maxProcessSteps + ")");
202
+ }
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Misc
207
+ // ---------------------------------------------------------------------------
208
+
209
+ function entityId(id, label) {
210
+ if (typeof id !== "string" || !ID_RE.test(id)) {
211
+ throw err(400, (label || "id") + " invalid: must match [a-zA-Z0-9_-]{1,100}");
212
+ }
213
+ }
214
+
215
+ module.exports = {
216
+ projectInput,
217
+ ticketInput,
218
+ releaseInput,
219
+ releaseDelete,
220
+ processStepInput,
221
+ definitionsInput,
222
+ changeStatusTransition,
223
+ snapshotLimits,
224
+ entityId
225
+ };