miriad-viz 0.1.0 → 0.1.2
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/chunk-ANKD3SEX.js +120 -0
- package/dist-cli/chunk-JITEBF25.js +63 -0
- package/dist-cli/chunk-O4B2276N.js +290 -0
- package/dist-cli/chunk-TJ2ZJRJV.js +168 -0
- package/dist-cli/chunk-XI2U2ON3.js +56 -0
- package/dist-cli/curate-CFY3COF2.js +424 -0
- package/dist-cli/extract-JKC5PQJ6.js +468 -0
- package/dist-cli/index.js +789 -0
- package/dist-cli/init-template-F6X3TAAM.js +8 -0
- package/dist-cli/parse-duration-NVLCEFAF.js +23 -0
- package/dist-cli/preview-5JY7W4YI.js +65 -0
- package/dist-cli/render-SZNSMBDT.js +95 -0
- package/dist-cli/transform-3TXINK7J.js +651 -0
- package/package.json +2 -2
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/cli/extract/extract-chat.ts
|
|
2
|
+
function extractMentions(content, sender, agents) {
|
|
3
|
+
const mentions = /* @__PURE__ */ new Set();
|
|
4
|
+
if (content.includes("@channel") && agents) {
|
|
5
|
+
for (const agent of agents) {
|
|
6
|
+
if (agent !== sender) mentions.add(agent);
|
|
7
|
+
}
|
|
8
|
+
return [...mentions].sort();
|
|
9
|
+
}
|
|
10
|
+
const matches = content.match(/@(\w+)/g);
|
|
11
|
+
if (!matches) return [];
|
|
12
|
+
for (const match of matches) {
|
|
13
|
+
const name = match.slice(1).toLowerCase();
|
|
14
|
+
if (name === "channel") continue;
|
|
15
|
+
if (name === sender) continue;
|
|
16
|
+
if (agents && !agents.has(name)) continue;
|
|
17
|
+
mentions.add(name);
|
|
18
|
+
}
|
|
19
|
+
return [...mentions].sort();
|
|
20
|
+
}
|
|
21
|
+
function hourKey(timestamp) {
|
|
22
|
+
return timestamp.slice(0, 13);
|
|
23
|
+
}
|
|
24
|
+
function detectBursts(messages, windowMinutes = 10, minMessages = 50) {
|
|
25
|
+
if (messages.length === 0) return [];
|
|
26
|
+
const sorted = [...messages].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
27
|
+
const bursts = [];
|
|
28
|
+
const windowMs = windowMinutes * 60 * 1e3;
|
|
29
|
+
let windowStart = 0;
|
|
30
|
+
for (let windowEnd = 0; windowEnd < sorted.length; windowEnd++) {
|
|
31
|
+
const endTime = new Date(sorted[windowEnd].timestamp).getTime();
|
|
32
|
+
while (windowStart < windowEnd && endTime - new Date(sorted[windowStart].timestamp).getTime() > windowMs) {
|
|
33
|
+
windowStart++;
|
|
34
|
+
}
|
|
35
|
+
const count = windowEnd - windowStart + 1;
|
|
36
|
+
if (count >= minMessages) {
|
|
37
|
+
const start = sorted[windowStart].timestamp;
|
|
38
|
+
const end = sorted[windowEnd].timestamp;
|
|
39
|
+
if (bursts.length === 0 || start > bursts[bursts.length - 1].end) {
|
|
40
|
+
bursts.push({ start, end, messages: count, label: "" });
|
|
41
|
+
} else {
|
|
42
|
+
const prev = bursts[bursts.length - 1];
|
|
43
|
+
prev.end = end;
|
|
44
|
+
prev.messages = Math.max(prev.messages, count);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return bursts;
|
|
49
|
+
}
|
|
50
|
+
function buildChatActivity(messages, channelName, knownAgents) {
|
|
51
|
+
if (messages.length === 0) {
|
|
52
|
+
throw new Error("No messages to process. Check channel name and extraction.");
|
|
53
|
+
}
|
|
54
|
+
const sorted = [...messages].sort((a, b) => a.timestamp.localeCompare(b.timestamp));
|
|
55
|
+
const agents = knownAgents ? new Set(knownAgents.map((a) => a.toLowerCase())) : new Set(sorted.map((m) => m.sender.toLowerCase()));
|
|
56
|
+
const agentTotals = {};
|
|
57
|
+
for (const msg of sorted) {
|
|
58
|
+
agentTotals[msg.sender] = (agentTotals[msg.sender] || 0) + 1;
|
|
59
|
+
}
|
|
60
|
+
const hourlyTotals = {};
|
|
61
|
+
for (const msg of sorted) {
|
|
62
|
+
const key = hourKey(msg.timestamp);
|
|
63
|
+
hourlyTotals[key] = (hourlyTotals[key] || 0) + 1;
|
|
64
|
+
}
|
|
65
|
+
const mentionGraph = {};
|
|
66
|
+
for (const msg of sorted) {
|
|
67
|
+
const mentions = extractMentions(msg.content, msg.sender, agents);
|
|
68
|
+
if (mentions.length === 0) continue;
|
|
69
|
+
if (!mentionGraph[msg.sender]) mentionGraph[msg.sender] = {};
|
|
70
|
+
for (const target of mentions) {
|
|
71
|
+
mentionGraph[msg.sender][target] = (mentionGraph[msg.sender][target] || 0) + 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const pairCounts = /* @__PURE__ */ new Map();
|
|
75
|
+
for (const [sender, targets] of Object.entries(mentionGraph)) {
|
|
76
|
+
for (const [target, count] of Object.entries(targets)) {
|
|
77
|
+
const key = [sender, target].sort().join("\u2194");
|
|
78
|
+
const existing = pairCounts.get(key);
|
|
79
|
+
if (existing) {
|
|
80
|
+
existing.total += count;
|
|
81
|
+
} else {
|
|
82
|
+
pairCounts.set(key, { agents: [sender, target].sort(), total: count });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const topPairs = [...pairCounts.values()].sort((a, b) => b.total - a.total).slice(0, 20);
|
|
87
|
+
const bursts = detectBursts(sorted);
|
|
88
|
+
const roleActivity = {};
|
|
89
|
+
const start = sorted[0].timestamp;
|
|
90
|
+
const end = sorted[sorted.length - 1].timestamp;
|
|
91
|
+
const hoursSpanned = (new Date(end).getTime() - new Date(start).getTime()) / (1e3 * 60 * 60);
|
|
92
|
+
return {
|
|
93
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
94
|
+
method: `miriad-viz extract-chat from channel "${channelName}"`,
|
|
95
|
+
coverage: {
|
|
96
|
+
start,
|
|
97
|
+
end,
|
|
98
|
+
hoursSpanned: Math.round(hoursSpanned * 10) / 10,
|
|
99
|
+
totalMessages: sorted.length,
|
|
100
|
+
errorMessages: sorted.filter(
|
|
101
|
+
(m) => m.content.startsWith("Error:") || m.content.startsWith("error:")
|
|
102
|
+
).length,
|
|
103
|
+
contentMessages: sorted.filter(
|
|
104
|
+
(m) => !m.content.startsWith("Error:") && !m.content.startsWith("error:")
|
|
105
|
+
).length,
|
|
106
|
+
note: `Extracted from #${channelName}`
|
|
107
|
+
},
|
|
108
|
+
agentTotals,
|
|
109
|
+
hourlyTotals,
|
|
110
|
+
mentionGraph,
|
|
111
|
+
topPairs,
|
|
112
|
+
bursts,
|
|
113
|
+
roleActivity
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export {
|
|
118
|
+
extractMentions,
|
|
119
|
+
buildChatActivity
|
|
120
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// src/cli/extract/extract-artifacts.ts
|
|
2
|
+
function buildArtifactData(artifacts, phasePatterns) {
|
|
3
|
+
const byType = {};
|
|
4
|
+
const byStatus = {};
|
|
5
|
+
for (const a of artifacts) {
|
|
6
|
+
byType[a.type] = (byType[a.type] || 0) + 1;
|
|
7
|
+
if (a.status) {
|
|
8
|
+
byStatus[a.status] = (byStatus[a.status] || 0) + 1;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const categories = {};
|
|
12
|
+
for (const a of artifacts) {
|
|
13
|
+
if (!categories[a.type]) {
|
|
14
|
+
categories[a.type] = { count: 0, items: [] };
|
|
15
|
+
}
|
|
16
|
+
categories[a.type].count++;
|
|
17
|
+
categories[a.type].items?.push({
|
|
18
|
+
slug: a.slug,
|
|
19
|
+
title: a.title,
|
|
20
|
+
...a.createdBy ? { createdBy: a.createdBy } : {}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const phases = {};
|
|
24
|
+
if (phasePatterns) {
|
|
25
|
+
for (const [phaseName, patterns] of Object.entries(phasePatterns)) {
|
|
26
|
+
const matching = artifacts.filter(
|
|
27
|
+
(a) => patterns.some(
|
|
28
|
+
(p) => a.slug.includes(p) || a.parentSlug?.includes(p) || a.title?.includes(p)
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
phases[phaseName] = {
|
|
32
|
+
artifacts: matching.map((a) => a.slug),
|
|
33
|
+
count: matching.length
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
38
|
+
for (const a of artifacts) {
|
|
39
|
+
const parent = a.parentSlug || "_root";
|
|
40
|
+
if (!byParent.has(parent)) byParent.set(parent, []);
|
|
41
|
+
byParent.get(parent).push(a);
|
|
42
|
+
}
|
|
43
|
+
for (const [parent, items] of byParent) {
|
|
44
|
+
phases[parent] = {
|
|
45
|
+
artifacts: items.map((a) => a.slug),
|
|
46
|
+
count: items.length
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
summary: {
|
|
52
|
+
total: artifacts.length,
|
|
53
|
+
byType,
|
|
54
|
+
byStatus
|
|
55
|
+
},
|
|
56
|
+
categories,
|
|
57
|
+
phases
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
buildArtifactData
|
|
63
|
+
};
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// src/transform/transform-raw.ts
|
|
2
|
+
function inferRole(roleString) {
|
|
3
|
+
const s = roleString.toLowerCase();
|
|
4
|
+
if (s.includes("human") || s === "user") return "human";
|
|
5
|
+
if (s.includes("lead")) return "lead";
|
|
6
|
+
if (s.includes("pm")) return "pm";
|
|
7
|
+
if (s.includes("tester") || s.includes("qa")) return "tester";
|
|
8
|
+
if (s.includes("reviewer") || s.includes("review")) return "reviewer";
|
|
9
|
+
if (s.includes("auditor") || s.includes("audit")) return "auditor";
|
|
10
|
+
if (s.includes("challenger") || s.includes("challenge")) return "challenger";
|
|
11
|
+
if (s.includes("scout")) return "scout";
|
|
12
|
+
if (s.includes("ideation")) return "ideation";
|
|
13
|
+
if (s.includes("research")) return "researcher";
|
|
14
|
+
if (s.includes("builder") || s.includes("build")) return "builder";
|
|
15
|
+
return "builder";
|
|
16
|
+
}
|
|
17
|
+
function transformToRawData(sources) {
|
|
18
|
+
const defaultAgent = sources.retroPageData.teamAssembly[0]?.agent.replace("@", "") ?? "unknown";
|
|
19
|
+
return {
|
|
20
|
+
project: {
|
|
21
|
+
name: sources.retroPageData.meta.title,
|
|
22
|
+
description: sources.retroPageData.meta.subtitle
|
|
23
|
+
},
|
|
24
|
+
agents: transformAgents(
|
|
25
|
+
sources.retroPageData,
|
|
26
|
+
sources.chatActivity.agentTotals,
|
|
27
|
+
sources.timelineEvents
|
|
28
|
+
),
|
|
29
|
+
commits: transformCommits(sources.commits, sources.prAgentMap, defaultAgent),
|
|
30
|
+
prs: transformPRs(sources.prs, sources.prAgentMap),
|
|
31
|
+
messages: transformMessages(sources.timelineEvents),
|
|
32
|
+
artifacts: sources.artifactData ? transformArtifacts(
|
|
33
|
+
sources.artifactData,
|
|
34
|
+
sources.retroPageData,
|
|
35
|
+
sources.prs,
|
|
36
|
+
sources.prAgentMap
|
|
37
|
+
) : []
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function buildFirstMessageTimeMap(timelineEvents) {
|
|
41
|
+
const map = /* @__PURE__ */ new Map();
|
|
42
|
+
if (!timelineEvents) return map;
|
|
43
|
+
for (const evt of timelineEvents.events) {
|
|
44
|
+
if (evt.type !== "message" && evt.type !== "beam") continue;
|
|
45
|
+
const sender = evt.from;
|
|
46
|
+
const ts = new Date(evt.t).getTime();
|
|
47
|
+
const existing = map.get(sender);
|
|
48
|
+
if (existing === void 0 || ts < existing) {
|
|
49
|
+
map.set(sender, ts);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return map;
|
|
53
|
+
}
|
|
54
|
+
function transformAgents(retroData, agentTotals, timelineEvents, agentRoles) {
|
|
55
|
+
const agents = [];
|
|
56
|
+
const seen = /* @__PURE__ */ new Set();
|
|
57
|
+
const firstMessageTime = buildFirstMessageTimeMap(timelineEvents);
|
|
58
|
+
const assemblyRoles = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const entry of retroData.teamAssembly) {
|
|
60
|
+
const id = entry.agent.replace("@", "");
|
|
61
|
+
assemblyRoles.set(id, entry.role);
|
|
62
|
+
}
|
|
63
|
+
function resolveRole(id) {
|
|
64
|
+
if (agentRoles?.[id]) {
|
|
65
|
+
return inferRole(agentRoles[id]);
|
|
66
|
+
}
|
|
67
|
+
const assemblyRole = assemblyRoles.get(id);
|
|
68
|
+
if (assemblyRole) {
|
|
69
|
+
return inferRole(assemblyRole);
|
|
70
|
+
}
|
|
71
|
+
return "builder";
|
|
72
|
+
}
|
|
73
|
+
function resolveJoinedAt(id, assemblyTime) {
|
|
74
|
+
const msgTime = firstMessageTime.get(id);
|
|
75
|
+
if (msgTime !== void 0 && assemblyTime !== void 0) {
|
|
76
|
+
return Math.min(msgTime, assemblyTime);
|
|
77
|
+
}
|
|
78
|
+
if (msgTime !== void 0) return msgTime;
|
|
79
|
+
if (assemblyTime !== void 0) return assemblyTime;
|
|
80
|
+
return new Date(retroData.meta.startDate).getTime();
|
|
81
|
+
}
|
|
82
|
+
for (const entry of retroData.teamAssembly) {
|
|
83
|
+
const id = entry.agent.replace("@", "");
|
|
84
|
+
if (seen.has(id)) continue;
|
|
85
|
+
seen.add(id);
|
|
86
|
+
agents.push({
|
|
87
|
+
id,
|
|
88
|
+
role: resolveRole(id),
|
|
89
|
+
joinedAt: resolveJoinedAt(id, new Date(entry.time).getTime())
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
for (const id of Object.keys(agentTotals)) {
|
|
93
|
+
if (seen.has(id)) continue;
|
|
94
|
+
agents.push({
|
|
95
|
+
id,
|
|
96
|
+
role: resolveRole(id),
|
|
97
|
+
joinedAt: resolveJoinedAt(id)
|
|
98
|
+
});
|
|
99
|
+
seen.add(id);
|
|
100
|
+
}
|
|
101
|
+
return agents;
|
|
102
|
+
}
|
|
103
|
+
function transformCommits(commits, prAgentMap, defaultAgent = "unknown") {
|
|
104
|
+
return commits.map((c) => {
|
|
105
|
+
const agent = resolveCommitAgent(c, prAgentMap, defaultAgent);
|
|
106
|
+
return {
|
|
107
|
+
sha: c.hash,
|
|
108
|
+
timestamp: new Date(c.date).getTime(),
|
|
109
|
+
agent,
|
|
110
|
+
message: c.subject,
|
|
111
|
+
filesChanged: c.files.length,
|
|
112
|
+
insertions: c.totalInsertions,
|
|
113
|
+
deletions: c.totalDeletions,
|
|
114
|
+
prNumber: c.prNumber ?? void 0
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function resolveCommitAgent(commit, prAgentMap, defaultAgent = "unknown") {
|
|
119
|
+
if (commit.prNumber != null) {
|
|
120
|
+
const agent = prAgentMap[String(commit.prNumber)];
|
|
121
|
+
if (agent) return agent;
|
|
122
|
+
}
|
|
123
|
+
const mergeMatch = commit.subject.match(/^Merge pull request #(\d+)/);
|
|
124
|
+
if (mergeMatch) {
|
|
125
|
+
const agent = prAgentMap[mergeMatch[1]];
|
|
126
|
+
if (agent) return agent;
|
|
127
|
+
}
|
|
128
|
+
const suffixMatch = commit.subject.match(/\(#(\d+)\)\s*$/);
|
|
129
|
+
if (suffixMatch) {
|
|
130
|
+
const agent = prAgentMap[suffixMatch[1]];
|
|
131
|
+
if (agent) return agent;
|
|
132
|
+
}
|
|
133
|
+
return defaultAgent;
|
|
134
|
+
}
|
|
135
|
+
function transformPRs(prs, prAgentMap) {
|
|
136
|
+
return prs.map((pr) => {
|
|
137
|
+
const agent = prAgentMap[String(pr.number)] ?? "unknown";
|
|
138
|
+
return {
|
|
139
|
+
number: pr.number,
|
|
140
|
+
title: pr.title,
|
|
141
|
+
agent,
|
|
142
|
+
createdAt: new Date(pr.createdAt).getTime(),
|
|
143
|
+
mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : null,
|
|
144
|
+
additions: pr.additions,
|
|
145
|
+
deletions: pr.deletions,
|
|
146
|
+
branch: pr.headRefName
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function inferArtifactType(slug) {
|
|
151
|
+
if (slug.includes(".app.") || slug.endsWith(".ts") || slug.endsWith(".js")) return "code";
|
|
152
|
+
if (slug.includes("screenshot") || slug.includes("qa-")) return "asset";
|
|
153
|
+
if (slug.includes("decision")) return "decision";
|
|
154
|
+
if (slug.includes("task") || slug.includes("phase-")) return "task";
|
|
155
|
+
if (slug.startsWith("spec-") || slug.startsWith("spec")) return "doc";
|
|
156
|
+
if (slug.includes("concept")) return "doc";
|
|
157
|
+
return "doc";
|
|
158
|
+
}
|
|
159
|
+
function resolveArtifactAgent(slug, prAgentMap, createdBy, defaultAgent) {
|
|
160
|
+
if (createdBy) return createdBy;
|
|
161
|
+
const prMatch = slug.match(/#?(\d+)/);
|
|
162
|
+
if (prMatch) {
|
|
163
|
+
const agent = prAgentMap[prMatch[1]];
|
|
164
|
+
if (agent) return agent;
|
|
165
|
+
}
|
|
166
|
+
return defaultAgent;
|
|
167
|
+
}
|
|
168
|
+
var CATEGORY_TYPE_MAP = {
|
|
169
|
+
qaScreenshots: "asset",
|
|
170
|
+
transitionSpecs: "doc",
|
|
171
|
+
transitionConcepts: "doc",
|
|
172
|
+
scenes: "doc",
|
|
173
|
+
vizConcepts: "code",
|
|
174
|
+
teamPrinciples: "doc",
|
|
175
|
+
retroData: "doc",
|
|
176
|
+
decisions: "decision"
|
|
177
|
+
};
|
|
178
|
+
function distributeTimestamps(count, bounds, prTimestamps) {
|
|
179
|
+
const phasePRTimes = prTimestamps.filter((t) => t >= bounds.start && t <= bounds.end);
|
|
180
|
+
const timestamps = [];
|
|
181
|
+
for (let i = 0; i < count; i++) {
|
|
182
|
+
let timestamp;
|
|
183
|
+
if (phasePRTimes.length > 0) {
|
|
184
|
+
const prIdx = i % phasePRTimes.length;
|
|
185
|
+
timestamp = phasePRTimes[prIdx] + i * 6e4;
|
|
186
|
+
} else {
|
|
187
|
+
const fraction = count > 1 ? i / (count - 1) : 0.5;
|
|
188
|
+
timestamp = bounds.start + fraction * (bounds.end - bounds.start);
|
|
189
|
+
}
|
|
190
|
+
timestamps.push(Math.round(Math.max(bounds.start, Math.min(bounds.end, timestamp))));
|
|
191
|
+
}
|
|
192
|
+
return timestamps;
|
|
193
|
+
}
|
|
194
|
+
function buildCategoryDistribution(categories) {
|
|
195
|
+
const entries = [];
|
|
196
|
+
let total = 0;
|
|
197
|
+
for (const [categoryName, categoryData] of Object.entries(categories)) {
|
|
198
|
+
if (categoryData.count === 0) continue;
|
|
199
|
+
entries.push({
|
|
200
|
+
type: CATEGORY_TYPE_MAP[categoryName] ?? "doc",
|
|
201
|
+
count: categoryData.count
|
|
202
|
+
});
|
|
203
|
+
total += categoryData.count;
|
|
204
|
+
}
|
|
205
|
+
if (total === 0) return [{ type: "doc", weight: 1 }];
|
|
206
|
+
return entries.sort((a, b) => b.count - a.count).map((e) => ({ type: e.type, weight: e.count / total }));
|
|
207
|
+
}
|
|
208
|
+
function buildCreatedByLookup(categories) {
|
|
209
|
+
const lookup = /* @__PURE__ */ new Map();
|
|
210
|
+
for (const categoryData of Object.values(categories)) {
|
|
211
|
+
for (const item of categoryData.items ?? []) {
|
|
212
|
+
if (typeof item === "object" && item.createdBy) {
|
|
213
|
+
lookup.set(item.slug, item.createdBy);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return lookup;
|
|
218
|
+
}
|
|
219
|
+
function transformArtifacts(artifactData, retroData, prs, prAgentMap) {
|
|
220
|
+
const artifacts = [];
|
|
221
|
+
const agentIds = retroData.teamAssembly.map((e) => e.agent.replace("@", ""));
|
|
222
|
+
const defaultAgent = agentIds[0] ?? "unknown";
|
|
223
|
+
const phaseBoundaries = retroData.phases.map((p) => ({
|
|
224
|
+
start: new Date(p.start).getTime(),
|
|
225
|
+
end: new Date(p.end).getTime()
|
|
226
|
+
}));
|
|
227
|
+
const prTimestamps = prs.map((pr) => new Date(pr.createdAt).getTime()).sort((a, b) => a - b);
|
|
228
|
+
const distribution = buildCategoryDistribution(artifactData.categories);
|
|
229
|
+
const createdByLookup = buildCreatedByLookup(artifactData.categories);
|
|
230
|
+
let phaseIndex = 0;
|
|
231
|
+
let globalBulkIndex = 0;
|
|
232
|
+
for (const [_phaseName, phaseData] of Object.entries(artifactData.phases)) {
|
|
233
|
+
if (phaseIndex >= phaseBoundaries.length) break;
|
|
234
|
+
const bounds = phaseBoundaries[phaseIndex];
|
|
235
|
+
phaseIndex++;
|
|
236
|
+
const totalCount = phaseData.count;
|
|
237
|
+
const slugs = phaseData.artifacts;
|
|
238
|
+
const bulkCount = Math.max(0, totalCount - slugs.length);
|
|
239
|
+
const timestamps = distributeTimestamps(totalCount, bounds, prTimestamps);
|
|
240
|
+
let tsIdx = 0;
|
|
241
|
+
for (const slug of slugs) {
|
|
242
|
+
artifacts.push({
|
|
243
|
+
slug,
|
|
244
|
+
type: inferArtifactType(slug),
|
|
245
|
+
createdAt: timestamps[tsIdx],
|
|
246
|
+
updatedAt: timestamps[tsIdx],
|
|
247
|
+
agent: resolveArtifactAgent(slug, prAgentMap, createdByLookup.get(slug), defaultAgent)
|
|
248
|
+
});
|
|
249
|
+
tsIdx++;
|
|
250
|
+
}
|
|
251
|
+
let distIdx = 0;
|
|
252
|
+
let distBudget = Math.round(distribution[0].weight * bulkCount);
|
|
253
|
+
for (let i = 0; i < bulkCount; i++) {
|
|
254
|
+
while (distBudget <= 0 && distIdx < distribution.length - 1) {
|
|
255
|
+
distIdx++;
|
|
256
|
+
distBudget = Math.round(distribution[distIdx].weight * bulkCount);
|
|
257
|
+
}
|
|
258
|
+
distBudget--;
|
|
259
|
+
const entry = distribution[distIdx];
|
|
260
|
+
const agent = agentIds.length > 0 ? agentIds[globalBulkIndex % agentIds.length] : defaultAgent;
|
|
261
|
+
artifacts.push({
|
|
262
|
+
slug: `phase${phaseIndex}-${entry.type}-${String(globalBulkIndex + 1).padStart(3, "0")}`,
|
|
263
|
+
type: entry.type,
|
|
264
|
+
createdAt: timestamps[tsIdx],
|
|
265
|
+
updatedAt: timestamps[tsIdx],
|
|
266
|
+
agent
|
|
267
|
+
});
|
|
268
|
+
tsIdx++;
|
|
269
|
+
globalBulkIndex++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
artifacts.sort((a, b) => a.createdAt - b.createdAt);
|
|
273
|
+
return artifacts;
|
|
274
|
+
}
|
|
275
|
+
function transformMessages(timelineEvents) {
|
|
276
|
+
const messageEvents = timelineEvents.events.filter(
|
|
277
|
+
(e) => e.type === "message" || e.type === "beam"
|
|
278
|
+
);
|
|
279
|
+
return messageEvents.map((evt, i) => ({
|
|
280
|
+
id: `msg-${String(i).padStart(4, "0")}`,
|
|
281
|
+
timestamp: new Date(evt.t).getTime(),
|
|
282
|
+
sender: evt.from,
|
|
283
|
+
mentions: [evt.to]
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export {
|
|
288
|
+
inferRole,
|
|
289
|
+
transformToRawData
|
|
290
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// src/cli/guided/types.ts
|
|
2
|
+
var STEPS = [
|
|
3
|
+
"init",
|
|
4
|
+
"extract",
|
|
5
|
+
"curate",
|
|
6
|
+
"transform",
|
|
7
|
+
"preview",
|
|
8
|
+
"audio",
|
|
9
|
+
"render"
|
|
10
|
+
];
|
|
11
|
+
function createProgressFile(project) {
|
|
12
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
13
|
+
return {
|
|
14
|
+
version: 1,
|
|
15
|
+
project,
|
|
16
|
+
steps: {
|
|
17
|
+
init: { status: "complete", completedAt: now },
|
|
18
|
+
extract: { status: "pending" },
|
|
19
|
+
curate: { status: "pending" },
|
|
20
|
+
transform: { status: "pending" },
|
|
21
|
+
preview: { status: "pending" },
|
|
22
|
+
audio: { status: "pending" },
|
|
23
|
+
render: { status: "pending" }
|
|
24
|
+
},
|
|
25
|
+
lastUpdated: now
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function getNextStep(progress) {
|
|
29
|
+
for (const step of STEPS) {
|
|
30
|
+
if (progress.steps[step].status !== "complete") {
|
|
31
|
+
return step;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
var STEP_CONFIG = {
|
|
37
|
+
init: {
|
|
38
|
+
mode: "inline",
|
|
39
|
+
label: "Initialize project",
|
|
40
|
+
outputFiles: [".miriad-viz.json"]
|
|
41
|
+
},
|
|
42
|
+
extract: {
|
|
43
|
+
mode: "background",
|
|
44
|
+
label: "Extract source data",
|
|
45
|
+
logFile: "extract.log",
|
|
46
|
+
outputFiles: ["data/git-commits.json", "data/chat-activity.json"]
|
|
47
|
+
},
|
|
48
|
+
curate: {
|
|
49
|
+
mode: "inline",
|
|
50
|
+
label: "Curate editorial content",
|
|
51
|
+
outputFiles: [
|
|
52
|
+
"data/retro-page-data.json",
|
|
53
|
+
"data/retro-page-quotes.json",
|
|
54
|
+
"data/timeline-events.json"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
transform: {
|
|
58
|
+
mode: "background",
|
|
59
|
+
label: "Transform to viz-data",
|
|
60
|
+
logFile: "transform.log",
|
|
61
|
+
outputFiles: ["output/viz-data.json"]
|
|
62
|
+
},
|
|
63
|
+
preview: {
|
|
64
|
+
mode: "inline",
|
|
65
|
+
label: "Preview visualization",
|
|
66
|
+
outputFiles: ["viewer/public/data/viz-data.json"]
|
|
67
|
+
},
|
|
68
|
+
audio: {
|
|
69
|
+
mode: "inline",
|
|
70
|
+
label: "Generate audio (ElevenLabs)",
|
|
71
|
+
outputFiles: []
|
|
72
|
+
},
|
|
73
|
+
render: {
|
|
74
|
+
mode: "background",
|
|
75
|
+
label: "Render video (Remotion)",
|
|
76
|
+
logFile: "render.log",
|
|
77
|
+
outputFiles: ["remotion/out/video.mp4"]
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// src/cli/guided/progress.ts
|
|
82
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
83
|
+
import { resolve } from "path";
|
|
84
|
+
var PROGRESS_FILENAME = ".miriad-viz.json";
|
|
85
|
+
function migrateProgressSteps(progress) {
|
|
86
|
+
let migrated = false;
|
|
87
|
+
for (const step of STEPS) {
|
|
88
|
+
if (!(step in progress.steps)) {
|
|
89
|
+
const stepIndex = STEPS.indexOf(step);
|
|
90
|
+
const nextStep = stepIndex < STEPS.length - 1 ? STEPS[stepIndex + 1] : null;
|
|
91
|
+
if (nextStep && progress.steps[nextStep]?.status !== "pending") {
|
|
92
|
+
progress.steps[step] = {
|
|
93
|
+
status: "complete",
|
|
94
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
95
|
+
};
|
|
96
|
+
} else {
|
|
97
|
+
progress.steps[step] = { status: "pending" };
|
|
98
|
+
}
|
|
99
|
+
migrated = true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return migrated;
|
|
103
|
+
}
|
|
104
|
+
function readProgress(dir) {
|
|
105
|
+
const path = resolve(dir, PROGRESS_FILENAME);
|
|
106
|
+
if (!existsSync(path)) return null;
|
|
107
|
+
try {
|
|
108
|
+
const raw = readFileSync(path, "utf-8");
|
|
109
|
+
const data = JSON.parse(raw);
|
|
110
|
+
if (data.version !== 1) {
|
|
111
|
+
throw new Error(`Unsupported progress file version: ${data.version}`);
|
|
112
|
+
}
|
|
113
|
+
if (migrateProgressSteps(data)) {
|
|
114
|
+
writeProgress(dir, data);
|
|
115
|
+
}
|
|
116
|
+
return data;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (err instanceof SyntaxError) {
|
|
119
|
+
throw new Error(`Corrupted progress file at ${path}: ${err.message}`);
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function writeProgress(dir, progress) {
|
|
125
|
+
progress.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
|
|
126
|
+
const path = resolve(dir, PROGRESS_FILENAME);
|
|
127
|
+
writeFileSync(path, `${JSON.stringify(progress, null, 2)}
|
|
128
|
+
`);
|
|
129
|
+
}
|
|
130
|
+
function markComplete(progress, step, outputs) {
|
|
131
|
+
progress.steps[step] = {
|
|
132
|
+
status: "complete",
|
|
133
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
134
|
+
outputs
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function markInProgress(progress, step) {
|
|
138
|
+
progress.steps[step] = { status: "in_progress" };
|
|
139
|
+
}
|
|
140
|
+
function markError(progress, step, error) {
|
|
141
|
+
progress.steps[step] = { status: "error", error };
|
|
142
|
+
}
|
|
143
|
+
function findProjectDir(startDir) {
|
|
144
|
+
let dir = resolve(startDir);
|
|
145
|
+
const root = resolve("/");
|
|
146
|
+
while (dir !== root) {
|
|
147
|
+
if (existsSync(resolve(dir, PROGRESS_FILENAME))) {
|
|
148
|
+
return dir;
|
|
149
|
+
}
|
|
150
|
+
const parent = resolve(dir, "..");
|
|
151
|
+
if (parent === dir) break;
|
|
152
|
+
dir = parent;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export {
|
|
158
|
+
createProgressFile,
|
|
159
|
+
getNextStep,
|
|
160
|
+
STEP_CONFIG,
|
|
161
|
+
PROGRESS_FILENAME,
|
|
162
|
+
readProgress,
|
|
163
|
+
writeProgress,
|
|
164
|
+
markComplete,
|
|
165
|
+
markInProgress,
|
|
166
|
+
markError,
|
|
167
|
+
findProjectDir
|
|
168
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/cli/guided/steps/init-template.ts
|
|
2
|
+
import { cpSync, existsSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { dirname, relative, resolve } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
var VALID_TEMPLATES = ["viewer", "remotion"];
|
|
6
|
+
function scaffoldTemplate(templateName, outDir, packageRoot) {
|
|
7
|
+
if (!VALID_TEMPLATES.includes(templateName)) {
|
|
8
|
+
throw new Error(`Template '${templateName}' not found. Valid: ${VALID_TEMPLATES.join(", ")}`);
|
|
9
|
+
}
|
|
10
|
+
if (existsSync(outDir)) {
|
|
11
|
+
throw new Error(`Directory already exists: ${outDir}`);
|
|
12
|
+
}
|
|
13
|
+
const templateDir = resolve(packageRoot, "template", templateName);
|
|
14
|
+
if (!existsSync(templateDir)) {
|
|
15
|
+
throw new Error(`Template directory not found: ${templateDir}`);
|
|
16
|
+
}
|
|
17
|
+
cpSync(templateDir, outDir, {
|
|
18
|
+
recursive: true,
|
|
19
|
+
filter: (src) => {
|
|
20
|
+
const rel = relative(templateDir, src);
|
|
21
|
+
return rel === "" || !rel.split("/").includes("node_modules");
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
const pkgPath = resolve(outDir, "package.json");
|
|
25
|
+
if (existsSync(pkgPath)) {
|
|
26
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
27
|
+
if (pkg.dependencies?.["miriad-viz"]?.startsWith("file:")) {
|
|
28
|
+
const parentPkgPath = resolve(packageRoot, "package.json");
|
|
29
|
+
const parentPkg = JSON.parse(readFileSync(parentPkgPath, "utf-8"));
|
|
30
|
+
const version = parentPkg.version;
|
|
31
|
+
if (packageRoot.split("/").includes("node_modules")) {
|
|
32
|
+
pkg.dependencies["miriad-viz"] = `^${version}`;
|
|
33
|
+
} else {
|
|
34
|
+
pkg.dependencies["miriad-viz"] = `file:${packageRoot}`;
|
|
35
|
+
}
|
|
36
|
+
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
37
|
+
`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function resolvePackageRoot(importMetaUrl) {
|
|
42
|
+
const thisFile = fileURLToPath(importMetaUrl);
|
|
43
|
+
let dir = dirname(thisFile);
|
|
44
|
+
for (let i = 0; i < 10; i++) {
|
|
45
|
+
if (existsSync(resolve(dir, "package.json")) && existsSync(resolve(dir, "template"))) {
|
|
46
|
+
return dir;
|
|
47
|
+
}
|
|
48
|
+
dir = resolve(dir, "..");
|
|
49
|
+
}
|
|
50
|
+
throw new Error("Could not find miriad-viz package root");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
scaffoldTemplate,
|
|
55
|
+
resolvePackageRoot
|
|
56
|
+
};
|