@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.
Files changed (156) hide show
  1. package/dist/cli-plugin.d.ts +12 -0
  2. package/dist/cli-plugin.d.ts.map +1 -0
  3. package/dist/cli-plugin.js +494 -0
  4. package/dist/cli-plugin.js.map +1 -0
  5. package/dist/commands/build.d.ts +14 -0
  6. package/dist/commands/build.d.ts.map +1 -0
  7. package/dist/commands/build.js +57 -0
  8. package/dist/commands/build.js.map +1 -0
  9. package/dist/commands/bundle-behaviors.d.ts +6 -0
  10. package/dist/commands/bundle-behaviors.d.ts.map +1 -0
  11. package/dist/commands/bundle-behaviors.js +24 -0
  12. package/dist/commands/bundle-behaviors.js.map +1 -0
  13. package/dist/commands/create.d.ts +21 -0
  14. package/dist/commands/create.d.ts.map +1 -0
  15. package/dist/commands/create.js +50 -0
  16. package/dist/commands/create.js.map +1 -0
  17. package/dist/commands/init.d.ts +17 -0
  18. package/dist/commands/init.d.ts.map +1 -0
  19. package/dist/commands/init.js +109 -0
  20. package/dist/commands/init.js.map +1 -0
  21. package/dist/commands/next.d.ts +34 -0
  22. package/dist/commands/next.d.ts.map +1 -0
  23. package/dist/commands/next.js +100 -0
  24. package/dist/commands/next.js.map +1 -0
  25. package/dist/commands/plan-behaviors.d.ts +2 -0
  26. package/dist/commands/plan-behaviors.d.ts.map +1 -0
  27. package/dist/commands/plan-behaviors.js +7 -0
  28. package/dist/commands/plan-behaviors.js.map +1 -0
  29. package/dist/commands/render-pipeline.d.ts +70 -0
  30. package/dist/commands/render-pipeline.d.ts.map +1 -0
  31. package/dist/commands/render-pipeline.js +1173 -0
  32. package/dist/commands/render-pipeline.js.map +1 -0
  33. package/dist/commands/serve.d.ts +13 -0
  34. package/dist/commands/serve.d.ts.map +1 -0
  35. package/dist/commands/serve.js +167 -0
  36. package/dist/commands/serve.js.map +1 -0
  37. package/dist/commands/status.d.ts +53 -0
  38. package/dist/commands/status.d.ts.map +1 -0
  39. package/dist/commands/status.js +181 -0
  40. package/dist/commands/status.js.map +1 -0
  41. package/dist/commands/templates.d.ts +37 -0
  42. package/dist/commands/templates.d.ts.map +1 -0
  43. package/dist/commands/templates.js +160 -0
  44. package/dist/commands/templates.js.map +1 -0
  45. package/dist/commands/update.d.ts +29 -0
  46. package/dist/commands/update.d.ts.map +1 -0
  47. package/dist/commands/update.js +238 -0
  48. package/dist/commands/update.js.map +1 -0
  49. package/dist/commands/validate.d.ts +29 -0
  50. package/dist/commands/validate.d.ts.map +1 -0
  51. package/dist/commands/validate.js +298 -0
  52. package/dist/commands/validate.js.map +1 -0
  53. package/dist/config.d.ts +3 -0
  54. package/dist/config.d.ts.map +1 -0
  55. package/dist/config.js +163 -0
  56. package/dist/config.js.map +1 -0
  57. package/dist/filter.d.ts +17 -0
  58. package/dist/filter.d.ts.map +1 -0
  59. package/dist/filter.js +72 -0
  60. package/dist/filter.js.map +1 -0
  61. package/dist/index.d.ts +7 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +144 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/pipeline.d.ts +23 -0
  66. package/dist/pipeline.d.ts.map +1 -0
  67. package/dist/pipeline.js +720 -0
  68. package/dist/pipeline.js.map +1 -0
  69. package/dist/scanner.d.ts +9 -0
  70. package/dist/scanner.d.ts.map +1 -0
  71. package/dist/scanner.js +234 -0
  72. package/dist/scanner.js.map +1 -0
  73. package/dist/schema/backlog.d.ts +7 -0
  74. package/dist/schema/backlog.d.ts.map +1 -0
  75. package/dist/schema/backlog.js +9 -0
  76. package/dist/schema/backlog.js.map +1 -0
  77. package/dist/schema/bug.d.ts +9 -0
  78. package/dist/schema/bug.d.ts.map +1 -0
  79. package/dist/schema/bug.js +11 -0
  80. package/dist/schema/bug.js.map +1 -0
  81. package/dist/schema/decision-log.d.ts +5 -0
  82. package/dist/schema/decision-log.d.ts.map +1 -0
  83. package/dist/schema/decision-log.js +7 -0
  84. package/dist/schema/decision-log.js.map +1 -0
  85. package/dist/schema/decision.d.ts +8 -0
  86. package/dist/schema/decision.d.ts.map +1 -0
  87. package/dist/schema/decision.js +10 -0
  88. package/dist/schema/decision.js.map +1 -0
  89. package/dist/schema/milestone.d.ts +6 -0
  90. package/dist/schema/milestone.d.ts.map +1 -0
  91. package/dist/schema/milestone.js +8 -0
  92. package/dist/schema/milestone.js.map +1 -0
  93. package/dist/schema/plan-activity.d.ts +4 -0
  94. package/dist/schema/plan-activity.d.ts.map +1 -0
  95. package/dist/schema/plan-activity.js +6 -0
  96. package/dist/schema/plan-activity.js.map +1 -0
  97. package/dist/schema/plan-progress.d.ts +4 -0
  98. package/dist/schema/plan-progress.d.ts.map +1 -0
  99. package/dist/schema/plan-progress.js +6 -0
  100. package/dist/schema/plan-progress.js.map +1 -0
  101. package/dist/schema/spec.d.ts +8 -0
  102. package/dist/schema/spec.d.ts.map +1 -0
  103. package/dist/schema/spec.js +10 -0
  104. package/dist/schema/spec.js.map +1 -0
  105. package/dist/schema/work.d.ts +10 -0
  106. package/dist/schema/work.d.ts.map +1 -0
  107. package/dist/schema/work.js +12 -0
  108. package/dist/schema/work.js.map +1 -0
  109. package/dist/tags/backlog.d.ts +4 -0
  110. package/dist/tags/backlog.d.ts.map +1 -0
  111. package/dist/tags/backlog.js +41 -0
  112. package/dist/tags/backlog.js.map +1 -0
  113. package/dist/tags/bug.d.ts +3 -0
  114. package/dist/tags/bug.d.ts.map +1 -0
  115. package/dist/tags/bug.js +58 -0
  116. package/dist/tags/bug.js.map +1 -0
  117. package/dist/tags/decision-log.d.ts +4 -0
  118. package/dist/tags/decision-log.d.ts.map +1 -0
  119. package/dist/tags/decision-log.js +35 -0
  120. package/dist/tags/decision-log.js.map +1 -0
  121. package/dist/tags/decision.d.ts +3 -0
  122. package/dist/tags/decision.d.ts.map +1 -0
  123. package/dist/tags/decision.js +54 -0
  124. package/dist/tags/decision.js.map +1 -0
  125. package/dist/tags/milestone.d.ts +3 -0
  126. package/dist/tags/milestone.d.ts.map +1 -0
  127. package/dist/tags/milestone.js +52 -0
  128. package/dist/tags/milestone.js.map +1 -0
  129. package/dist/tags/plan-activity.d.ts +4 -0
  130. package/dist/tags/plan-activity.d.ts.map +1 -0
  131. package/dist/tags/plan-activity.js +31 -0
  132. package/dist/tags/plan-activity.js.map +1 -0
  133. package/dist/tags/plan-progress.d.ts +4 -0
  134. package/dist/tags/plan-progress.d.ts.map +1 -0
  135. package/dist/tags/plan-progress.js +31 -0
  136. package/dist/tags/plan-progress.js.map +1 -0
  137. package/dist/tags/spec.d.ts +3 -0
  138. package/dist/tags/spec.d.ts.map +1 -0
  139. package/dist/tags/spec.js +57 -0
  140. package/dist/tags/spec.js.map +1 -0
  141. package/dist/tags/work.d.ts +3 -0
  142. package/dist/tags/work.d.ts.map +1 -0
  143. package/dist/tags/work.js +68 -0
  144. package/dist/tags/work.js.map +1 -0
  145. package/dist/types.d.ts +69 -0
  146. package/dist/types.d.ts.map +1 -0
  147. package/dist/types.js +22 -0
  148. package/dist/types.js.map +1 -0
  149. package/dist/util.d.ts +8 -0
  150. package/dist/util.d.ts.map +1 -0
  151. package/dist/util.js +32 -0
  152. package/dist/util.js.map +1 -0
  153. package/package.json +47 -0
  154. package/styles/default.css +580 -0
  155. package/styles/minimal.css +379 -0
  156. package/styles/tokens.css +13 -0
@@ -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