@grainulation/mill 1.0.0
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.
- package/CHANGELOG.md +12 -0
- package/LICENSE +21 -0
- package/README.md +76 -0
- package/bin/mill.js +320 -0
- package/lib/exporters/csv.js +83 -0
- package/lib/exporters/json-ld.js +44 -0
- package/lib/exporters/markdown.js +116 -0
- package/lib/exporters/pdf.js +104 -0
- package/lib/formats/bibtex.js +76 -0
- package/lib/formats/changelog.js +102 -0
- package/lib/formats/csv.js +92 -0
- package/lib/formats/dot.js +129 -0
- package/lib/formats/evidence-matrix.js +87 -0
- package/lib/formats/executive-summary.js +130 -0
- package/lib/formats/github-issues.js +89 -0
- package/lib/formats/graphml.js +118 -0
- package/lib/formats/html-report.js +181 -0
- package/lib/formats/jira-csv.js +89 -0
- package/lib/formats/json-ld.js +28 -0
- package/lib/formats/markdown.js +118 -0
- package/lib/formats/ndjson.js +25 -0
- package/lib/formats/obsidian.js +136 -0
- package/lib/formats/opml.js +108 -0
- package/lib/formats/ris.js +70 -0
- package/lib/formats/rss.js +100 -0
- package/lib/formats/sankey.js +72 -0
- package/lib/formats/slide-deck.js +200 -0
- package/lib/formats/sql.js +116 -0
- package/lib/formats/static-site.js +169 -0
- package/lib/formats/treemap.js +65 -0
- package/lib/formats/typescript-defs.js +147 -0
- package/lib/formats/yaml.js +144 -0
- package/lib/formats.js +60 -0
- package/lib/index.js +14 -0
- package/lib/json-ld-common.js +72 -0
- package/lib/publishers/clipboard.js +70 -0
- package/lib/publishers/static.js +152 -0
- package/lib/serve-mcp.js +340 -0
- package/lib/server.js +535 -0
- package/package.json +53 -0
- package/public/grainulation-tokens.css +321 -0
- package/public/index.html +891 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: markdown
|
|
3
|
+
*
|
|
4
|
+
* Converts compilation.json (wheat compiler output) to clean Markdown.
|
|
5
|
+
* Used by the mill server for live preview and export.
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
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';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a compilation object to Markdown.
|
|
16
|
+
* @param {object} compilation - The compilation.json content
|
|
17
|
+
* @returns {string} Markdown output
|
|
18
|
+
*/
|
|
19
|
+
export function convert(compilation) {
|
|
20
|
+
const lines = [];
|
|
21
|
+
const meta = compilation.meta || {};
|
|
22
|
+
const claims = compilation.claims || [];
|
|
23
|
+
const conflicts = compilation.conflicts || [];
|
|
24
|
+
const certificate = compilation.certificate || {};
|
|
25
|
+
|
|
26
|
+
// Title
|
|
27
|
+
const title = meta.sprint || meta.question || 'Sprint Export';
|
|
28
|
+
lines.push(`# ${title}`);
|
|
29
|
+
lines.push('');
|
|
30
|
+
|
|
31
|
+
// Meta block
|
|
32
|
+
if (meta.question) {
|
|
33
|
+
lines.push(`> **Question:** ${meta.question}`);
|
|
34
|
+
lines.push('');
|
|
35
|
+
}
|
|
36
|
+
if (meta.audience) {
|
|
37
|
+
lines.push(`**Audience:** ${meta.audience}`);
|
|
38
|
+
lines.push('');
|
|
39
|
+
}
|
|
40
|
+
if (certificate.compiled_at) {
|
|
41
|
+
lines.push(`*Compiled: ${certificate.compiled_at}*`);
|
|
42
|
+
lines.push('');
|
|
43
|
+
}
|
|
44
|
+
|
|
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('');
|
|
51
|
+
lines.push(`- **Total claims:** ${claims.length}`);
|
|
52
|
+
lines.push(`- **Active:** ${active}`);
|
|
53
|
+
if (superseded) lines.push(`- **Superseded:** ${superseded}`);
|
|
54
|
+
if (reverted) lines.push(`- **Reverted:** ${reverted}`);
|
|
55
|
+
if (conflicts.length) lines.push(`- **Conflicts:** ${conflicts.length}`);
|
|
56
|
+
lines.push('');
|
|
57
|
+
|
|
58
|
+
// Group claims by type
|
|
59
|
+
const byType = {};
|
|
60
|
+
for (const claim of claims) {
|
|
61
|
+
if (claim.status === 'reverted') continue;
|
|
62
|
+
const type = claim.type || 'unknown';
|
|
63
|
+
if (!byType[type]) byType[type] = [];
|
|
64
|
+
byType[type].push(claim);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const typeOrder = ['constraint', 'factual', 'recommendation', 'risk', 'estimate', 'feedback'];
|
|
68
|
+
const sortedTypes = typeOrder.filter(t => byType[t]);
|
|
69
|
+
for (const t of Object.keys(byType)) {
|
|
70
|
+
if (!sortedTypes.includes(t)) sortedTypes.push(t);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const type of sortedTypes) {
|
|
74
|
+
const group = byType[type];
|
|
75
|
+
if (!group || group.length === 0) continue;
|
|
76
|
+
|
|
77
|
+
lines.push(`## ${capitalize(type)}s (${group.length})`);
|
|
78
|
+
lines.push('');
|
|
79
|
+
|
|
80
|
+
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 || '';
|
|
86
|
+
lines.push(`- **${claim.id}**${conf}${tier}${status}: ${body}`);
|
|
87
|
+
}
|
|
88
|
+
lines.push('');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Conflicts
|
|
92
|
+
if (conflicts.length > 0) {
|
|
93
|
+
lines.push('## Conflicts');
|
|
94
|
+
lines.push('');
|
|
95
|
+
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'}`);
|
|
98
|
+
if (conflict.resolution) {
|
|
99
|
+
lines.push(` - *Resolution:* ${conflict.resolution}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
lines.push('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Certificate
|
|
106
|
+
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('');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function capitalize(str) {
|
|
117
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
118
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: ndjson
|
|
3
|
+
*
|
|
4
|
+
* One JSON object per line (Newline Delimited JSON).
|
|
5
|
+
* Each line is a claim with all fields preserved.
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
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';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a compilation object to NDJSON.
|
|
16
|
+
* @param {object} compilation - The compilation.json content
|
|
17
|
+
* @returns {string} NDJSON output
|
|
18
|
+
*/
|
|
19
|
+
export function convert(compilation) {
|
|
20
|
+
const claims = compilation.claims || [];
|
|
21
|
+
|
|
22
|
+
if (claims.length === 0) return '';
|
|
23
|
+
|
|
24
|
+
return claims.map(c => JSON.stringify(c)).join('\n') + '\n';
|
|
25
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: obsidian
|
|
3
|
+
*
|
|
4
|
+
* Converts compilation.json claims to Obsidian-flavored markdown
|
|
5
|
+
* with YAML front matter, wikilinks, and backlinks.
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
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)';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a compilation object to Obsidian markdown.
|
|
16
|
+
* @param {object} compilation - The compilation.json content
|
|
17
|
+
* @returns {string} Markdown output
|
|
18
|
+
*/
|
|
19
|
+
export function convert(compilation) {
|
|
20
|
+
const meta = compilation.meta || {};
|
|
21
|
+
const claims = compilation.claims || [];
|
|
22
|
+
const sprint = meta.sprint || 'unknown';
|
|
23
|
+
const question = meta.question || '';
|
|
24
|
+
const audience = meta.audience || '';
|
|
25
|
+
|
|
26
|
+
const grouped = groupByType(claims);
|
|
27
|
+
const lines = [];
|
|
28
|
+
|
|
29
|
+
// YAML front matter
|
|
30
|
+
lines.push('---');
|
|
31
|
+
lines.push(`sprint: ${sprint}`);
|
|
32
|
+
if (question) lines.push(`question: "${escapeFrontMatter(question)}"`);
|
|
33
|
+
if (audience) lines.push(`audience: "${escapeFrontMatter(audience)}"`);
|
|
34
|
+
lines.push(`claim_count: ${claims.length}`);
|
|
35
|
+
lines.push('tags: [wheat, sprint, export]');
|
|
36
|
+
lines.push('---');
|
|
37
|
+
lines.push('');
|
|
38
|
+
|
|
39
|
+
// Title
|
|
40
|
+
lines.push(`# Sprint: ${sprint}`);
|
|
41
|
+
lines.push('');
|
|
42
|
+
|
|
43
|
+
if (question) {
|
|
44
|
+
lines.push(`> ${question}`);
|
|
45
|
+
lines.push('');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Claims grouped by type
|
|
49
|
+
lines.push('## Claims');
|
|
50
|
+
lines.push('');
|
|
51
|
+
|
|
52
|
+
const typeOrder = ['constraint', 'factual', 'estimate', 'risk', 'recommendation', 'feedback'];
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
|
|
55
|
+
for (const type of typeOrder) {
|
|
56
|
+
if (grouped[type]) {
|
|
57
|
+
lines.push(...renderGroup(type, grouped[type]));
|
|
58
|
+
seen.add(type);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const type of Object.keys(grouped)) {
|
|
63
|
+
if (!seen.has(type)) {
|
|
64
|
+
lines.push(...renderGroup(type, grouped[type]));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Connections section
|
|
69
|
+
lines.push('## Connections');
|
|
70
|
+
lines.push('');
|
|
71
|
+
lines.push('- Related: [[compilation]] [[claims]]');
|
|
72
|
+
|
|
73
|
+
if (compilation.certificate) {
|
|
74
|
+
lines.push('- Certificate: [[certificate]]');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
lines.push('');
|
|
78
|
+
return lines.join('\n') + '\n';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function groupByType(claims) {
|
|
82
|
+
const groups = {};
|
|
83
|
+
for (const claim of claims) {
|
|
84
|
+
const type = claim.type || 'other';
|
|
85
|
+
if (!groups[type]) groups[type] = [];
|
|
86
|
+
groups[type].push(claim);
|
|
87
|
+
}
|
|
88
|
+
return groups;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function renderGroup(type, claims) {
|
|
92
|
+
const label = capitalize(type) + 's';
|
|
93
|
+
const lines = [];
|
|
94
|
+
|
|
95
|
+
lines.push(`### ${label}`);
|
|
96
|
+
|
|
97
|
+
for (const claim of claims) {
|
|
98
|
+
const id = claim.id || '???';
|
|
99
|
+
const content = claim.content || claim.text || '';
|
|
100
|
+
const summary = truncate(content, 120);
|
|
101
|
+
const evidence = getEvidence(claim);
|
|
102
|
+
const tags = formatTags(claim);
|
|
103
|
+
|
|
104
|
+
lines.push(`- [[${id}]] ${summary} #${type} #${evidence}${tags}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
lines.push('');
|
|
108
|
+
return lines;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
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';
|
|
115
|
+
}
|
|
116
|
+
return claim.evidence_tier || 'stated';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function formatTags(claim) {
|
|
120
|
+
const tags = Array.isArray(claim.tags) ? claim.tags : [];
|
|
121
|
+
if (tags.length === 0) return '';
|
|
122
|
+
return ' ' + tags.map(t => `#${t.replace(/\s+/g, '-')}`).join(' ');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function capitalize(str) {
|
|
126
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function truncate(str, max) {
|
|
130
|
+
if (str.length <= max) return str;
|
|
131
|
+
return str.slice(0, max - 3) + '...';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function escapeFrontMatter(str) {
|
|
135
|
+
return String(str).replace(/"/g, '\\"');
|
|
136
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: opml
|
|
3
|
+
*
|
|
4
|
+
* Converts compilation.json claims to OPML 2.0 outline,
|
|
5
|
+
* grouped by claim type.
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
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)';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a compilation object to OPML XML.
|
|
16
|
+
* @param {object} compilation - The compilation.json content
|
|
17
|
+
* @returns {string} OPML XML output
|
|
18
|
+
*/
|
|
19
|
+
export function convert(compilation) {
|
|
20
|
+
const meta = compilation.meta || {};
|
|
21
|
+
const claims = compilation.claims || [];
|
|
22
|
+
const sprint = meta.sprint || 'unknown';
|
|
23
|
+
|
|
24
|
+
const grouped = groupByType(claims);
|
|
25
|
+
const lines = [];
|
|
26
|
+
|
|
27
|
+
lines.push('<?xml version="1.0" encoding="utf-8"?>');
|
|
28
|
+
lines.push('<opml version="2.0">');
|
|
29
|
+
lines.push(` <head><title>${esc(sprint)}</title></head>`);
|
|
30
|
+
lines.push(' <body>');
|
|
31
|
+
|
|
32
|
+
const typeOrder = ['constraint', 'factual', 'estimate', 'risk', 'recommendation', 'feedback'];
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
|
|
35
|
+
for (const type of typeOrder) {
|
|
36
|
+
if (grouped[type]) {
|
|
37
|
+
lines.push(...renderGroup(type, grouped[type]));
|
|
38
|
+
seen.add(type);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Remaining types not in the predefined order
|
|
43
|
+
for (const type of Object.keys(grouped)) {
|
|
44
|
+
if (!seen.has(type)) {
|
|
45
|
+
lines.push(...renderGroup(type, grouped[type]));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lines.push(' </body>');
|
|
50
|
+
lines.push('</opml>');
|
|
51
|
+
|
|
52
|
+
return lines.join('\n') + '\n';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function groupByType(claims) {
|
|
56
|
+
const groups = {};
|
|
57
|
+
for (const claim of claims) {
|
|
58
|
+
const type = claim.type || 'other';
|
|
59
|
+
if (!groups[type]) groups[type] = [];
|
|
60
|
+
groups[type].push(claim);
|
|
61
|
+
}
|
|
62
|
+
return groups;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderGroup(type, claims) {
|
|
66
|
+
const label = capitalize(type) + 's';
|
|
67
|
+
const lines = [];
|
|
68
|
+
|
|
69
|
+
lines.push(` <outline text="${esc(label)} (${claims.length})">`);
|
|
70
|
+
|
|
71
|
+
for (const claim of claims) {
|
|
72
|
+
const id = claim.id || '???';
|
|
73
|
+
const content = claim.content || claim.text || '';
|
|
74
|
+
const summary = truncate(content, 100);
|
|
75
|
+
const evidence = getEvidence(claim);
|
|
76
|
+
lines.push(` <outline text="${esc(id)}: ${esc(summary)}" _note="evidence: ${esc(evidence)}"/>`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
lines.push(' </outline>');
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
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';
|
|
87
|
+
}
|
|
88
|
+
return claim.evidence_tier || 'stated';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function capitalize(str) {
|
|
92
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function truncate(str, max) {
|
|
96
|
+
if (str.length <= max) return str;
|
|
97
|
+
return str.slice(0, max - 3) + '...';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function esc(str) {
|
|
101
|
+
if (str == null) return '';
|
|
102
|
+
return String(str)
|
|
103
|
+
.replace(/&/g, '&')
|
|
104
|
+
.replace(/</g, '<')
|
|
105
|
+
.replace(/>/g, '>')
|
|
106
|
+
.replace(/"/g, '"')
|
|
107
|
+
.replace(/'/g, ''');
|
|
108
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: ris
|
|
3
|
+
*
|
|
4
|
+
* Converts compilation.json claims to RIS tagged citation format.
|
|
5
|
+
* Each claim becomes a TY-ER record block.
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const name = 'ris';
|
|
10
|
+
export const extension = '.ris';
|
|
11
|
+
export const mimeType = 'application/x-research-info-systems; charset=utf-8';
|
|
12
|
+
export const description = 'Claims as RIS citation records (one TY-ER block per claim)';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a compilation object to RIS.
|
|
16
|
+
* @param {object} compilation - The compilation.json content
|
|
17
|
+
* @returns {string} RIS output
|
|
18
|
+
*/
|
|
19
|
+
export function convert(compilation) {
|
|
20
|
+
const claims = compilation.claims || [];
|
|
21
|
+
const meta = compilation.meta || {};
|
|
22
|
+
const author = meta.sprint ? `wheat sprint: ${meta.sprint}` : 'wheat sprint';
|
|
23
|
+
const year = new Date().getFullYear().toString();
|
|
24
|
+
|
|
25
|
+
if (claims.length === 0) {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const records = claims.map(claim => claimToRecord(claim, author, year));
|
|
30
|
+
return records.join('\n') + '\n';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function claimToRecord(claim, author, year) {
|
|
34
|
+
const id = String(claim.id || 'unknown');
|
|
35
|
+
const title = 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 || '';
|
|
41
|
+
const tags = Array.isArray(claim.tags) ? claim.tags : [];
|
|
42
|
+
const confidence = claim.confidence != null ? `, confidence: ${claim.confidence}` : '';
|
|
43
|
+
|
|
44
|
+
const noteParts = [
|
|
45
|
+
type ? `type: ${type}` : '',
|
|
46
|
+
evidence ? `evidence: ${evidence}` : '',
|
|
47
|
+
status ? `status: ${status}` : '',
|
|
48
|
+
].filter(Boolean).join(', ') + confidence;
|
|
49
|
+
|
|
50
|
+
const lines = [
|
|
51
|
+
`TY - GEN`,
|
|
52
|
+
`ID - ${id}`,
|
|
53
|
+
`TI - ${title}`,
|
|
54
|
+
`AU - ${author}`,
|
|
55
|
+
`PY - ${year}`,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
for (const tag of tags) {
|
|
59
|
+
lines.push(`KW - ${tag}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (noteParts) {
|
|
63
|
+
lines.push(`N1 - ${noteParts}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
lines.push(`ER - `);
|
|
67
|
+
lines.push('');
|
|
68
|
+
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: rss
|
|
3
|
+
*
|
|
4
|
+
* Converts compilation.json claims to an Atom XML feed.
|
|
5
|
+
* Each claim becomes a feed entry with category and content.
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const name = 'rss';
|
|
10
|
+
export const extension = '.xml';
|
|
11
|
+
export const mimeType = 'application/atom+xml; charset=utf-8';
|
|
12
|
+
export const description = 'Claims as Atom XML feed (one entry per claim)';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a compilation object to Atom XML.
|
|
16
|
+
* @param {object} compilation - The compilation.json content
|
|
17
|
+
* @returns {string} Atom XML output
|
|
18
|
+
*/
|
|
19
|
+
export function convert(compilation) {
|
|
20
|
+
const claims = compilation.claims || [];
|
|
21
|
+
const meta = compilation.meta || {};
|
|
22
|
+
const sprintName = meta.sprint || 'unnamed';
|
|
23
|
+
const updated = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
24
|
+
|
|
25
|
+
const lines = [];
|
|
26
|
+
lines.push(`<?xml version="1.0" encoding="utf-8"?>`);
|
|
27
|
+
lines.push(`<feed xmlns="http://www.w3.org/2005/Atom">`);
|
|
28
|
+
lines.push(` <title>${escapeXml(`Sprint: ${sprintName}`)}</title>`);
|
|
29
|
+
lines.push(` <id>urn:wheat:sprint:${escapeXml(sprintName)}</id>`);
|
|
30
|
+
lines.push(` <updated>${updated}</updated>`);
|
|
31
|
+
|
|
32
|
+
if (meta.question) {
|
|
33
|
+
lines.push(` <subtitle>${escapeXml(meta.question)}</subtitle>`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
for (const claim of claims) {
|
|
37
|
+
lines.push(claimToEntry(claim));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
lines.push(`</feed>`);
|
|
41
|
+
lines.push('');
|
|
42
|
+
|
|
43
|
+
return lines.join('\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function claimToEntry(claim) {
|
|
47
|
+
const id = String(claim.id || 'unknown');
|
|
48
|
+
const text = claim.content || claim.text || '';
|
|
49
|
+
const type = claim.type || '';
|
|
50
|
+
const status = claim.status || '';
|
|
51
|
+
const evidence = typeof claim.evidence === 'string'
|
|
52
|
+
? claim.evidence
|
|
53
|
+
: (claim.evidence?.tier ?? claim.evidence_tier ?? '');
|
|
54
|
+
const tags = Array.isArray(claim.tags) ? claim.tags : [];
|
|
55
|
+
const updated = new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
56
|
+
|
|
57
|
+
const summaryParts = [
|
|
58
|
+
type ? `type: ${type}` : '',
|
|
59
|
+
evidence ? `evidence: ${evidence}` : '',
|
|
60
|
+
status ? `status: ${status}` : '',
|
|
61
|
+
].filter(Boolean).join(', ');
|
|
62
|
+
|
|
63
|
+
const lines = [];
|
|
64
|
+
lines.push(` <entry>`);
|
|
65
|
+
lines.push(` <title>${escapeXml(`${id}: ${truncate(text, 80)}`)}</title>`);
|
|
66
|
+
lines.push(` <id>urn:wheat:${escapeXml(id)}</id>`);
|
|
67
|
+
lines.push(` <updated>${updated}</updated>`);
|
|
68
|
+
lines.push(` <content type="text">${escapeXml(text)}</content>`);
|
|
69
|
+
|
|
70
|
+
if (summaryParts) {
|
|
71
|
+
lines.push(` <summary>${escapeXml(summaryParts)}</summary>`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (type) {
|
|
75
|
+
lines.push(` <category term="${escapeXml(type)}"/>`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const tag of tags) {
|
|
79
|
+
lines.push(` <category term="${escapeXml(tag)}"/>`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lines.push(` </entry>`);
|
|
83
|
+
|
|
84
|
+
return lines.join('\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function escapeXml(value) {
|
|
88
|
+
if (value == null) return '';
|
|
89
|
+
return String(value)
|
|
90
|
+
.replace(/&/g, '&')
|
|
91
|
+
.replace(/</g, '<')
|
|
92
|
+
.replace(/>/g, '>')
|
|
93
|
+
.replace(/"/g, '"')
|
|
94
|
+
.replace(/'/g, ''');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function truncate(str, max) {
|
|
98
|
+
if (str.length <= max) return str;
|
|
99
|
+
return str.slice(0, max - 3) + '...';
|
|
100
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: sankey
|
|
3
|
+
*
|
|
4
|
+
* Converts compilation.json claims to Sankey JSON for D3-sankey.
|
|
5
|
+
* Three-column flow: type -> evidence_tier -> status.
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const name = 'sankey';
|
|
10
|
+
export const extension = '.json';
|
|
11
|
+
export const mimeType = 'application/json; charset=utf-8';
|
|
12
|
+
export const description = 'Claims as Sankey flow JSON (type -> evidence tier -> status)';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a compilation object to Sankey JSON.
|
|
16
|
+
* @param {object} compilation - The compilation.json content
|
|
17
|
+
* @returns {string} JSON output
|
|
18
|
+
*/
|
|
19
|
+
export function convert(compilation) {
|
|
20
|
+
const claims = compilation.claims || [];
|
|
21
|
+
|
|
22
|
+
// Count flows: type -> evidence, evidence -> status
|
|
23
|
+
const typeToEvidence = {};
|
|
24
|
+
const evidenceToStatus = {};
|
|
25
|
+
const nodeSet = new Set();
|
|
26
|
+
|
|
27
|
+
for (const claim of claims) {
|
|
28
|
+
const type = 'type:' + (claim.type || 'other');
|
|
29
|
+
const evidence = 'evidence:' + getEvidence(claim) || 'evidence:unknown';
|
|
30
|
+
const status = 'status:' + (claim.status || 'unknown');
|
|
31
|
+
|
|
32
|
+
nodeSet.add(type);
|
|
33
|
+
nodeSet.add(evidence);
|
|
34
|
+
nodeSet.add(status);
|
|
35
|
+
|
|
36
|
+
// type -> evidence link
|
|
37
|
+
const teKey = `${type}|||${evidence}`;
|
|
38
|
+
typeToEvidence[teKey] = (typeToEvidence[teKey] || 0) + 1;
|
|
39
|
+
|
|
40
|
+
// evidence -> status link
|
|
41
|
+
const esKey = `${evidence}|||${status}`;
|
|
42
|
+
evidenceToStatus[esKey] = (evidenceToStatus[esKey] || 0) + 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const nodes = Array.from(nodeSet)
|
|
46
|
+
.sort()
|
|
47
|
+
.map(id => ({ id }));
|
|
48
|
+
|
|
49
|
+
const links = [];
|
|
50
|
+
|
|
51
|
+
for (const [key, value] of Object.entries(typeToEvidence)) {
|
|
52
|
+
const [source, target] = key.split('|||');
|
|
53
|
+
links.push({ source, target, value });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const [key, value] of Object.entries(evidenceToStatus)) {
|
|
57
|
+
const [source, target] = key.split('|||');
|
|
58
|
+
links.push({ source, target, value });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const result = { nodes, links };
|
|
62
|
+
|
|
63
|
+
return JSON.stringify(result, null, 2) + '\n';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getEvidence(claim) {
|
|
67
|
+
if (typeof claim.evidence === 'string') return claim.evidence;
|
|
68
|
+
if (typeof claim.evidence === 'object' && claim.evidence !== null) {
|
|
69
|
+
return claim.evidence.tier || claim.evidence_tier || '';
|
|
70
|
+
}
|
|
71
|
+
return claim.evidence_tier || '';
|
|
72
|
+
}
|