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.
- package/ARCHITECTURE.md +334 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +239 -0
- package/frontend/css/storymap.css +4637 -0
- package/frontend/js/adapters.js +423 -0
- package/frontend/js/card-animate.js +384 -0
- package/frontend/js/core/graph.js +825 -0
- package/frontend/js/core.js +3908 -0
- package/frontend/js/dialog-ticket-import.js +506 -0
- package/frontend/js/dnd.js +322 -0
- package/frontend/js/filter.js +215 -0
- package/frontend/js/main.js +2499 -0
- package/frontend/js/project-io.js +109 -0
- package/frontend/js/query-autocomplete.js +196 -0
- package/frontend/js/query-engine.js +339 -0
- package/frontend/js/query.js +280 -0
- package/frontend/js/renderer-card.js +639 -0
- package/frontend/js/renderer-dependencies.js +974 -0
- package/frontend/js/renderer-kanban.js +505 -0
- package/frontend/js/renderer-storymap.js +2530 -0
- package/frontend/js/renderer-ticket-editor.js +455 -0
- package/frontend/js/renderer-ticket-modal.js +758 -0
- package/frontend/js/save-pipeline.js +170 -0
- package/frontend/js/smartbar-autocomplete.js +162 -0
- package/frontend/js/store.js +197 -0
- package/frontend/js/ticket-editor-boot.js +24 -0
- package/frontend/js/ticket-form.js +2095 -0
- package/frontend/js/ticket-import.js +477 -0
- package/frontend/js/ui-shell.js +441 -0
- package/frontend/js/view-process-steps.js +233 -0
- package/frontend/js/view-requirements.js +361 -0
- package/frontend/js/view-settings.js +1864 -0
- package/frontend/js/view-table.js +659 -0
- package/frontend/js/wheel-pan.js +65 -0
- package/frontend/storymap.html +87 -0
- package/frontend/ticket-editor.html +29 -0
- package/package.json +76 -0
- package/server/bus.js +16 -0
- package/server/core/graph.js +10 -0
- package/server/core.js +10 -0
- package/server/identity.js +134 -0
- package/server/index.js +283 -0
- package/server/ingest.js +212 -0
- package/server/mcp.js +2510 -0
- package/server/server.js +1599 -0
- package/server/slice.js +103 -0
- package/server/storage.js +571 -0
- package/server/validation.js +225 -0
- package/shared/core/graph.js +825 -0
- package/shared/core.js +3908 -0
- package/shared/project-io.js +109 -0
- package/shared/query-autocomplete.js +196 -0
- package/shared/query-engine.js +339 -0
- package/shared/query.js +280 -0
- package/shared/ticket-import.js +477 -0
- package/skill/SKILL.md +458 -0
- package/skill/reference/anatomy.md +196 -0
- package/skill/reference/spec-evolution.md +52 -0
- package/skill/reference/tools.md +156 -0
- 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
|
+
};
|