@peter.prib/renderengine 0.2.11 → 0.4.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 (36) hide show
  1. package/README.md +22 -6
  2. package/REFERENCE.md +117 -2
  3. package/dist/renderengine-workspace.umd.js +2973 -1203
  4. package/dist/renderengine.umd.js +1178 -158
  5. package/package.json +1 -1
  6. package/src/charting/ModelCheckDialog.js +10 -3
  7. package/src/charting/ModelComparisonDialog.js +10 -3
  8. package/src/charting/ModelFileFormat.js +4 -1
  9. package/src/charting/archimate/ArchimateCanvasEditor.js +14 -2
  10. package/src/charting/archimate/ArchimateElementTemplate.js +23 -0
  11. package/src/charting/arrangement/layouts.js +9 -4
  12. package/src/charting/datamodel/DataModel.js +18 -71
  13. package/src/charting/datamodel/DataModelCompared.js +1 -31
  14. package/src/charting/diagramModel/ListCompared.js +35 -0
  15. package/src/charting/model/Benchmark.js +59 -0
  16. package/src/charting/model/EditableModel.js +104 -0
  17. package/src/charting/{datamodel/DataModelEditCommand.js → model/ModelEditCommand.js} +47 -47
  18. package/src/charting/requirements/RequirementsChecker.js +76 -0
  19. package/src/charting/requirements/RequirementsCompared.js +33 -0
  20. package/src/charting/requirements/RequirementsFileFormat.js +32 -0
  21. package/src/charting/requirements/RequirementsImportFormats.js +48 -0
  22. package/src/charting/requirements/RequirementsModel.js +305 -0
  23. package/src/charting/requirements/Storyboard.js +94 -0
  24. package/src/charting/requirements/TrackerExport.js +273 -0
  25. package/src/charting/requirements/index.js +7 -0
  26. package/src/index.js +6 -0
  27. package/src/workspace/Workspace.js +329 -321
  28. package/src/workspace/index.js +2 -0
  29. package/src/workspace/parts/ArchimateCanvasPart.js +31 -1
  30. package/src/workspace/parts/DataModelPart.js +668 -735
  31. package/src/workspace/parts/EditorPart.js +1 -1
  32. package/src/workspace/parts/ModelFile.js +3 -1
  33. package/src/workspace/parts/ModelHosted.js +176 -0
  34. package/src/workspace/parts/ModelsPart.js +146 -7
  35. package/src/workspace/parts/RequirementsPart.js +465 -0
  36. package/src/workspace/storage/ModelLibrary.js +8 -2
@@ -0,0 +1,94 @@
1
+ import { criteriaFrom, narrativeFrom, withoutNarrative } from './TrackerExport.js';
2
+
3
+ /**
4
+ * Storyboard — epics and stories from a storyboard or requirements document
5
+ * written in Markdown (or plain text with Markdown headings), as teams keep
6
+ * them beside or before a tracker:
7
+ *
8
+ * - a heading that says "Epic" (`## Epic: Checkout`) is an epic, one that says
9
+ * "Story" (`### Story: Pay by card`) a story; otherwise the document's one
10
+ * title (`#`) is its name, the next level down are epics and the level
11
+ * below that stories;
12
+ * - under an epic, a list item that reads "As a … I want …" is a story too;
13
+ * - under a story, "As a … I want … so that …" is its narrative, the list
14
+ * under "Acceptance criteria" (or its Given/When/Then lines) its criteria,
15
+ * and the rest its description — a picture (`![caption](file)`) kept there
16
+ * by its caption.
17
+ *
18
+ * `read(text)` gives a RequirementsModel spec (`{name, epics, stories}`); ids
19
+ * are made from names (prefix: `prefix`, so two storyboards read into one
20
+ * model stay apart).
21
+ */
22
+ export class Storyboard {
23
+ /** @param {{prefix?: string}} [options] */
24
+ constructor({ prefix = '' } = {}) {
25
+ this.prefix = prefix;
26
+ }
27
+
28
+ read(text) {
29
+ const lines = String(text ?? '').replace(/\r\n?/g, '\n').split('\n');
30
+ const headings = lines.map((line, at) => ({ at, m: /^(#{1,6})\s+(.*?)\s*#*\s*$/.exec(line) })).filter(h => h.m)
31
+ .map(({ at, m }) => ({ at, level: m[1].length, text: m[2].trim() }));
32
+ const kindOf = (h) => (/^epic\b/i.test(h.text) ? 'epic' : /^(user\s+)?story\b/i.test(h.text) || /\bas an?\s+\S/i.test(h.text) ? 'story' : null);
33
+ const top = Math.min(...headings.map(h => h.level));
34
+ const tops = headings.filter(h => h.level === top);
35
+ // One top heading, a title (#), names the document; its children are the epics.
36
+ const named = top === 1 && tops.length === 1 && !kindOf(tops[0]) && headings.length > 1;
37
+ const epicLevel = named ? top + 1 : top;
38
+ const strip = (t) => t.replace(/^(epic|(user\s+)?story)\s*[:\-–—]?\s*/i, '').trim();
39
+
40
+ const epics = [], stories = [];
41
+ const ids = new Set();
42
+ const idFor = (name, kind) => {
43
+ const base = `${this.prefix}${String(name).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || kind}`;
44
+ let id = base;
45
+ for (let n = 2; ids.has(id); n++) id = `${base}-${n}`;
46
+ ids.add(id);
47
+ return id;
48
+ };
49
+ const bodyOf = (h, i) => lines.slice(h.at + 1, headings[i + 1]?.at ?? lines.length).join('\n');
50
+ let epic = null;
51
+ headings.forEach((h, i) => {
52
+ if (named && h === tops[0]) return;
53
+ const kind = kindOf(h) ?? (h.level === epicLevel ? 'epic' : h.level === epicLevel + 1 ? 'story' : null);
54
+ const body = bodyOf(h, i);
55
+ if (kind === 'epic') {
56
+ epic = { id: idFor(strip(h.text), 'epic'), name: strip(h.text), description: '' };
57
+ epics.push(epic);
58
+ const listed = this._listedStories(body);
59
+ epic.description = listed.rest;
60
+ for (const s of listed.stories) stories.push({ ...s, id: idFor(s.name, 'story'), epic: epic.id });
61
+ } else if (kind === 'story') {
62
+ stories.push({ ...this._story(strip(h.text), body), id: idFor(strip(h.text), 'story'), epic: epic?.id ?? '' });
63
+ }
64
+ });
65
+ if (!headings.length || (!epics.length && !stories.length)) {
66
+ for (const s of this._listedStories(lines.join('\n')).stories) stories.push({ ...s, id: idFor(s.name, 'story') });
67
+ }
68
+ return { name: named ? tops[0].text : '', epics, stories };
69
+ }
70
+
71
+ /** A story from its heading and what is under it. @private */
72
+ _story(name, body) {
73
+ const pictures = [...body.matchAll(/!\[([^\]]*)\]\([^)]*\)/g)].map(m => m[1]).filter(Boolean);
74
+ const text = body.replace(/!\[([^\]]*)\]\([^)]*\)/g, '').trim();
75
+ const said = narrativeFrom(text);
76
+ const { as, want, soThat } = said.want ? said : narrativeFrom(name);
77
+ const { criteria, rest } = criteriaFrom(text);
78
+ const description = [withoutNarrative(rest), ...pictures.map(c => `Picture: ${c}`)].filter(Boolean).join('\n');
79
+ return { name, as, want, soThat, description, criteria };
80
+ }
81
+
82
+ /** "As a … I want …" list items: stories; the rest of the text, kept. @private */
83
+ _listedStories(body) {
84
+ const stories = [];
85
+ const rest = [];
86
+ for (const line of body.split('\n')) {
87
+ const item = /^\s*(?:[-*•+]|\d+[.)])\s+(.*)$/.exec(line);
88
+ const said = item && narrativeFrom(item[1]);
89
+ if (said?.want) stories.push({ name: said.want.replace(/^to\s+/i, '').replace(/^./, c => c.toUpperCase()), ...said, description: '', criteria: [] });
90
+ else rest.push(line);
91
+ }
92
+ return { stories, rest: rest.join('\n').trim() };
93
+ }
94
+ }
@@ -0,0 +1,273 @@
1
+ import { parseCsvTable } from '../../csvTable.js';
2
+
3
+ /**
4
+ * Where each tracker's export keeps what a story needs: per tracker, the
5
+ * column names (CSV) each field may be under, first found wins; which issue
6
+ * types are epics; how its list fields are separated. One definition per
7
+ * tracker — the CSV and the JSON readers both map onto these fields. A host
8
+ * gives its own (a custom field's column) through `new TrackerExport({ columns })`.
9
+ */
10
+ export const TRACKERS = Object.freeze({
11
+ jira: {
12
+ label: 'Jira',
13
+ // Columns only its exports have, to tell it by.
14
+ marks: ['Issue key', 'Issue Type'],
15
+ columns: {
16
+ key: ['Issue key', 'Key'], id: ['Issue id'], type: ['Issue Type'], title: ['Summary'],
17
+ description: ['Description'], parent: ['Parent', 'Parent id', 'Parent key', 'Custom field (Epic Link)', 'Epic Link'],
18
+ status: ['Status'], priority: ['Priority'],
19
+ points: ['Custom field (Story Points)', 'Story Points', 'Custom field (Story point estimate)', 'Story point estimate'],
20
+ release: ['Fix Version/s', 'Fix versions', 'Sprint'], labels: ['Labels'],
21
+ criteria: ['Custom field (Acceptance Criteria)', 'Acceptance Criteria'], url: [],
22
+ },
23
+ epicTypes: ['Epic'], skipTypes: ['Sub-task', 'Subtask'], labelSeparator: /[\s,]+/,
24
+ },
25
+ azure: {
26
+ label: 'Azure DevOps',
27
+ marks: ['Work Item Type'],
28
+ columns: {
29
+ key: ['ID'], id: ['ID'], type: ['Work Item Type'], title: ['Title', 'Title 1', 'Title 2', 'Title 3'],
30
+ description: ['Description'], parent: ['Parent'], status: ['State'], priority: ['Priority'],
31
+ points: ['Story Points', 'Effort', 'Size'], release: ['Iteration Path'], labels: ['Tags'],
32
+ criteria: ['Acceptance Criteria'], url: [],
33
+ },
34
+ epicTypes: ['Epic', 'Feature'], skipTypes: ['Task'], labelSeparator: /\s*;\s*/,
35
+ },
36
+ gitlab: {
37
+ label: 'GitLab',
38
+ marks: ['Issue ID', 'URL'],
39
+ columns: {
40
+ key: ['Issue ID', 'IID'], id: ['Issue ID', 'IID'], type: [], title: ['Title'], description: ['Description'],
41
+ parent: ['Epic ID'], parentName: ['Epic Title'], status: ['State'], priority: [], points: ['Weight'],
42
+ release: ['Milestone'], labels: ['Labels'], criteria: [], url: ['URL'],
43
+ },
44
+ epicTypes: [], skipTypes: [], labelSeparator: /\s*,\s*/,
45
+ },
46
+ });
47
+
48
+ /**
49
+ * Where Jira's JSON keeps what has no field of its own: custom fields, by key.
50
+ * Jira Cloud's "Story point estimate" is customfield_10016 unless a site
51
+ * changed it; a host names its own (`columns: { points: ['customfield_10026'] }`).
52
+ */
53
+ const JIRA_FIELDS = Object.freeze({
54
+ points: ['story_points', 'storyPoints', 'customfield_10016'],
55
+ criteria: ['acceptanceCriteria'],
56
+ });
57
+
58
+ /** Text with HTML in it (Azure DevOps keeps descriptions as HTML) as lines of plain text. */
59
+ function plainText(value) {
60
+ if (value == null) return '';
61
+ if (typeof value === 'object') return documentText(value);
62
+ return String(value)
63
+ .replace(/<\s*(br|\/p|\/div|\/li|\/h\d)\s*\/?>/gi, '\n').replace(/<\s*li[^>]*>/gi, '- ')
64
+ .replace(/<[^>]+>/g, '')
65
+ .replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, '&')
66
+ .replace(/\r\n?/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
67
+ }
68
+
69
+ /** Jira Cloud's description, a document of nodes, as text: each paragraph and list item a line. */
70
+ function documentText(node) {
71
+ if (!node || typeof node !== 'object') return '';
72
+ if (node.type === 'text') return node.text ?? '';
73
+ const inner = (node.content ?? []).map(documentText).join('');
74
+ return ['paragraph', 'heading', 'listItem', 'codeBlock'].includes(node.type) ? `${node.type === 'listItem' ? '- ' : ''}${inner.trim()}\n` : inner;
75
+ }
76
+
77
+ /** The lines of a list: "- x", "* x", "1. x", "• x", "[ ] x" — each item a line, its marker gone. */
78
+ function listItems(text) {
79
+ return String(text ?? '').split('\n')
80
+ .map(line => line.replace(/^\s*(?:[-*•+]|\d+[.)]|\[[ xX]?\])\s+/, '').trim())
81
+ .filter(Boolean);
82
+ }
83
+
84
+ /**
85
+ * Acceptance criteria in a story's text: the lines under an "Acceptance
86
+ * criteria" heading, else its Given/When/Then scenarios (one each), else none.
87
+ * @returns {{criteria: Array<string>, rest: string}} - and the text without them
88
+ */
89
+ export function criteriaFrom(text) {
90
+ const s = plainText(text);
91
+ const heading = /^\s*(?:#+\s*|\*\*)?acceptance criteria\b[^\n]*\n/im.exec(s);
92
+ if (heading) {
93
+ const after = s.slice(heading.index + heading[0].length);
94
+ const end = after.search(/\n\s*\n\s*(?:#+\s|\*\*|[A-Z][^\n:]{0,40}:\s*\n)/);
95
+ const block = end < 0 ? after : after.slice(0, end);
96
+ return { criteria: listItems(block), rest: (s.slice(0, heading.index) + (end < 0 ? '' : after.slice(end))).replace(/\n{3,}/g, '\n\n').trim() };
97
+ }
98
+ const scenarios = [];
99
+ let current = null;
100
+ for (const line of s.split('\n')) {
101
+ const t = line.replace(/^\s*(?:[-*•]\s+)?/, '').trim();
102
+ if (/^given\b/i.test(t)) { current = [t]; scenarios.push(current); } else if (current && /^(when|then|and|but)\b/i.test(t)) current.push(t); else current = null;
103
+ }
104
+ if (scenarios.length) return { criteria: scenarios.map(lines => lines.join(' ')), rest: s.split('\n').filter(l => !/^\s*(?:[-*•]\s+)?(given|when|then|and|but)\b/i.test(l)).join('\n').trim() };
105
+ return { criteria: [], rest: s };
106
+ }
107
+
108
+ /** A field that holds only acceptance criteria: as criteriaFrom reads them, else each of its lines one. */
109
+ function criteriaField(text) {
110
+ const found = criteriaFrom(text).criteria;
111
+ return found.length ? found : listItems(plainText(text));
112
+ }
113
+
114
+ /** "As a <who>, I want <what> so that <why>" in a text: its three parts (empty where it says nothing). */
115
+ export function narrativeFrom(text) {
116
+ const m = /\bas an?\s+([^,\n]+?)\s*,?\s+i\s+(?:want|need|would like)\s+([^\n]+?)(?:\s*,?\s+so\s+that\s+([^\n]+?))?\s*(?:[.\n]|$)/i.exec(plainText(text));
117
+ return m ? { as: m[1].trim(), want: m[2].trim(), soThat: (m[3] ?? '').trim() } : { as: '', want: '', soThat: '' };
118
+ }
119
+
120
+ /** A text without its "As a … I want …" line: what is left to describe the story, once its sentence is read (narrativeFrom). */
121
+ export function withoutNarrative(text) {
122
+ return String(text ?? '').replace(/^\s*\**\s*as an?\s[^\n]*\bi\s+(want|need|would like)\b[^\n]*\n?/im, '').trim();
123
+ }
124
+
125
+ /**
126
+ * TrackerExport — epics and stories from what an issue tracker exports: Jira,
127
+ * Azure DevOps or GitLab (TRACKERS), as CSV or as their API's JSON. An issue
128
+ * of an epic type becomes an epic (GitLab's epics are named by their issues'
129
+ * Epic ID and title); the rest become stories under their parent epic, with
130
+ * their status, priority, points, labels and release (fix version, iteration
131
+ * or milestone). Acceptance criteria come from their own field, else from
132
+ * the description (criteriaFrom); "As a … I want … so that …" from the
133
+ * description or the title (narrativeFrom). Each keeps where it came from
134
+ * (`source: {system, key, url?}`), and its id is the tracker's key, so
135
+ * reading a newer export over a model replaces what it had.
136
+ *
137
+ * `read(text)` gives a RequirementsModel spec (`{epics, releases, stories}`).
138
+ */
139
+ export class TrackerExport {
140
+ /**
141
+ * @param {Object} [options]
142
+ * @param {string} [options.tracker] - 'jira', 'azure', 'gitlab' (default: told from the export)
143
+ * @param {Object<string, Array<string>>} [options.columns] - Column names to look in first, by field (in Jira's JSON, field keys)
144
+ */
145
+ constructor({ tracker = null, columns = {} } = {}) {
146
+ this.tracker = tracker;
147
+ this.columns = columns;
148
+ }
149
+
150
+ /** What a tracker is called ('Jira'). */
151
+ static label(tracker) {
152
+ return TRACKERS[tracker]?.label ?? 'Tracker';
153
+ }
154
+
155
+ /** Which tracker an export's columns (or JSON) are from, or null. */
156
+ static detect(text) {
157
+ const json = TrackerExport._json(text);
158
+ if (json) {
159
+ if (Array.isArray(json.issues) && json.issues[0]?.fields) return 'jira';
160
+ if (Array.isArray(json.value ?? json.workItems) && (json.value ?? json.workItems)[0]?.fields) return 'azure';
161
+ if (Array.isArray(json) && json[0] && 'iid' in json[0]) return 'gitlab';
162
+ return null;
163
+ }
164
+ const { columns } = parseCsvTable(text);
165
+ const has = new Set(columns.map(c => c.toLowerCase()));
166
+ const found = Object.entries(TRACKERS).find(([, t]) => t.marks.every(m => has.has(m.toLowerCase())));
167
+ return found?.[0] ?? null;
168
+ }
169
+
170
+ /** @private */
171
+ static _json(text) {
172
+ const s = String(text ?? '').trim();
173
+ if (!s.startsWith('{') && !s.startsWith('[')) return null;
174
+ try { return JSON.parse(s); } catch { return null; }
175
+ }
176
+
177
+ /**
178
+ * The export as a requirements model's spec.
179
+ * @returns {{epics: Array, releases: Array, stories: Array, tracker: string|null}}
180
+ */
181
+ read(text) {
182
+ const tracker = this.tracker ?? TrackerExport.detect(text) ?? 'jira';
183
+ const def = TRACKERS[tracker];
184
+ if (!def) throw new Error(`Unknown tracker: "${tracker}"`);
185
+ const json = TrackerExport._json(text);
186
+ const issues = json ? this._fromJson(tracker, json) : this._fromCsv(def, text);
187
+ return { ...this._model(tracker, def, issues), tracker };
188
+ }
189
+
190
+ /** CSV rows as issues: each field from the first of its columns there. @private */
191
+ _fromCsv(def, text) {
192
+ const { columns, rows } = parseCsvTable(text);
193
+ const lower = new Map(columns.map(c => [c.toLowerCase(), c]));
194
+ const columnOf = (field) => [...(this.columns[field] ?? []), ...(def.columns[field] ?? [])].map(c => lower.get(c.toLowerCase())).find(Boolean);
195
+ const fields = Object.keys({ ...def.columns, ...this.columns, parentName: [] });
196
+ const at = Object.fromEntries(fields.map(f => [f, columnOf(f)]));
197
+ return rows.map(row => Object.fromEntries(fields.map(f => [f, at[f] ? row[at[f]] : ''])));
198
+ }
199
+
200
+ /** An API's JSON as issues, in the CSV's fields. @private */
201
+ _fromJson(tracker, json) {
202
+ if (tracker === 'jira') {
203
+ return (json.issues ?? []).map(({ key, id, fields: f = {} }) => {
204
+ const custom = (names) => Object.entries(f).find(([k]) => names.some(n => k.toLowerCase().includes(n)))?.[1];
205
+ // A field the host named (its custom field's key), else Jira Cloud's usual one.
206
+ const field = (name) => [...(this.columns[name] ?? []), ...(JIRA_FIELDS[name] ?? [])].map(k => f[k]).find(v => v != null && v !== '');
207
+ return {
208
+ key, id: String(id ?? ''), type: f.issuetype?.name ?? '', title: f.summary ?? '', description: f.description ?? '',
209
+ parent: f.parent?.key ?? f.parent?.id ?? '', status: f.status?.name ?? '', priority: f.priority?.name ?? '',
210
+ points: field('points') ?? custom(['storypoint', 'story_point']) ?? '',
211
+ release: (f.fixVersions ?? [])[0]?.name ?? '', labels: (f.labels ?? []).join(' '),
212
+ criteria: field('criteria') ?? custom(['acceptance']) ?? '', url: '',
213
+ };
214
+ });
215
+ }
216
+ if (tracker === 'azure') {
217
+ return (json.value ?? json.workItems ?? []).map(({ id, fields: f = {}, url }) => ({
218
+ key: String(id), id: String(id), type: f['System.WorkItemType'] ?? '', title: f['System.Title'] ?? '',
219
+ description: f['System.Description'] ?? '', parent: String(f['System.Parent'] ?? ''), status: f['System.State'] ?? '',
220
+ priority: String(f['Microsoft.VSTS.Common.Priority'] ?? ''),
221
+ points: f['Microsoft.VSTS.Scheduling.StoryPoints'] ?? f['Microsoft.VSTS.Scheduling.Effort'] ?? '',
222
+ release: f['System.IterationPath'] ?? '', labels: f['System.Tags'] ?? '',
223
+ criteria: f['Microsoft.VSTS.Common.AcceptanceCriteria'] ?? '', url: url ?? '',
224
+ }));
225
+ }
226
+ return (Array.isArray(json) ? json : []).map(i => ({
227
+ key: String(i.iid ?? i.id), id: String(i.iid ?? i.id), type: '', title: i.title ?? '', description: i.description ?? '',
228
+ parent: String(i.epic?.id ?? i.epic_iid ?? ''), parentName: i.epic?.title ?? '', status: i.state ?? '', priority: '',
229
+ points: i.weight ?? '', release: i.milestone?.title ?? '', labels: (i.labels ?? []).map(l => l.name ?? l).join(','),
230
+ criteria: '', url: i.web_url ?? '',
231
+ }));
232
+ }
233
+
234
+ /** Issues as epics, releases and stories. @private */
235
+ _model(tracker, def, issues) {
236
+ const is = (list, type) => list.some(t => t.toLowerCase() === String(type ?? '').trim().toLowerCase());
237
+ const keyOf = new Map(); // an issue's id or key → its key (Jira's CSV names a parent by its id)
238
+ for (const i of issues) for (const k of [i.key, i.id]) if (k) keyOf.set(String(k), String(i.key || i.id));
239
+ const source = (i, key) => ({ system: tracker, key, ...(i.url && { url: i.url }) });
240
+
241
+ const epics = new Map();
242
+ for (const i of issues) {
243
+ if (!is(def.epicTypes, i.type)) continue;
244
+ const key = String(i.key || i.id);
245
+ epics.set(key, { id: key, name: plainText(i.title), description: plainText(i.description), source: source(i, key) });
246
+ }
247
+ const releases = new Map();
248
+ const stories = [];
249
+ for (const i of issues) {
250
+ if (is(def.epicTypes, i.type) || is(def.skipTypes, i.type) || !String(i.title ?? '').trim()) continue;
251
+ const key = String(i.key || i.id);
252
+ const parent = i.parent ? keyOf.get(String(i.parent)) ?? String(i.parent) : '';
253
+ if (parent && !epics.has(parent) && i.parentName) epics.set(parent, { id: parent, name: plainText(i.parentName), description: '', source: { system: tracker, key: parent } });
254
+ const release = plainText(i.release);
255
+ if (release && !releases.has(release)) releases.set(release, { id: release, name: release.split(/[\\/]/).pop() });
256
+ const own = criteriaField(i.criteria);
257
+ const inText = own.length ? { criteria: [], rest: plainText(i.description) } : criteriaFrom(i.description);
258
+ const said = narrativeFrom(i.description);
259
+ const { as, want, soThat } = said.want ? said : narrativeFrom(i.title);
260
+ stories.push({
261
+ id: key, name: plainText(i.title), epic: parent, release,
262
+ as, want, soThat, description: said.want ? withoutNarrative(inText.rest) : inText.rest,
263
+ criteria: own.length ? own : inText.criteria,
264
+ status: plainText(i.status), priority: plainText(i.priority), points: i.points === '' ? null : i.points,
265
+ labels: String(i.labels ?? '').split(def.labelSeparator).map(l => l.trim()).filter(Boolean),
266
+ source: source(i, key), traces: [],
267
+ });
268
+ }
269
+ // A story whose parent is no epic here (a story's sub-item, an epic not exported) is under none.
270
+ for (const s of stories) if (s.epic && !epics.has(s.epic)) s.epic = '';
271
+ return { epics: [...epics.values()], releases: [...releases.values()], stories };
272
+ }
273
+ }
@@ -0,0 +1,7 @@
1
+ export { RequirementsModel } from './RequirementsModel.js';
2
+ export { RequirementsChecker } from './RequirementsChecker.js';
3
+ export { EpicsCompared, ReleasesCompared, StoriesCompared } from './RequirementsCompared.js';
4
+ export { RequirementsFileFormat } from './RequirementsFileFormat.js';
5
+ export { TrackerExportFileFormat, StoryboardFileFormat } from './RequirementsImportFormats.js';
6
+ export { TrackerExport, TRACKERS, criteriaFrom, narrativeFrom, withoutNarrative } from './TrackerExport.js';
7
+ export { Storyboard } from './Storyboard.js';
package/src/index.js CHANGED
@@ -166,6 +166,10 @@ import { buildTypeCategoryTree, buildPropertyTree } from './charting/modelTree/b
166
166
  import { DiagramTabs } from './charting/modelTree/DiagramTabs.js';
167
167
  import { DataModelNotation, DataModelLevel, AttributedLevel, ConceptualLevel, LogicalLevel, PhysicalLevel, NotationStyle, RelationshipKind, AssociationKind, GeneralizationKind, EntityRole, FactRole, DimensionRole } from './datamodel.js';
168
168
  import { DataModel, DataModelPerspective, DataModelContext, LevelSchema, LevelMapping, ServiceDomain, DataModelFileFormat, ServiceLandscape, BianLandscapeReader, BianSemanticApiReader, DataType, EnumerationType, StructuredType, NamingStandard, NameStyle, ClassificationScheme, ClassificationLens, SqlDdl, SqlDdlFileFormat, ModelScorecard, ScorecardCategory, MeasuredCategory } from './charting/datamodel/index.js';
169
+ import { RequirementsModel, RequirementsChecker, RequirementsFileFormat, TrackerExportFileFormat, StoryboardFileFormat, TrackerExport, TRACKERS, Storyboard } from './charting/requirements/index.js';
170
+ import { EditableModel } from './charting/model/EditableModel.js';
171
+ import { ModelEditCommand } from './charting/model/ModelEditCommand.js';
172
+ import { Benchmark } from './charting/model/Benchmark.js';
169
173
  import { BianNotation, BianFunctionalPattern, BianActionTerm } from './bian.js';
170
174
  import {
171
175
  ARCHI_VISUAL_OBJECTS, ARCHI_VIEW_TYPES,
@@ -224,6 +228,8 @@ export {
224
228
  buildModelTreePanel, buildAnalysisPanel, buildAnalysisData, buildTypeCategoryTree, buildPropertyTree, DiagramTabs,
225
229
  DataModelNotation, DataModelLevel, AttributedLevel, ConceptualLevel, LogicalLevel, PhysicalLevel, NotationStyle, RelationshipKind, AssociationKind, GeneralizationKind, EntityRole, FactRole, DimensionRole,
226
230
  DataModel, DataModelPerspective, DataModelContext, LevelSchema, LevelMapping, ServiceDomain, DataModelFileFormat, ServiceLandscape, BianLandscapeReader, BianSemanticApiReader, DataType, EnumerationType, StructuredType, NamingStandard, NameStyle, ClassificationScheme, ClassificationLens, SqlDdl, SqlDdlFileFormat, ModelScorecard, ScorecardCategory, MeasuredCategory,
231
+ RequirementsModel, RequirementsChecker, RequirementsFileFormat, TrackerExportFileFormat, StoryboardFileFormat, TrackerExport, TRACKERS, Storyboard,
232
+ EditableModel, ModelEditCommand, Benchmark,
227
233
  BianNotation, BianFunctionalPattern, BianActionTerm,
228
234
  exportElementsCSV, exportRelationshipsCSV, exportModelCSV,
229
235
  importElementsCSV, importRelationshipsCSV, importModelCSV,