@grainulation/mill 1.0.0 → 1.0.1

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 (41) 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.js +41 -34
  10. package/lib/formats/changelog.js +27 -26
  11. package/lib/formats/confluence-adf.js +312 -0
  12. package/lib/formats/csv.js +41 -37
  13. package/lib/formats/dot.js +45 -34
  14. package/lib/formats/evidence-matrix.js +17 -16
  15. package/lib/formats/executive-summary.js +89 -41
  16. package/lib/formats/github-issues.js +40 -33
  17. package/lib/formats/graphml.js +45 -32
  18. package/lib/formats/html-report.js +110 -63
  19. package/lib/formats/jira-csv.js +30 -29
  20. package/lib/formats/json-ld.js +6 -6
  21. package/lib/formats/markdown.js +53 -36
  22. package/lib/formats/ndjson.js +6 -6
  23. package/lib/formats/obsidian.js +43 -35
  24. package/lib/formats/opml.js +38 -28
  25. package/lib/formats/ris.js +29 -23
  26. package/lib/formats/rss.js +31 -28
  27. package/lib/formats/sankey.js +16 -15
  28. package/lib/formats/slide-deck.js +145 -57
  29. package/lib/formats/sql.js +57 -53
  30. package/lib/formats/static-site.js +64 -52
  31. package/lib/formats/treemap.js +16 -15
  32. package/lib/formats/typescript-defs.js +79 -76
  33. package/lib/formats/yaml.js +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 +7 -3
@@ -3,13 +3,15 @@
3
3
  *
4
4
  * Self-contained HTML report with inline CSS (dark theme).
5
5
  * Claims grouped by type with colored badges, filterable via CSS class toggles.
6
+ * Accessible: semantic landmarks, ARIA roles, keyboard support, reduced-motion.
6
7
  * Zero dependencies — node built-in only.
7
8
  */
8
9
 
9
- export const name = 'html-report';
10
- export const extension = '.html';
11
- export const mimeType = 'text/html; charset=utf-8';
12
- export const description = 'Self-contained dark-theme HTML report with type filters and claim cards';
10
+ export const name = "html-report";
11
+ export const extension = ".html";
12
+ export const mimeType = "text/html; charset=utf-8";
13
+ export const description =
14
+ "Self-contained dark-theme HTML report with type filters and claim cards";
13
15
 
14
16
  /**
15
17
  * Convert a compilation object to an HTML report.
@@ -22,76 +24,96 @@ export function convert(compilation) {
22
24
  const conflicts = compilation.conflicts || [];
23
25
  const certificate = compilation.certificate || {};
24
26
 
25
- const title = meta.sprint || meta.question || 'Sprint Report';
27
+ const title = meta.sprint || meta.question || "Sprint Report";
26
28
  const compiled = certificate.compiled_at || new Date().toISOString();
27
29
 
28
30
  // Group claims by type (skip reverted)
29
31
  const byType = {};
30
32
  const typeCounts = {};
31
33
  for (const c of claims) {
32
- if (c.status === 'reverted') continue;
33
- const t = c.type || 'unknown';
34
+ if (c.status === "reverted") continue;
35
+ const t = c.type || "unknown";
34
36
  if (!byType[t]) byType[t] = [];
35
37
  byType[t].push(c);
36
38
  typeCounts[t] = (typeCounts[t] || 0) + 1;
37
39
  }
38
40
 
39
- const typeOrder = ['constraint', 'factual', 'recommendation', 'risk', 'estimate', 'feedback'];
40
- const sortedTypes = typeOrder.filter(t => byType[t]);
41
+ const typeOrder = [
42
+ "constraint",
43
+ "factual",
44
+ "recommendation",
45
+ "risk",
46
+ "estimate",
47
+ "feedback",
48
+ ];
49
+ const sortedTypes = typeOrder.filter((t) => byType[t]);
41
50
  for (const t of Object.keys(byType)) {
42
51
  if (!sortedTypes.includes(t)) sortedTypes.push(t);
43
52
  }
44
53
 
45
54
  const typeColors = {
46
- constraint: '#e74c3c',
47
- factual: '#3498db',
48
- recommendation: '#2ecc71',
49
- risk: '#f39c12',
50
- estimate: '#9b59b6',
51
- feedback: '#1abc9c',
52
- unknown: '#95a5a6',
55
+ constraint: "#e74c3c",
56
+ factual: "#3498db",
57
+ recommendation: "#2ecc71",
58
+ risk: "#f39c12",
59
+ estimate: "#9b59b6",
60
+ feedback: "#1abc9c",
61
+ unknown: "#95a5a6",
53
62
  };
54
63
 
55
- const active = claims.filter(c => c.status === 'active').length;
64
+ const active = claims.filter((c) => c.status === "active").length;
56
65
 
57
- // Build filter buttons
58
- const filterButtons = sortedTypes.map(t => {
59
- const color = typeColors[t] || typeColors.unknown;
60
- return `<button class="filter-btn active" data-type="${esc(t)}" style="--badge-color:${color}" onclick="toggleType('${esc(t)}')">${capitalize(t)} (${typeCounts[t]})</button>`;
61
- }).join('\n ');
66
+ // Build filter buttons with aria-pressed
67
+ const filterButtons = sortedTypes
68
+ .map((t) => {
69
+ const color = typeColors[t] || typeColors.unknown;
70
+ return `<button class="filter-btn active" data-type="${esc(t)}" style="--badge-color:${color}" aria-pressed="true" onclick="toggleType('${esc(t)}')">${capitalize(t)} (${typeCounts[t]})</button>`;
71
+ })
72
+ .join("\n ");
62
73
 
63
74
  // Build type sections
64
- const sections = sortedTypes.map(t => {
65
- const group = byType[t];
66
- const color = typeColors[t] || typeColors.unknown;
67
- const cards = group.map(c => {
68
- const body = esc(c.content || c.text || '');
69
- const evidenceTier = typeof c.evidence === 'string' ? c.evidence : (c.evidence?.tier || c.evidence_tier || '');
70
- const conf = c.confidence != null ? Math.round(c.confidence * 100) : null;
71
- const tags = Array.isArray(c.tags) ? c.tags.map(t => `<span class="tag">${esc(t)}</span>`).join('') : '';
72
- const confBar = conf != null
73
- ? `<div class="conf-bar"><div class="conf-fill" style="width:${conf}%"></div><span class="conf-label">${conf}%</span></div>`
74
- : '';
75
- return `
76
- <div class="claim-card">
75
+ const sections = sortedTypes
76
+ .map((t) => {
77
+ const group = byType[t];
78
+ const color = typeColors[t] || typeColors.unknown;
79
+ const cards = group
80
+ .map((c) => {
81
+ const body = esc(c.content || c.text || "");
82
+ const evidenceTier =
83
+ typeof c.evidence === "string"
84
+ ? c.evidence
85
+ : c.evidence?.tier || c.evidence_tier || "";
86
+ const conf =
87
+ c.confidence != null ? Math.round(c.confidence * 100) : null;
88
+ const tags = Array.isArray(c.tags)
89
+ ? c.tags.map((t) => `<span class="tag">${esc(t)}</span>`).join("")
90
+ : "";
91
+ const confBar =
92
+ conf != null
93
+ ? `<div class="conf-bar" role="progressbar" aria-valuenow="${conf}" aria-valuemin="0" aria-valuemax="100" aria-label="Confidence: ${conf}%"><div class="conf-fill" style="width:${conf}%"></div><span class="conf-label">${conf}%</span></div>`
94
+ : "";
95
+ return `
96
+ <article class="claim-card" aria-labelledby="claim-${esc(c.id)}">
77
97
  <div class="card-header">
78
- <span class="claim-id">${esc(c.id)}</span>
79
- ${evidenceTier ? `<span class="evidence-badge">${esc(evidenceTier)}</span>` : ''}
80
- <span class="status-badge status-${esc(c.status || 'active')}">${esc(c.status || 'active')}</span>
98
+ <span class="claim-id" id="claim-${esc(c.id)}">${esc(c.id)}</span>
99
+ ${evidenceTier ? `<span class="evidence-badge">evidence: ${esc(evidenceTier)}</span>` : ""}
100
+ <span class="status-badge status-${esc(c.status || "active")}">status: ${esc(c.status || "active")}</span>
81
101
  </div>
82
102
  <p class="card-body">${body}</p>
83
- ${tags ? `<div class="card-tags">${tags}</div>` : ''}
103
+ ${tags ? `<div class="card-tags" aria-label="Tags">${tags}</div>` : ""}
84
104
  ${confBar}
85
- </div>`;
86
- }).join('\n');
105
+ </article>`;
106
+ })
107
+ .join("\n");
87
108
 
88
- return `
89
- <section class="type-section" data-type="${esc(t)}">
109
+ return `
110
+ <section class="type-section" data-type="${esc(t)}" aria-label="${capitalize(t)} claims">
90
111
  <h2 style="border-left:4px solid ${color};padding-left:12px">${capitalize(t)}s (${group.length})</h2>
91
112
  <div class="cards">${cards}
92
113
  </div>
93
114
  </section>`;
94
- }).join('\n');
115
+ })
116
+ .join("\n");
95
117
 
96
118
  return `<!DOCTYPE html>
97
119
  <html lang="en">
@@ -103,6 +125,8 @@ export function convert(compilation) {
103
125
  :root { --bg:#0a0e1a; --surface:#111827; --border:#1e293b; --text:#e2e8f0; --muted:#94a3b8; }
104
126
  * { margin:0; padding:0; box-sizing:border-box; }
105
127
  body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; line-height:1.6; padding:2rem; }
128
+ a.skip-link { position:absolute; top:-40px; left:0; background:var(--surface); color:var(--text); padding:0.5rem 1rem; z-index:100; border-radius:0 0 4px 0; text-decoration:none; }
129
+ a.skip-link:focus { top:0; outline:2px solid #3b82f6; }
106
130
  header { margin-bottom:2rem; }
107
131
  header h1 { font-size:1.8rem; margin-bottom:0.5rem; }
108
132
  header p { color:var(--muted); font-size:0.9rem; }
@@ -112,10 +136,11 @@ export function convert(compilation) {
112
136
  .stat .label { font-size:0.75rem; color:var(--muted); text-transform:uppercase; }
113
137
  .filters { display:flex; gap:0.5rem; flex-wrap:wrap; margin-bottom:2rem; }
114
138
  .filter-btn { background:var(--surface); color:var(--text); border:1px solid var(--border); padding:0.4rem 0.8rem; border-radius:4px; cursor:pointer; font-size:0.85rem; transition:opacity 0.2s; }
139
+ .filter-btn:focus-visible { outline:2px solid #3b82f6; outline-offset:2px; }
115
140
  .filter-btn.active { border-color:var(--badge-color); box-shadow:0 0 0 1px var(--badge-color); }
116
141
  .filter-btn:not(.active) { opacity:0.4; }
117
142
  .type-section { margin-bottom:2rem; }
118
- .type-section.hidden { display:none; }
143
+ .type-section[hidden] { display:none; }
119
144
  .type-section h2 { font-size:1.2rem; margin-bottom:1rem; }
120
145
  .cards { display:grid; gap:0.75rem; }
121
146
  .claim-card { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:1rem; }
@@ -132,48 +157,70 @@ export function convert(compilation) {
132
157
  .conf-fill { height:100%; background:#3b82f6; border-radius:3px; }
133
158
  .conf-label { position:absolute; right:0; top:-16px; font-size:0.7rem; color:var(--muted); }
134
159
  footer { margin-top:3rem; padding-top:1rem; border-top:1px solid var(--border); color:var(--muted); font-size:0.8rem; font-family:monospace; }
160
+ @media (prefers-reduced-motion:reduce) { .filter-btn { transition:none; } }
161
+ @media print { body { background:#fff; color:#000; } .skip-link { display:none; } .filter-btn { border-color:#ccc; } }
135
162
  </style>
136
163
  </head>
137
164
  <body>
138
- <header>
165
+ <a class="skip-link" href="#main-content">Skip to content</a>
166
+ <header role="banner">
139
167
  <h1>${esc(title)}</h1>
140
- ${meta.question ? `<p>${esc(meta.question)}</p>` : ''}
141
- <p>Compiled: ${esc(compiled)}${meta.audience ? ` | Audience: ${esc(meta.audience)}` : ''}</p>
168
+ ${meta.question ? `<p>${esc(meta.question)}</p>` : ""}
169
+ <p>Compiled: <time datetime="${esc(compiled)}">${esc(compiled)}</time>${meta.audience ? ` | Audience: ${esc(meta.audience)}` : ""}</p>
142
170
  </header>
143
171
 
144
- <div class="stats-bar">
145
- <div class="stat"><div class="num">${claims.length}</div><div class="label">Total</div></div>
146
- <div class="stat"><div class="num">${active}</div><div class="label">Active</div></div>
147
- ${sortedTypes.map(t => `<div class="stat"><div class="num">${typeCounts[t]}</div><div class="label">${capitalize(t)}</div></div>`).join('\n ')}
148
- ${conflicts.length ? `<div class="stat"><div class="num">${conflicts.length}</div><div class="label">Conflicts</div></div>` : ''}
172
+ <main id="main-content" role="main">
173
+ <section aria-label="Summary statistics">
174
+ <h2 class="sr-only">Summary</h2>
175
+ <div class="stats-bar" role="list">
176
+ <div class="stat" role="listitem"><div class="num" aria-label="${claims.length} total claims">${claims.length}</div><div class="label">Total</div></div>
177
+ <div class="stat" role="listitem"><div class="num" aria-label="${active} active claims">${active}</div><div class="label">Active</div></div>
178
+ ${sortedTypes.map((t) => `<div class="stat" role="listitem"><div class="num" aria-label="${typeCounts[t]} ${t} claims">${typeCounts[t]}</div><div class="label">${capitalize(t)}</div></div>`).join("\n ")}
179
+ ${conflicts.length ? `<div class="stat" role="listitem"><div class="num" aria-label="${conflicts.length} conflicts">${conflicts.length}</div><div class="label">Conflicts</div></div>` : ""}
149
180
  </div>
181
+ </section>
150
182
 
151
- <div class="filters">
183
+ <nav class="filters" aria-label="Filter claims by type">
152
184
  ${filterButtons}
153
- </div>
185
+ </nav>
154
186
 
155
187
  ${sections}
188
+ </main>
156
189
 
157
- <footer>
158
- Certificate: ${certificate.claim_count || claims.length} claims | sha256:${(certificate.sha256 || 'unknown').slice(0, 16)}
190
+ <footer role="contentinfo">
191
+ <p>Certificate: ${certificate.claim_count || claims.length} claims | sha256:${(certificate.sha256 || "unknown").slice(0, 16)}</p>
159
192
  </footer>
160
193
 
161
194
  <script>
162
195
  function toggleType(type) {
163
- const btn = document.querySelector('.filter-btn[data-type="' + type + '"]');
164
- const section = document.querySelector('.type-section[data-type="' + type + '"]');
196
+ var btn = document.querySelector('.filter-btn[data-type="' + type + '"]');
197
+ var section = document.querySelector('.type-section[data-type="' + type + '"]');
165
198
  if (!btn || !section) return;
166
- btn.classList.toggle('active');
167
- section.classList.toggle('hidden');
199
+ var isActive = btn.classList.toggle('active');
200
+ btn.setAttribute('aria-pressed', String(isActive));
201
+ if (isActive) { section.removeAttribute('hidden'); } else { section.setAttribute('hidden', ''); }
168
202
  }
203
+ // Keyboard support: Enter/Space toggles filter buttons
204
+ document.querySelectorAll('.filter-btn').forEach(function(btn) {
205
+ btn.addEventListener('keydown', function(e) {
206
+ if (e.key === 'Enter' || e.key === ' ') {
207
+ e.preventDefault();
208
+ btn.click();
209
+ }
210
+ });
211
+ });
169
212
  </script>
170
213
  </body>
171
214
  </html>`;
172
215
  }
173
216
 
174
217
  function esc(str) {
175
- if (str == null) return '';
176
- return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
218
+ if (str == null) return "";
219
+ return String(str)
220
+ .replace(/&/g, "&amp;")
221
+ .replace(/</g, "&lt;")
222
+ .replace(/>/g, "&gt;")
223
+ .replace(/"/g, "&quot;");
177
224
  }
178
225
 
179
226
  function capitalize(str) {
@@ -6,18 +6,19 @@
6
6
  * Zero dependencies — node built-in only.
7
7
  */
8
8
 
9
- export const name = 'jira-csv';
10
- export const extension = '.csv';
11
- export const mimeType = 'text/csv; charset=utf-8';
12
- export const description = 'Claims as Jira-compatible CSV (Summary, Description, Issue Type, Priority, Labels, Status)';
9
+ export const name = "jira-csv";
10
+ export const extension = ".csv";
11
+ export const mimeType = "text/csv; charset=utf-8";
12
+ export const description =
13
+ "Claims as Jira-compatible CSV (Summary, Description, Issue Type, Priority, Labels, Status)";
13
14
 
14
15
  const COLUMNS = [
15
- 'Summary',
16
- 'Description',
17
- 'Issue Type',
18
- 'Priority',
19
- 'Labels',
20
- 'Status',
16
+ "Summary",
17
+ "Description",
18
+ "Issue Type",
19
+ "Priority",
20
+ "Labels",
21
+ "Status",
21
22
  ];
22
23
 
23
24
  /**
@@ -29,23 +30,23 @@ export function convert(compilation) {
29
30
  const claims = compilation.claims || [];
30
31
 
31
32
  if (claims.length === 0) {
32
- return COLUMNS.join(',') + '\n';
33
+ return COLUMNS.join(",") + "\n";
33
34
  }
34
35
 
35
- const header = COLUMNS.join(',');
36
+ const header = COLUMNS.join(",");
36
37
  const rows = claims.map(claimToRow);
37
38
 
38
- return [header, ...rows].join('\n') + '\n';
39
+ return [header, ...rows].join("\n") + "\n";
39
40
  }
40
41
 
41
42
  function claimToRow(claim) {
42
- const type = claim.type || '';
43
- const content = claim.content || claim.text || '';
44
- const summary = `${claim.id || ''}: ${truncate(content, 120)}`;
43
+ const type = claim.type || "";
44
+ const content = claim.content || claim.text || "";
45
+ const summary = `${claim.id || ""}: ${truncate(content, 120)}`;
45
46
  const description = content;
46
47
  const { issueType, priority } = mapTypeToJira(type);
47
- const tags = Array.isArray(claim.tags) ? claim.tags.join(' ') : '';
48
- const status = claim.status || '';
48
+ const tags = Array.isArray(claim.tags) ? claim.tags.join(" ") : "";
49
+ const status = claim.status || "";
49
50
 
50
51
  return [
51
52
  escapeField(summary),
@@ -54,35 +55,35 @@ function claimToRow(claim) {
54
55
  escapeField(priority),
55
56
  escapeField(tags),
56
57
  escapeField(status),
57
- ].join(',');
58
+ ].join(",");
58
59
  }
59
60
 
60
61
  function mapTypeToJira(type) {
61
62
  switch (type) {
62
- case 'risk':
63
- return { issueType: 'Bug', priority: 'High' };
64
- case 'recommendation':
65
- return { issueType: 'Task', priority: 'Medium' };
66
- case 'constraint':
67
- return { issueType: 'Task', priority: 'High' };
63
+ case "risk":
64
+ return { issueType: "Bug", priority: "High" };
65
+ case "recommendation":
66
+ return { issueType: "Task", priority: "Medium" };
67
+ case "constraint":
68
+ return { issueType: "Task", priority: "High" };
68
69
  default:
69
- return { issueType: 'Task', priority: 'Low' };
70
+ return { issueType: "Task", priority: "Low" };
70
71
  }
71
72
  }
72
73
 
73
74
  function truncate(str, max) {
74
75
  if (str.length <= max) return str;
75
- return str.slice(0, max - 3) + '...';
76
+ return str.slice(0, max - 3) + "...";
76
77
  }
77
78
 
78
79
  function escapeField(value) {
79
- if (value == null) return '';
80
+ if (value == null) return "";
80
81
  let str = String(value);
81
82
  // CWE-1236: Prevent CSV injection by prefixing formula-triggering characters
82
83
  if (/^[=+\-@\t\r]/.test(str)) {
83
84
  str = "'" + str;
84
85
  }
85
- if (str.includes(',') || str.includes('"') || str.includes('\n')) {
86
+ if (str.includes(",") || str.includes('"') || str.includes("\n")) {
86
87
  return `"${str.replace(/"/g, '""')}"`;
87
88
  }
88
89
  return str;
@@ -5,14 +5,14 @@
5
5
  * Zero dependencies — node built-in only.
6
6
  */
7
7
 
8
- import { createRequire } from 'node:module';
8
+ import { createRequire } from "node:module";
9
9
  const require = createRequire(import.meta.url);
10
- const { buildReport } = require('../json-ld-common.js');
10
+ const { buildReport } = require("../json-ld-common.js");
11
11
 
12
- export const name = 'json-ld';
13
- export const extension = '.jsonld';
14
- export const mimeType = 'application/ld+json; charset=utf-8';
15
- export const description = 'JSON-LD semantic export (schema.org/Report)';
12
+ export const name = "json-ld";
13
+ export const extension = ".jsonld";
14
+ export const mimeType = "application/ld+json; charset=utf-8";
15
+ export const description = "JSON-LD semantic export (schema.org/Report)";
16
16
 
17
17
  /**
18
18
  * Convert a compilation object to JSON-LD.
@@ -6,10 +6,10 @@
6
6
  * Zero dependencies — node built-in only.
7
7
  */
8
8
 
9
- export const name = 'markdown';
10
- export const extension = '.md';
11
- export const mimeType = 'text/markdown; charset=utf-8';
12
- export const description = 'Clean Markdown document from compilation data';
9
+ export const name = "markdown";
10
+ export const extension = ".md";
11
+ export const mimeType = "text/markdown; charset=utf-8";
12
+ export const description = "Clean Markdown document from compilation data";
13
13
 
14
14
  /**
15
15
  * Convert a compilation object to Markdown.
@@ -24,48 +24,55 @@ export function convert(compilation) {
24
24
  const certificate = compilation.certificate || {};
25
25
 
26
26
  // Title
27
- const title = meta.sprint || meta.question || 'Sprint Export';
27
+ const title = meta.sprint || meta.question || "Sprint Export";
28
28
  lines.push(`# ${title}`);
29
- lines.push('');
29
+ lines.push("");
30
30
 
31
31
  // Meta block
32
32
  if (meta.question) {
33
33
  lines.push(`> **Question:** ${meta.question}`);
34
- lines.push('');
34
+ lines.push("");
35
35
  }
36
36
  if (meta.audience) {
37
37
  lines.push(`**Audience:** ${meta.audience}`);
38
- lines.push('');
38
+ lines.push("");
39
39
  }
40
40
  if (certificate.compiled_at) {
41
41
  lines.push(`*Compiled: ${certificate.compiled_at}*`);
42
- lines.push('');
42
+ lines.push("");
43
43
  }
44
44
 
45
45
  // Summary stats
46
- const active = claims.filter(c => c.status === 'active').length;
47
- const superseded = claims.filter(c => c.status === 'superseded').length;
48
- const reverted = claims.filter(c => c.status === 'reverted').length;
49
- lines.push('## Summary');
50
- lines.push('');
46
+ const active = claims.filter((c) => c.status === "active").length;
47
+ const superseded = claims.filter((c) => c.status === "superseded").length;
48
+ const reverted = claims.filter((c) => c.status === "reverted").length;
49
+ lines.push("## Summary");
50
+ lines.push("");
51
51
  lines.push(`- **Total claims:** ${claims.length}`);
52
52
  lines.push(`- **Active:** ${active}`);
53
53
  if (superseded) lines.push(`- **Superseded:** ${superseded}`);
54
54
  if (reverted) lines.push(`- **Reverted:** ${reverted}`);
55
55
  if (conflicts.length) lines.push(`- **Conflicts:** ${conflicts.length}`);
56
- lines.push('');
56
+ lines.push("");
57
57
 
58
58
  // Group claims by type
59
59
  const byType = {};
60
60
  for (const claim of claims) {
61
- if (claim.status === 'reverted') continue;
62
- const type = claim.type || 'unknown';
61
+ if (claim.status === "reverted") continue;
62
+ const type = claim.type || "unknown";
63
63
  if (!byType[type]) byType[type] = [];
64
64
  byType[type].push(claim);
65
65
  }
66
66
 
67
- const typeOrder = ['constraint', 'factual', 'recommendation', 'risk', 'estimate', 'feedback'];
68
- const sortedTypes = typeOrder.filter(t => byType[t]);
67
+ const typeOrder = [
68
+ "constraint",
69
+ "factual",
70
+ "recommendation",
71
+ "risk",
72
+ "estimate",
73
+ "feedback",
74
+ ];
75
+ const sortedTypes = typeOrder.filter((t) => byType[t]);
69
76
  for (const t of Object.keys(byType)) {
70
77
  if (!sortedTypes.includes(t)) sortedTypes.push(t);
71
78
  }
@@ -75,42 +82,52 @@ export function convert(compilation) {
75
82
  if (!group || group.length === 0) continue;
76
83
 
77
84
  lines.push(`## ${capitalize(type)}s (${group.length})`);
78
- lines.push('');
85
+ lines.push("");
79
86
 
80
87
  for (const claim of group) {
81
- const conf = claim.confidence != null ? ` [${Math.round(claim.confidence * 100)}%]` : '';
82
- const evidenceStr = typeof claim.evidence === 'string' ? claim.evidence : claim.evidence?.tier;
83
- const tier = evidenceStr ? ` (${evidenceStr})` : '';
84
- const status = claim.status === 'superseded' ? ' ~~superseded~~' : '';
85
- const body = claim.content || claim.text || '';
88
+ const conf =
89
+ claim.confidence != null
90
+ ? ` [${Math.round(claim.confidence * 100)}%]`
91
+ : "";
92
+ const evidenceStr =
93
+ typeof claim.evidence === "string"
94
+ ? claim.evidence
95
+ : claim.evidence?.tier;
96
+ const tier = evidenceStr ? ` (${evidenceStr})` : "";
97
+ const status = claim.status === "superseded" ? " ~~superseded~~" : "";
98
+ const body = claim.content || claim.text || "";
86
99
  lines.push(`- **${claim.id}**${conf}${tier}${status}: ${body}`);
87
100
  }
88
- lines.push('');
101
+ lines.push("");
89
102
  }
90
103
 
91
104
  // Conflicts
92
105
  if (conflicts.length > 0) {
93
- lines.push('## Conflicts');
94
- lines.push('');
106
+ lines.push("## Conflicts");
107
+ lines.push("");
95
108
  for (const conflict of conflicts) {
96
- const resolved = conflict.resolution ? ' (resolved)' : '';
97
- lines.push(`- **${conflict.ids?.join(' vs ') || 'unknown'}**${resolved}: ${conflict.description || conflict.reason || 'No description'}`);
109
+ const resolved = conflict.resolution ? " (resolved)" : "";
110
+ lines.push(
111
+ `- **${conflict.ids?.join(" vs ") || "unknown"}**${resolved}: ${conflict.description || conflict.reason || "No description"}`,
112
+ );
98
113
  if (conflict.resolution) {
99
114
  lines.push(` - *Resolution:* ${conflict.resolution}`);
100
115
  }
101
116
  }
102
- lines.push('');
117
+ lines.push("");
103
118
  }
104
119
 
105
120
  // Certificate
106
121
  if (certificate.sha256 || certificate.claim_count) {
107
- lines.push('---');
108
- lines.push('');
109
- lines.push(`*Certificate: ${certificate.claim_count || claims.length} claims, sha256:${(certificate.sha256 || 'unknown').slice(0, 12)}*`);
110
- lines.push('');
122
+ lines.push("---");
123
+ lines.push("");
124
+ lines.push(
125
+ `*Certificate: ${certificate.claim_count || claims.length} claims, sha256:${(certificate.sha256 || "unknown").slice(0, 12)}*`,
126
+ );
127
+ lines.push("");
111
128
  }
112
129
 
113
- return lines.join('\n');
130
+ return lines.join("\n");
114
131
  }
115
132
 
116
133
  function capitalize(str) {
@@ -6,10 +6,10 @@
6
6
  * Zero dependencies — node built-in only.
7
7
  */
8
8
 
9
- export const name = 'ndjson';
10
- export const extension = '.ndjson';
11
- export const mimeType = 'application/x-ndjson';
12
- export const description = 'Newline-delimited JSON — one claim object per line';
9
+ export const name = "ndjson";
10
+ export const extension = ".ndjson";
11
+ export const mimeType = "application/x-ndjson";
12
+ export const description = "Newline-delimited JSON — one claim object per line";
13
13
 
14
14
  /**
15
15
  * Convert a compilation object to NDJSON.
@@ -19,7 +19,7 @@ export const description = 'Newline-delimited JSON — one claim object per line
19
19
  export function convert(compilation) {
20
20
  const claims = compilation.claims || [];
21
21
 
22
- if (claims.length === 0) return '';
22
+ if (claims.length === 0) return "";
23
23
 
24
- return claims.map(c => JSON.stringify(c)).join('\n') + '\n';
24
+ return claims.map((c) => JSON.stringify(c)).join("\n") + "\n";
25
25
  }