@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.
Files changed (49) hide show
  1. package/CODE_OF_CONDUCT.md +25 -0
  2. package/CONTRIBUTING.md +101 -0
  3. package/README.md +90 -42
  4. package/bin/mill.js +233 -67
  5. package/lib/exporters/csv.js +35 -30
  6. package/lib/exporters/json-ld.js +19 -13
  7. package/lib/exporters/markdown.js +83 -44
  8. package/lib/exporters/pdf.js +15 -15
  9. package/lib/formats/bibtex.mjs +83 -0
  10. package/lib/formats/{changelog.js → changelog.mjs} +27 -26
  11. package/lib/formats/confluence-adf.mjs +312 -0
  12. package/lib/formats/{csv.js → csv.mjs} +41 -37
  13. package/lib/formats/{dot.js → dot.mjs} +45 -34
  14. package/lib/formats/{evidence-matrix.js → evidence-matrix.mjs} +17 -16
  15. package/lib/formats/executive-summary.mjs +178 -0
  16. package/lib/formats/github-issues.mjs +96 -0
  17. package/lib/formats/{graphml.js → graphml.mjs} +45 -32
  18. package/lib/formats/html-report.mjs +228 -0
  19. package/lib/formats/{jira-csv.js → jira-csv.mjs} +30 -29
  20. package/lib/formats/{json-ld.js → json-ld.mjs} +6 -6
  21. package/lib/formats/{markdown.js → markdown.mjs} +53 -36
  22. package/lib/formats/{ndjson.js → ndjson.mjs} +6 -6
  23. package/lib/formats/{obsidian.js → obsidian.mjs} +43 -35
  24. package/lib/formats/{opml.js → opml.mjs} +38 -28
  25. package/lib/formats/ris.mjs +76 -0
  26. package/lib/formats/{rss.js → rss.mjs} +31 -28
  27. package/lib/formats/{sankey.js → sankey.mjs} +16 -15
  28. package/lib/formats/slide-deck.mjs +288 -0
  29. package/lib/formats/sql.mjs +120 -0
  30. package/lib/formats/{static-site.js → static-site.mjs} +64 -52
  31. package/lib/formats/{treemap.js → treemap.mjs} +16 -15
  32. package/lib/formats/typescript-defs.mjs +150 -0
  33. package/lib/formats/{yaml.js → yaml.mjs} +58 -40
  34. package/lib/formats.js +16 -16
  35. package/lib/index.js +5 -5
  36. package/lib/json-ld-common.js +37 -31
  37. package/lib/publishers/clipboard.js +21 -19
  38. package/lib/publishers/static.js +27 -12
  39. package/lib/serve-mcp.js +158 -83
  40. package/lib/server.js +252 -142
  41. package/package.json +6 -3
  42. package/lib/formats/bibtex.js +0 -76
  43. package/lib/formats/executive-summary.js +0 -130
  44. package/lib/formats/github-issues.js +0 -89
  45. package/lib/formats/html-report.js +0 -181
  46. package/lib/formats/ris.js +0 -70
  47. package/lib/formats/slide-deck.js +0 -200
  48. package/lib/formats/sql.js +0 -116
  49. 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 = 'csv';
10
- export const extension = '.csv';
11
- export const mimeType = 'text/csv; charset=utf-8';
12
- export const description = 'Claims as CSV spreadsheet (id, type, topic, content, evidence, status)';
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
- 'id',
16
- 'type',
17
- 'topic',
18
- 'content',
19
- 'evidence_tier',
20
- 'evidence_source',
21
- 'confidence',
22
- 'status',
23
- 'tags',
24
- 'created',
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(',') + '\n';
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('\n') + '\n';
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 'content':
49
+ case "content":
49
50
  // Claims may use 'text' or 'content' for the main body
50
- return escapeField(claim.content || claim.text || '');
51
- case 'topic':
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 'evidence_tier':
57
+ case "evidence_tier":
57
58
  // evidence may be a string (tier directly) or object { tier, source }
58
- if (typeof claim.evidence === 'string') return escapeField(claim.evidence);
59
- return escapeField(claim.evidence?.tier ?? claim.evidence_tier ?? '');
60
- case 'evidence_source':
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 === 'object' && claim.source !== null) {
63
- return escapeField(claim.source.origin || claim.source.artifact || '');
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 === 'object' && claim.evidence?.source) {
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 'tags':
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 'confidence':
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(',') || str.includes('"') || str.includes('\n')) {
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 = 'dot';
10
- export const extension = '.dot';
11
- export const mimeType = 'text/vnd.graphviz; charset=utf-8';
12
- export const description = 'Claims as Graphviz DOT graph (type clusters, tag edges)';
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: { border: '#f87171', fill: '#2d1f1f', label: 'Constraints' },
16
- factual: { border: '#60a5fa', fill: '#1f2937', label: 'Factual' },
17
- estimate: { border: '#a78bfa', fill: '#1f1f2d', label: 'Estimates' },
18
- risk: { border: '#fb923c', fill: '#2d2517', label: 'Risks' },
19
- recommendation: { border: '#34d399', fill: '#172d1f', label: 'Recommendations' },
20
- feedback: { border: '#fbbf24', fill: '#2d2a17', label: 'Feedback' },
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: '#9ca3af', fill: '#1f1f1f', label: 'Other' };
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 || 'sprint';
37
+ const sprintName = compilation.meta?.sprint || "sprint";
33
38
 
34
39
  const lines = [];
35
40
  lines.push(`digraph ${escId(sprintName)} {`);
36
- lines.push(' rankdir=LR;');
37
- lines.push(' node [shape=box, style=filled, fontname="monospace", fontsize=10];');
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 || 'other';
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(` ${id} [label="${escDot(claim.id + '\\n' + label)}" fillcolor="${colors.fill}"];`);
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(' // Tag edges');
81
+ lines.push(" // Tag edges");
73
82
  for (const edge of edges) {
74
- lines.push(` ${escId(edge.source)} -> ${escId(edge.target)} [label="${escDot('tag: ' + edge.tag)}" style=dashed];`);
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('\n') + '\n';
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) + '...' : str;
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, '\\n');
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 = 'evidence-matrix';
10
- export const extension = '.csv';
11
- export const mimeType = 'text/csv; charset=utf-8';
12
- export const description = 'Pivot table CSV: claim types vs evidence tiers with counts';
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 = ['stated', 'web', 'documented', 'tested', 'production'];
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 === 'reverted') continue;
31
+ if (claim.status === "reverted") continue;
31
32
 
32
- const type = claim.type || 'unknown';
33
- const tier = extractTier(claim) || 'unknown';
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(['type', ...tierColumns, 'total'].join(','));
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(['total', ...colTotals, grandTotal].join(','));
77
+ lines.push(["total", ...colTotals, grandTotal].join(","));
77
78
 
78
- return lines.join('\n') + '\n';
79
+ return lines.join("\n") + "\n";
79
80
  }
80
81
 
81
82
  function extractTier(claim) {
82
- if (typeof claim.evidence === 'string') return claim.evidence;
83
- if (typeof claim.evidence === 'object' && claim.evidence !== null) {
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;