@refrakt-md/plan 0.9.5 → 0.9.7
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/cards.d.ts +23 -0
- package/dist/cards.d.ts.map +1 -0
- package/dist/cards.js +150 -0
- package/dist/cards.js.map +1 -0
- package/dist/cli-plugin.d.ts.map +1 -1
- package/dist/cli-plugin.js +68 -5
- package/dist/cli-plugin.js.map +1 -1
- package/dist/commands/build.d.ts.map +1 -1
- package/dist/commands/build.js +13 -0
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/create.js +3 -3
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/history.d.ts +21 -0
- package/dist/commands/history.d.ts.map +1 -0
- package/dist/commands/history.js +261 -0
- package/dist/commands/history.js.map +1 -0
- package/dist/commands/init.d.ts +7 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +149 -26
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/next.d.ts.map +1 -1
- package/dist/commands/next.js +22 -6
- package/dist/commands/next.js.map +1 -1
- package/dist/commands/plan-behaviors.js +2 -1
- package/dist/commands/plan-behaviors.js.map +1 -1
- package/dist/commands/render-pipeline.d.ts.map +1 -1
- package/dist/commands/render-pipeline.js +148 -128
- package/dist/commands/render-pipeline.js.map +1 -1
- package/dist/commands/serve.d.ts.map +1 -1
- package/dist/commands/serve.js +54 -0
- package/dist/commands/serve.js.map +1 -1
- package/dist/commands/templates.js +13 -13
- package/dist/commands/templates.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +39 -11
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/validate.d.ts.map +1 -1
- package/dist/commands/validate.js +119 -1
- package/dist/commands/validate.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +86 -23
- package/dist/config.js.map +1 -1
- package/dist/diff.d.ts +35 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +88 -0
- package/dist/diff.js.map +1 -0
- package/dist/entity-tabs-behavior.d.ts +13 -0
- package/dist/entity-tabs-behavior.d.ts.map +1 -0
- package/dist/entity-tabs-behavior.js +94 -0
- package/dist/entity-tabs-behavior.js.map +1 -0
- package/dist/history.d.ts +89 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +336 -0
- package/dist/history.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline.d.ts +11 -11
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/pipeline.js +468 -202
- package/dist/pipeline.js.map +1 -1
- package/dist/relationships.d.ts +36 -0
- package/dist/relationships.d.ts.map +1 -0
- package/dist/relationships.js +128 -0
- package/dist/relationships.js.map +1 -0
- package/dist/scanner-core.d.ts +10 -0
- package/dist/scanner-core.d.ts.map +1 -0
- package/dist/scanner-core.js +230 -0
- package/dist/scanner-core.js.map +1 -0
- package/dist/scanner.d.ts +2 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +7 -124
- package/dist/scanner.js.map +1 -1
- package/dist/tags/bug.d.ts.map +1 -1
- package/dist/tags/bug.js +22 -1
- package/dist/tags/bug.js.map +1 -1
- package/dist/tags/decision.d.ts.map +1 -1
- package/dist/tags/decision.js +23 -1
- package/dist/tags/decision.js.map +1 -1
- package/dist/tags/milestone.d.ts.map +1 -1
- package/dist/tags/milestone.js +7 -4
- package/dist/tags/milestone.js.map +1 -1
- package/dist/tags/plan-history.d.ts +4 -0
- package/dist/tags/plan-history.d.ts.map +1 -0
- package/dist/tags/plan-history.js +46 -0
- package/dist/tags/plan-history.js.map +1 -0
- package/dist/tags/spec.d.ts.map +1 -1
- package/dist/tags/spec.js +4 -5
- package/dist/tags/spec.js.map +1 -1
- package/dist/tags/work.d.ts.map +1 -1
- package/dist/tags/work.js +28 -7
- package/dist/tags/work.js.map +1 -1
- package/dist/types.d.ts +20 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/util.d.ts +2 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +10 -0
- package/dist/util.js.map +1 -1
- package/package.json +26 -9
- package/styles/default.css +0 -586
- package/styles/minimal.css +0 -386
- package/styles/tokens.css +0 -13
package/dist/pipeline.js
CHANGED
|
@@ -3,15 +3,20 @@ import { BACKLOG_SENTINEL } from './tags/backlog.js';
|
|
|
3
3
|
import { DECISION_LOG_SENTINEL } from './tags/decision-log.js';
|
|
4
4
|
import { PLAN_PROGRESS_SENTINEL } from './tags/plan-progress.js';
|
|
5
5
|
import { PLAN_ACTIVITY_SENTINEL } from './tags/plan-activity.js';
|
|
6
|
+
import { PLAN_HISTORY_SENTINEL } from './tags/plan-history.js';
|
|
6
7
|
import { parseFilter, matchesFilter, sortEntities, groupEntities } from './filter.js';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { extractBatchHistory, readHistoryCache, writeHistoryCache, } from './history.js';
|
|
10
|
+
import { buildRelationships } from './relationships.js';
|
|
11
|
+
import { buildEntityCard, buildDecisionEntry } from './cards.js';
|
|
7
12
|
const { Tag } = Markdoc;
|
|
8
13
|
const PLAN_RUNE_TYPES = new Set(['spec', 'work', 'bug', 'decision', 'milestone']);
|
|
9
14
|
/** Fields to extract from each rune type's property meta tags */
|
|
10
15
|
const RUNE_FIELDS = {
|
|
11
16
|
spec: ['id', 'status', 'version', 'supersedes', 'tags', 'modified'],
|
|
12
|
-
work: ['id', 'status', 'priority', 'complexity', 'assignee', 'milestone', 'tags', 'modified'],
|
|
13
|
-
bug: ['id', 'status', 'severity', 'assignee', 'milestone', 'tags', 'modified'],
|
|
14
|
-
decision: ['id', 'status', 'date', 'supersedes', 'tags', 'modified'],
|
|
17
|
+
work: ['id', 'status', 'priority', 'complexity', 'assignee', 'milestone', 'source', 'tags', 'modified'],
|
|
18
|
+
bug: ['id', 'status', 'severity', 'assignee', 'milestone', 'source', 'tags', 'modified'],
|
|
19
|
+
decision: ['id', 'status', 'date', 'supersedes', 'source', 'tags', 'modified'],
|
|
15
20
|
milestone: ['name', 'status', 'target', 'modified'],
|
|
16
21
|
};
|
|
17
22
|
function walkTags(node, fn) {
|
|
@@ -114,125 +119,44 @@ function formatModifiedDate(value) {
|
|
|
114
119
|
return '';
|
|
115
120
|
return new Date(ts).toISOString().slice(0, 10);
|
|
116
121
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
};
|
|
129
|
-
const SEVERITY_SENTIMENT = {
|
|
130
|
-
critical: 'negative', major: 'caution', minor: 'neutral', trivial: 'neutral',
|
|
131
|
-
};
|
|
132
|
-
/** Build a metadata badge matching the dimension system output */
|
|
133
|
-
function buildMetaBadge(label, value, opts) {
|
|
134
|
-
const labelAttrs = { 'data-meta-label': '' };
|
|
135
|
-
if (opts.labelHidden)
|
|
136
|
-
labelAttrs['data-meta-label-hidden'] = '';
|
|
137
|
-
const labelEl = new Tag('span', labelAttrs, [label]);
|
|
138
|
-
const valueEl = new Tag('span', { 'data-meta-value': '' }, [value]);
|
|
139
|
-
const attrs = {
|
|
140
|
-
'data-meta-type': opts.metaType,
|
|
141
|
-
'data-meta-rank': opts.metaRank,
|
|
142
|
-
};
|
|
143
|
-
if (opts.sentiment)
|
|
144
|
-
attrs['data-meta-sentiment'] = opts.sentiment;
|
|
145
|
-
return new Tag('span', attrs, [labelEl, valueEl]);
|
|
122
|
+
/**
|
|
123
|
+
* Module-level store for dependency refs extracted from ## Dependencies sections.
|
|
124
|
+
* Maps entityId → array of dependency entity IDs.
|
|
125
|
+
* Set by render-pipeline.ts from scanner data before register() runs.
|
|
126
|
+
*/
|
|
127
|
+
const _scannerDependencies = new Map();
|
|
128
|
+
/** Set scanner dependency data for the pipeline's aggregate() hook to consume */
|
|
129
|
+
export function setScannerDependencies(deps) {
|
|
130
|
+
_scannerDependencies.clear();
|
|
131
|
+
for (const [k, v] of deps)
|
|
132
|
+
_scannerDependencies.set(k, v);
|
|
146
133
|
}
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
buildMetaBadge('ID:', id, { metaType: 'id', metaRank: 'primary', labelHidden: true }),
|
|
156
|
-
];
|
|
157
|
-
const headerRight = [];
|
|
158
|
-
const statusSentiment = type === 'work' ? WORK_STATUS_SENTIMENT[status]
|
|
159
|
-
: type === 'bug' ? BUG_STATUS_SENTIMENT[status]
|
|
160
|
-
: undefined;
|
|
161
|
-
headerRight.push(buildMetaBadge('Status:', status, { metaType: 'status', metaRank: 'primary', sentiment: statusSentiment, labelHidden: true }));
|
|
162
|
-
// Progress in header (no circle indicator)
|
|
163
|
-
const checkedCount = Number(entity.data.checkedCount ?? 0);
|
|
164
|
-
const totalCount = Number(entity.data.totalCount ?? 0);
|
|
165
|
-
if (totalCount > 0) {
|
|
166
|
-
headerRight.push(new Tag('span', {
|
|
167
|
-
class: 'rf-backlog__card-progress',
|
|
168
|
-
'data-checked': String(checkedCount),
|
|
169
|
-
'data-total': String(totalCount),
|
|
170
|
-
}, [`${checkedCount}/${totalCount}`]));
|
|
171
|
-
}
|
|
172
|
-
const header = new Tag('div', { 'data-section': 'header' }, [
|
|
173
|
-
new Tag('span', { class: 'rf-backlog__card-header-left' }, headerLeft),
|
|
174
|
-
new Tag('span', { class: 'rf-backlog__card-header-right' }, headerRight),
|
|
175
|
-
]);
|
|
176
|
-
// Body: title
|
|
177
|
-
const titleEl = new Tag('div', { 'data-section': 'title' }, [title]);
|
|
178
|
-
// Footer: secondary metadata pills
|
|
179
|
-
const footerBadges = [];
|
|
180
|
-
if (type === 'work') {
|
|
181
|
-
const priority = String(entity.data.priority ?? '');
|
|
182
|
-
const complexity = String(entity.data.complexity ?? '');
|
|
183
|
-
if (priority)
|
|
184
|
-
footerBadges.push(buildMetaBadge('Priority:', priority, { metaType: 'category', metaRank: 'secondary', sentiment: PRIORITY_SENTIMENT[priority] }));
|
|
185
|
-
if (complexity && complexity !== 'unknown')
|
|
186
|
-
footerBadges.push(buildMetaBadge('Complexity:', complexity, { metaType: 'quantity', metaRank: 'secondary' }));
|
|
187
|
-
}
|
|
188
|
-
else if (type === 'bug') {
|
|
189
|
-
const severity = String(entity.data.severity ?? '');
|
|
190
|
-
if (severity)
|
|
191
|
-
footerBadges.push(buildMetaBadge('Severity:', severity, { metaType: 'category', metaRank: 'secondary', sentiment: SEVERITY_SENTIMENT[severity] }));
|
|
192
|
-
}
|
|
193
|
-
const milestone = String(entity.data.milestone ?? '');
|
|
194
|
-
if (milestone)
|
|
195
|
-
footerBadges.push(buildMetaBadge('Milestone:', milestone, { metaType: 'tag', metaRank: 'secondary', labelHidden: true }));
|
|
196
|
-
const sections = [header, titleEl];
|
|
197
|
-
if (footerBadges.length > 0) {
|
|
198
|
-
sections.push(new Tag('div', { 'data-section': 'footer' }, footerBadges));
|
|
199
|
-
}
|
|
200
|
-
const children = entity.sourceUrl
|
|
201
|
-
? [new Tag('a', { class: 'rf-backlog__card-link', href: entity.sourceUrl }, sections)]
|
|
202
|
-
: sections;
|
|
203
|
-
return new Tag('article', {
|
|
204
|
-
class: 'rf-backlog__card',
|
|
205
|
-
'data-type': type,
|
|
206
|
-
'data-status': status,
|
|
207
|
-
'data-id': id,
|
|
208
|
-
}, children);
|
|
134
|
+
/**
|
|
135
|
+
* Module-level store for the plan directory path.
|
|
136
|
+
* Set by render-pipeline.ts before aggregate() runs.
|
|
137
|
+
*/
|
|
138
|
+
let _planDir;
|
|
139
|
+
/** Set the plan directory path for the pipeline's aggregate() hook to consume */
|
|
140
|
+
export function setPlanDir(dir) {
|
|
141
|
+
_planDir = dir;
|
|
209
142
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const innerChildren = [header, titleEl];
|
|
228
|
-
const children = entity.sourceUrl
|
|
229
|
-
? [new Tag('a', { class: 'rf-decision-log__link', href: entity.sourceUrl }, innerChildren)]
|
|
230
|
-
: innerChildren;
|
|
231
|
-
return new Tag('li', {
|
|
232
|
-
class: 'rf-decision-log__entry',
|
|
233
|
-
'data-status': status,
|
|
234
|
-
'data-id': id,
|
|
235
|
-
}, children);
|
|
143
|
+
/** Parse a comma-separated `source` attribute into typed ID references */
|
|
144
|
+
function parseSourceIds(source) {
|
|
145
|
+
if (!source)
|
|
146
|
+
return [];
|
|
147
|
+
const refs = [];
|
|
148
|
+
for (const raw of source.split(',')) {
|
|
149
|
+
const id = raw.trim();
|
|
150
|
+
if (!id)
|
|
151
|
+
continue;
|
|
152
|
+
const match = id.match(/^(WORK|SPEC|BUG|ADR)-\d+$/);
|
|
153
|
+
if (match) {
|
|
154
|
+
const type = ID_PREFIX_TO_TYPE[match[1]];
|
|
155
|
+
if (type)
|
|
156
|
+
refs.push({ id, type });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return refs;
|
|
236
160
|
}
|
|
237
161
|
/**
|
|
238
162
|
* Module-level store for ID references found during registration.
|
|
@@ -240,9 +164,16 @@ function buildDecisionEntry(entity) {
|
|
|
240
164
|
* Populated by register(), consumed by aggregate().
|
|
241
165
|
*/
|
|
242
166
|
const _idReferences = new Map();
|
|
167
|
+
/**
|
|
168
|
+
* Module-level store for structured source references (from source= attribute).
|
|
169
|
+
* Maps entityId → array of source entity IDs (with type).
|
|
170
|
+
* These produce 'implements' / 'implemented-by' relationships.
|
|
171
|
+
*/
|
|
172
|
+
const _sourceReferences = new Map();
|
|
243
173
|
export const planPipelineHooks = {
|
|
244
174
|
register(pages, registry, ctx) {
|
|
245
175
|
_idReferences.clear();
|
|
176
|
+
_sourceReferences.clear();
|
|
246
177
|
for (const page of pages) {
|
|
247
178
|
walkTags(page.renderable, (tag) => {
|
|
248
179
|
const runeType = tag.attributes['data-rune'];
|
|
@@ -277,6 +208,12 @@ export const planPipelineHooks = {
|
|
|
277
208
|
if (refs.length > 0) {
|
|
278
209
|
_idReferences.set(entityId, refs);
|
|
279
210
|
}
|
|
211
|
+
// Extract structured source references from source= attribute
|
|
212
|
+
const sourceVal = String(data.source ?? '');
|
|
213
|
+
const sourceRefs = parseSourceIds(sourceVal).filter(r => r.id !== entityId);
|
|
214
|
+
if (sourceRefs.length > 0) {
|
|
215
|
+
_sourceReferences.set(entityId, sourceRefs);
|
|
216
|
+
}
|
|
280
217
|
registry.register({
|
|
281
218
|
type: runeType,
|
|
282
219
|
id: entityId,
|
|
@@ -286,61 +223,46 @@ export const planPipelineHooks = {
|
|
|
286
223
|
});
|
|
287
224
|
}
|
|
288
225
|
},
|
|
289
|
-
aggregate(registry) {
|
|
290
|
-
// Build
|
|
291
|
-
const relationships = new Map();
|
|
292
|
-
function addRel(id, rel) {
|
|
293
|
-
if (!relationships.has(id))
|
|
294
|
-
relationships.set(id, []);
|
|
295
|
-
relationships.get(id).push(rel);
|
|
296
|
-
}
|
|
297
|
-
// Build a lookup of all registered entities for validation
|
|
226
|
+
aggregate(registry, ctx) {
|
|
227
|
+
// Build a lookup of all registered entities for relationship building
|
|
298
228
|
const allEntities = new Map();
|
|
299
229
|
for (const type of registry.getTypes()) {
|
|
300
230
|
for (const entity of registry.getAll(type)) {
|
|
301
231
|
allEntities.set(entity.id, entity);
|
|
302
232
|
}
|
|
303
233
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
addRel(ref.id, {
|
|
325
|
-
fromId: ref.id, fromType: toEntity.type,
|
|
326
|
-
toId: fromId, toType: fromEntity.type,
|
|
327
|
-
kind: 'blocks',
|
|
328
|
-
});
|
|
234
|
+
// Build bidirectional relationship index using extracted module
|
|
235
|
+
const relationships = buildRelationships(allEntities, _sourceReferences, _scannerDependencies, _idReferences);
|
|
236
|
+
// Extract git history for all entities
|
|
237
|
+
let history = new Map();
|
|
238
|
+
let repositoryUrl;
|
|
239
|
+
try {
|
|
240
|
+
const planDir = _planDir ?? 'plan';
|
|
241
|
+
const cache = readHistoryCache(planDir);
|
|
242
|
+
history = extractBatchHistory(planDir, '.', { cache });
|
|
243
|
+
writeHistoryCache(planDir, cache);
|
|
244
|
+
// Parse repository URL from git remote
|
|
245
|
+
try {
|
|
246
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
247
|
+
encoding: 'utf-8',
|
|
248
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
249
|
+
}).trim();
|
|
250
|
+
// Convert SSH URLs to HTTPS
|
|
251
|
+
const sshMatch = remoteUrl.match(/^git@([^:]+):(.+?)(?:\.git)?$/);
|
|
252
|
+
if (sshMatch) {
|
|
253
|
+
repositoryUrl = `https://${sshMatch[1]}/${sshMatch[2]}`;
|
|
329
254
|
}
|
|
330
|
-
else {
|
|
331
|
-
|
|
332
|
-
addRel(fromId, {
|
|
333
|
-
fromId, fromType: fromEntity.type,
|
|
334
|
-
toId: ref.id, toType: toEntity.type,
|
|
335
|
-
kind: 'related',
|
|
336
|
-
});
|
|
337
|
-
addRel(ref.id, {
|
|
338
|
-
fromId: ref.id, fromType: toEntity.type,
|
|
339
|
-
toId: fromId, toType: fromEntity.type,
|
|
340
|
-
kind: 'related',
|
|
341
|
-
});
|
|
255
|
+
else if (remoteUrl.startsWith('https://')) {
|
|
256
|
+
repositoryUrl = remoteUrl.replace(/\.git$/, '');
|
|
342
257
|
}
|
|
343
258
|
}
|
|
259
|
+
catch {
|
|
260
|
+
// No remote configured
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
// Git not available or not a git repo — history will be empty
|
|
265
|
+
ctx.warn(`Could not extract git history: ${err instanceof Error ? err.message : String(err)}`);
|
|
344
266
|
}
|
|
345
267
|
return {
|
|
346
268
|
workEntities: registry.getAll('work'),
|
|
@@ -349,6 +271,8 @@ export const planPipelineHooks = {
|
|
|
349
271
|
specEntities: registry.getAll('spec'),
|
|
350
272
|
milestoneEntities: registry.getAll('milestone'),
|
|
351
273
|
relationships,
|
|
274
|
+
history,
|
|
275
|
+
repositoryUrl,
|
|
352
276
|
};
|
|
353
277
|
},
|
|
354
278
|
postProcess(page, aggregated) {
|
|
@@ -377,6 +301,11 @@ export const planPipelineHooks = {
|
|
|
377
301
|
modified = true;
|
|
378
302
|
return resolvePlanActivity(tag, planData);
|
|
379
303
|
}
|
|
304
|
+
// Handle plan-history sentinel
|
|
305
|
+
if (tag.attributes['data-rune'] === 'plan-history' && hasSentinel(tag, PLAN_HISTORY_SENTINEL)) {
|
|
306
|
+
modified = true;
|
|
307
|
+
return resolvePlanHistory(tag, planData);
|
|
308
|
+
}
|
|
380
309
|
// Inject auto-backlog into milestone rune tags
|
|
381
310
|
if (tag.attributes['data-rune'] === 'milestone') {
|
|
382
311
|
const milestoneName = readField(tag, 'name');
|
|
@@ -388,7 +317,7 @@ export const planPipelineHooks = {
|
|
|
388
317
|
}
|
|
389
318
|
}
|
|
390
319
|
}
|
|
391
|
-
//
|
|
320
|
+
// Wrap entity content in tab-group with Overview / Relationships / History panels
|
|
392
321
|
if (PLAN_RUNE_TYPES.has(tag.attributes['data-rune'])) {
|
|
393
322
|
const runeType = tag.attributes['data-rune'];
|
|
394
323
|
const entityId = runeType === 'milestone'
|
|
@@ -396,12 +325,32 @@ export const planPipelineHooks = {
|
|
|
396
325
|
: readField(tag, 'id');
|
|
397
326
|
if (entityId) {
|
|
398
327
|
const rels = planData.relationships.get(entityId);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
328
|
+
const relationshipsSection = (rels && rels.length > 0)
|
|
329
|
+
? buildRelationshipsSection(rels, planData)
|
|
330
|
+
: null;
|
|
331
|
+
const historySection = buildAutoHistorySection(entityId, planData);
|
|
332
|
+
// Only add tabs if there is content for at least one extra panel
|
|
333
|
+
if (relationshipsSection || historySection) {
|
|
334
|
+
modified = true;
|
|
335
|
+
// Partition children: structural (meta fields, title, blurb) stay at top;
|
|
336
|
+
// body content goes into the Overview tab panel.
|
|
337
|
+
// Note: postProcess runs BEFORE the identity transform, so children
|
|
338
|
+
// have data-name/data-field from createComponentRenderable but no
|
|
339
|
+
// data-section or BEM classes yet.
|
|
340
|
+
const PREAMBLE_NAMES = new Set(['title', 'blurb']);
|
|
341
|
+
const structural = [];
|
|
342
|
+
const bodyContent = [];
|
|
343
|
+
for (const child of tag.children) {
|
|
344
|
+
if (Markdoc.Tag.isTag(child) && (child.attributes['data-field'] != null ||
|
|
345
|
+
PREAMBLE_NAMES.has(child.attributes['data-name']))) {
|
|
346
|
+
structural.push(child);
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
bodyContent.push(child);
|
|
350
|
+
}
|
|
404
351
|
}
|
|
352
|
+
const tabWrapper = buildEntityTabGroup(bodyContent, relationshipsSection, historySection);
|
|
353
|
+
return new Tag(tag.name, tag.attributes, [...structural, tabWrapper]);
|
|
405
354
|
}
|
|
406
355
|
}
|
|
407
356
|
}
|
|
@@ -683,8 +632,289 @@ function resolvePlanActivity(tag, data) {
|
|
|
683
632
|
newChildren.push(list);
|
|
684
633
|
return new Tag(tag.name, tag.attributes, newChildren);
|
|
685
634
|
}
|
|
686
|
-
|
|
687
|
-
|
|
635
|
+
// ─── Plan History Resolution ───
|
|
636
|
+
function formatHistoryDate(isoDate) {
|
|
637
|
+
const d = new Date(isoDate);
|
|
638
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
639
|
+
return `${months[d.getMonth()]} ${d.getDate()}`;
|
|
640
|
+
}
|
|
641
|
+
function buildAttrChangeTag(change) {
|
|
642
|
+
const children = [
|
|
643
|
+
new Tag('span', { class: 'rf-plan-history__field' }, [change.field]),
|
|
644
|
+
];
|
|
645
|
+
if (change.from !== null) {
|
|
646
|
+
children.push(new Tag('span', { class: 'rf-plan-history__value', 'data-type': 'remove' }, [change.from]));
|
|
647
|
+
}
|
|
648
|
+
if (change.from !== null && change.to !== null) {
|
|
649
|
+
children.push(new Tag('span', { class: 'rf-plan-history__arrow' }, ['→']));
|
|
650
|
+
}
|
|
651
|
+
if (change.to !== null) {
|
|
652
|
+
const prefix = change.from === null ? '+' : '';
|
|
653
|
+
children.push(new Tag('span', { class: 'rf-plan-history__value', 'data-type': 'add' }, [prefix + change.to]));
|
|
654
|
+
}
|
|
655
|
+
if (change.from !== null && change.to === null) {
|
|
656
|
+
// Removed attribute — show as removal only
|
|
657
|
+
}
|
|
658
|
+
return new Tag('span', { class: 'rf-plan-history__change' }, children);
|
|
659
|
+
}
|
|
660
|
+
function buildEventTag(event, repositoryUrl, collapseThreshold = 3) {
|
|
661
|
+
const dateTag = new Tag('time', { class: 'rf-plan-history__date' }, [formatHistoryDate(event.date)]);
|
|
662
|
+
const hashChildren = [event.shortHash];
|
|
663
|
+
const hashAttrs = { class: 'rf-plan-history__hash' };
|
|
664
|
+
if (repositoryUrl) {
|
|
665
|
+
const hashTag = new Tag('a', {
|
|
666
|
+
class: 'rf-plan-history__hash',
|
|
667
|
+
href: `${repositoryUrl}/commit/${event.hash}`,
|
|
668
|
+
}, [event.shortHash]);
|
|
669
|
+
return buildEventTagInner(event, dateTag, hashTag, collapseThreshold);
|
|
670
|
+
}
|
|
671
|
+
const hashTag = new Tag('code', hashAttrs, hashChildren);
|
|
672
|
+
return buildEventTagInner(event, dateTag, hashTag, collapseThreshold);
|
|
673
|
+
}
|
|
674
|
+
function buildEventTagInner(event, dateTag, hashTag, collapseThreshold) {
|
|
675
|
+
const changesChildren = [];
|
|
676
|
+
if (event.kind === 'created') {
|
|
677
|
+
const attrs = event.initialAttributes ?? {};
|
|
678
|
+
const parts = Object.entries(attrs)
|
|
679
|
+
.filter(([k]) => k !== 'id' && k !== 'name')
|
|
680
|
+
.map(([, v]) => v);
|
|
681
|
+
changesChildren.push(new Tag('span', { class: 'rf-plan-history__created' }, [`Created (${parts.join(', ')})`]));
|
|
682
|
+
}
|
|
683
|
+
if (event.attributeChanges) {
|
|
684
|
+
for (const change of event.attributeChanges) {
|
|
685
|
+
changesChildren.push(buildAttrChangeTag(change));
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (event.criteriaChanges) {
|
|
689
|
+
const items = event.criteriaChanges.map(c => {
|
|
690
|
+
const marker = c.action === 'checked' ? '☑' : c.action === 'unchecked' ? '☐' : c.action === 'added' ? '+' : '−';
|
|
691
|
+
return new Tag('li', { 'data-action': c.action }, [`${marker} ${c.text}`]);
|
|
692
|
+
});
|
|
693
|
+
// Collapse if over threshold
|
|
694
|
+
if (items.length > collapseThreshold) {
|
|
695
|
+
const visible = items.slice(0, collapseThreshold);
|
|
696
|
+
const remaining = items.length - collapseThreshold;
|
|
697
|
+
visible.push(new Tag('li', { class: 'rf-plan-history__more' }, [`+${remaining} more criteria`]));
|
|
698
|
+
changesChildren.push(new Tag('ul', { class: 'rf-plan-history__criteria' }, visible));
|
|
699
|
+
}
|
|
700
|
+
else {
|
|
701
|
+
changesChildren.push(new Tag('ul', { class: 'rf-plan-history__criteria' }, items));
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (event.kind === 'resolution') {
|
|
705
|
+
changesChildren.push(new Tag('span', { class: 'rf-plan-history__resolution' }, ['Resolution recorded']));
|
|
706
|
+
}
|
|
707
|
+
if (event.kind === 'content') {
|
|
708
|
+
changesChildren.push(new Tag('span', { class: 'rf-plan-history__content-edit' }, ['Content edited']));
|
|
709
|
+
}
|
|
710
|
+
const changesDiv = new Tag('div', { class: 'rf-plan-history__changes' }, changesChildren);
|
|
711
|
+
return new Tag('li', {
|
|
712
|
+
class: 'rf-plan-history__event',
|
|
713
|
+
'data-kind': event.kind,
|
|
714
|
+
}, [dateTag, hashTag, changesDiv]);
|
|
715
|
+
}
|
|
716
|
+
function resolvePlanHistory(tag, data) {
|
|
717
|
+
const entityId = readField(tag, 'id');
|
|
718
|
+
const limit = parseInt(readField(tag, 'limit') || '20', 10);
|
|
719
|
+
const typeFilter = readField(tag, 'type') || 'all';
|
|
720
|
+
const group = readField(tag, 'group') || 'commit';
|
|
721
|
+
const allEntities = [
|
|
722
|
+
...data.workEntities,
|
|
723
|
+
...data.bugEntities,
|
|
724
|
+
...data.decisionEntities,
|
|
725
|
+
...data.specEntities,
|
|
726
|
+
...data.milestoneEntities,
|
|
727
|
+
];
|
|
728
|
+
let listContent;
|
|
729
|
+
let isGlobal = false;
|
|
730
|
+
if (entityId) {
|
|
731
|
+
// Per-entity mode: find the entity's file path and look up its history
|
|
732
|
+
const entity = allEntities.find(e => e.id === entityId || e.data.name === entityId);
|
|
733
|
+
if (!entity) {
|
|
734
|
+
listContent = new Tag('ol', { 'data-name': 'events', class: 'rf-plan-history__events' }, [
|
|
735
|
+
new Tag('li', { class: 'rf-plan-history__empty' }, [`No history found for ${entityId}`]),
|
|
736
|
+
]);
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
// Find history by matching entity file path
|
|
740
|
+
let entityEvents = [];
|
|
741
|
+
for (const [file, events] of data.history) {
|
|
742
|
+
// Match by file path containing the entity ID or by checking attributes
|
|
743
|
+
if (events.length > 0 && events[0].initialAttributes) {
|
|
744
|
+
const eventId = events[0].initialAttributes.id ?? events[0].initialAttributes.name;
|
|
745
|
+
if (eventId === entityId) {
|
|
746
|
+
entityEvents = events;
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
// Reverse to newest-first, apply limit
|
|
752
|
+
const limited = [...entityEvents].reverse().slice(0, limit);
|
|
753
|
+
const items = limited.map(e => buildEventTag(e, data.repositoryUrl));
|
|
754
|
+
listContent = new Tag('ol', { 'data-name': 'events', class: 'rf-plan-history__events' }, items.length > 0 ? items : [new Tag('li', { class: 'rf-plan-history__empty' }, ['No history available'])]);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
// Global feed mode
|
|
759
|
+
isGlobal = true;
|
|
760
|
+
const typeSet = typeFilter !== 'all' ? new Set(typeFilter.split(',').map(t => t.trim())) : null;
|
|
761
|
+
const entityByFile = new Map(allEntities.map(e => {
|
|
762
|
+
// Try to find the file path from history keys
|
|
763
|
+
const filePath = e.data.file;
|
|
764
|
+
return [filePath ?? e.id, e];
|
|
765
|
+
}));
|
|
766
|
+
// Group events by commit
|
|
767
|
+
const commitMap = new Map();
|
|
768
|
+
for (const [file, events] of data.history) {
|
|
769
|
+
// Determine entity type for filtering
|
|
770
|
+
const firstEvent = events[0];
|
|
771
|
+
const initialType = firstEvent?.initialAttributes?.id?.split('-')[0]?.toLowerCase();
|
|
772
|
+
const entityTypeMap = { work: 'work', spec: 'spec', bug: 'bug', adr: 'decision' };
|
|
773
|
+
const entityType = entityTypeMap[initialType ?? ''];
|
|
774
|
+
if (typeSet && entityType && !typeSet.has(entityType))
|
|
775
|
+
continue;
|
|
776
|
+
const entityId = firstEvent?.initialAttributes?.id ?? firstEvent?.initialAttributes?.name ?? file;
|
|
777
|
+
for (const event of events) {
|
|
778
|
+
if (event.kind === 'content')
|
|
779
|
+
continue; // Skip content events in global feed
|
|
780
|
+
let commitGroup = commitMap.get(event.hash);
|
|
781
|
+
if (!commitGroup) {
|
|
782
|
+
commitGroup = {
|
|
783
|
+
hash: event.hash,
|
|
784
|
+
shortHash: event.shortHash,
|
|
785
|
+
date: event.date,
|
|
786
|
+
message: event.message,
|
|
787
|
+
entities: [],
|
|
788
|
+
};
|
|
789
|
+
commitMap.set(event.hash, commitGroup);
|
|
790
|
+
}
|
|
791
|
+
commitGroup.entities.push({ id: String(entityId), event });
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// Sort commits newest-first, apply limit
|
|
795
|
+
const sortedCommits = [...commitMap.values()]
|
|
796
|
+
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
|
797
|
+
.slice(0, limit);
|
|
798
|
+
const commitItems = sortedCommits.map(commit => {
|
|
799
|
+
const dateTag = new Tag('time', { class: 'rf-plan-history__date' }, [formatHistoryDate(commit.date)]);
|
|
800
|
+
const hashAttrs = { class: 'rf-plan-history__hash' };
|
|
801
|
+
let hashTag;
|
|
802
|
+
if (data.repositoryUrl) {
|
|
803
|
+
hashTag = new Tag('a', {
|
|
804
|
+
class: 'rf-plan-history__hash',
|
|
805
|
+
href: `${data.repositoryUrl}/commit/${commit.hash}`,
|
|
806
|
+
}, [commit.shortHash]);
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
hashTag = new Tag('code', hashAttrs, [commit.shortHash]);
|
|
810
|
+
}
|
|
811
|
+
const messageTag = new Tag('span', { class: 'rf-plan-history__commit-message' }, [commit.message]);
|
|
812
|
+
const entitySummaries = commit.entities.map(({ id, event }) => {
|
|
813
|
+
const parts = [];
|
|
814
|
+
if (event.kind === 'created') {
|
|
815
|
+
const attrs = event.initialAttributes ?? {};
|
|
816
|
+
const vals = Object.entries(attrs).filter(([k]) => k !== 'id' && k !== 'name').map(([, v]) => v);
|
|
817
|
+
parts.push(`Created (${vals.join(', ')})`);
|
|
818
|
+
}
|
|
819
|
+
if (event.attributeChanges) {
|
|
820
|
+
for (const c of event.attributeChanges) {
|
|
821
|
+
if (c.from === null)
|
|
822
|
+
parts.push(`${c.field}: +${c.to}`);
|
|
823
|
+
else if (c.to === null)
|
|
824
|
+
parts.push(`${c.field}: -${c.from}`);
|
|
825
|
+
else
|
|
826
|
+
parts.push(`${c.field}: ${c.from} → ${c.to}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (event.criteriaChanges && event.criteriaChanges.length > 0) {
|
|
830
|
+
const checked = event.criteriaChanges.filter(c => c.action === 'checked').length;
|
|
831
|
+
const total = event.criteriaChanges.length;
|
|
832
|
+
parts.push(`☑ ${checked}/${total}`);
|
|
833
|
+
}
|
|
834
|
+
if (event.kind === 'resolution')
|
|
835
|
+
parts.push('Resolution recorded');
|
|
836
|
+
return new Tag('div', { class: 'rf-plan-history__entity-summary' }, [
|
|
837
|
+
new Tag('span', { class: 'rf-plan-history__entity-id' }, [id]),
|
|
838
|
+
new Tag('span', { class: 'rf-plan-history__entity-changes' }, [parts.join(', ')]),
|
|
839
|
+
]);
|
|
840
|
+
});
|
|
841
|
+
return new Tag('li', { class: 'rf-plan-history__event' }, [
|
|
842
|
+
dateTag, hashTag, messageTag,
|
|
843
|
+
new Tag('div', { class: 'rf-plan-history__changes' }, entitySummaries),
|
|
844
|
+
]);
|
|
845
|
+
});
|
|
846
|
+
listContent = new Tag('ol', { 'data-name': 'events', class: 'rf-plan-history__events' }, commitItems.length > 0 ? commitItems : [new Tag('li', { class: 'rf-plan-history__empty' }, ['No history available'])]);
|
|
847
|
+
}
|
|
848
|
+
const attrs = { ...tag.attributes };
|
|
849
|
+
if (isGlobal) {
|
|
850
|
+
attrs.class = ((attrs.class ?? '') + ' rf-plan-history--global').trim();
|
|
851
|
+
}
|
|
852
|
+
const newChildren = tag.children.filter((c) => !(Markdoc.Tag.isTag(c) && (c.attributes['data-field'] === PLAN_HISTORY_SENTINEL ||
|
|
853
|
+
c.attributes['data-name'] === 'events' ||
|
|
854
|
+
c.attributes['data-name'] === 'items')));
|
|
855
|
+
newChildren.push(listContent);
|
|
856
|
+
return new Tag(tag.name, attrs, newChildren);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Build a tab-group wrapper for entity pages with Overview, Relationships, and History panels.
|
|
860
|
+
* Emits the same HTML contract that tabsBehavior expects.
|
|
861
|
+
*/
|
|
862
|
+
function buildEntityTabGroup(bodyContent, relationshipsSection, historySection) {
|
|
863
|
+
const tabButtons = [];
|
|
864
|
+
const tabPanels = [];
|
|
865
|
+
// Overview tab (always present)
|
|
866
|
+
tabButtons.push(new Tag('button', {
|
|
867
|
+
role: 'tab',
|
|
868
|
+
class: 'rf-plan-entity-tabs__tab',
|
|
869
|
+
'data-tab': 'overview',
|
|
870
|
+
}, ['Overview']));
|
|
871
|
+
tabPanels.push(new Tag('div', {
|
|
872
|
+
role: 'tabpanel',
|
|
873
|
+
class: 'rf-plan-entity-tabs__panel',
|
|
874
|
+
'data-tab': 'overview',
|
|
875
|
+
}, bodyContent));
|
|
876
|
+
// Relationships tab (only if there are relationships)
|
|
877
|
+
if (relationshipsSection) {
|
|
878
|
+
tabButtons.push(new Tag('button', {
|
|
879
|
+
role: 'tab',
|
|
880
|
+
class: 'rf-plan-entity-tabs__tab',
|
|
881
|
+
'data-tab': 'relationships',
|
|
882
|
+
}, ['Relationships']));
|
|
883
|
+
tabPanels.push(new Tag('div', {
|
|
884
|
+
role: 'tabpanel',
|
|
885
|
+
class: 'rf-plan-entity-tabs__panel',
|
|
886
|
+
'data-tab': 'relationships',
|
|
887
|
+
}, [relationshipsSection]));
|
|
888
|
+
}
|
|
889
|
+
// History tab (only if there is history)
|
|
890
|
+
if (historySection) {
|
|
891
|
+
tabButtons.push(new Tag('button', {
|
|
892
|
+
role: 'tab',
|
|
893
|
+
class: 'rf-plan-entity-tabs__tab',
|
|
894
|
+
'data-tab': 'history',
|
|
895
|
+
}, ['History']));
|
|
896
|
+
tabPanels.push(new Tag('div', {
|
|
897
|
+
role: 'tabpanel',
|
|
898
|
+
class: 'rf-plan-entity-tabs__panel',
|
|
899
|
+
'data-tab': 'history',
|
|
900
|
+
}, [historySection]));
|
|
901
|
+
}
|
|
902
|
+
const tabList = new Tag('div', {
|
|
903
|
+
'data-name': 'tabs',
|
|
904
|
+
role: 'tablist',
|
|
905
|
+
class: 'rf-plan-entity-tabs__tabs',
|
|
906
|
+
}, tabButtons);
|
|
907
|
+
const panels = new Tag('div', {
|
|
908
|
+
'data-name': 'panels',
|
|
909
|
+
class: 'rf-plan-entity-tabs__panels',
|
|
910
|
+
}, tabPanels);
|
|
911
|
+
return new Tag('div', {
|
|
912
|
+
class: 'rf-plan-entity-tabs',
|
|
913
|
+
'data-rune': 'plan-entity-tabs',
|
|
914
|
+
}, [tabList, panels]);
|
|
915
|
+
}
|
|
916
|
+
const KIND_ORDER = { 'blocked-by': 0, 'blocks': 1, 'depends-on': 2, 'dependency-of': 3, 'implements': 4, 'implemented-by': 5, 'informs': 6, 'informed-by': 7, 'related': 8 };
|
|
917
|
+
const KIND_LABELS = { 'blocked-by': 'Blocked by', 'blocks': 'Blocks', 'depends-on': 'Depends on', 'dependency-of': 'Dependency of', 'implements': 'Implements', 'implemented-by': 'Implemented by', 'informs': 'Informs', 'informed-by': 'Decisions', 'related': 'Related' };
|
|
688
918
|
/** Look up an entity across all aggregated type arrays */
|
|
689
919
|
function findEntity(id, data) {
|
|
690
920
|
const allArrays = [data.workEntities, data.bugEntities, data.decisionEntities, data.specEntities, data.milestoneEntities];
|
|
@@ -720,37 +950,42 @@ function buildRelationshipsSection(rels, data) {
|
|
|
720
950
|
for (const kind of sortedKinds) {
|
|
721
951
|
const kindRels = byKind.get(kind);
|
|
722
952
|
const label = KIND_LABELS[kind] || kind;
|
|
723
|
-
|
|
953
|
+
// "Informed by" renders decision entry cards
|
|
954
|
+
if (kind === 'informed-by') {
|
|
955
|
+
const entries = [];
|
|
956
|
+
for (const rel of kindRels) {
|
|
957
|
+
const target = findEntity(rel.toId, data);
|
|
958
|
+
if (target) {
|
|
959
|
+
entries.push(buildDecisionEntry(target));
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (entries.length > 0) {
|
|
963
|
+
groups.push(new Tag('div', {
|
|
964
|
+
class: 'rf-plan-relationships__group',
|
|
965
|
+
'data-kind': kind,
|
|
966
|
+
}, [
|
|
967
|
+
new Tag('h3', { class: 'rf-plan-relationships__group-title' }, [label]),
|
|
968
|
+
new Tag('ol', { class: 'rf-plan-relationships__decisions' }, entries),
|
|
969
|
+
]));
|
|
970
|
+
}
|
|
971
|
+
continue;
|
|
972
|
+
}
|
|
973
|
+
const cards = [];
|
|
724
974
|
for (const rel of kindRels) {
|
|
725
|
-
const
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
class: 'rf-plan-relationships__status',
|
|
734
|
-
'data-status': status,
|
|
735
|
-
}, [status]),
|
|
736
|
-
new Tag('span', { class: 'rf-plan-relationships__type' }, [type]),
|
|
737
|
-
...(title ? [new Tag('span', { class: 'rf-plan-relationships__title' }, [title])] : []),
|
|
738
|
-
];
|
|
739
|
-
const children = target?.sourceUrl
|
|
740
|
-
? [new Tag('a', { class: 'rf-plan-relationships__link', href: target.sourceUrl }, innerChildren)]
|
|
741
|
-
: innerChildren;
|
|
742
|
-
items.push(new Tag('li', {
|
|
743
|
-
class: 'rf-plan-relationships__item',
|
|
975
|
+
const target = findEntity(rel.toId, data);
|
|
976
|
+
if (target) {
|
|
977
|
+
cards.push(buildEntityCard(target));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
if (cards.length > 0) {
|
|
981
|
+
groups.push(new Tag('div', {
|
|
982
|
+
class: 'rf-plan-relationships__group',
|
|
744
983
|
'data-kind': kind,
|
|
745
|
-
},
|
|
984
|
+
}, [
|
|
985
|
+
new Tag('h3', { class: 'rf-plan-relationships__group-title' }, [label]),
|
|
986
|
+
new Tag('div', { class: 'rf-plan-relationships__cards' }, cards),
|
|
987
|
+
]));
|
|
746
988
|
}
|
|
747
|
-
groups.push(new Tag('div', {
|
|
748
|
-
class: 'rf-plan-relationships__group',
|
|
749
|
-
'data-kind': kind,
|
|
750
|
-
}, [
|
|
751
|
-
new Tag('h3', { class: 'rf-plan-relationships__group-title' }, [label]),
|
|
752
|
-
new Tag('ul', { class: 'rf-plan-relationships__list' }, items),
|
|
753
|
-
]));
|
|
754
989
|
}
|
|
755
990
|
if (groups.length === 0)
|
|
756
991
|
return null;
|
|
@@ -762,4 +997,35 @@ function buildRelationshipsSection(rels, data) {
|
|
|
762
997
|
...groups,
|
|
763
998
|
]);
|
|
764
999
|
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Build an auto-injected History section for an entity page.
|
|
1002
|
+
* Returns null for entities with only a single commit (created and never modified).
|
|
1003
|
+
*/
|
|
1004
|
+
function buildAutoHistorySection(entityId, data) {
|
|
1005
|
+
// Find history events by matching the entity ID in the first event's attributes
|
|
1006
|
+
let entityEvents = [];
|
|
1007
|
+
for (const [, events] of data.history) {
|
|
1008
|
+
if (events.length > 0 && events[0].initialAttributes) {
|
|
1009
|
+
const eventId = events[0].initialAttributes.id ?? events[0].initialAttributes.name;
|
|
1010
|
+
if (eventId === entityId) {
|
|
1011
|
+
entityEvents = events;
|
|
1012
|
+
break;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
// Skip entities with only a single commit (creation only) — no meaningful history
|
|
1017
|
+
if (entityEvents.length <= 1)
|
|
1018
|
+
return null;
|
|
1019
|
+
// Build timeline (newest-first), limit to 20 events
|
|
1020
|
+
const limited = [...entityEvents].reverse().slice(0, 20);
|
|
1021
|
+
const items = limited.map(e => buildEventTag(e, data.repositoryUrl));
|
|
1022
|
+
const list = new Tag('ol', { class: 'rf-plan-history__events' }, items);
|
|
1023
|
+
return new Tag('section', {
|
|
1024
|
+
class: 'rf-plan-history',
|
|
1025
|
+
'data-name': 'history',
|
|
1026
|
+
}, [
|
|
1027
|
+
new Tag('h2', { class: 'rf-plan-history__heading' }, ['History']),
|
|
1028
|
+
list,
|
|
1029
|
+
]);
|
|
1030
|
+
}
|
|
765
1031
|
//# sourceMappingURL=pipeline.js.map
|