@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
@@ -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
  }
@@ -6,10 +6,11 @@
6
6
  * Zero dependencies — node built-in only.
7
7
  */
8
8
 
9
- export const name = 'obsidian';
10
- export const extension = '.md';
11
- export const mimeType = 'text/markdown; charset=utf-8';
12
- export const description = 'Claims as Obsidian vault page (YAML front matter, wikilinks, backlinks)';
9
+ export const name = "obsidian";
10
+ export const extension = ".md";
11
+ export const mimeType = "text/markdown; charset=utf-8";
12
+ export const description =
13
+ "Claims as Obsidian vault page (YAML front matter, wikilinks, backlinks)";
13
14
 
14
15
  /**
15
16
  * Convert a compilation object to Obsidian markdown.
@@ -19,37 +20,44 @@ export const description = 'Claims as Obsidian vault page (YAML front matter, wi
19
20
  export function convert(compilation) {
20
21
  const meta = compilation.meta || {};
21
22
  const claims = compilation.claims || [];
22
- const sprint = meta.sprint || 'unknown';
23
- const question = meta.question || '';
24
- const audience = meta.audience || '';
23
+ const sprint = meta.sprint || "unknown";
24
+ const question = meta.question || "";
25
+ const audience = meta.audience || "";
25
26
 
26
27
  const grouped = groupByType(claims);
27
28
  const lines = [];
28
29
 
29
30
  // YAML front matter
30
- lines.push('---');
31
+ lines.push("---");
31
32
  lines.push(`sprint: ${sprint}`);
32
33
  if (question) lines.push(`question: "${escapeFrontMatter(question)}"`);
33
34
  if (audience) lines.push(`audience: "${escapeFrontMatter(audience)}"`);
34
35
  lines.push(`claim_count: ${claims.length}`);
35
- lines.push('tags: [wheat, sprint, export]');
36
- lines.push('---');
37
- lines.push('');
36
+ lines.push("tags: [wheat, sprint, export]");
37
+ lines.push("---");
38
+ lines.push("");
38
39
 
39
40
  // Title
40
41
  lines.push(`# Sprint: ${sprint}`);
41
- lines.push('');
42
+ lines.push("");
42
43
 
43
44
  if (question) {
44
45
  lines.push(`> ${question}`);
45
- lines.push('');
46
+ lines.push("");
46
47
  }
47
48
 
48
49
  // Claims grouped by type
49
- lines.push('## Claims');
50
- lines.push('');
51
-
52
- const typeOrder = ['constraint', 'factual', 'estimate', 'risk', 'recommendation', 'feedback'];
50
+ lines.push("## Claims");
51
+ lines.push("");
52
+
53
+ const typeOrder = [
54
+ "constraint",
55
+ "factual",
56
+ "estimate",
57
+ "risk",
58
+ "recommendation",
59
+ "feedback",
60
+ ];
53
61
  const seen = new Set();
54
62
 
55
63
  for (const type of typeOrder) {
@@ -66,22 +74,22 @@ export function convert(compilation) {
66
74
  }
67
75
 
68
76
  // Connections section
69
- lines.push('## Connections');
70
- lines.push('');
71
- lines.push('- Related: [[compilation]] [[claims]]');
77
+ lines.push("## Connections");
78
+ lines.push("");
79
+ lines.push("- Related: [[compilation]] [[claims]]");
72
80
 
73
81
  if (compilation.certificate) {
74
- lines.push('- Certificate: [[certificate]]');
82
+ lines.push("- Certificate: [[certificate]]");
75
83
  }
76
84
 
77
- lines.push('');
78
- return lines.join('\n') + '\n';
85
+ lines.push("");
86
+ return lines.join("\n") + "\n";
79
87
  }
80
88
 
81
89
  function groupByType(claims) {
82
90
  const groups = {};
83
91
  for (const claim of claims) {
84
- const type = claim.type || 'other';
92
+ const type = claim.type || "other";
85
93
  if (!groups[type]) groups[type] = [];
86
94
  groups[type].push(claim);
87
95
  }
@@ -89,14 +97,14 @@ function groupByType(claims) {
89
97
  }
90
98
 
91
99
  function renderGroup(type, claims) {
92
- const label = capitalize(type) + 's';
100
+ const label = capitalize(type) + "s";
93
101
  const lines = [];
94
102
 
95
103
  lines.push(`### ${label}`);
96
104
 
97
105
  for (const claim of claims) {
98
- const id = claim.id || '???';
99
- const content = claim.content || claim.text || '';
106
+ const id = claim.id || "???";
107
+ const content = claim.content || claim.text || "";
100
108
  const summary = truncate(content, 120);
101
109
  const evidence = getEvidence(claim);
102
110
  const tags = formatTags(claim);
@@ -104,22 +112,22 @@ function renderGroup(type, claims) {
104
112
  lines.push(`- [[${id}]] ${summary} #${type} #${evidence}${tags}`);
105
113
  }
106
114
 
107
- lines.push('');
115
+ lines.push("");
108
116
  return lines;
109
117
  }
110
118
 
111
119
  function getEvidence(claim) {
112
- if (typeof claim.evidence === 'string') return claim.evidence;
113
- if (typeof claim.evidence === 'object' && claim.evidence !== null) {
114
- return claim.evidence.tier || claim.evidence_tier || 'stated';
120
+ if (typeof claim.evidence === "string") return claim.evidence;
121
+ if (typeof claim.evidence === "object" && claim.evidence !== null) {
122
+ return claim.evidence.tier || claim.evidence_tier || "stated";
115
123
  }
116
- return claim.evidence_tier || 'stated';
124
+ return claim.evidence_tier || "stated";
117
125
  }
118
126
 
119
127
  function formatTags(claim) {
120
128
  const tags = Array.isArray(claim.tags) ? claim.tags : [];
121
- if (tags.length === 0) return '';
122
- return ' ' + tags.map(t => `#${t.replace(/\s+/g, '-')}`).join(' ');
129
+ if (tags.length === 0) return "";
130
+ return " " + tags.map((t) => `#${t.replace(/\s+/g, "-")}`).join(" ");
123
131
  }
124
132
 
125
133
  function capitalize(str) {
@@ -128,7 +136,7 @@ function capitalize(str) {
128
136
 
129
137
  function truncate(str, max) {
130
138
  if (str.length <= max) return str;
131
- return str.slice(0, max - 3) + '...';
139
+ return str.slice(0, max - 3) + "...";
132
140
  }
133
141
 
134
142
  function escapeFrontMatter(str) {
@@ -6,10 +6,11 @@
6
6
  * Zero dependencies — node built-in only.
7
7
  */
8
8
 
9
- export const name = 'opml';
10
- export const extension = '.opml';
11
- export const mimeType = 'text/x-opml; charset=utf-8';
12
- export const description = 'Claims as OPML 2.0 outline grouped by type (for RSS readers and outliners)';
9
+ export const name = "opml";
10
+ export const extension = ".opml";
11
+ export const mimeType = "text/x-opml; charset=utf-8";
12
+ export const description =
13
+ "Claims as OPML 2.0 outline grouped by type (for RSS readers and outliners)";
13
14
 
14
15
  /**
15
16
  * Convert a compilation object to OPML XML.
@@ -19,7 +20,7 @@ export const description = 'Claims as OPML 2.0 outline grouped by type (for RSS
19
20
  export function convert(compilation) {
20
21
  const meta = compilation.meta || {};
21
22
  const claims = compilation.claims || [];
22
- const sprint = meta.sprint || 'unknown';
23
+ const sprint = meta.sprint || "unknown";
23
24
 
24
25
  const grouped = groupByType(claims);
25
26
  const lines = [];
@@ -27,9 +28,16 @@ export function convert(compilation) {
27
28
  lines.push('<?xml version="1.0" encoding="utf-8"?>');
28
29
  lines.push('<opml version="2.0">');
29
30
  lines.push(` <head><title>${esc(sprint)}</title></head>`);
30
- lines.push(' <body>');
31
-
32
- const typeOrder = ['constraint', 'factual', 'estimate', 'risk', 'recommendation', 'feedback'];
31
+ lines.push(" <body>");
32
+
33
+ const typeOrder = [
34
+ "constraint",
35
+ "factual",
36
+ "estimate",
37
+ "risk",
38
+ "recommendation",
39
+ "feedback",
40
+ ];
33
41
  const seen = new Set();
34
42
 
35
43
  for (const type of typeOrder) {
@@ -46,16 +54,16 @@ export function convert(compilation) {
46
54
  }
47
55
  }
48
56
 
49
- lines.push(' </body>');
50
- lines.push('</opml>');
57
+ lines.push(" </body>");
58
+ lines.push("</opml>");
51
59
 
52
- return lines.join('\n') + '\n';
60
+ return lines.join("\n") + "\n";
53
61
  }
54
62
 
55
63
  function groupByType(claims) {
56
64
  const groups = {};
57
65
  for (const claim of claims) {
58
- const type = claim.type || 'other';
66
+ const type = claim.type || "other";
59
67
  if (!groups[type]) groups[type] = [];
60
68
  groups[type].push(claim);
61
69
  }
@@ -63,29 +71,31 @@ function groupByType(claims) {
63
71
  }
64
72
 
65
73
  function renderGroup(type, claims) {
66
- const label = capitalize(type) + 's';
74
+ const label = capitalize(type) + "s";
67
75
  const lines = [];
68
76
 
69
77
  lines.push(` <outline text="${esc(label)} (${claims.length})">`);
70
78
 
71
79
  for (const claim of claims) {
72
- const id = claim.id || '???';
73
- const content = claim.content || claim.text || '';
80
+ const id = claim.id || "???";
81
+ const content = claim.content || claim.text || "";
74
82
  const summary = truncate(content, 100);
75
83
  const evidence = getEvidence(claim);
76
- lines.push(` <outline text="${esc(id)}: ${esc(summary)}" _note="evidence: ${esc(evidence)}"/>`);
84
+ lines.push(
85
+ ` <outline text="${esc(id)}: ${esc(summary)}" _note="evidence: ${esc(evidence)}"/>`,
86
+ );
77
87
  }
78
88
 
79
- lines.push(' </outline>');
89
+ lines.push(" </outline>");
80
90
  return lines;
81
91
  }
82
92
 
83
93
  function getEvidence(claim) {
84
- if (typeof claim.evidence === 'string') return claim.evidence;
85
- if (typeof claim.evidence === 'object' && claim.evidence !== null) {
86
- return claim.evidence.tier || claim.evidence_tier || 'stated';
94
+ if (typeof claim.evidence === "string") return claim.evidence;
95
+ if (typeof claim.evidence === "object" && claim.evidence !== null) {
96
+ return claim.evidence.tier || claim.evidence_tier || "stated";
87
97
  }
88
- return claim.evidence_tier || 'stated';
98
+ return claim.evidence_tier || "stated";
89
99
  }
90
100
 
91
101
  function capitalize(str) {
@@ -94,15 +104,15 @@ function capitalize(str) {
94
104
 
95
105
  function truncate(str, max) {
96
106
  if (str.length <= max) return str;
97
- return str.slice(0, max - 3) + '...';
107
+ return str.slice(0, max - 3) + "...";
98
108
  }
99
109
 
100
110
  function esc(str) {
101
- if (str == null) return '';
111
+ if (str == null) return "";
102
112
  return String(str)
103
- .replace(/&/g, '&amp;')
104
- .replace(/</g, '&lt;')
105
- .replace(/>/g, '&gt;')
106
- .replace(/"/g, '&quot;')
107
- .replace(/'/g, '&apos;');
113
+ .replace(/&/g, "&amp;")
114
+ .replace(/</g, "&lt;")
115
+ .replace(/>/g, "&gt;")
116
+ .replace(/"/g, "&quot;")
117
+ .replace(/'/g, "&apos;");
108
118
  }