@grainulation/mill 1.0.0 → 1.0.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/CODE_OF_CONDUCT.md +25 -0
- package/CONTRIBUTING.md +101 -0
- package/README.md +90 -42
- package/bin/mill.js +233 -67
- package/lib/exporters/csv.js +35 -30
- package/lib/exporters/json-ld.js +19 -13
- package/lib/exporters/markdown.js +83 -44
- package/lib/exporters/pdf.js +15 -15
- package/lib/formats/bibtex.mjs +83 -0
- package/lib/formats/{changelog.js → changelog.mjs} +27 -26
- package/lib/formats/confluence-adf.mjs +312 -0
- package/lib/formats/{csv.js → csv.mjs} +41 -37
- package/lib/formats/{dot.js → dot.mjs} +45 -34
- package/lib/formats/{evidence-matrix.js → evidence-matrix.mjs} +17 -16
- package/lib/formats/executive-summary.mjs +178 -0
- package/lib/formats/github-issues.mjs +96 -0
- package/lib/formats/{graphml.js → graphml.mjs} +45 -32
- package/lib/formats/html-report.mjs +228 -0
- package/lib/formats/{jira-csv.js → jira-csv.mjs} +30 -29
- package/lib/formats/{json-ld.js → json-ld.mjs} +6 -6
- package/lib/formats/{markdown.js → markdown.mjs} +53 -36
- package/lib/formats/{ndjson.js → ndjson.mjs} +6 -6
- package/lib/formats/{obsidian.js → obsidian.mjs} +43 -35
- package/lib/formats/{opml.js → opml.mjs} +38 -28
- package/lib/formats/ris.mjs +76 -0
- package/lib/formats/{rss.js → rss.mjs} +31 -28
- package/lib/formats/{sankey.js → sankey.mjs} +16 -15
- package/lib/formats/slide-deck.mjs +288 -0
- package/lib/formats/sql.mjs +120 -0
- package/lib/formats/{static-site.js → static-site.mjs} +64 -52
- package/lib/formats/{treemap.js → treemap.mjs} +16 -15
- package/lib/formats/typescript-defs.mjs +150 -0
- package/lib/formats/{yaml.js → yaml.mjs} +58 -40
- package/lib/formats.js +16 -16
- package/lib/index.js +5 -5
- package/lib/json-ld-common.js +37 -31
- package/lib/publishers/clipboard.js +21 -19
- package/lib/publishers/static.js +27 -12
- package/lib/serve-mcp.js +158 -83
- package/lib/server.js +252 -142
- package/package.json +6 -3
- package/lib/formats/bibtex.js +0 -76
- package/lib/formats/executive-summary.js +0 -130
- package/lib/formats/github-issues.js +0 -89
- package/lib/formats/html-report.js +0 -181
- package/lib/formats/ris.js +0 -70
- package/lib/formats/slide-deck.js +0 -200
- package/lib/formats/sql.js +0 -116
- package/lib/formats/typescript-defs.js +0 -147
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: confluence-adf
|
|
3
|
+
*
|
|
4
|
+
* Converts compilation.json to Atlassian Document Format (ADF) JSON.
|
|
5
|
+
* ADF is the native document format for Confluence Cloud REST API.
|
|
6
|
+
* Output can be POST'd directly to /wiki/api/v2/pages.
|
|
7
|
+
* Zero dependencies — node built-in only.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const name = "confluence-adf";
|
|
11
|
+
export const extension = ".adf.json";
|
|
12
|
+
export const mimeType = "application/json; charset=utf-8";
|
|
13
|
+
export const description =
|
|
14
|
+
"Confluence ADF (Atlassian Document Format) for direct API upload";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert a compilation object to Confluence ADF JSON.
|
|
18
|
+
* @param {object} compilation - The compilation.json content
|
|
19
|
+
* @returns {string} ADF JSON output
|
|
20
|
+
*/
|
|
21
|
+
export function convert(compilation) {
|
|
22
|
+
const meta = compilation.meta || {};
|
|
23
|
+
const claims = compilation.claims || [];
|
|
24
|
+
const conflicts = compilation.conflicts || [];
|
|
25
|
+
const certificate = compilation.certificate || {};
|
|
26
|
+
|
|
27
|
+
const title = meta.sprint || meta.question || "Sprint Report";
|
|
28
|
+
const compiled = certificate.compiled_at || new Date().toISOString();
|
|
29
|
+
const active = claims.filter((c) => c.status === "active").length;
|
|
30
|
+
|
|
31
|
+
const content = [];
|
|
32
|
+
|
|
33
|
+
// Title heading
|
|
34
|
+
content.push(heading(1, title));
|
|
35
|
+
|
|
36
|
+
// Question as blockquote
|
|
37
|
+
if (meta.question) {
|
|
38
|
+
content.push(blockquote(meta.question));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Meta paragraph
|
|
42
|
+
const metaParts = [`Compiled: ${compiled}`];
|
|
43
|
+
if (meta.audience) metaParts.push(`Audience: ${meta.audience}`);
|
|
44
|
+
content.push(paragraph([text(metaParts.join(" | "), [mark("em")])]));
|
|
45
|
+
|
|
46
|
+
// Summary panel
|
|
47
|
+
content.push(heading(2, "Summary"));
|
|
48
|
+
content.push(
|
|
49
|
+
table(
|
|
50
|
+
[tableRow([tableHeader("Metric"), tableHeader("Count")])],
|
|
51
|
+
[
|
|
52
|
+
tableRow([tableCell("Total claims"), tableCell(String(claims.length))]),
|
|
53
|
+
tableRow([tableCell("Active"), tableCell(String(active))]),
|
|
54
|
+
...(conflicts.length > 0
|
|
55
|
+
? [
|
|
56
|
+
tableRow([
|
|
57
|
+
tableCell("Conflicts"),
|
|
58
|
+
tableCell(String(conflicts.length)),
|
|
59
|
+
]),
|
|
60
|
+
]
|
|
61
|
+
: []),
|
|
62
|
+
],
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Group claims by type (skip reverted)
|
|
67
|
+
const byType = {};
|
|
68
|
+
for (const c of claims) {
|
|
69
|
+
if (c.status === "reverted") continue;
|
|
70
|
+
const t = c.type || "unknown";
|
|
71
|
+
if (!byType[t]) byType[t] = [];
|
|
72
|
+
byType[t].push(c);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const typeOrder = [
|
|
76
|
+
"constraint",
|
|
77
|
+
"factual",
|
|
78
|
+
"recommendation",
|
|
79
|
+
"risk",
|
|
80
|
+
"estimate",
|
|
81
|
+
"feedback",
|
|
82
|
+
];
|
|
83
|
+
const sortedTypes = typeOrder.filter((t) => byType[t]);
|
|
84
|
+
for (const t of Object.keys(byType)) {
|
|
85
|
+
if (!sortedTypes.includes(t)) sortedTypes.push(t);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Status macro mapping for claim types
|
|
89
|
+
const statusColors = {
|
|
90
|
+
risk: "red",
|
|
91
|
+
constraint: "red",
|
|
92
|
+
recommendation: "green",
|
|
93
|
+
factual: "blue",
|
|
94
|
+
estimate: "purple",
|
|
95
|
+
feedback: "yellow",
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Per-type sections
|
|
99
|
+
for (const t of sortedTypes) {
|
|
100
|
+
const group = byType[t];
|
|
101
|
+
content.push(heading(2, `${capitalize(t)}s (${group.length})`));
|
|
102
|
+
|
|
103
|
+
// Add info/warning panels for risks and constraints
|
|
104
|
+
if (t === "risk" && group.length > 0) {
|
|
105
|
+
content.push(
|
|
106
|
+
panel(
|
|
107
|
+
"warning",
|
|
108
|
+
group.length + " risk(s) identified -- review before proceeding",
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
} else if (t === "constraint" && group.length > 0) {
|
|
112
|
+
content.push(
|
|
113
|
+
panel(
|
|
114
|
+
"error",
|
|
115
|
+
group.length + " hard constraint(s) -- these are non-negotiable",
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Claims table
|
|
121
|
+
content.push(
|
|
122
|
+
table(
|
|
123
|
+
[
|
|
124
|
+
tableRow([
|
|
125
|
+
tableHeader("ID"),
|
|
126
|
+
tableHeader("Content"),
|
|
127
|
+
tableHeader("Evidence"),
|
|
128
|
+
tableHeader("Confidence"),
|
|
129
|
+
tableHeader("Status"),
|
|
130
|
+
]),
|
|
131
|
+
],
|
|
132
|
+
group.map((c) => {
|
|
133
|
+
const body = c.content || c.text || "";
|
|
134
|
+
const tier =
|
|
135
|
+
typeof c.evidence === "string"
|
|
136
|
+
? c.evidence
|
|
137
|
+
: c.evidence?.tier || c.evidence_tier || "";
|
|
138
|
+
const conf =
|
|
139
|
+
c.confidence != null ? Math.round(c.confidence * 100) + "%" : "--";
|
|
140
|
+
const status = c.status || "active";
|
|
141
|
+
return tableRow([
|
|
142
|
+
tableCell(c.id || ""),
|
|
143
|
+
tableCell(body),
|
|
144
|
+
tableCell(tier),
|
|
145
|
+
tableCell(conf),
|
|
146
|
+
tableCellWithStatus(status, statusColors[t] || "neutral"),
|
|
147
|
+
]);
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Tags as labels
|
|
153
|
+
const allTags = new Set();
|
|
154
|
+
for (const c of group) {
|
|
155
|
+
if (Array.isArray(c.tags)) c.tags.forEach((tag) => allTags.add(tag));
|
|
156
|
+
}
|
|
157
|
+
if (allTags.size > 0) {
|
|
158
|
+
content.push(
|
|
159
|
+
paragraph([
|
|
160
|
+
text("Tags: ", [mark("strong")]),
|
|
161
|
+
text([...allTags].join(", ")),
|
|
162
|
+
]),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Wrap large sections in expand macro
|
|
167
|
+
if (group.length > 10) {
|
|
168
|
+
content.push(
|
|
169
|
+
paragraph([
|
|
170
|
+
text(
|
|
171
|
+
`Showing all ${group.length} ${t} claims. Consider using Confluence page filtering for large datasets.`,
|
|
172
|
+
[mark("em")],
|
|
173
|
+
),
|
|
174
|
+
]),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Conflicts section
|
|
180
|
+
if (conflicts.length > 0) {
|
|
181
|
+
content.push(heading(2, `Conflicts (${conflicts.length})`));
|
|
182
|
+
const items = conflicts.map((c) => {
|
|
183
|
+
const ids = Array.isArray(c.ids)
|
|
184
|
+
? c.ids.join(" vs ")
|
|
185
|
+
: c.between || "unknown";
|
|
186
|
+
const desc = c.description || c.reason || "";
|
|
187
|
+
const resolved = c.resolution ? ` [resolved: ${c.resolution}]` : "";
|
|
188
|
+
return bulletItem(`${ids}: ${desc}${resolved}`);
|
|
189
|
+
});
|
|
190
|
+
content.push(bulletList(items));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Certificate footer
|
|
194
|
+
content.push(rule());
|
|
195
|
+
content.push(
|
|
196
|
+
paragraph([
|
|
197
|
+
text(
|
|
198
|
+
`Certificate: ${certificate.claim_count || claims.length} claims | sha256:${(certificate.sha256 || "unknown").slice(0, 16)}`,
|
|
199
|
+
[mark("code")],
|
|
200
|
+
),
|
|
201
|
+
]),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const doc = {
|
|
205
|
+
version: 1,
|
|
206
|
+
type: "doc",
|
|
207
|
+
content,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
return JSON.stringify(doc, null, 2) + "\n";
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- ADF node builders ---
|
|
214
|
+
|
|
215
|
+
function heading(level, text_) {
|
|
216
|
+
return {
|
|
217
|
+
type: "heading",
|
|
218
|
+
attrs: { level },
|
|
219
|
+
content: [{ type: "text", text: text_ }],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function paragraph(inlines) {
|
|
224
|
+
return { type: "paragraph", content: inlines };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function text(value, marks_) {
|
|
228
|
+
const node = { type: "text", text: value };
|
|
229
|
+
if (marks_ && marks_.length > 0) node.marks = marks_;
|
|
230
|
+
return node;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function mark(type) {
|
|
234
|
+
return { type };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function blockquote(text_) {
|
|
238
|
+
return {
|
|
239
|
+
type: "blockquote",
|
|
240
|
+
content: [paragraph([text(text_)])],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function bulletList(items) {
|
|
245
|
+
return { type: "bulletList", content: items };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function bulletItem(text_) {
|
|
249
|
+
return {
|
|
250
|
+
type: "listItem",
|
|
251
|
+
content: [paragraph([text(text_)])],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function panel(panelType, text_) {
|
|
256
|
+
return {
|
|
257
|
+
type: "panel",
|
|
258
|
+
attrs: { panelType },
|
|
259
|
+
content: [paragraph([text(text_)])],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function rule() {
|
|
264
|
+
return { type: "rule" };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function table(headerRows, bodyRows) {
|
|
268
|
+
return {
|
|
269
|
+
type: "table",
|
|
270
|
+
attrs: { isNumberColumnEnabled: false, layout: "default" },
|
|
271
|
+
content: [...headerRows, ...bodyRows],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function tableRow(cells) {
|
|
276
|
+
return { type: "tableRow", content: cells };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function tableHeader(text_) {
|
|
280
|
+
return {
|
|
281
|
+
type: "tableHeader",
|
|
282
|
+
attrs: {},
|
|
283
|
+
content: [paragraph([text(text_, [mark("strong")])])],
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function tableCell(text_) {
|
|
288
|
+
return {
|
|
289
|
+
type: "tableCell",
|
|
290
|
+
attrs: {},
|
|
291
|
+
content: [paragraph([text(text_)])],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function tableCellWithStatus(statusText, color) {
|
|
296
|
+
return {
|
|
297
|
+
type: "tableCell",
|
|
298
|
+
attrs: {},
|
|
299
|
+
content: [
|
|
300
|
+
paragraph([
|
|
301
|
+
{
|
|
302
|
+
type: "status",
|
|
303
|
+
attrs: { text: statusText, color, localId: "", style: "" },
|
|
304
|
+
},
|
|
305
|
+
]),
|
|
306
|
+
],
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function capitalize(str) {
|
|
311
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
312
|
+
}
|
|
@@ -6,22 +6,23 @@
|
|
|
6
6
|
* Zero dependencies — node built-in only.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export const name =
|
|
10
|
-
export const extension =
|
|
11
|
-
export const mimeType =
|
|
12
|
-
export const description =
|
|
9
|
+
export const name = "csv";
|
|
10
|
+
export const extension = ".csv";
|
|
11
|
+
export const mimeType = "text/csv; charset=utf-8";
|
|
12
|
+
export const description =
|
|
13
|
+
"Claims as CSV spreadsheet (id, type, topic, content, evidence, status)";
|
|
13
14
|
|
|
14
15
|
const COLUMNS = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
"id",
|
|
17
|
+
"type",
|
|
18
|
+
"topic",
|
|
19
|
+
"content",
|
|
20
|
+
"evidence_tier",
|
|
21
|
+
"evidence_source",
|
|
22
|
+
"confidence",
|
|
23
|
+
"status",
|
|
24
|
+
"tags",
|
|
25
|
+
"created",
|
|
25
26
|
];
|
|
26
27
|
|
|
27
28
|
/**
|
|
@@ -33,59 +34,62 @@ export function convert(compilation) {
|
|
|
33
34
|
const claims = compilation.claims || [];
|
|
34
35
|
|
|
35
36
|
if (claims.length === 0) {
|
|
36
|
-
return COLUMNS.join(
|
|
37
|
+
return COLUMNS.join(",") + "\n";
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
const header = COLUMNS.join(
|
|
40
|
+
const header = COLUMNS.join(",");
|
|
40
41
|
const rows = claims.map(claimToRow);
|
|
41
42
|
|
|
42
|
-
return [header, ...rows].join(
|
|
43
|
+
return [header, ...rows].join("\n") + "\n";
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
function claimToRow(claim) {
|
|
46
|
-
return COLUMNS.map(col => {
|
|
47
|
+
return COLUMNS.map((col) => {
|
|
47
48
|
switch (col) {
|
|
48
|
-
case
|
|
49
|
+
case "content":
|
|
49
50
|
// Claims may use 'text' or 'content' for the main body
|
|
50
|
-
return escapeField(claim.content || claim.text ||
|
|
51
|
-
case
|
|
51
|
+
return escapeField(claim.content || claim.text || "");
|
|
52
|
+
case "topic":
|
|
52
53
|
// Use claim.topic directly, or fall back to first tag
|
|
53
54
|
return escapeField(
|
|
54
|
-
claim.topic || (Array.isArray(claim.tags) ? claim.tags[0] ||
|
|
55
|
+
claim.topic || (Array.isArray(claim.tags) ? claim.tags[0] || "" : ""),
|
|
55
56
|
);
|
|
56
|
-
case
|
|
57
|
+
case "evidence_tier":
|
|
57
58
|
// evidence may be a string (tier directly) or object { tier, source }
|
|
58
|
-
if (typeof claim.evidence ===
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
if (typeof claim.evidence === "string")
|
|
60
|
+
return escapeField(claim.evidence);
|
|
61
|
+
return escapeField(claim.evidence?.tier ?? claim.evidence_tier ?? "");
|
|
62
|
+
case "evidence_source":
|
|
61
63
|
// source may be an object { origin, artifact } or a string
|
|
62
|
-
if (typeof claim.source ===
|
|
63
|
-
return escapeField(
|
|
64
|
+
if (typeof claim.source === "object" && claim.source !== null) {
|
|
65
|
+
return escapeField(
|
|
66
|
+
claim.source.origin || claim.source.artifact || "",
|
|
67
|
+
);
|
|
64
68
|
}
|
|
65
|
-
if (typeof claim.evidence ===
|
|
69
|
+
if (typeof claim.evidence === "object" && claim.evidence?.source) {
|
|
66
70
|
return escapeField(claim.evidence.source);
|
|
67
71
|
}
|
|
68
|
-
return escapeField(claim.source ??
|
|
69
|
-
case
|
|
72
|
+
return escapeField(claim.source ?? "");
|
|
73
|
+
case "tags":
|
|
70
74
|
return escapeField(
|
|
71
|
-
Array.isArray(claim.tags) ? claim.tags.join(
|
|
75
|
+
Array.isArray(claim.tags) ? claim.tags.join("; ") : "",
|
|
72
76
|
);
|
|
73
|
-
case
|
|
74
|
-
return claim.confidence != null ? String(claim.confidence) :
|
|
77
|
+
case "confidence":
|
|
78
|
+
return claim.confidence != null ? String(claim.confidence) : "";
|
|
75
79
|
default:
|
|
76
80
|
return escapeField(claim[col]);
|
|
77
81
|
}
|
|
78
|
-
}).join(
|
|
82
|
+
}).join(",");
|
|
79
83
|
}
|
|
80
84
|
|
|
81
85
|
function escapeField(value) {
|
|
82
|
-
if (value == null) return
|
|
86
|
+
if (value == null) return "";
|
|
83
87
|
let str = String(value);
|
|
84
88
|
// CWE-1236: Prevent CSV injection by prefixing formula-triggering characters
|
|
85
89
|
if (/^[=+\-@\t\r]/.test(str)) {
|
|
86
90
|
str = "'" + str;
|
|
87
91
|
}
|
|
88
|
-
if (str.includes(
|
|
92
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
89
93
|
return `"${str.replace(/"/g, '""')}"`;
|
|
90
94
|
}
|
|
91
95
|
return str;
|
|
@@ -6,21 +6,26 @@
|
|
|
6
6
|
* Zero dependencies — node built-in only.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export const name =
|
|
10
|
-
export const extension =
|
|
11
|
-
export const mimeType =
|
|
12
|
-
export const description =
|
|
9
|
+
export const name = "dot";
|
|
10
|
+
export const extension = ".dot";
|
|
11
|
+
export const mimeType = "text/vnd.graphviz; charset=utf-8";
|
|
12
|
+
export const description =
|
|
13
|
+
"Claims as Graphviz DOT graph (type clusters, tag edges)";
|
|
13
14
|
|
|
14
15
|
const TYPE_COLORS = {
|
|
15
|
-
constraint:
|
|
16
|
-
factual:
|
|
17
|
-
estimate:
|
|
18
|
-
risk:
|
|
19
|
-
recommendation: {
|
|
20
|
-
|
|
16
|
+
constraint: { border: "#f87171", fill: "#2d1f1f", label: "Constraints" },
|
|
17
|
+
factual: { border: "#60a5fa", fill: "#1f2937", label: "Factual" },
|
|
18
|
+
estimate: { border: "#a78bfa", fill: "#1f1f2d", label: "Estimates" },
|
|
19
|
+
risk: { border: "#fb923c", fill: "#2d2517", label: "Risks" },
|
|
20
|
+
recommendation: {
|
|
21
|
+
border: "#34d399",
|
|
22
|
+
fill: "#172d1f",
|
|
23
|
+
label: "Recommendations",
|
|
24
|
+
},
|
|
25
|
+
feedback: { border: "#fbbf24", fill: "#2d2a17", label: "Feedback" },
|
|
21
26
|
};
|
|
22
27
|
|
|
23
|
-
const DEFAULT_COLOR = { border:
|
|
28
|
+
const DEFAULT_COLOR = { border: "#9ca3af", fill: "#1f1f1f", label: "Other" };
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
31
|
* Convert a compilation object to DOT format.
|
|
@@ -29,19 +34,21 @@ const DEFAULT_COLOR = { border: '#9ca3af', fill: '#1f1f1f', label: 'Other' };
|
|
|
29
34
|
*/
|
|
30
35
|
export function convert(compilation) {
|
|
31
36
|
const claims = compilation.claims || [];
|
|
32
|
-
const sprintName = compilation.meta?.sprint ||
|
|
37
|
+
const sprintName = compilation.meta?.sprint || "sprint";
|
|
33
38
|
|
|
34
39
|
const lines = [];
|
|
35
40
|
lines.push(`digraph ${escId(sprintName)} {`);
|
|
36
|
-
lines.push(
|
|
37
|
-
lines.push(
|
|
41
|
+
lines.push(" rankdir=LR;");
|
|
42
|
+
lines.push(
|
|
43
|
+
' node [shape=box, style=filled, fontname="monospace", fontsize=10];',
|
|
44
|
+
);
|
|
38
45
|
lines.push(' edge [fontname="monospace", fontsize=8];');
|
|
39
|
-
lines.push(
|
|
46
|
+
lines.push("");
|
|
40
47
|
|
|
41
48
|
// Group claims by type
|
|
42
49
|
const groups = {};
|
|
43
50
|
for (const claim of claims) {
|
|
44
|
-
const t = claim.type ||
|
|
51
|
+
const t = claim.type || "other";
|
|
45
52
|
if (!groups[t]) groups[t] = [];
|
|
46
53
|
groups[t].push(claim);
|
|
47
54
|
}
|
|
@@ -53,32 +60,36 @@ export function convert(compilation) {
|
|
|
53
60
|
lines.push(` label="${escDot(colors.label || type)}";`);
|
|
54
61
|
lines.push(` color="${colors.border}";`);
|
|
55
62
|
lines.push(` style=dashed;`);
|
|
56
|
-
lines.push(
|
|
63
|
+
lines.push("");
|
|
57
64
|
|
|
58
65
|
for (const claim of group) {
|
|
59
|
-
const id = escId(claim.id ||
|
|
60
|
-
const content = claim.content || claim.text ||
|
|
66
|
+
const id = escId(claim.id || "");
|
|
67
|
+
const content = claim.content || claim.text || "";
|
|
61
68
|
const label = truncate(content, 40);
|
|
62
|
-
lines.push(
|
|
69
|
+
lines.push(
|
|
70
|
+
` ${id} [label="${escDot(claim.id + "\\n" + label)}" fillcolor="${colors.fill}"];`,
|
|
71
|
+
);
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
lines.push(
|
|
66
|
-
lines.push(
|
|
74
|
+
lines.push(" }");
|
|
75
|
+
lines.push("");
|
|
67
76
|
}
|
|
68
77
|
|
|
69
78
|
// Tag edges
|
|
70
79
|
const edges = buildTagEdges(claims);
|
|
71
80
|
if (edges.length > 0) {
|
|
72
|
-
lines.push(
|
|
81
|
+
lines.push(" // Tag edges");
|
|
73
82
|
for (const edge of edges) {
|
|
74
|
-
lines.push(
|
|
83
|
+
lines.push(
|
|
84
|
+
` ${escId(edge.source)} -> ${escId(edge.target)} [label="${escDot("tag: " + edge.tag)}" style=dashed];`,
|
|
85
|
+
);
|
|
75
86
|
}
|
|
76
|
-
lines.push(
|
|
87
|
+
lines.push("");
|
|
77
88
|
}
|
|
78
89
|
|
|
79
|
-
lines.push(
|
|
90
|
+
lines.push("}");
|
|
80
91
|
|
|
81
|
-
return lines.join(
|
|
92
|
+
return lines.join("\n") + "\n";
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
function buildTagEdges(claims) {
|
|
@@ -88,7 +99,7 @@ function buildTagEdges(claims) {
|
|
|
88
99
|
const tags = Array.isArray(claim.tags) ? claim.tags : [];
|
|
89
100
|
for (const tag of tags) {
|
|
90
101
|
if (!tagMap[tag]) tagMap[tag] = [];
|
|
91
|
-
tagMap[tag].push(claim.id ||
|
|
102
|
+
tagMap[tag].push(claim.id || "");
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -111,19 +122,19 @@ function buildTagEdges(claims) {
|
|
|
111
122
|
}
|
|
112
123
|
|
|
113
124
|
function truncate(str, max) {
|
|
114
|
-
if (!str) return
|
|
115
|
-
return str.length > max ? str.slice(0, max - 3) +
|
|
125
|
+
if (!str) return "";
|
|
126
|
+
return str.length > max ? str.slice(0, max - 3) + "..." : str;
|
|
116
127
|
}
|
|
117
128
|
|
|
118
129
|
function escId(str) {
|
|
119
130
|
// DOT identifiers: replace non-alphanumeric with underscores
|
|
120
|
-
return String(str).replace(/[^a-zA-Z0-9_]/g,
|
|
131
|
+
return String(str).replace(/[^a-zA-Z0-9_]/g, "_");
|
|
121
132
|
}
|
|
122
133
|
|
|
123
134
|
function escDot(str) {
|
|
124
|
-
if (str == null) return
|
|
135
|
+
if (str == null) return "";
|
|
125
136
|
return String(str)
|
|
126
|
-
.replace(/\\/g,
|
|
137
|
+
.replace(/\\/g, "\\\\")
|
|
127
138
|
.replace(/"/g, '\\"')
|
|
128
|
-
.replace(/\n/g,
|
|
139
|
+
.replace(/\n/g, "\\n");
|
|
129
140
|
}
|
|
@@ -6,12 +6,13 @@
|
|
|
6
6
|
* Zero dependencies — node built-in only.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export const name =
|
|
10
|
-
export const extension =
|
|
11
|
-
export const mimeType =
|
|
12
|
-
export const description =
|
|
9
|
+
export const name = "evidence-matrix";
|
|
10
|
+
export const extension = ".csv";
|
|
11
|
+
export const mimeType = "text/csv; charset=utf-8";
|
|
12
|
+
export const description =
|
|
13
|
+
"Pivot table CSV: claim types vs evidence tiers with counts";
|
|
13
14
|
|
|
14
|
-
const TIER_ORDER = [
|
|
15
|
+
const TIER_ORDER = ["stated", "web", "documented", "tested", "production"];
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Convert a compilation object to an evidence matrix CSV.
|
|
@@ -27,10 +28,10 @@ export function convert(compilation) {
|
|
|
27
28
|
const allTiers = new Set();
|
|
28
29
|
|
|
29
30
|
for (const claim of claims) {
|
|
30
|
-
if (claim.status ===
|
|
31
|
+
if (claim.status === "reverted") continue;
|
|
31
32
|
|
|
32
|
-
const type = claim.type ||
|
|
33
|
-
const tier = extractTier(claim) ||
|
|
33
|
+
const type = claim.type || "unknown";
|
|
34
|
+
const tier = extractTier(claim) || "unknown";
|
|
34
35
|
|
|
35
36
|
allTypes.add(type);
|
|
36
37
|
allTiers.add(tier);
|
|
@@ -55,17 +56,17 @@ export function convert(compilation) {
|
|
|
55
56
|
const lines = [];
|
|
56
57
|
|
|
57
58
|
// Header
|
|
58
|
-
lines.push([
|
|
59
|
+
lines.push(["type", ...tierColumns, "total"].join(","));
|
|
59
60
|
|
|
60
61
|
// Data rows
|
|
61
62
|
for (const type of typeRows) {
|
|
62
|
-
const counts = tierColumns.map(tier => pivot[type]?.[tier] || 0);
|
|
63
|
+
const counts = tierColumns.map((tier) => pivot[type]?.[tier] || 0);
|
|
63
64
|
const total = counts.reduce((sum, n) => sum + n, 0);
|
|
64
|
-
lines.push([type, ...counts, total].join(
|
|
65
|
+
lines.push([type, ...counts, total].join(","));
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
// Totals row
|
|
68
|
-
const colTotals = tierColumns.map(tier => {
|
|
69
|
+
const colTotals = tierColumns.map((tier) => {
|
|
69
70
|
let sum = 0;
|
|
70
71
|
for (const type of typeRows) {
|
|
71
72
|
sum += pivot[type]?.[tier] || 0;
|
|
@@ -73,14 +74,14 @@ export function convert(compilation) {
|
|
|
73
74
|
return sum;
|
|
74
75
|
});
|
|
75
76
|
const grandTotal = colTotals.reduce((sum, n) => sum + n, 0);
|
|
76
|
-
lines.push([
|
|
77
|
+
lines.push(["total", ...colTotals, grandTotal].join(","));
|
|
77
78
|
|
|
78
|
-
return lines.join(
|
|
79
|
+
return lines.join("\n") + "\n";
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
function extractTier(claim) {
|
|
82
|
-
if (typeof claim.evidence ===
|
|
83
|
-
if (typeof claim.evidence ===
|
|
83
|
+
if (typeof claim.evidence === "string") return claim.evidence;
|
|
84
|
+
if (typeof claim.evidence === "object" && claim.evidence !== null) {
|
|
84
85
|
return claim.evidence.tier || null;
|
|
85
86
|
}
|
|
86
87
|
return claim.evidence_tier || null;
|