@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 with CSS scroll-snap. One slide per claim type group.
5
5
  * Dark theme matching grainulation design tokens.
6
+ * Accessible: slide landmarks, aria-roledescription, live region, reduced-motion.
6
7
  * Zero dependencies — node built-in only.
7
8
  */
8
9
 
9
- export const name = 'slide-deck';
10
- export const extension = '.html';
11
- export const mimeType = 'text/html; charset=utf-8';
12
- export const description = 'Scroll-snap slide deck: one slide per type group with keyboard navigation';
10
+ export const name = "slide-deck";
11
+ export const extension = ".html";
12
+ export const mimeType = "text/html; charset=utf-8";
13
+ export const description =
14
+ "Scroll-snap slide deck: one slide per type group with keyboard navigation";
13
15
 
14
16
  /**
15
17
  * Convert a compilation object to a slide deck HTML page.
@@ -22,60 +24,76 @@ 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 Deck';
27
+ const title = meta.sprint || meta.question || "Sprint Deck";
26
28
  const compiled = certificate.compiled_at || new Date().toISOString();
27
- const active = claims.filter(c => c.status === 'active').length;
29
+ const active = claims.filter((c) => c.status === "active").length;
28
30
 
29
31
  // Group by type (skip reverted)
30
32
  const byType = {};
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
  }
37
39
 
38
- const typeOrder = ['constraint', 'factual', 'recommendation', 'risk', 'estimate', 'feedback'];
39
- const sortedTypes = typeOrder.filter(t => byType[t]);
40
+ const typeOrder = [
41
+ "constraint",
42
+ "factual",
43
+ "recommendation",
44
+ "risk",
45
+ "estimate",
46
+ "feedback",
47
+ ];
48
+ const sortedTypes = typeOrder.filter((t) => byType[t]);
40
49
  for (const t of Object.keys(byType)) {
41
50
  if (!sortedTypes.includes(t)) sortedTypes.push(t);
42
51
  }
43
52
 
44
53
  const typeColors = {
45
- constraint: '#e74c3c',
46
- factual: '#3498db',
47
- recommendation: '#2ecc71',
48
- risk: '#f39c12',
49
- estimate: '#9b59b6',
50
- feedback: '#1abc9c',
51
- unknown: '#95a5a6',
54
+ constraint: "#e74c3c",
55
+ factual: "#3498db",
56
+ recommendation: "#2ecc71",
57
+ risk: "#f39c12",
58
+ estimate: "#9b59b6",
59
+ feedback: "#1abc9c",
60
+ unknown: "#95a5a6",
52
61
  };
53
62
 
63
+ let slideNum = 0;
64
+ // Calculate total slides: title + summary + types + optional conflicts + certificate
65
+ const totalSlides =
66
+ 2 + sortedTypes.length + (conflicts.length > 0 ? 1 : 0) + 1;
67
+
54
68
  // Title slide
55
69
  const slides = [];
70
+ slideNum++;
56
71
  slides.push(`
57
- <section class="slide">
72
+ <section class="slide" role="group" aria-roledescription="slide" aria-label="Slide ${slideNum} of ${totalSlides}: Title">
58
73
  <div class="slide-content title-slide">
59
74
  <h1>${esc(title)}</h1>
60
- ${meta.question ? `<p class="question">${esc(meta.question)}</p>` : ''}
75
+ ${meta.question ? `<p class="question">${esc(meta.question)}</p>` : ""}
61
76
  <p class="meta">${esc(compiled)} | ${claims.length} claims</p>
62
77
  </div>
63
78
  </section>`);
64
79
 
65
80
  // Summary slide
66
- const typeStats = sortedTypes.map(t => {
67
- const color = typeColors[t] || typeColors.unknown;
68
- return `<div class="type-stat"><span class="dot" style="background:${color}"></span>${capitalize(t)}: ${byType[t].length}</div>`;
69
- }).join('\n ');
81
+ slideNum++;
82
+ const typeStats = sortedTypes
83
+ .map((t) => {
84
+ const color = typeColors[t] || typeColors.unknown;
85
+ return `<div class="type-stat"><span class="dot" style="background:${color}" aria-hidden="true"></span>${capitalize(t)}: ${byType[t].length}</div>`;
86
+ })
87
+ .join("\n ");
70
88
 
71
89
  slides.push(`
72
- <section class="slide">
90
+ <section class="slide" role="group" aria-roledescription="slide" aria-label="Slide ${slideNum} of ${totalSlides}: Summary">
73
91
  <div class="slide-content">
74
92
  <h2>Summary</h2>
75
93
  <div class="summary-grid">
76
94
  <div class="big-stat"><span class="num">${claims.length}</span><span class="label">Total</span></div>
77
95
  <div class="big-stat"><span class="num">${active}</span><span class="label">Active</span></div>
78
- ${conflicts.length ? `<div class="big-stat"><span class="num">${conflicts.length}</span><span class="label">Conflicts</span></div>` : ''}
96
+ ${conflicts.length ? `<div class="big-stat"><span class="num">${conflicts.length}</span><span class="label">Conflicts</span></div>` : ""}
79
97
  </div>
80
98
  <div class="type-stats">
81
99
  ${typeStats}
@@ -85,16 +103,20 @@ export function convert(compilation) {
85
103
 
86
104
  // One slide per type group
87
105
  for (const t of sortedTypes) {
106
+ slideNum++;
88
107
  const group = byType[t];
89
108
  const color = typeColors[t] || typeColors.unknown;
90
- const items = group.map(c => {
91
- const body = esc(c.content || c.text || '');
92
- const conf = c.confidence != null ? ` (${Math.round(c.confidence * 100)}%)` : '';
93
- return `<li><strong>${esc(c.id)}</strong>${conf}: ${body}</li>`;
94
- }).join('\n ');
109
+ const items = group
110
+ .map((c) => {
111
+ const body = esc(c.content || c.text || "");
112
+ const conf =
113
+ c.confidence != null ? ` (${Math.round(c.confidence * 100)}%)` : "";
114
+ return `<li><strong>${esc(c.id)}</strong>${conf}: ${body}</li>`;
115
+ })
116
+ .join("\n ");
95
117
 
96
118
  slides.push(`
97
- <section class="slide">
119
+ <section class="slide" role="group" aria-roledescription="slide" aria-label="Slide ${slideNum} of ${totalSlides}: ${capitalize(t)}s">
98
120
  <div class="slide-content">
99
121
  <h2 style="border-left:4px solid ${color};padding-left:12px">${capitalize(t)}s (${group.length})</h2>
100
122
  <ul class="claim-list">
@@ -106,14 +128,17 @@ export function convert(compilation) {
106
128
 
107
129
  // Conflicts slide (if any)
108
130
  if (conflicts.length > 0) {
109
- const conflictItems = conflicts.map(c => {
110
- const ids = c.ids?.join(' vs ') || 'unknown';
111
- const resolved = c.resolution ? ' [resolved]' : '';
112
- return `<li><strong>${esc(ids)}</strong>${resolved}: ${esc(c.description || c.reason || '')}</li>`;
113
- }).join('\n ');
131
+ slideNum++;
132
+ const conflictItems = conflicts
133
+ .map((c) => {
134
+ const ids = c.ids?.join(" vs ") || "unknown";
135
+ const resolved = c.resolution ? " [resolved]" : "";
136
+ return `<li><strong>${esc(ids)}</strong>${resolved}: ${esc(c.description || c.reason || "")}</li>`;
137
+ })
138
+ .join("\n ");
114
139
 
115
140
  slides.push(`
116
- <section class="slide">
141
+ <section class="slide" role="group" aria-roledescription="slide" aria-label="Slide ${slideNum} of ${totalSlides}: Conflicts">
117
142
  <div class="slide-content">
118
143
  <h2 style="border-left:4px solid #e74c3c;padding-left:12px">Conflicts (${conflicts.length})</h2>
119
144
  <ul class="claim-list">
@@ -124,12 +149,13 @@ export function convert(compilation) {
124
149
  }
125
150
 
126
151
  // Certificate slide
152
+ slideNum++;
127
153
  slides.push(`
128
- <section class="slide">
154
+ <section class="slide" role="group" aria-roledescription="slide" aria-label="Slide ${slideNum} of ${totalSlides}: Certificate">
129
155
  <div class="slide-content title-slide">
130
156
  <h2>Certificate</h2>
131
157
  <p class="mono">${certificate.claim_count || claims.length} claims</p>
132
- <p class="mono">sha256:${esc((certificate.sha256 || 'unknown').slice(0, 24))}</p>
158
+ <p class="mono">sha256:${esc((certificate.sha256 || "unknown").slice(0, 24))}</p>
133
159
  <p class="meta">${esc(compiled)}</p>
134
160
  </div>
135
161
  </section>`);
@@ -145,7 +171,10 @@ export function convert(compilation) {
145
171
  * { margin:0; padding:0; box-sizing:border-box; }
146
172
  html { scroll-snap-type:y mandatory; overflow-y:scroll; }
147
173
  body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; line-height:1.6; }
174
+ a.skip-nav { 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; font-size:0.9rem; }
175
+ a.skip-nav:focus { top:0; outline:2px solid #3b82f6; }
148
176
  .slide { scroll-snap-align:start; height:100vh; display:flex; align-items:center; justify-content:center; padding:2rem; }
177
+ .slide:focus { outline:2px solid #3b82f6; outline-offset:-2px; }
149
178
  .slide-content { max-width:800px; width:100%; }
150
179
  .title-slide { text-align:center; }
151
180
  .title-slide h1 { font-size:2.4rem; margin-bottom:0.75rem; }
@@ -164,35 +193,94 @@ export function convert(compilation) {
164
193
  .claim-list { list-style:none; max-height:70vh; overflow-y:auto; }
165
194
  .claim-list li { padding:0.6rem 0; border-bottom:1px solid var(--border); font-size:0.9rem; }
166
195
  .claim-list li strong { font-family:monospace; font-size:0.8rem; }
196
+ .slide-counter { position:fixed; bottom:1rem; right:1rem; background:var(--surface); color:var(--muted); padding:0.25rem 0.6rem; border-radius:4px; font-size:0.75rem; font-family:monospace; z-index:10; }
197
+ .sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
198
+ .nav-hint { position:fixed; bottom:1rem; left:1rem; background:var(--surface); color:var(--muted); padding:0.25rem 0.6rem; border-radius:4px; font-size:0.7rem; font-family:monospace; z-index:10; opacity:0.6; }
199
+ @media (prefers-reduced-motion:reduce) { html { scroll-snap-type:none; scroll-behavior:auto; } }
200
+ @media print { .skip-nav, .slide-counter, .nav-hint { display:none; } .slide { height:auto; page-break-after:always; } }
167
201
  </style>
168
202
  </head>
169
203
  <body>
170
- ${slides.join('\n')}
204
+ <a class="skip-nav" href="#slide-1">Skip to first slide</a>
205
+ <main role="main" aria-label="Slide deck: ${esc(title)}">
206
+ ${slides.join("\n")}
207
+ </main>
208
+ <div class="slide-counter" aria-live="polite" aria-atomic="true"></div>
209
+ <div class="nav-hint" aria-hidden="true">Arrow keys / PgUp / PgDn / Home / End to navigate</div>
171
210
  <script>
172
- document.addEventListener('keydown', function(e) {
173
- if (e.key === 'ArrowDown' || e.key === 'PageDown') {
174
- e.preventDefault();
175
- const slides = document.querySelectorAll('.slide');
176
- const vh = window.innerHeight;
177
- const current = Math.round(window.scrollY / vh);
178
- const next = Math.min(current + 1, slides.length - 1);
179
- slides[next].scrollIntoView({ behavior: 'smooth' });
180
- } else if (e.key === 'ArrowUp' || e.key === 'PageUp') {
181
- e.preventDefault();
182
- const vh = window.innerHeight;
183
- const current = Math.round(window.scrollY / vh);
184
- const prev = Math.max(current - 1, 0);
185
- document.querySelectorAll('.slide')[prev].scrollIntoView({ behavior: 'smooth' });
211
+ (function() {
212
+ var slides = document.querySelectorAll('.slide');
213
+ var counter = document.querySelector('.slide-counter');
214
+ var total = slides.length;
215
+ var prefersReduced = window.matchMedia('(prefers-reduced-motion:reduce)').matches;
216
+ var scrollBehavior = prefersReduced ? 'auto' : 'smooth';
217
+
218
+ // Give slides tabindex for focus management
219
+ slides.forEach(function(slide, i) {
220
+ slide.id = slide.id || 'slide-' + (i + 1);
221
+ slide.setAttribute('tabindex', '-1');
222
+ });
223
+
224
+ function currentSlideIndex() {
225
+ var vh = window.innerHeight;
226
+ return Math.round(window.scrollY / vh);
186
227
  }
187
- });
228
+
229
+ function goToSlide(index) {
230
+ var target = Math.max(0, Math.min(index, slides.length - 1));
231
+ slides[target].scrollIntoView({ behavior: scrollBehavior });
232
+ slides[target].focus({ preventScroll: true });
233
+ }
234
+
235
+ function updateCounter() {
236
+ var current = currentSlideIndex() + 1;
237
+ counter.textContent = current + ' / ' + total;
238
+ }
239
+ updateCounter();
240
+ window.addEventListener('scroll', updateCounter, { passive: true });
241
+
242
+ document.addEventListener('keydown', function(e) {
243
+ var current = currentSlideIndex();
244
+ switch (e.key) {
245
+ case 'ArrowDown':
246
+ case 'PageDown':
247
+ case 'ArrowRight':
248
+ e.preventDefault();
249
+ goToSlide(current + 1);
250
+ break;
251
+ case 'ArrowUp':
252
+ case 'PageUp':
253
+ case 'ArrowLeft':
254
+ e.preventDefault();
255
+ goToSlide(current - 1);
256
+ break;
257
+ case 'Home':
258
+ e.preventDefault();
259
+ goToSlide(0);
260
+ break;
261
+ case 'End':
262
+ e.preventDefault();
263
+ goToSlide(slides.length - 1);
264
+ break;
265
+ case 'Escape':
266
+ // Move focus to body (exit slide focus)
267
+ document.body.focus();
268
+ break;
269
+ }
270
+ });
271
+ })();
188
272
  </script>
189
273
  </body>
190
274
  </html>`;
191
275
  }
192
276
 
193
277
  function esc(str) {
194
- if (str == null) return '';
195
- return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
278
+ if (str == null) return "";
279
+ return String(str)
280
+ .replace(/&/g, "&amp;")
281
+ .replace(/</g, "&lt;")
282
+ .replace(/>/g, "&gt;")
283
+ .replace(/"/g, "&quot;");
196
284
  }
197
285
 
198
286
  function capitalize(str) {
@@ -6,10 +6,10 @@
6
6
  * Zero dependencies — node built-in only.
7
7
  */
8
8
 
9
- export const name = 'sql';
10
- export const extension = '.sql';
11
- export const mimeType = 'application/sql; charset=utf-8';
12
- export const description = 'SQL schema and INSERT statements for claims data';
9
+ export const name = "sql";
10
+ export const extension = ".sql";
11
+ export const mimeType = "application/sql; charset=utf-8";
12
+ export const description = "SQL schema and INSERT statements for claims data";
13
13
 
14
14
  /**
15
15
  * Convert a compilation object to SQL statements.
@@ -23,94 +23,98 @@ export function convert(compilation) {
23
23
 
24
24
  const lines = [];
25
25
 
26
- lines.push('-- Auto-generated by mill sql format');
26
+ lines.push("-- Auto-generated by mill sql format");
27
27
  if (meta.sprint) lines.push(`-- Sprint: ${meta.sprint}`);
28
- if (certificate.compiled_at) lines.push(`-- Compiled: ${certificate.compiled_at}`);
29
- lines.push('');
28
+ if (certificate.compiled_at)
29
+ lines.push(`-- Compiled: ${certificate.compiled_at}`);
30
+ lines.push("");
30
31
 
31
32
  // Claims table
32
- lines.push('CREATE TABLE IF NOT EXISTS claims (');
33
- lines.push(' id TEXT PRIMARY KEY,');
34
- lines.push(' type TEXT,');
35
- lines.push(' content TEXT,');
36
- lines.push(' evidence_tier TEXT,');
37
- lines.push(' status TEXT,');
38
- lines.push(' confidence REAL,');
39
- lines.push(' source TEXT,');
40
- lines.push(' tags TEXT,');
41
- lines.push(' created TEXT');
42
- lines.push(');');
43
- lines.push('');
33
+ lines.push("CREATE TABLE IF NOT EXISTS claims (");
34
+ lines.push(" id TEXT PRIMARY KEY,");
35
+ lines.push(" type TEXT,");
36
+ lines.push(" content TEXT,");
37
+ lines.push(" evidence_tier TEXT,");
38
+ lines.push(" status TEXT,");
39
+ lines.push(" confidence REAL,");
40
+ lines.push(" source TEXT,");
41
+ lines.push(" tags TEXT,");
42
+ lines.push(" created TEXT");
43
+ lines.push(");");
44
+ lines.push("");
44
45
 
45
46
  // Conflicts table
46
- lines.push('CREATE TABLE IF NOT EXISTS conflicts (');
47
- lines.push(' id INTEGER PRIMARY KEY AUTOINCREMENT,');
48
- lines.push(' claim_ids TEXT,');
49
- lines.push(' description TEXT,');
50
- lines.push(' resolution TEXT');
51
- lines.push(');');
52
- lines.push('');
47
+ lines.push("CREATE TABLE IF NOT EXISTS conflicts (");
48
+ lines.push(" id INTEGER PRIMARY KEY AUTOINCREMENT,");
49
+ lines.push(" claim_ids TEXT,");
50
+ lines.push(" description TEXT,");
51
+ lines.push(" resolution TEXT");
52
+ lines.push(");");
53
+ lines.push("");
53
54
 
54
55
  // Insert claims
55
56
  if (claims.length > 0) {
56
- lines.push('-- Claims');
57
+ lines.push("-- Claims");
57
58
  for (const claim of claims) {
58
- const id = esc(claim.id || '');
59
- const type = esc(claim.type || '');
60
- const content = esc(claim.content || claim.text || '');
59
+ const id = esc(claim.id || "");
60
+ const type = esc(claim.type || "");
61
+ const content = esc(claim.content || claim.text || "");
61
62
  const evidenceTier = esc(extractTier(claim));
62
- const status = esc(claim.status || '');
63
- const confidence = claim.confidence != null ? String(claim.confidence) : 'NULL';
63
+ const status = esc(claim.status || "");
64
+ const confidence =
65
+ claim.confidence != null ? String(claim.confidence) : "NULL";
64
66
  const source = esc(extractSource(claim));
65
- const tags = esc(Array.isArray(claim.tags) ? claim.tags.join(',') : '');
66
- const created = esc(claim.created || '');
67
+ const tags = esc(Array.isArray(claim.tags) ? claim.tags.join(",") : "");
68
+ const created = esc(claim.created || "");
67
69
 
68
70
  lines.push(
69
- `INSERT INTO claims VALUES ('${id}', '${type}', '${content}', '${evidenceTier}', '${status}', ${confidence}, '${source}', '${tags}', '${created}');`
71
+ `INSERT INTO claims VALUES ('${id}', '${type}', '${content}', '${evidenceTier}', '${status}', ${confidence}, '${source}', '${tags}', '${created}');`,
70
72
  );
71
73
  }
72
- lines.push('');
74
+ lines.push("");
73
75
  }
74
76
 
75
77
  // Insert conflicts
76
78
  const conflicts = compilation.conflicts || [];
77
79
  if (conflicts.length > 0) {
78
- lines.push('-- Conflicts');
80
+ lines.push("-- Conflicts");
79
81
  for (const conflict of conflicts) {
80
- const claimIds = esc(Array.isArray(conflict.ids) ? conflict.ids.join(',') : '');
81
- const description = esc(conflict.description || conflict.reason || '');
82
- const resolution = esc(conflict.resolution || '');
82
+ const claimIds = esc(
83
+ Array.isArray(conflict.ids) ? conflict.ids.join(",") : "",
84
+ );
85
+ const description = esc(conflict.description || conflict.reason || "");
86
+ const resolution = esc(conflict.resolution || "");
83
87
 
84
88
  lines.push(
85
- `INSERT INTO conflicts (claim_ids, description, resolution) VALUES ('${claimIds}', '${description}', '${resolution}');`
89
+ `INSERT INTO conflicts (claim_ids, description, resolution) VALUES ('${claimIds}', '${description}', '${resolution}');`,
86
90
  );
87
91
  }
88
- lines.push('');
92
+ lines.push("");
89
93
  }
90
94
 
91
- return lines.join('\n');
95
+ return lines.join("\n");
92
96
  }
93
97
 
94
98
  function esc(value) {
95
- if (value == null) return '';
99
+ if (value == null) return "";
96
100
  return String(value).replace(/'/g, "''");
97
101
  }
98
102
 
99
103
  function extractTier(claim) {
100
- if (typeof claim.evidence === 'string') return claim.evidence;
101
- if (typeof claim.evidence === 'object' && claim.evidence !== null) {
102
- return claim.evidence.tier || '';
104
+ if (typeof claim.evidence === "string") return claim.evidence;
105
+ if (typeof claim.evidence === "object" && claim.evidence !== null) {
106
+ return claim.evidence.tier || "";
103
107
  }
104
- return claim.evidence_tier || '';
108
+ return claim.evidence_tier || "";
105
109
  }
106
110
 
107
111
  function extractSource(claim) {
108
- if (typeof claim.source === 'string') return claim.source;
109
- if (typeof claim.source === 'object' && claim.source !== null) {
110
- return claim.source.origin || claim.source.artifact || '';
112
+ if (typeof claim.source === "string") return claim.source;
113
+ if (typeof claim.source === "object" && claim.source !== null) {
114
+ return claim.source.origin || claim.source.artifact || "";
111
115
  }
112
- if (typeof claim.evidence === 'object' && claim.evidence?.source) {
116
+ if (typeof claim.evidence === "object" && claim.evidence?.source) {
113
117
  return claim.evidence.source;
114
118
  }
115
- return '';
119
+ return "";
116
120
  }