@refrakt-md/plan 0.9.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/dist/cli-plugin.d.ts +12 -0
- package/dist/cli-plugin.d.ts.map +1 -0
- package/dist/cli-plugin.js +494 -0
- package/dist/cli-plugin.js.map +1 -0
- package/dist/commands/build.d.ts +14 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +57 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/bundle-behaviors.d.ts +6 -0
- package/dist/commands/bundle-behaviors.d.ts.map +1 -0
- package/dist/commands/bundle-behaviors.js +24 -0
- package/dist/commands/bundle-behaviors.js.map +1 -0
- package/dist/commands/create.d.ts +21 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +50 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/init.d.ts +17 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +109 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/next.d.ts +34 -0
- package/dist/commands/next.d.ts.map +1 -0
- package/dist/commands/next.js +100 -0
- package/dist/commands/next.js.map +1 -0
- package/dist/commands/plan-behaviors.d.ts +2 -0
- package/dist/commands/plan-behaviors.d.ts.map +1 -0
- package/dist/commands/plan-behaviors.js +7 -0
- package/dist/commands/plan-behaviors.js.map +1 -0
- package/dist/commands/render-pipeline.d.ts +70 -0
- package/dist/commands/render-pipeline.d.ts.map +1 -0
- package/dist/commands/render-pipeline.js +1173 -0
- package/dist/commands/render-pipeline.js.map +1 -0
- package/dist/commands/serve.d.ts +13 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +167 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/status.d.ts +53 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +181 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/templates.d.ts +37 -0
- package/dist/commands/templates.d.ts.map +1 -0
- package/dist/commands/templates.js +160 -0
- package/dist/commands/templates.js.map +1 -0
- package/dist/commands/update.d.ts +29 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +238 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/validate.d.ts +29 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +298 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +163 -0
- package/dist/config.js.map +1 -0
- package/dist/filter.d.ts +17 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +72 -0
- package/dist/filter.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +144 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline.d.ts +23 -0
- package/dist/pipeline.d.ts.map +1 -0
- package/dist/pipeline.js +720 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/scanner.d.ts +9 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +234 -0
- package/dist/scanner.js.map +1 -0
- package/dist/schema/backlog.d.ts +7 -0
- package/dist/schema/backlog.d.ts.map +1 -0
- package/dist/schema/backlog.js +9 -0
- package/dist/schema/backlog.js.map +1 -0
- package/dist/schema/bug.d.ts +9 -0
- package/dist/schema/bug.d.ts.map +1 -0
- package/dist/schema/bug.js +11 -0
- package/dist/schema/bug.js.map +1 -0
- package/dist/schema/decision-log.d.ts +5 -0
- package/dist/schema/decision-log.d.ts.map +1 -0
- package/dist/schema/decision-log.js +7 -0
- package/dist/schema/decision-log.js.map +1 -0
- package/dist/schema/decision.d.ts +8 -0
- package/dist/schema/decision.d.ts.map +1 -0
- package/dist/schema/decision.js +10 -0
- package/dist/schema/decision.js.map +1 -0
- package/dist/schema/milestone.d.ts +6 -0
- package/dist/schema/milestone.d.ts.map +1 -0
- package/dist/schema/milestone.js +8 -0
- package/dist/schema/milestone.js.map +1 -0
- package/dist/schema/plan-activity.d.ts +4 -0
- package/dist/schema/plan-activity.d.ts.map +1 -0
- package/dist/schema/plan-activity.js +6 -0
- package/dist/schema/plan-activity.js.map +1 -0
- package/dist/schema/plan-progress.d.ts +4 -0
- package/dist/schema/plan-progress.d.ts.map +1 -0
- package/dist/schema/plan-progress.js +6 -0
- package/dist/schema/plan-progress.js.map +1 -0
- package/dist/schema/spec.d.ts +8 -0
- package/dist/schema/spec.d.ts.map +1 -0
- package/dist/schema/spec.js +10 -0
- package/dist/schema/spec.js.map +1 -0
- package/dist/schema/work.d.ts +10 -0
- package/dist/schema/work.d.ts.map +1 -0
- package/dist/schema/work.js +12 -0
- package/dist/schema/work.js.map +1 -0
- package/dist/tags/backlog.d.ts +4 -0
- package/dist/tags/backlog.d.ts.map +1 -0
- package/dist/tags/backlog.js +41 -0
- package/dist/tags/backlog.js.map +1 -0
- package/dist/tags/bug.d.ts +3 -0
- package/dist/tags/bug.d.ts.map +1 -0
- package/dist/tags/bug.js +58 -0
- package/dist/tags/bug.js.map +1 -0
- package/dist/tags/decision-log.d.ts +4 -0
- package/dist/tags/decision-log.d.ts.map +1 -0
- package/dist/tags/decision-log.js +35 -0
- package/dist/tags/decision-log.js.map +1 -0
- package/dist/tags/decision.d.ts +3 -0
- package/dist/tags/decision.d.ts.map +1 -0
- package/dist/tags/decision.js +54 -0
- package/dist/tags/decision.js.map +1 -0
- package/dist/tags/milestone.d.ts +3 -0
- package/dist/tags/milestone.d.ts.map +1 -0
- package/dist/tags/milestone.js +52 -0
- package/dist/tags/milestone.js.map +1 -0
- package/dist/tags/plan-activity.d.ts +4 -0
- package/dist/tags/plan-activity.d.ts.map +1 -0
- package/dist/tags/plan-activity.js +31 -0
- package/dist/tags/plan-activity.js.map +1 -0
- package/dist/tags/plan-progress.d.ts +4 -0
- package/dist/tags/plan-progress.d.ts.map +1 -0
- package/dist/tags/plan-progress.js +31 -0
- package/dist/tags/plan-progress.js.map +1 -0
- package/dist/tags/spec.d.ts +3 -0
- package/dist/tags/spec.d.ts.map +1 -0
- package/dist/tags/spec.js +57 -0
- package/dist/tags/spec.js.map +1 -0
- package/dist/tags/work.d.ts +3 -0
- package/dist/tags/work.d.ts.map +1 -0
- package/dist/tags/work.js +68 -0
- package/dist/tags/work.js.map +1 -0
- package/dist/types.d.ts +69 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +22 -0
- package/dist/types.js.map +1 -0
- package/dist/util.d.ts +8 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +32 -0
- package/dist/util.js.map +1 -0
- package/package.json +47 -0
- package/styles/default.css +580 -0
- package/styles/minimal.css +379 -0
- package/styles/tokens.css +13 -0
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,720 @@
|
|
|
1
|
+
import Markdoc from '@markdoc/markdoc';
|
|
2
|
+
import { BACKLOG_SENTINEL } from './tags/backlog.js';
|
|
3
|
+
import { DECISION_LOG_SENTINEL } from './tags/decision-log.js';
|
|
4
|
+
import { PLAN_PROGRESS_SENTINEL } from './tags/plan-progress.js';
|
|
5
|
+
import { PLAN_ACTIVITY_SENTINEL } from './tags/plan-activity.js';
|
|
6
|
+
import { parseFilter, matchesFilter, sortEntities, groupEntities } from './filter.js';
|
|
7
|
+
const { Tag } = Markdoc;
|
|
8
|
+
const PLAN_RUNE_TYPES = new Set(['spec', 'work', 'bug', 'decision', 'milestone']);
|
|
9
|
+
/** Fields to extract from each rune type's property meta tags */
|
|
10
|
+
const RUNE_FIELDS = {
|
|
11
|
+
spec: ['id', 'status', 'version', 'supersedes', 'tags'],
|
|
12
|
+
work: ['id', 'status', 'priority', 'complexity', 'assignee', 'milestone', 'tags'],
|
|
13
|
+
bug: ['id', 'status', 'severity', 'assignee', 'milestone', 'tags'],
|
|
14
|
+
decision: ['id', 'status', 'date', 'supersedes', 'tags'],
|
|
15
|
+
milestone: ['name', 'status', 'target'],
|
|
16
|
+
};
|
|
17
|
+
function walkTags(node, fn) {
|
|
18
|
+
if (Markdoc.Tag.isTag(node)) {
|
|
19
|
+
fn(node);
|
|
20
|
+
for (const child of node.children)
|
|
21
|
+
walkTags(child, fn);
|
|
22
|
+
}
|
|
23
|
+
else if (Array.isArray(node)) {
|
|
24
|
+
node.forEach(n => walkTags(n, fn));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function mapTags(node, fn) {
|
|
28
|
+
if (Markdoc.Tag.isTag(node)) {
|
|
29
|
+
const mapped = fn(node);
|
|
30
|
+
if (mapped !== node)
|
|
31
|
+
return mapped;
|
|
32
|
+
const newChildren = node.children.map(c => mapTags(c, fn));
|
|
33
|
+
const changed = newChildren.some((c, i) => c !== node.children[i]);
|
|
34
|
+
return changed ? new Tag(node.name, node.attributes, newChildren) : node;
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(node))
|
|
37
|
+
return node.map(n => mapTags(n, fn));
|
|
38
|
+
return node;
|
|
39
|
+
}
|
|
40
|
+
function readField(tag, field) {
|
|
41
|
+
const meta = tag.children.find((c) => Markdoc.Tag.isTag(c) && c.attributes['data-field'] === field);
|
|
42
|
+
return Markdoc.Tag.isTag(meta) ? meta.attributes.content ?? '' : '';
|
|
43
|
+
}
|
|
44
|
+
function hasSentinel(tag, sentinel) {
|
|
45
|
+
return tag.children.some((c) => Markdoc.Tag.isTag(c) && c.attributes['data-field'] === sentinel);
|
|
46
|
+
}
|
|
47
|
+
function extractTitle(tag) {
|
|
48
|
+
for (const child of tag.children) {
|
|
49
|
+
if (!Markdoc.Tag.isTag(child))
|
|
50
|
+
continue;
|
|
51
|
+
if (child.attributes['data-name'] === 'title' || child.name === 'header') {
|
|
52
|
+
return extractTextContent(child);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
function extractTextContent(node) {
|
|
58
|
+
if (typeof node === 'string')
|
|
59
|
+
return node;
|
|
60
|
+
if (!Markdoc.Tag.isTag(node))
|
|
61
|
+
return '';
|
|
62
|
+
return node.children.map(c => extractTextContent(c)).join('');
|
|
63
|
+
}
|
|
64
|
+
/** Count checkbox items ([ ] and [x]) in a renderable tree's text content */
|
|
65
|
+
function countCheckboxes(tag) {
|
|
66
|
+
const text = extractTextContent(tag);
|
|
67
|
+
const unchecked = (text.match(/\[ \]/g) || []).length;
|
|
68
|
+
const checked = (text.match(/\[x\]/gi) || []).length;
|
|
69
|
+
return { checked, total: checked + unchecked };
|
|
70
|
+
}
|
|
71
|
+
/** Pattern matching entity ID references in content */
|
|
72
|
+
const ID_REF_PATTERN = /\b(WORK|SPEC|BUG|ADR)-(\d+)\b/g;
|
|
73
|
+
const ID_PREFIX_TO_TYPE = {
|
|
74
|
+
WORK: 'work',
|
|
75
|
+
SPEC: 'spec',
|
|
76
|
+
BUG: 'bug',
|
|
77
|
+
ADR: 'decision',
|
|
78
|
+
};
|
|
79
|
+
/** Extract all entity ID references from a tag's text content */
|
|
80
|
+
function extractIdReferences(tag) {
|
|
81
|
+
const text = extractTextContent(tag);
|
|
82
|
+
const refs = [];
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
let match;
|
|
85
|
+
ID_REF_PATTERN.lastIndex = 0;
|
|
86
|
+
while ((match = ID_REF_PATTERN.exec(text)) !== null) {
|
|
87
|
+
const id = match[0]; // e.g. "WORK-048"
|
|
88
|
+
const type = ID_PREFIX_TO_TYPE[match[1]];
|
|
89
|
+
if (type && !seen.has(id)) {
|
|
90
|
+
seen.add(id);
|
|
91
|
+
refs.push({ id, type });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return refs;
|
|
95
|
+
}
|
|
96
|
+
// ─── Sentiment maps (matching rune configs in config.ts) ───
|
|
97
|
+
const WORK_STATUS_SENTIMENT = {
|
|
98
|
+
draft: 'neutral', ready: 'neutral', 'in-progress': 'neutral',
|
|
99
|
+
review: 'caution', done: 'positive', blocked: 'negative',
|
|
100
|
+
};
|
|
101
|
+
const BUG_STATUS_SENTIMENT = {
|
|
102
|
+
reported: 'neutral', confirmed: 'caution', 'in-progress': 'neutral',
|
|
103
|
+
fixed: 'positive', wontfix: 'neutral', duplicate: 'neutral',
|
|
104
|
+
};
|
|
105
|
+
const PRIORITY_SENTIMENT = {
|
|
106
|
+
critical: 'negative', high: 'caution', medium: 'neutral', low: 'neutral',
|
|
107
|
+
};
|
|
108
|
+
const SEVERITY_SENTIMENT = {
|
|
109
|
+
critical: 'negative', major: 'caution', minor: 'neutral', trivial: 'neutral',
|
|
110
|
+
};
|
|
111
|
+
/** Build a metadata badge matching the dimension system output */
|
|
112
|
+
function buildMetaBadge(label, value, opts) {
|
|
113
|
+
const labelAttrs = { 'data-meta-label': '' };
|
|
114
|
+
if (opts.labelHidden)
|
|
115
|
+
labelAttrs['data-meta-label-hidden'] = '';
|
|
116
|
+
const labelEl = new Tag('span', labelAttrs, [label]);
|
|
117
|
+
const valueEl = new Tag('span', { 'data-meta-value': '' }, [value]);
|
|
118
|
+
const attrs = {
|
|
119
|
+
'data-meta-type': opts.metaType,
|
|
120
|
+
'data-meta-rank': opts.metaRank,
|
|
121
|
+
};
|
|
122
|
+
if (opts.sentiment)
|
|
123
|
+
attrs['data-meta-sentiment'] = opts.sentiment;
|
|
124
|
+
return new Tag('span', attrs, [labelEl, valueEl]);
|
|
125
|
+
}
|
|
126
|
+
/** Build a compact summary card Tag for a work/bug entity */
|
|
127
|
+
function buildEntityCard(entity) {
|
|
128
|
+
const id = String(entity.data.id ?? entity.id);
|
|
129
|
+
const title = String(entity.data.title ?? '');
|
|
130
|
+
const status = String(entity.data.status ?? '');
|
|
131
|
+
const type = entity.type;
|
|
132
|
+
const badges = [
|
|
133
|
+
buildMetaBadge('ID:', id, { metaType: 'id', metaRank: 'primary', labelHidden: true }),
|
|
134
|
+
];
|
|
135
|
+
if (type === 'work') {
|
|
136
|
+
badges.push(buildMetaBadge('Status:', status, { metaType: 'status', metaRank: 'primary', sentiment: WORK_STATUS_SENTIMENT[status], labelHidden: true }));
|
|
137
|
+
const priority = String(entity.data.priority ?? '');
|
|
138
|
+
const complexity = String(entity.data.complexity ?? '');
|
|
139
|
+
if (priority)
|
|
140
|
+
badges.push(buildMetaBadge('Priority:', priority, { metaType: 'category', metaRank: 'primary', sentiment: PRIORITY_SENTIMENT[priority] }));
|
|
141
|
+
if (complexity && complexity !== 'unknown')
|
|
142
|
+
badges.push(buildMetaBadge('Complexity:', complexity, { metaType: 'quantity', metaRank: 'secondary' }));
|
|
143
|
+
}
|
|
144
|
+
else if (type === 'bug') {
|
|
145
|
+
badges.push(buildMetaBadge('Status:', status, { metaType: 'status', metaRank: 'primary', sentiment: BUG_STATUS_SENTIMENT[status], labelHidden: true }));
|
|
146
|
+
const severity = String(entity.data.severity ?? '');
|
|
147
|
+
if (severity)
|
|
148
|
+
badges.push(buildMetaBadge('Severity:', severity, { metaType: 'category', metaRank: 'primary', sentiment: SEVERITY_SENTIMENT[severity] }));
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
badges.push(buildMetaBadge('Status:', status, { metaType: 'status', metaRank: 'primary', labelHidden: true }));
|
|
152
|
+
}
|
|
153
|
+
const milestone = String(entity.data.milestone ?? '');
|
|
154
|
+
if (milestone)
|
|
155
|
+
badges.push(buildMetaBadge('Milestone:', milestone, { metaType: 'tag', metaRank: 'secondary' }));
|
|
156
|
+
// Add checklist progress if available
|
|
157
|
+
const checkedCount = Number(entity.data.checkedCount ?? 0);
|
|
158
|
+
const totalCount = Number(entity.data.totalCount ?? 0);
|
|
159
|
+
if (totalCount > 0) {
|
|
160
|
+
badges.push(new Tag('span', {
|
|
161
|
+
class: 'rf-backlog__card-progress',
|
|
162
|
+
'data-checked': String(checkedCount),
|
|
163
|
+
'data-total': String(totalCount),
|
|
164
|
+
}, [`${checkedCount}/${totalCount}`]));
|
|
165
|
+
}
|
|
166
|
+
const header = new Tag('div', { 'data-section': 'header' }, badges);
|
|
167
|
+
const titleEl = new Tag('div', { 'data-section': 'title' }, [title]);
|
|
168
|
+
const children = entity.sourceUrl
|
|
169
|
+
? [new Tag('a', { class: 'rf-backlog__card-link', href: entity.sourceUrl }, [header, titleEl])]
|
|
170
|
+
: [header, titleEl];
|
|
171
|
+
return new Tag('article', {
|
|
172
|
+
class: 'rf-backlog__card',
|
|
173
|
+
'data-type': type,
|
|
174
|
+
'data-status': status,
|
|
175
|
+
'data-id': id,
|
|
176
|
+
}, children);
|
|
177
|
+
}
|
|
178
|
+
const DECISION_STATUS_SENTIMENT = {
|
|
179
|
+
proposed: 'neutral', accepted: 'positive', superseded: 'caution', deprecated: 'negative',
|
|
180
|
+
};
|
|
181
|
+
/** Build a decision log entry Tag */
|
|
182
|
+
function buildDecisionEntry(entity) {
|
|
183
|
+
const id = String(entity.data.id ?? entity.id);
|
|
184
|
+
const title = String(entity.data.title ?? '');
|
|
185
|
+
const status = String(entity.data.status ?? '');
|
|
186
|
+
const date = String(entity.data.date ?? '');
|
|
187
|
+
const badges = [
|
|
188
|
+
buildMetaBadge('ID:', id, { metaType: 'id', metaRank: 'primary', labelHidden: true }),
|
|
189
|
+
buildMetaBadge('Status:', status, { metaType: 'status', metaRank: 'primary', sentiment: DECISION_STATUS_SENTIMENT[status], labelHidden: true }),
|
|
190
|
+
];
|
|
191
|
+
if (date)
|
|
192
|
+
badges.push(buildMetaBadge('Date:', date, { metaType: 'temporal', metaRank: 'secondary' }));
|
|
193
|
+
const header = new Tag('div', { 'data-section': 'header' }, badges);
|
|
194
|
+
const titleEl = new Tag('div', { 'data-section': 'title' }, [title]);
|
|
195
|
+
const innerChildren = [header, titleEl];
|
|
196
|
+
const children = entity.sourceUrl
|
|
197
|
+
? [new Tag('a', { class: 'rf-decision-log__link', href: entity.sourceUrl }, innerChildren)]
|
|
198
|
+
: innerChildren;
|
|
199
|
+
return new Tag('li', {
|
|
200
|
+
class: 'rf-decision-log__entry',
|
|
201
|
+
'data-status': status,
|
|
202
|
+
'data-id': id,
|
|
203
|
+
}, children);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Module-level store for ID references found during registration.
|
|
207
|
+
* Maps entityId → array of referenced entity IDs (with type).
|
|
208
|
+
* Populated by register(), consumed by aggregate().
|
|
209
|
+
*/
|
|
210
|
+
const _idReferences = new Map();
|
|
211
|
+
export const planPipelineHooks = {
|
|
212
|
+
register(pages, registry, ctx) {
|
|
213
|
+
_idReferences.clear();
|
|
214
|
+
for (const page of pages) {
|
|
215
|
+
walkTags(page.renderable, (tag) => {
|
|
216
|
+
const runeType = tag.attributes['data-rune'];
|
|
217
|
+
if (!PLAN_RUNE_TYPES.has(runeType))
|
|
218
|
+
return;
|
|
219
|
+
const fields = RUNE_FIELDS[runeType];
|
|
220
|
+
if (!fields)
|
|
221
|
+
return;
|
|
222
|
+
const data = {};
|
|
223
|
+
for (const field of fields) {
|
|
224
|
+
data[field] = readField(tag, field);
|
|
225
|
+
}
|
|
226
|
+
const entityId = runeType === 'milestone'
|
|
227
|
+
? data.name
|
|
228
|
+
: data.id;
|
|
229
|
+
if (!entityId) {
|
|
230
|
+
ctx.warn(`Plan ${runeType} missing ${runeType === 'milestone' ? 'name' : 'id'} attribute`, page.url);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const title = extractTitle(tag);
|
|
234
|
+
data.title = title;
|
|
235
|
+
// Count checklist progress for work and bug items
|
|
236
|
+
if (runeType === 'work' || runeType === 'bug') {
|
|
237
|
+
const { checked, total } = countCheckboxes(tag);
|
|
238
|
+
if (total > 0) {
|
|
239
|
+
data.checkedCount = checked;
|
|
240
|
+
data.totalCount = total;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Scan content for ID references
|
|
244
|
+
const refs = extractIdReferences(tag).filter(r => r.id !== entityId);
|
|
245
|
+
if (refs.length > 0) {
|
|
246
|
+
_idReferences.set(entityId, refs);
|
|
247
|
+
}
|
|
248
|
+
registry.register({
|
|
249
|
+
type: runeType,
|
|
250
|
+
id: entityId,
|
|
251
|
+
sourceUrl: page.url,
|
|
252
|
+
data,
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
aggregate(registry) {
|
|
258
|
+
// Build bidirectional relationship index from ID references
|
|
259
|
+
const relationships = new Map();
|
|
260
|
+
function addRel(id, rel) {
|
|
261
|
+
if (!relationships.has(id))
|
|
262
|
+
relationships.set(id, []);
|
|
263
|
+
relationships.get(id).push(rel);
|
|
264
|
+
}
|
|
265
|
+
// Build a lookup of all registered entities for validation
|
|
266
|
+
const allEntities = new Map();
|
|
267
|
+
for (const type of registry.getTypes()) {
|
|
268
|
+
for (const entity of registry.getAll(type)) {
|
|
269
|
+
allEntities.set(entity.id, entity);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (const [fromId, refs] of _idReferences) {
|
|
273
|
+
const fromEntity = allEntities.get(fromId);
|
|
274
|
+
if (!fromEntity)
|
|
275
|
+
continue;
|
|
276
|
+
for (const ref of refs) {
|
|
277
|
+
const toEntity = allEntities.get(ref.id);
|
|
278
|
+
if (!toEntity)
|
|
279
|
+
continue; // Reference to unknown entity — skip
|
|
280
|
+
// Determine relationship kind
|
|
281
|
+
// If entity A has status "blocked" and references entity B, A is "blocked-by" B
|
|
282
|
+
const fromStatus = String(fromEntity.data.status ?? '');
|
|
283
|
+
const isBlockedBy = fromStatus === 'blocked';
|
|
284
|
+
if (isBlockedBy) {
|
|
285
|
+
// A is blocked by B
|
|
286
|
+
addRel(fromId, {
|
|
287
|
+
fromId, fromType: fromEntity.type,
|
|
288
|
+
toId: ref.id, toType: toEntity.type,
|
|
289
|
+
kind: 'blocked-by',
|
|
290
|
+
});
|
|
291
|
+
// B blocks A
|
|
292
|
+
addRel(ref.id, {
|
|
293
|
+
fromId: ref.id, fromType: toEntity.type,
|
|
294
|
+
toId: fromId, toType: fromEntity.type,
|
|
295
|
+
kind: 'blocks',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
// General related reference (bidirectional)
|
|
300
|
+
addRel(fromId, {
|
|
301
|
+
fromId, fromType: fromEntity.type,
|
|
302
|
+
toId: ref.id, toType: toEntity.type,
|
|
303
|
+
kind: 'related',
|
|
304
|
+
});
|
|
305
|
+
addRel(ref.id, {
|
|
306
|
+
fromId: ref.id, fromType: toEntity.type,
|
|
307
|
+
toId: fromId, toType: fromEntity.type,
|
|
308
|
+
kind: 'related',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
workEntities: registry.getAll('work'),
|
|
315
|
+
bugEntities: registry.getAll('bug'),
|
|
316
|
+
decisionEntities: registry.getAll('decision'),
|
|
317
|
+
specEntities: registry.getAll('spec'),
|
|
318
|
+
milestoneEntities: registry.getAll('milestone'),
|
|
319
|
+
relationships,
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
postProcess(page, aggregated) {
|
|
323
|
+
const planData = aggregated['plan'];
|
|
324
|
+
if (!planData)
|
|
325
|
+
return page;
|
|
326
|
+
let modified = false;
|
|
327
|
+
const newRenderable = mapTags(page.renderable, (tag) => {
|
|
328
|
+
// Handle backlog sentinel
|
|
329
|
+
if (tag.attributes['data-rune'] === 'backlog' && hasSentinel(tag, BACKLOG_SENTINEL)) {
|
|
330
|
+
modified = true;
|
|
331
|
+
return resolveBacklog(tag, planData);
|
|
332
|
+
}
|
|
333
|
+
// Handle decision-log sentinel
|
|
334
|
+
if (tag.attributes['data-rune'] === 'decision-log' && hasSentinel(tag, DECISION_LOG_SENTINEL)) {
|
|
335
|
+
modified = true;
|
|
336
|
+
return resolveDecisionLog(tag, planData);
|
|
337
|
+
}
|
|
338
|
+
// Handle plan-progress sentinel
|
|
339
|
+
if (tag.attributes['data-rune'] === 'plan-progress' && hasSentinel(tag, PLAN_PROGRESS_SENTINEL)) {
|
|
340
|
+
modified = true;
|
|
341
|
+
return resolvePlanProgress(tag, planData);
|
|
342
|
+
}
|
|
343
|
+
// Handle plan-activity sentinel
|
|
344
|
+
if (tag.attributes['data-rune'] === 'plan-activity' && hasSentinel(tag, PLAN_ACTIVITY_SENTINEL)) {
|
|
345
|
+
modified = true;
|
|
346
|
+
return resolvePlanActivity(tag, planData);
|
|
347
|
+
}
|
|
348
|
+
// Inject auto-backlog into milestone rune tags
|
|
349
|
+
if (tag.attributes['data-rune'] === 'milestone') {
|
|
350
|
+
const milestoneName = readField(tag, 'name');
|
|
351
|
+
if (milestoneName) {
|
|
352
|
+
const backlog = buildMilestoneBacklog(milestoneName, planData);
|
|
353
|
+
if (backlog) {
|
|
354
|
+
modified = true;
|
|
355
|
+
tag = new Tag(tag.name, tag.attributes, [...tag.children, backlog]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Inject relationships section into entity rune tags
|
|
360
|
+
if (PLAN_RUNE_TYPES.has(tag.attributes['data-rune'])) {
|
|
361
|
+
const runeType = tag.attributes['data-rune'];
|
|
362
|
+
const entityId = runeType === 'milestone'
|
|
363
|
+
? readField(tag, 'name')
|
|
364
|
+
: readField(tag, 'id');
|
|
365
|
+
if (entityId) {
|
|
366
|
+
const rels = planData.relationships.get(entityId);
|
|
367
|
+
if (rels && rels.length > 0) {
|
|
368
|
+
const section = buildRelationshipsSection(rels, planData);
|
|
369
|
+
if (section) {
|
|
370
|
+
modified = true;
|
|
371
|
+
return new Tag(tag.name, tag.attributes, [...tag.children, section]);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return tag;
|
|
377
|
+
});
|
|
378
|
+
if (!modified)
|
|
379
|
+
return page;
|
|
380
|
+
return { ...page, renderable: newRenderable };
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
function resolveBacklog(tag, data) {
|
|
384
|
+
const filterExpr = readField(tag, 'filter');
|
|
385
|
+
const sortField = readField(tag, 'sort') || 'priority';
|
|
386
|
+
const groupField = readField(tag, 'group');
|
|
387
|
+
const show = readField(tag, 'show') || 'all';
|
|
388
|
+
// Collect entities by type
|
|
389
|
+
// "all" defaults to work+bug for backward compatibility; other types must be explicit
|
|
390
|
+
let entities = [];
|
|
391
|
+
if (show === 'all' || show === 'work')
|
|
392
|
+
entities.push(...data.workEntities);
|
|
393
|
+
if (show === 'all' || show === 'bug')
|
|
394
|
+
entities.push(...data.bugEntities);
|
|
395
|
+
if (show === 'spec')
|
|
396
|
+
entities.push(...data.specEntities);
|
|
397
|
+
if (show === 'decision')
|
|
398
|
+
entities.push(...data.decisionEntities);
|
|
399
|
+
if (show === 'milestone')
|
|
400
|
+
entities.push(...data.milestoneEntities);
|
|
401
|
+
// Apply filter
|
|
402
|
+
const filter = parseFilter(filterExpr);
|
|
403
|
+
entities = entities.filter(e => matchesFilter(e, filter));
|
|
404
|
+
// Sort
|
|
405
|
+
entities = sortEntities(entities, sortField);
|
|
406
|
+
// Build output
|
|
407
|
+
let children;
|
|
408
|
+
if (groupField) {
|
|
409
|
+
const groups = groupEntities(entities, groupField);
|
|
410
|
+
children = [];
|
|
411
|
+
for (const [groupName, groupEntities_] of groups) {
|
|
412
|
+
const groupTitle = new Tag('h3', { class: 'rf-backlog__group-title' }, [groupName]);
|
|
413
|
+
const cards = groupEntities_.map(e => buildEntityCard(e));
|
|
414
|
+
const groupDiv = new Tag('div', { class: 'rf-backlog__group', 'data-group': groupName }, [groupTitle, ...cards]);
|
|
415
|
+
children.push(groupDiv);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
children = entities.map(e => buildEntityCard(e));
|
|
420
|
+
}
|
|
421
|
+
const itemsDiv = new Tag('div', { 'data-name': 'items' }, children);
|
|
422
|
+
// Rebuild tag, replacing the sentinel and placeholder with resolved content
|
|
423
|
+
const newChildren = tag.children.filter((c) => !(Markdoc.Tag.isTag(c) && (c.attributes['data-field'] === BACKLOG_SENTINEL ||
|
|
424
|
+
c.attributes['data-name'] === 'items')));
|
|
425
|
+
newChildren.push(itemsDiv);
|
|
426
|
+
return new Tag(tag.name, tag.attributes, newChildren);
|
|
427
|
+
}
|
|
428
|
+
/** Build auto-backlog section for a milestone, showing assigned work/bug items grouped by status */
|
|
429
|
+
function buildMilestoneBacklog(milestoneName, data) {
|
|
430
|
+
// Collect work and bug entities assigned to this milestone
|
|
431
|
+
let entities = [
|
|
432
|
+
...data.workEntities,
|
|
433
|
+
...data.bugEntities,
|
|
434
|
+
].filter(e => String(e.data.milestone ?? '') === milestoneName);
|
|
435
|
+
if (entities.length === 0)
|
|
436
|
+
return null;
|
|
437
|
+
// Sort by priority within each group
|
|
438
|
+
entities = sortEntities(entities, 'priority');
|
|
439
|
+
// Group by status
|
|
440
|
+
const groups = groupEntities(entities, 'status');
|
|
441
|
+
// Calculate aggregate progress from checklist counts
|
|
442
|
+
let totalChecked = 0;
|
|
443
|
+
let totalCheckboxes = 0;
|
|
444
|
+
for (const e of entities) {
|
|
445
|
+
const checked = Number(e.data.checkedCount ?? 0);
|
|
446
|
+
const total = Number(e.data.totalCount ?? 0);
|
|
447
|
+
if (total > 0) {
|
|
448
|
+
totalChecked += checked;
|
|
449
|
+
totalCheckboxes += total;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const children = [];
|
|
453
|
+
// Add aggregate progress if any items have checklists
|
|
454
|
+
if (totalCheckboxes > 0) {
|
|
455
|
+
const fraction = `${totalChecked}/${totalCheckboxes}`;
|
|
456
|
+
const pct = Math.round((totalChecked / totalCheckboxes) * 100);
|
|
457
|
+
children.push(new Tag('div', {
|
|
458
|
+
class: 'rf-milestone__progress',
|
|
459
|
+
'data-checked': String(totalChecked),
|
|
460
|
+
'data-total': String(totalCheckboxes),
|
|
461
|
+
'data-percent': String(pct),
|
|
462
|
+
}, [
|
|
463
|
+
new Tag('span', { class: 'rf-milestone__progress-label' }, [`Progress: ${fraction} criteria`]),
|
|
464
|
+
new Tag('span', { class: 'rf-milestone__progress-bar', style: `--rf-progress: ${pct}%` }, []),
|
|
465
|
+
]));
|
|
466
|
+
}
|
|
467
|
+
// Build status-grouped cards as tabs (or flat list for single group)
|
|
468
|
+
const groupEntries = [...groups.entries()];
|
|
469
|
+
if (groupEntries.length === 1) {
|
|
470
|
+
// Single status — no tabs needed, render flat
|
|
471
|
+
const [groupName, groupItems] = groupEntries[0];
|
|
472
|
+
const cards = groupItems.map(e => buildEntityCard(e));
|
|
473
|
+
children.push(new Tag('div', {
|
|
474
|
+
class: 'rf-milestone__backlog-group',
|
|
475
|
+
'data-status': groupName,
|
|
476
|
+
}, [new Tag('h3', { class: 'rf-milestone__backlog-group-label' }, [groupName]), ...cards]));
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
// Multiple statuses — render as tabs
|
|
480
|
+
const tabButtons = [];
|
|
481
|
+
const tabPanels = [];
|
|
482
|
+
for (const [groupName, groupItems] of groupEntries) {
|
|
483
|
+
const label = groupName.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
484
|
+
tabButtons.push(new Tag('button', {
|
|
485
|
+
role: 'tab',
|
|
486
|
+
class: 'rf-milestone__tab',
|
|
487
|
+
'data-status': groupName,
|
|
488
|
+
}, [`${label} (${groupItems.length})`]));
|
|
489
|
+
const cards = groupItems.map(e => buildEntityCard(e));
|
|
490
|
+
tabPanels.push(new Tag('div', {
|
|
491
|
+
role: 'tabpanel',
|
|
492
|
+
class: 'rf-milestone__panel',
|
|
493
|
+
'data-status': groupName,
|
|
494
|
+
}, cards));
|
|
495
|
+
}
|
|
496
|
+
children.push(new Tag('div', {
|
|
497
|
+
'data-name': 'tabs',
|
|
498
|
+
role: 'tablist',
|
|
499
|
+
class: 'rf-milestone__tabs',
|
|
500
|
+
}, tabButtons));
|
|
501
|
+
children.push(new Tag('div', {
|
|
502
|
+
'data-name': 'panels',
|
|
503
|
+
class: 'rf-milestone__panels',
|
|
504
|
+
}, tabPanels));
|
|
505
|
+
}
|
|
506
|
+
return new Tag('div', { class: 'rf-milestone__backlog', 'data-name': 'backlog', 'data-rune': 'milestone-backlog' }, children);
|
|
507
|
+
}
|
|
508
|
+
function resolveDecisionLog(tag, data) {
|
|
509
|
+
const filterExpr = readField(tag, 'filter');
|
|
510
|
+
const sortField = readField(tag, 'sort') || 'date';
|
|
511
|
+
let entities = [...data.decisionEntities];
|
|
512
|
+
// Apply filter
|
|
513
|
+
const filter = parseFilter(filterExpr);
|
|
514
|
+
entities = entities.filter(e => matchesFilter(e, filter));
|
|
515
|
+
// Sort
|
|
516
|
+
entities = sortEntities(entities, sortField);
|
|
517
|
+
const entries = entities.map(e => buildDecisionEntry(e));
|
|
518
|
+
const list = new Tag('ol', { 'data-name': 'items', class: 'rf-decision-log__list' }, entries);
|
|
519
|
+
// Rebuild tag
|
|
520
|
+
const newChildren = tag.children.filter((c) => !(Markdoc.Tag.isTag(c) && (c.attributes['data-field'] === DECISION_LOG_SENTINEL ||
|
|
521
|
+
c.attributes['data-name'] === 'items')));
|
|
522
|
+
newChildren.push(list);
|
|
523
|
+
return new Tag(tag.name, tag.attributes, newChildren);
|
|
524
|
+
}
|
|
525
|
+
// --- Status labels for display ---
|
|
526
|
+
const STATUS_LABELS = {
|
|
527
|
+
work: ['done', 'in-progress', 'review', 'ready', 'blocked', 'draft', 'pending'],
|
|
528
|
+
bug: ['fixed', 'in-progress', 'confirmed', 'reported', 'wontfix', 'duplicate'],
|
|
529
|
+
spec: ['accepted', 'review', 'draft', 'superseded', 'deprecated'],
|
|
530
|
+
decision: ['accepted', 'proposed', 'superseded', 'deprecated'],
|
|
531
|
+
milestone: ['complete', 'active', 'planning'],
|
|
532
|
+
};
|
|
533
|
+
const TYPE_LABELS = {
|
|
534
|
+
work: 'work items',
|
|
535
|
+
bug: 'bugs',
|
|
536
|
+
spec: 'specs',
|
|
537
|
+
decision: 'decisions',
|
|
538
|
+
milestone: 'milestones',
|
|
539
|
+
};
|
|
540
|
+
function resolvePlanProgress(tag, data) {
|
|
541
|
+
const show = readField(tag, 'show') || 'all';
|
|
542
|
+
const typeMap = {
|
|
543
|
+
work: data.workEntities,
|
|
544
|
+
bug: data.bugEntities,
|
|
545
|
+
spec: data.specEntities,
|
|
546
|
+
decision: data.decisionEntities,
|
|
547
|
+
milestone: data.milestoneEntities,
|
|
548
|
+
};
|
|
549
|
+
const types = show === 'all' ? Object.keys(typeMap) : show.split(',').map(s => s.trim());
|
|
550
|
+
const rows = [];
|
|
551
|
+
for (const type of types) {
|
|
552
|
+
const entities = typeMap[type];
|
|
553
|
+
if (!entities || entities.length === 0)
|
|
554
|
+
continue;
|
|
555
|
+
const statusCounts = new Map();
|
|
556
|
+
for (const e of entities) {
|
|
557
|
+
const status = String(e.data.status ?? 'unknown');
|
|
558
|
+
statusCounts.set(status, (statusCounts.get(status) ?? 0) + 1);
|
|
559
|
+
}
|
|
560
|
+
// Build status count spans in canonical order
|
|
561
|
+
const statusOrder = STATUS_LABELS[type] ?? [];
|
|
562
|
+
const countSpans = [];
|
|
563
|
+
for (const status of statusOrder) {
|
|
564
|
+
const count = statusCounts.get(status);
|
|
565
|
+
if (!count)
|
|
566
|
+
continue;
|
|
567
|
+
countSpans.push(new Tag('span', {
|
|
568
|
+
class: 'rf-plan-progress__count',
|
|
569
|
+
'data-status': status,
|
|
570
|
+
}, [`${count} ${status}`]));
|
|
571
|
+
}
|
|
572
|
+
// Include any statuses not in the canonical list
|
|
573
|
+
for (const [status, count] of statusCounts) {
|
|
574
|
+
if (statusOrder.includes(status))
|
|
575
|
+
continue;
|
|
576
|
+
countSpans.push(new Tag('span', {
|
|
577
|
+
class: 'rf-plan-progress__count',
|
|
578
|
+
'data-status': status,
|
|
579
|
+
}, [`${count} ${status}`]));
|
|
580
|
+
}
|
|
581
|
+
const label = TYPE_LABELS[type] ?? type;
|
|
582
|
+
const row = new Tag('div', {
|
|
583
|
+
class: 'rf-plan-progress__row',
|
|
584
|
+
'data-type': type,
|
|
585
|
+
}, [
|
|
586
|
+
new Tag('span', { class: 'rf-plan-progress__label' }, [`${entities.length} ${label}`]),
|
|
587
|
+
new Tag('span', { class: 'rf-plan-progress__counts' }, countSpans),
|
|
588
|
+
]);
|
|
589
|
+
rows.push(row);
|
|
590
|
+
}
|
|
591
|
+
const itemsDiv = new Tag('div', { 'data-name': 'items' }, rows);
|
|
592
|
+
const newChildren = tag.children.filter((c) => !(Markdoc.Tag.isTag(c) && (c.attributes['data-field'] === PLAN_PROGRESS_SENTINEL ||
|
|
593
|
+
c.attributes['data-name'] === 'items')));
|
|
594
|
+
newChildren.push(itemsDiv);
|
|
595
|
+
return new Tag(tag.name, tag.attributes, newChildren);
|
|
596
|
+
}
|
|
597
|
+
function resolvePlanActivity(tag, data) {
|
|
598
|
+
const limit = parseInt(readField(tag, 'limit') || '10', 10);
|
|
599
|
+
// Collect all entities with mtime from their source URL registration data
|
|
600
|
+
const allEntities = [
|
|
601
|
+
...data.workEntities,
|
|
602
|
+
...data.bugEntities,
|
|
603
|
+
...data.decisionEntities,
|
|
604
|
+
...data.specEntities,
|
|
605
|
+
...data.milestoneEntities,
|
|
606
|
+
];
|
|
607
|
+
// Sort by mtime descending (entities with mtime data)
|
|
608
|
+
const withMtime = allEntities
|
|
609
|
+
.filter(e => e.data.mtime != null && Number(e.data.mtime) > 0)
|
|
610
|
+
.sort((a, b) => Number(b.data.mtime) - Number(a.data.mtime))
|
|
611
|
+
.slice(0, limit);
|
|
612
|
+
const entries = withMtime.map(e => {
|
|
613
|
+
const id = String(e.data.id ?? e.id);
|
|
614
|
+
const title = String(e.data.title ?? '');
|
|
615
|
+
const status = String(e.data.status ?? '');
|
|
616
|
+
const type = e.type;
|
|
617
|
+
const mtime = Number(e.data.mtime);
|
|
618
|
+
const dateStr = new Date(mtime).toISOString().slice(0, 10);
|
|
619
|
+
const innerChildren = [
|
|
620
|
+
new Tag('time', { class: 'rf-plan-activity__date' }, [dateStr]),
|
|
621
|
+
new Tag('span', { class: 'rf-plan-activity__type' }, [type]),
|
|
622
|
+
new Tag('span', { class: 'rf-plan-activity__id' }, [id]),
|
|
623
|
+
new Tag('span', { class: 'rf-plan-activity__status', 'data-status': status }, [status]),
|
|
624
|
+
new Tag('span', { class: 'rf-plan-activity__title' }, [title]),
|
|
625
|
+
];
|
|
626
|
+
const children = e.sourceUrl
|
|
627
|
+
? [new Tag('a', { class: 'rf-plan-activity__link', href: e.sourceUrl }, innerChildren)]
|
|
628
|
+
: innerChildren;
|
|
629
|
+
return new Tag('li', {
|
|
630
|
+
class: 'rf-plan-activity__entry',
|
|
631
|
+
'data-type': type,
|
|
632
|
+
'data-status': status,
|
|
633
|
+
}, children);
|
|
634
|
+
});
|
|
635
|
+
const list = new Tag('ol', { 'data-name': 'items', class: 'rf-plan-activity__list' }, entries);
|
|
636
|
+
const newChildren = tag.children.filter((c) => !(Markdoc.Tag.isTag(c) && (c.attributes['data-field'] === PLAN_ACTIVITY_SENTINEL ||
|
|
637
|
+
c.attributes['data-name'] === 'items')));
|
|
638
|
+
newChildren.push(list);
|
|
639
|
+
return new Tag(tag.name, tag.attributes, newChildren);
|
|
640
|
+
}
|
|
641
|
+
const KIND_ORDER = { 'blocked-by': 0, 'blocks': 1, 'related': 2 };
|
|
642
|
+
const KIND_LABELS = { 'blocked-by': 'Blocked by', 'blocks': 'Blocks', 'related': 'Related' };
|
|
643
|
+
/** Look up an entity across all aggregated type arrays */
|
|
644
|
+
function findEntity(id, data) {
|
|
645
|
+
const allArrays = [data.workEntities, data.bugEntities, data.decisionEntities, data.specEntities, data.milestoneEntities];
|
|
646
|
+
for (const arr of allArrays) {
|
|
647
|
+
const found = arr.find(e => e.id === id);
|
|
648
|
+
if (found)
|
|
649
|
+
return found;
|
|
650
|
+
}
|
|
651
|
+
return undefined;
|
|
652
|
+
}
|
|
653
|
+
function buildRelationshipsSection(rels, data) {
|
|
654
|
+
// Group by kind
|
|
655
|
+
const byKind = new Map();
|
|
656
|
+
for (const rel of rels) {
|
|
657
|
+
const kind = rel.kind;
|
|
658
|
+
if (!byKind.has(kind))
|
|
659
|
+
byKind.set(kind, []);
|
|
660
|
+
byKind.get(kind).push(rel);
|
|
661
|
+
}
|
|
662
|
+
// Deduplicate: same target ID within a kind group
|
|
663
|
+
for (const [kind, kindRels] of byKind) {
|
|
664
|
+
const seen = new Set();
|
|
665
|
+
byKind.set(kind, kindRels.filter(r => {
|
|
666
|
+
const key = r.toId;
|
|
667
|
+
if (seen.has(key))
|
|
668
|
+
return false;
|
|
669
|
+
seen.add(key);
|
|
670
|
+
return true;
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
const groups = [];
|
|
674
|
+
const sortedKinds = [...byKind.keys()].sort((a, b) => (KIND_ORDER[a] ?? 9) - (KIND_ORDER[b] ?? 9));
|
|
675
|
+
for (const kind of sortedKinds) {
|
|
676
|
+
const kindRels = byKind.get(kind);
|
|
677
|
+
const label = KIND_LABELS[kind] || kind;
|
|
678
|
+
const items = [];
|
|
679
|
+
for (const rel of kindRels) {
|
|
680
|
+
const targetId = rel.toId;
|
|
681
|
+
const target = findEntity(targetId, data);
|
|
682
|
+
const title = target ? String(target.data.title ?? '') : '';
|
|
683
|
+
const status = target ? String(target.data.status ?? '') : '';
|
|
684
|
+
const type = target ? target.type : rel.toType;
|
|
685
|
+
const innerChildren = [
|
|
686
|
+
new Tag('span', { class: 'rf-plan-relationships__id' }, [targetId]),
|
|
687
|
+
new Tag('span', {
|
|
688
|
+
class: 'rf-plan-relationships__status',
|
|
689
|
+
'data-status': status,
|
|
690
|
+
}, [status]),
|
|
691
|
+
new Tag('span', { class: 'rf-plan-relationships__type' }, [type]),
|
|
692
|
+
...(title ? [new Tag('span', { class: 'rf-plan-relationships__title' }, [title])] : []),
|
|
693
|
+
];
|
|
694
|
+
const children = target?.sourceUrl
|
|
695
|
+
? [new Tag('a', { class: 'rf-plan-relationships__link', href: target.sourceUrl }, innerChildren)]
|
|
696
|
+
: innerChildren;
|
|
697
|
+
items.push(new Tag('li', {
|
|
698
|
+
class: 'rf-plan-relationships__item',
|
|
699
|
+
'data-kind': kind,
|
|
700
|
+
}, children));
|
|
701
|
+
}
|
|
702
|
+
groups.push(new Tag('div', {
|
|
703
|
+
class: 'rf-plan-relationships__group',
|
|
704
|
+
'data-kind': kind,
|
|
705
|
+
}, [
|
|
706
|
+
new Tag('h3', { class: 'rf-plan-relationships__group-title' }, [label]),
|
|
707
|
+
new Tag('ul', { class: 'rf-plan-relationships__list' }, items),
|
|
708
|
+
]));
|
|
709
|
+
}
|
|
710
|
+
if (groups.length === 0)
|
|
711
|
+
return null;
|
|
712
|
+
return new Tag('section', {
|
|
713
|
+
class: 'rf-plan-relationships',
|
|
714
|
+
'data-name': 'relationships',
|
|
715
|
+
}, [
|
|
716
|
+
new Tag('h2', { class: 'rf-plan-relationships__heading' }, ['Relationships']),
|
|
717
|
+
...groups,
|
|
718
|
+
]);
|
|
719
|
+
}
|
|
720
|
+
//# sourceMappingURL=pipeline.js.map
|