@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
@@ -1,7 +1,7 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const fs = require('node:fs');
4
- const path = require('node:path');
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
5
 
6
6
  /**
7
7
  * Convert HTML artifacts to clean Markdown.
@@ -13,80 +13,113 @@ function htmlToMarkdown(html) {
13
13
  let md = html;
14
14
 
15
15
  // Remove doctype, head, scripts, styles
16
- md = md.replace(/<!DOCTYPE[^>]*>/gi, '');
17
- md = md.replace(/<head[\s\S]*?<\/head>/gi, '');
18
- md = md.replace(/<script[\s\S]*?<\/script>/gi, '');
19
- md = md.replace(/<style[\s\S]*?<\/style>/gi, '');
16
+ md = md.replace(/<!DOCTYPE[^>]*>/gi, "");
17
+ md = md.replace(/<head[\s\S]*?<\/head>/gi, "");
18
+ md = md.replace(/<script[\s\S]*?<\/script>/gi, "");
19
+ md = md.replace(/<style[\s\S]*?<\/style>/gi, "");
20
20
 
21
21
  // Headings
22
22
  md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `# ${strip(c)}\n\n`);
23
- md = md.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, (_, c) => `## ${strip(c)}\n\n`);
24
- md = md.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, (_, c) => `### ${strip(c)}\n\n`);
25
- md = md.replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, (_, c) => `#### ${strip(c)}\n\n`);
26
- md = md.replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, (_, c) => `##### ${strip(c)}\n\n`);
27
- md = md.replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, (_, c) => `###### ${strip(c)}\n\n`);
23
+ md = md.replace(
24
+ /<h2[^>]*>([\s\S]*?)<\/h2>/gi,
25
+ (_, c) => `## ${strip(c)}\n\n`,
26
+ );
27
+ md = md.replace(
28
+ /<h3[^>]*>([\s\S]*?)<\/h3>/gi,
29
+ (_, c) => `### ${strip(c)}\n\n`,
30
+ );
31
+ md = md.replace(
32
+ /<h4[^>]*>([\s\S]*?)<\/h4>/gi,
33
+ (_, c) => `#### ${strip(c)}\n\n`,
34
+ );
35
+ md = md.replace(
36
+ /<h5[^>]*>([\s\S]*?)<\/h5>/gi,
37
+ (_, c) => `##### ${strip(c)}\n\n`,
38
+ );
39
+ md = md.replace(
40
+ /<h6[^>]*>([\s\S]*?)<\/h6>/gi,
41
+ (_, c) => `###### ${strip(c)}\n\n`,
42
+ );
28
43
 
29
44
  // Bold, italic, code
30
- md = md.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi, '**$1**');
31
- md = md.replace(/<b[^>]*>([\s\S]*?)<\/b>/gi, '**$1**');
32
- md = md.replace(/<em[^>]*>([\s\S]*?)<\/em>/gi, '*$1*');
33
- md = md.replace(/<i[^>]*>([\s\S]*?)<\/i>/gi, '*$1*');
34
- md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`');
45
+ md = md.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi, "**$1**");
46
+ md = md.replace(/<b[^>]*>([\s\S]*?)<\/b>/gi, "**$1**");
47
+ md = md.replace(/<em[^>]*>([\s\S]*?)<\/em>/gi, "*$1*");
48
+ md = md.replace(/<i[^>]*>([\s\S]*?)<\/i>/gi, "*$1*");
49
+ md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, "`$1`");
35
50
 
36
51
  // Pre/code blocks
37
- md = md.replace(/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi, (_, c) => {
38
- return '\n```\n' + decodeEntities(c) + '\n```\n\n';
39
- });
52
+ md = md.replace(
53
+ /<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi,
54
+ (_, c) => {
55
+ return "\n```\n" + decodeEntities(c) + "\n```\n\n";
56
+ },
57
+ );
40
58
  md = md.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, c) => {
41
- return '\n```\n' + decodeEntities(c) + '\n```\n\n';
59
+ return "\n```\n" + decodeEntities(c) + "\n```\n\n";
42
60
  });
43
61
 
44
62
  // Links
45
- md = md.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, '[$2]($1)');
63
+ md = md.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)");
46
64
 
47
65
  // Images
48
- md = md.replace(/<img[^>]+src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, '![$2]($1)');
49
- md = md.replace(/<img[^>]+src="([^"]*)"[^>]*\/?>/gi, '![]($1)');
66
+ md = md.replace(
67
+ /<img[^>]+src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi,
68
+ "![$2]($1)",
69
+ );
70
+ md = md.replace(/<img[^>]+src="([^"]*)"[^>]*\/?>/gi, "![]($1)");
50
71
 
51
72
  // Lists
52
- md = md.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, (_, c) => `- ${strip(c).trim()}\n`);
53
- md = md.replace(/<\/?[ou]l[^>]*>/gi, '\n');
73
+ md = md.replace(
74
+ /<li[^>]*>([\s\S]*?)<\/li>/gi,
75
+ (_, c) => `- ${strip(c).trim()}\n`,
76
+ );
77
+ md = md.replace(/<\/?[ou]l[^>]*>/gi, "\n");
54
78
 
55
79
  // Paragraphs and breaks
56
- md = md.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, (_, c) => `${strip(c).trim()}\n\n`);
57
- md = md.replace(/<br\s*\/?>/gi, '\n');
58
- md = md.replace(/<hr\s*\/?>/gi, '\n---\n\n');
80
+ md = md.replace(
81
+ /<p[^>]*>([\s\S]*?)<\/p>/gi,
82
+ (_, c) => `${strip(c).trim()}\n\n`,
83
+ );
84
+ md = md.replace(/<br\s*\/?>/gi, "\n");
85
+ md = md.replace(/<hr\s*\/?>/gi, "\n---\n\n");
59
86
 
60
87
  // Blockquotes
61
88
  md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, c) => {
62
- return strip(c).trim().split('\n').map((l) => `> ${l}`).join('\n') + '\n\n';
89
+ return (
90
+ strip(c)
91
+ .trim()
92
+ .split("\n")
93
+ .map((l) => `> ${l}`)
94
+ .join("\n") + "\n\n"
95
+ );
63
96
  });
64
97
 
65
98
  // Strip remaining tags
66
- md = md.replace(/<[^>]+>/g, '');
99
+ md = md.replace(/<[^>]+>/g, "");
67
100
 
68
101
  // Decode common entities
69
102
  md = decodeEntities(md);
70
103
 
71
104
  // Normalize whitespace
72
- md = md.replace(/\n{3,}/g, '\n\n');
73
- md = md.trim() + '\n';
105
+ md = md.replace(/\n{3,}/g, "\n\n");
106
+ md = md.trim() + "\n";
74
107
 
75
108
  return md;
76
109
  }
77
110
 
78
111
  function strip(html) {
79
- return html.replace(/<[^>]+>/g, '');
112
+ return html.replace(/<[^>]+>/g, "");
80
113
  }
81
114
 
82
115
  function decodeEntities(str) {
83
116
  return str
84
- .replace(/&amp;/g, '&')
85
- .replace(/&lt;/g, '<')
86
- .replace(/&gt;/g, '>')
117
+ .replace(/&amp;/g, "&")
118
+ .replace(/&lt;/g, "<")
119
+ .replace(/&gt;/g, ">")
87
120
  .replace(/&quot;/g, '"')
88
121
  .replace(/&#39;/g, "'")
89
- .replace(/&nbsp;/g, ' ');
122
+ .replace(/&nbsp;/g, " ");
90
123
  }
91
124
 
92
125
  function deriveOutputPath(inputPath, explicit) {
@@ -97,20 +130,26 @@ function deriveOutputPath(inputPath, explicit) {
97
130
  }
98
131
 
99
132
  async function exportMarkdown(inputPath, outputPath) {
100
- const html = fs.readFileSync(inputPath, 'utf-8');
133
+ const html = fs.readFileSync(inputPath, "utf-8");
101
134
  const trimmed = html.trimStart();
102
- if (!trimmed.startsWith('<') && !trimmed.startsWith('<!DOCTYPE') && !trimmed.startsWith('<html')) {
103
- process.stderr.write('Warning: Input does not appear to be HTML. Markdown conversion may produce unexpected results.\n');
135
+ if (
136
+ !trimmed.startsWith("<") &&
137
+ !trimmed.startsWith("<!DOCTYPE") &&
138
+ !trimmed.startsWith("<html")
139
+ ) {
140
+ process.stderr.write(
141
+ "Warning: Input does not appear to be HTML. Markdown conversion may produce unexpected results.\n",
142
+ );
104
143
  }
105
144
  const md = htmlToMarkdown(html);
106
145
  const out = deriveOutputPath(inputPath, outputPath);
107
- fs.writeFileSync(out, md, 'utf-8');
146
+ fs.writeFileSync(out, md, "utf-8");
108
147
  return { outputPath: out, message: `Markdown written to ${out}` };
109
148
  }
110
149
 
111
150
  module.exports = {
112
- name: 'markdown',
113
- description: 'Convert HTML artifacts to clean Markdown',
151
+ name: "markdown",
152
+ description: "Convert HTML artifacts to clean Markdown",
114
153
  export: exportMarkdown,
115
154
  htmlToMarkdown,
116
155
  };
@@ -1,11 +1,11 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
3
  // NOTE: PDF is the one export format that requires external tools (md-to-pdf
4
4
  // for Markdown, puppeteer for HTML). These are fetched via npx on first run,
5
5
  // which needs network access. All other mill export formats are zero-dep.
6
6
 
7
- const path = require('node:path');
8
- const { execFile } = require('node:child_process');
7
+ const path = require("node:path");
8
+ const { execFile } = require("node:child_process");
9
9
 
10
10
  /**
11
11
  * Export HTML or Markdown files to PDF.
@@ -34,7 +34,8 @@ function exec(cmd, args) {
34
34
 
35
35
  function npxToolError(toolName, originalError) {
36
36
  const msg = originalError.message || String(originalError);
37
- const isNotFound = /not found|ENOENT|ERR_MODULE_NOT_FOUND|Cannot find module/i.test(msg);
37
+ const isNotFound =
38
+ /not found|ENOENT|ERR_MODULE_NOT_FOUND|Cannot find module/i.test(msg);
38
39
  const isNetwork = /ENETUNREACH|ENOTFOUND|fetch failed|EAI_AGAIN/i.test(msg);
39
40
  const isTimeout = /timed out|ETIMEDOUT/i.test(msg);
40
41
 
@@ -51,19 +52,18 @@ function npxToolError(toolName, originalError) {
51
52
  ` Or fall back to: mill export --format markdown`;
52
53
  }
53
54
 
54
- return new Error(`PDF export failed -- ${toolName} unavailable or broken.\n\n${hint}\n\nOriginal error: ${msg}`);
55
+ return new Error(
56
+ `PDF export failed -- ${toolName} unavailable or broken.\n\n${hint}\n\nOriginal error: ${msg}`,
57
+ );
55
58
  }
56
59
 
57
60
  async function exportFromMarkdown(inputPath, outputPath) {
58
61
  const out = deriveOutputPath(inputPath, outputPath);
59
62
  // md-to-pdf reads from file, writes pdf alongside or to --dest
60
63
  try {
61
- await exec('npx', [
62
- '--yes', 'md-to-pdf', inputPath,
63
- '--dest', out,
64
- ]);
64
+ await exec("npx", ["--yes", "md-to-pdf", inputPath, "--dest", out]);
65
65
  } catch (err) {
66
- throw npxToolError('md-to-pdf', err);
66
+ throw npxToolError("md-to-pdf", err);
67
67
  }
68
68
  return { outputPath: out, message: `PDF written to ${out}` };
69
69
  }
@@ -82,23 +82,23 @@ async function exportFromHtml(inputPath, outputPath) {
82
82
  })();
83
83
  `;
84
84
  try {
85
- await exec('node', ['-e', script]);
85
+ await exec("node", ["-e", script]);
86
86
  } catch (err) {
87
- throw npxToolError('puppeteer', err);
87
+ throw npxToolError("puppeteer", err);
88
88
  }
89
89
  return { outputPath: out, message: `PDF written to ${out}` };
90
90
  }
91
91
 
92
92
  async function exportPdf(inputPath, outputPath) {
93
93
  const ext = path.extname(inputPath).toLowerCase();
94
- if (ext === '.md' || ext === '.markdown') {
94
+ if (ext === ".md" || ext === ".markdown") {
95
95
  return exportFromMarkdown(inputPath, outputPath);
96
96
  }
97
97
  return exportFromHtml(inputPath, outputPath);
98
98
  }
99
99
 
100
100
  module.exports = {
101
- name: 'pdf',
102
- description: 'Export HTML or Markdown to PDF',
101
+ name: "pdf",
102
+ description: "Export HTML or Markdown to PDF",
103
103
  export: exportPdf,
104
104
  };
@@ -6,10 +6,11 @@
6
6
  * Zero dependencies — node built-in only.
7
7
  */
8
8
 
9
- export const name = 'bibtex';
10
- export const extension = '.bib';
11
- export const mimeType = 'application/x-bibtex; charset=utf-8';
12
- export const description = 'Claims as BibTeX bibliography entries (@misc per claim)';
9
+ export const name = "bibtex";
10
+ export const extension = ".bib";
11
+ export const mimeType = "application/x-bibtex; charset=utf-8";
12
+ export const description =
13
+ "Claims as BibTeX bibliography entries (@misc per claim)";
13
14
 
14
15
  /**
15
16
  * Convert a compilation object to BibTeX.
@@ -20,36 +21,42 @@ export function convert(compilation) {
20
21
  const claims = compilation.claims || [];
21
22
  const meta = compilation.meta || {};
22
23
  const year = new Date().getFullYear().toString();
23
- const author = meta.sprint ? `wheat sprint: ${meta.sprint}` : 'wheat sprint';
24
+ const author = meta.sprint ? `wheat sprint: ${meta.sprint}` : "wheat sprint";
24
25
 
25
26
  if (claims.length === 0) {
26
27
  return `% No claims in compilation\n`;
27
28
  }
28
29
 
29
- const entries = claims.map(claim => claimToEntry(claim, author, year));
30
- return entries.join('\n\n') + '\n';
30
+ const entries = claims.map((claim) => claimToEntry(claim, author, year));
31
+ return entries.join("\n\n") + "\n";
31
32
  }
32
33
 
33
34
  function claimToEntry(claim, author, year) {
34
- const id = String(claim.id || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
35
- const title = escapeBibtex(claim.content || claim.text || '');
36
- const type = claim.type || '';
37
- const evidence = typeof claim.evidence === 'string'
38
- ? claim.evidence
39
- : (claim.evidence?.tier ?? claim.evidence_tier ?? '');
40
- const status = claim.status || '';
35
+ const id = String(claim.id || "unknown").replace(/[^a-zA-Z0-9_-]/g, "_");
36
+ const title = escapeBibtex(claim.content || claim.text || "");
37
+ const type = claim.type || "";
38
+ const evidence =
39
+ typeof claim.evidence === "string"
40
+ ? claim.evidence
41
+ : (claim.evidence?.tier ?? claim.evidence_tier ?? "");
42
+ const status = claim.status || "";
41
43
  const tags = Array.isArray(claim.tags) ? claim.tags : [];
42
- const confidence = claim.confidence != null ? `, confidence: ${claim.confidence}` : '';
44
+ const confidence =
45
+ claim.confidence != null ? `, confidence: ${claim.confidence}` : "";
43
46
 
44
- const noteParts = [
45
- type ? `type: ${type}` : '',
46
- evidence ? `evidence: ${evidence}` : '',
47
- status ? `status: ${status}` : '',
48
- ].filter(Boolean).join(', ') + confidence;
47
+ const noteParts =
48
+ [
49
+ type ? `type: ${type}` : "",
50
+ evidence ? `evidence: ${evidence}` : "",
51
+ status ? `status: ${status}` : "",
52
+ ]
53
+ .filter(Boolean)
54
+ .join(", ") + confidence;
49
55
 
50
- const keywordsLine = tags.length > 0
51
- ? `\n keywords = {${tags.map(escapeBibtex).join(', ')}},`
52
- : '';
56
+ const keywordsLine =
57
+ tags.length > 0
58
+ ? `\n keywords = {${tags.map(escapeBibtex).join(", ")}},`
59
+ : "";
53
60
 
54
61
  return [
55
62
  `@misc{claim_${id},`,
@@ -58,19 +65,19 @@ function claimToEntry(claim, author, year) {
58
65
  ` year = {${year}},`,
59
66
  ` note = {${escapeBibtex(noteParts)}},` + keywordsLine,
60
67
  `}`,
61
- ].join('\n');
68
+ ].join("\n");
62
69
  }
63
70
 
64
71
  function escapeBibtex(value) {
65
- if (value == null) return '';
72
+ if (value == null) return "";
66
73
  return String(value)
67
- .replace(/\\/g, '\\textbackslash{}')
68
- .replace(/&/g, '\\&')
69
- .replace(/%/g, '\\%')
70
- .replace(/#/g, '\\#')
71
- .replace(/_/g, '\\_')
72
- .replace(/\{/g, '\\{')
73
- .replace(/\}/g, '\\}')
74
- .replace(/~/g, '\\textasciitilde{}')
75
- .replace(/\^/g, '\\textasciicircum{}');
74
+ .replace(/\\/g, "\\textbackslash{}")
75
+ .replace(/&/g, "\\&")
76
+ .replace(/%/g, "\\%")
77
+ .replace(/#/g, "\\#")
78
+ .replace(/_/g, "\\_")
79
+ .replace(/\{/g, "\\{")
80
+ .replace(/\}/g, "\\}")
81
+ .replace(/~/g, "\\textasciitilde{}")
82
+ .replace(/\^/g, "\\textasciicircum{}");
76
83
  }
@@ -6,10 +6,11 @@
6
6
  * Zero dependencies — node built-in only.
7
7
  */
8
8
 
9
- export const name = 'changelog';
10
- export const extension = '.md';
11
- export const mimeType = 'text/markdown; charset=utf-8';
12
- export const description = 'Claims changelog grouped by status (active, resolved, conflicts)';
9
+ export const name = "changelog";
10
+ export const extension = ".md";
11
+ export const mimeType = "text/markdown; charset=utf-8";
12
+ export const description =
13
+ "Claims changelog grouped by status (active, resolved, conflicts)";
13
14
 
14
15
  /**
15
16
  * Convert a compilation object to changelog markdown.
@@ -20,83 +21,83 @@ export function convert(compilation) {
20
21
  const claims = compilation.claims || [];
21
22
  const conflicts = compilation.conflicts || [];
22
23
  const meta = compilation.meta || {};
23
- const sprintName = meta.sprint || 'unnamed';
24
+ const sprintName = meta.sprint || "unnamed";
24
25
 
25
26
  const groups = {};
26
27
  for (const claim of claims) {
27
- const status = claim.status || 'unknown';
28
+ const status = claim.status || "unknown";
28
29
  if (!groups[status]) groups[status] = [];
29
30
  groups[status].push(claim);
30
31
  }
31
32
 
32
33
  const lines = [];
33
34
  lines.push(`# Changelog — ${sprintName}`);
34
- lines.push('');
35
+ lines.push("");
35
36
 
36
37
  // Active claims first
37
38
  if (groups.active) {
38
39
  lines.push(`## Active (${groups.active.length} claims)`);
39
- lines.push('');
40
+ lines.push("");
40
41
  for (const claim of groups.active) {
41
42
  lines.push(`- ${claim.id}: ${claimText(claim)}`);
42
43
  }
43
- lines.push('');
44
+ lines.push("");
44
45
  }
45
46
 
46
47
  // Resolved claims
47
48
  if (groups.resolved) {
48
49
  lines.push(`## Resolved (${groups.resolved.length} claims)`);
49
- lines.push('');
50
+ lines.push("");
50
51
  for (const claim of groups.resolved) {
51
52
  lines.push(`- ${claim.id}: ${claimText(claim)}`);
52
53
  }
53
- lines.push('');
54
+ lines.push("");
54
55
  }
55
56
 
56
57
  // All other statuses
57
- const shown = new Set(['active', 'resolved']);
58
+ const shown = new Set(["active", "resolved"]);
58
59
  const otherStatuses = Object.keys(groups)
59
- .filter(s => !shown.has(s))
60
+ .filter((s) => !shown.has(s))
60
61
  .sort();
61
62
 
62
63
  for (const status of otherStatuses) {
63
64
  const group = groups[status];
64
65
  const label = status.charAt(0).toUpperCase() + status.slice(1);
65
66
  lines.push(`## ${label} (${group.length} claims)`);
66
- lines.push('');
67
+ lines.push("");
67
68
  for (const claim of group) {
68
69
  lines.push(`- ${claim.id}: ${claimText(claim)}`);
69
70
  }
70
- lines.push('');
71
+ lines.push("");
71
72
  }
72
73
 
73
74
  // Conflicts section
74
75
  if (conflicts.length > 0) {
75
76
  lines.push(`## Conflicts (${conflicts.length})`);
76
- lines.push('');
77
+ lines.push("");
77
78
  for (const conflict of conflicts) {
78
79
  const ids = Array.isArray(conflict.claim_ids)
79
- ? conflict.claim_ids.join(' vs ')
80
- : (conflict.between || 'unknown');
81
- const desc = conflict.description || conflict.reason || '';
80
+ ? conflict.claim_ids.join(" vs ")
81
+ : conflict.between || "unknown";
82
+ const desc = conflict.description || conflict.reason || "";
82
83
  lines.push(`- ${ids}: ${desc}`);
83
84
  }
84
- lines.push('');
85
+ lines.push("");
85
86
  } else {
86
87
  lines.push(`## Conflicts (0)`);
87
- lines.push('');
88
- lines.push('No conflicts.');
89
- lines.push('');
88
+ lines.push("");
89
+ lines.push("No conflicts.");
90
+ lines.push("");
90
91
  }
91
92
 
92
- return lines.join('\n');
93
+ return lines.join("\n");
93
94
  }
94
95
 
95
96
  function claimText(claim) {
96
- const text = claim.content || claim.text || '';
97
+ const text = claim.content || claim.text || "";
97
98
  // Truncate long claims for readability
98
99
  if (text.length > 120) {
99
- return text.slice(0, 117) + '...';
100
+ return text.slice(0, 117) + "...";
100
101
  }
101
102
  return text;
102
103
  }