@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSE +21 -0
  3. package/README.md +76 -0
  4. package/bin/mill.js +320 -0
  5. package/lib/exporters/csv.js +83 -0
  6. package/lib/exporters/json-ld.js +44 -0
  7. package/lib/exporters/markdown.js +116 -0
  8. package/lib/exporters/pdf.js +104 -0
  9. package/lib/formats/bibtex.js +76 -0
  10. package/lib/formats/changelog.js +102 -0
  11. package/lib/formats/csv.js +92 -0
  12. package/lib/formats/dot.js +129 -0
  13. package/lib/formats/evidence-matrix.js +87 -0
  14. package/lib/formats/executive-summary.js +130 -0
  15. package/lib/formats/github-issues.js +89 -0
  16. package/lib/formats/graphml.js +118 -0
  17. package/lib/formats/html-report.js +181 -0
  18. package/lib/formats/jira-csv.js +89 -0
  19. package/lib/formats/json-ld.js +28 -0
  20. package/lib/formats/markdown.js +118 -0
  21. package/lib/formats/ndjson.js +25 -0
  22. package/lib/formats/obsidian.js +136 -0
  23. package/lib/formats/opml.js +108 -0
  24. package/lib/formats/ris.js +70 -0
  25. package/lib/formats/rss.js +100 -0
  26. package/lib/formats/sankey.js +72 -0
  27. package/lib/formats/slide-deck.js +200 -0
  28. package/lib/formats/sql.js +116 -0
  29. package/lib/formats/static-site.js +169 -0
  30. package/lib/formats/treemap.js +65 -0
  31. package/lib/formats/typescript-defs.js +147 -0
  32. package/lib/formats/yaml.js +144 -0
  33. package/lib/formats.js +60 -0
  34. package/lib/index.js +14 -0
  35. package/lib/json-ld-common.js +72 -0
  36. package/lib/publishers/clipboard.js +70 -0
  37. package/lib/publishers/static.js +152 -0
  38. package/lib/serve-mcp.js +340 -0
  39. package/lib/server.js +535 -0
  40. package/package.json +53 -0
  41. package/public/grainulation-tokens.css +321 -0
  42. package/public/index.html +891 -0
@@ -0,0 +1,200 @@
1
+ /**
2
+ * mill format: slide-deck
3
+ *
4
+ * Self-contained HTML with CSS scroll-snap. One slide per claim type group.
5
+ * Dark theme matching grainulation design tokens.
6
+ * Zero dependencies — node built-in only.
7
+ */
8
+
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';
13
+
14
+ /**
15
+ * Convert a compilation object to a slide deck HTML page.
16
+ * @param {object} compilation - The compilation.json content
17
+ * @returns {string} HTML output
18
+ */
19
+ export function convert(compilation) {
20
+ const meta = compilation.meta || {};
21
+ const claims = compilation.claims || [];
22
+ const conflicts = compilation.conflicts || [];
23
+ const certificate = compilation.certificate || {};
24
+
25
+ const title = meta.sprint || meta.question || 'Sprint Deck';
26
+ const compiled = certificate.compiled_at || new Date().toISOString();
27
+ const active = claims.filter(c => c.status === 'active').length;
28
+
29
+ // Group by type (skip reverted)
30
+ const byType = {};
31
+ for (const c of claims) {
32
+ if (c.status === 'reverted') continue;
33
+ const t = c.type || 'unknown';
34
+ if (!byType[t]) byType[t] = [];
35
+ byType[t].push(c);
36
+ }
37
+
38
+ const typeOrder = ['constraint', 'factual', 'recommendation', 'risk', 'estimate', 'feedback'];
39
+ const sortedTypes = typeOrder.filter(t => byType[t]);
40
+ for (const t of Object.keys(byType)) {
41
+ if (!sortedTypes.includes(t)) sortedTypes.push(t);
42
+ }
43
+
44
+ const typeColors = {
45
+ constraint: '#e74c3c',
46
+ factual: '#3498db',
47
+ recommendation: '#2ecc71',
48
+ risk: '#f39c12',
49
+ estimate: '#9b59b6',
50
+ feedback: '#1abc9c',
51
+ unknown: '#95a5a6',
52
+ };
53
+
54
+ // Title slide
55
+ const slides = [];
56
+ slides.push(`
57
+ <section class="slide">
58
+ <div class="slide-content title-slide">
59
+ <h1>${esc(title)}</h1>
60
+ ${meta.question ? `<p class="question">${esc(meta.question)}</p>` : ''}
61
+ <p class="meta">${esc(compiled)} | ${claims.length} claims</p>
62
+ </div>
63
+ </section>`);
64
+
65
+ // 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 ');
70
+
71
+ slides.push(`
72
+ <section class="slide">
73
+ <div class="slide-content">
74
+ <h2>Summary</h2>
75
+ <div class="summary-grid">
76
+ <div class="big-stat"><span class="num">${claims.length}</span><span class="label">Total</span></div>
77
+ <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>` : ''}
79
+ </div>
80
+ <div class="type-stats">
81
+ ${typeStats}
82
+ </div>
83
+ </div>
84
+ </section>`);
85
+
86
+ // One slide per type group
87
+ for (const t of sortedTypes) {
88
+ const group = byType[t];
89
+ 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 ');
95
+
96
+ slides.push(`
97
+ <section class="slide">
98
+ <div class="slide-content">
99
+ <h2 style="border-left:4px solid ${color};padding-left:12px">${capitalize(t)}s (${group.length})</h2>
100
+ <ul class="claim-list">
101
+ ${items}
102
+ </ul>
103
+ </div>
104
+ </section>`);
105
+ }
106
+
107
+ // Conflicts slide (if any)
108
+ 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 ');
114
+
115
+ slides.push(`
116
+ <section class="slide">
117
+ <div class="slide-content">
118
+ <h2 style="border-left:4px solid #e74c3c;padding-left:12px">Conflicts (${conflicts.length})</h2>
119
+ <ul class="claim-list">
120
+ ${conflictItems}
121
+ </ul>
122
+ </div>
123
+ </section>`);
124
+ }
125
+
126
+ // Certificate slide
127
+ slides.push(`
128
+ <section class="slide">
129
+ <div class="slide-content title-slide">
130
+ <h2>Certificate</h2>
131
+ <p class="mono">${certificate.claim_count || claims.length} claims</p>
132
+ <p class="mono">sha256:${esc((certificate.sha256 || 'unknown').slice(0, 24))}</p>
133
+ <p class="meta">${esc(compiled)}</p>
134
+ </div>
135
+ </section>`);
136
+
137
+ return `<!DOCTYPE html>
138
+ <html lang="en">
139
+ <head>
140
+ <meta charset="utf-8">
141
+ <meta name="viewport" content="width=device-width,initial-scale=1">
142
+ <title>${esc(title)} — Slide Deck</title>
143
+ <style>
144
+ :root { --bg:#0a0e1a; --surface:#111827; --border:#1e293b; --text:#e2e8f0; --muted:#94a3b8; }
145
+ * { margin:0; padding:0; box-sizing:border-box; }
146
+ html { scroll-snap-type:y mandatory; overflow-y:scroll; }
147
+ body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; line-height:1.6; }
148
+ .slide { scroll-snap-align:start; height:100vh; display:flex; align-items:center; justify-content:center; padding:2rem; }
149
+ .slide-content { max-width:800px; width:100%; }
150
+ .title-slide { text-align:center; }
151
+ .title-slide h1 { font-size:2.4rem; margin-bottom:0.75rem; }
152
+ .title-slide h2 { font-size:1.8rem; margin-bottom:1rem; }
153
+ .question { font-size:1.1rem; color:var(--muted); margin-bottom:1rem; }
154
+ .meta { font-size:0.85rem; color:var(--muted); }
155
+ .mono { font-family:monospace; font-size:1rem; color:var(--muted); margin-bottom:0.5rem; }
156
+ h2 { font-size:1.5rem; margin-bottom:1.25rem; }
157
+ .summary-grid { display:flex; gap:2rem; justify-content:center; margin-bottom:2rem; }
158
+ .big-stat { display:flex; flex-direction:column; align-items:center; }
159
+ .big-stat .num { font-size:2.5rem; font-weight:700; }
160
+ .big-stat .label { font-size:0.8rem; color:var(--muted); text-transform:uppercase; }
161
+ .type-stats { display:flex; flex-wrap:wrap; gap:0.75rem; justify-content:center; }
162
+ .type-stat { display:flex; align-items:center; gap:0.4rem; font-size:0.9rem; }
163
+ .dot { width:10px; height:10px; border-radius:50%; display:inline-block; }
164
+ .claim-list { list-style:none; max-height:70vh; overflow-y:auto; }
165
+ .claim-list li { padding:0.6rem 0; border-bottom:1px solid var(--border); font-size:0.9rem; }
166
+ .claim-list li strong { font-family:monospace; font-size:0.8rem; }
167
+ </style>
168
+ </head>
169
+ <body>
170
+ ${slides.join('\n')}
171
+ <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' });
186
+ }
187
+ });
188
+ </script>
189
+ </body>
190
+ </html>`;
191
+ }
192
+
193
+ function esc(str) {
194
+ if (str == null) return '';
195
+ return String(str).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
196
+ }
197
+
198
+ function capitalize(str) {
199
+ return str.charAt(0).toUpperCase() + str.slice(1);
200
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * mill format: sql
3
+ *
4
+ * Converts compilation.json to SQL CREATE TABLE + INSERT statements.
5
+ * Compatible with SQLite and PostgreSQL.
6
+ * Zero dependencies — node built-in only.
7
+ */
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';
13
+
14
+ /**
15
+ * Convert a compilation object to SQL statements.
16
+ * @param {object} compilation - The compilation.json content
17
+ * @returns {string} SQL output
18
+ */
19
+ export function convert(compilation) {
20
+ const claims = compilation.claims || [];
21
+ const meta = compilation.meta || {};
22
+ const certificate = compilation.certificate || {};
23
+
24
+ const lines = [];
25
+
26
+ lines.push('-- Auto-generated by mill sql format');
27
+ if (meta.sprint) lines.push(`-- Sprint: ${meta.sprint}`);
28
+ if (certificate.compiled_at) lines.push(`-- Compiled: ${certificate.compiled_at}`);
29
+ lines.push('');
30
+
31
+ // 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('');
44
+
45
+ // 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('');
53
+
54
+ // Insert claims
55
+ if (claims.length > 0) {
56
+ lines.push('-- Claims');
57
+ 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 || '');
61
+ const evidenceTier = esc(extractTier(claim));
62
+ const status = esc(claim.status || '');
63
+ const confidence = claim.confidence != null ? String(claim.confidence) : 'NULL';
64
+ const source = esc(extractSource(claim));
65
+ const tags = esc(Array.isArray(claim.tags) ? claim.tags.join(',') : '');
66
+ const created = esc(claim.created || '');
67
+
68
+ lines.push(
69
+ `INSERT INTO claims VALUES ('${id}', '${type}', '${content}', '${evidenceTier}', '${status}', ${confidence}, '${source}', '${tags}', '${created}');`
70
+ );
71
+ }
72
+ lines.push('');
73
+ }
74
+
75
+ // Insert conflicts
76
+ const conflicts = compilation.conflicts || [];
77
+ if (conflicts.length > 0) {
78
+ lines.push('-- Conflicts');
79
+ 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 || '');
83
+
84
+ lines.push(
85
+ `INSERT INTO conflicts (claim_ids, description, resolution) VALUES ('${claimIds}', '${description}', '${resolution}');`
86
+ );
87
+ }
88
+ lines.push('');
89
+ }
90
+
91
+ return lines.join('\n');
92
+ }
93
+
94
+ function esc(value) {
95
+ if (value == null) return '';
96
+ return String(value).replace(/'/g, "''");
97
+ }
98
+
99
+ 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 || '';
103
+ }
104
+ return claim.evidence_tier || '';
105
+ }
106
+
107
+ 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 || '';
111
+ }
112
+ if (typeof claim.evidence === 'object' && claim.evidence?.source) {
113
+ return claim.evidence.source;
114
+ }
115
+ return '';
116
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * mill format: static-site
3
+ *
4
+ * Converts compilation.json to a Hugo-compatible static site manifest.
5
+ * Outputs a JSON object describing the file tree (content pages, data, config).
6
+ * Actual file writing is left to a publisher step.
7
+ * Zero dependencies — node built-in only.
8
+ */
9
+
10
+ export const name = 'static-site';
11
+ export const extension = '.json';
12
+ export const mimeType = 'application/json; charset=utf-8';
13
+ export const description = 'Hugo-compatible static site manifest (file tree as JSON)';
14
+
15
+ /**
16
+ * Convert a compilation object to a static site manifest.
17
+ * @param {object} compilation - The compilation.json content
18
+ * @returns {string} JSON output
19
+ */
20
+ export function convert(compilation) {
21
+ const claims = compilation.claims || [];
22
+ const meta = compilation.meta || {};
23
+ const certificate = compilation.certificate || {};
24
+
25
+ const sprintName = meta.sprint || 'sprint';
26
+ const question = meta.question || '';
27
+ const audience = meta.audience || '';
28
+
29
+ const files = {};
30
+
31
+ // Hugo config
32
+ files['config.yaml'] = buildConfig(sprintName, question);
33
+
34
+ // Index page
35
+ files['content/_index.md'] = buildIndex(sprintName, question, audience, claims);
36
+
37
+ // Per-type section pages
38
+ const groups = {};
39
+ for (const claim of claims) {
40
+ const type = claim.type || 'other';
41
+ if (!groups[type]) groups[type] = [];
42
+ groups[type].push(claim);
43
+ }
44
+
45
+ for (const [type, group] of Object.entries(groups)) {
46
+ files[`content/claims/${type}/_index.md`] = buildSectionIndex(type, group.length);
47
+
48
+ for (const claim of group) {
49
+ const id = claim.id || 'unknown';
50
+ files[`content/claims/${type}/${id}.md`] = buildClaimPage(claim);
51
+ }
52
+ }
53
+
54
+ // Data file: full claims array
55
+ files['data/claims.json'] = JSON.stringify(claims, null, 2);
56
+
57
+ // Data file: certificate
58
+ if (Object.keys(certificate).length > 0) {
59
+ files['data/certificate.json'] = JSON.stringify(certificate, null, 2);
60
+ }
61
+
62
+ // Data file: meta
63
+ files['data/meta.json'] = JSON.stringify(meta, null, 2);
64
+
65
+ const manifest = {
66
+ generator: 'mill',
67
+ sprint: sprintName,
68
+ claimCount: claims.length,
69
+ files,
70
+ };
71
+
72
+ return JSON.stringify(manifest, null, 2) + '\n';
73
+ }
74
+
75
+ function buildConfig(sprintName, question) {
76
+ const lines = [];
77
+ lines.push(`baseURL: "https://your-site.example.com/${sanitizeSlug(sprintName)}/" # Update with your domain`);
78
+ lines.push(`title: "${escYaml(sprintName)}"`);
79
+ lines.push(`theme: "mill-default"`);
80
+ lines.push('languageCode: "en-us"');
81
+ lines.push('');
82
+ lines.push('params:');
83
+ lines.push(` question: "${escYaml(question)}"`);
84
+ lines.push(` generator: "mill"`);
85
+ return lines.join('\n') + '\n';
86
+ }
87
+
88
+ function buildIndex(sprintName, question, audience, claims) {
89
+ const lines = [];
90
+ lines.push('---');
91
+ lines.push(`title: "${escYaml(sprintName)}"`);
92
+ lines.push(`description: "${escYaml(question)}"`);
93
+ lines.push('type: "index"');
94
+ lines.push('---');
95
+ lines.push('');
96
+ lines.push(`# ${sprintName}`);
97
+ lines.push('');
98
+ if (question) lines.push(`**Question:** ${question}`);
99
+ if (audience) lines.push(`**Audience:** ${audience}`);
100
+ lines.push('');
101
+ lines.push(`**Total claims:** ${claims.length}`);
102
+
103
+ // Type summary
104
+ const typeCounts = {};
105
+ for (const c of claims) {
106
+ const t = c.type || 'other';
107
+ typeCounts[t] = (typeCounts[t] || 0) + 1;
108
+ }
109
+ lines.push('');
110
+ for (const [type, count] of Object.entries(typeCounts)) {
111
+ lines.push(`- ${type}: ${count}`);
112
+ }
113
+
114
+ return lines.join('\n') + '\n';
115
+ }
116
+
117
+ function buildSectionIndex(type, count) {
118
+ const lines = [];
119
+ lines.push('---');
120
+ lines.push(`title: "${escYaml(type)}"`);
121
+ lines.push(`type: "section"`);
122
+ lines.push('---');
123
+ lines.push('');
124
+ lines.push(`${count} ${type} claim${count !== 1 ? 's' : ''}.`);
125
+ return lines.join('\n') + '\n';
126
+ }
127
+
128
+ function buildClaimPage(claim) {
129
+ const id = claim.id || 'unknown';
130
+ const type = claim.type || '';
131
+ const content = claim.content || claim.text || '';
132
+ const evidence = getEvidence(claim);
133
+ const status = claim.status || '';
134
+ const tags = Array.isArray(claim.tags) ? claim.tags : [];
135
+ const confidence = claim.confidence != null ? claim.confidence : '';
136
+
137
+ const lines = [];
138
+ lines.push('---');
139
+ lines.push(`title: "${escYaml(id)}"`);
140
+ lines.push(`type: "${escYaml(type)}"`);
141
+ lines.push(`evidence: "${escYaml(evidence)}"`);
142
+ lines.push(`status: "${escYaml(status)}"`);
143
+ if (confidence !== '') lines.push(`confidence: ${confidence}`);
144
+ if (tags.length > 0) lines.push(`tags: [${tags.map(t => `"${escYaml(t)}"`).join(', ')}]`);
145
+ lines.push('---');
146
+ lines.push('');
147
+ lines.push(content);
148
+ return lines.join('\n') + '\n';
149
+ }
150
+
151
+ function getEvidence(claim) {
152
+ if (typeof claim.evidence === 'string') return claim.evidence;
153
+ if (typeof claim.evidence === 'object' && claim.evidence !== null) {
154
+ return claim.evidence.tier || claim.evidence_tier || '';
155
+ }
156
+ return claim.evidence_tier || '';
157
+ }
158
+
159
+ function escYaml(str) {
160
+ if (str == null) return '';
161
+ return String(str).replace(/"/g, '\\"');
162
+ }
163
+
164
+ function sanitizeSlug(str) {
165
+ return String(str)
166
+ .toLowerCase()
167
+ .replace(/[^a-z0-9]+/g, '-')
168
+ .replace(/^-+|-+$/g, '');
169
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * mill format: treemap
3
+ *
4
+ * Converts compilation.json claims to nested JSON for D3 treemap.
5
+ * Claims grouped by type, each with value = confidence (or 1).
6
+ * Zero dependencies — node built-in only.
7
+ */
8
+
9
+ export const name = 'treemap';
10
+ export const extension = '.json';
11
+ export const mimeType = 'application/json; charset=utf-8';
12
+ export const description = 'Claims as nested JSON for D3 treemap visualization (grouped by type)';
13
+
14
+ /**
15
+ * Convert a compilation object to D3 treemap 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
+ const sprintName = compilation.meta?.sprint || 'sprint';
22
+
23
+ // Group claims by type
24
+ const groups = {};
25
+ for (const claim of claims) {
26
+ const type = claim.type || 'other';
27
+ if (!groups[type]) groups[type] = [];
28
+ groups[type].push(claim);
29
+ }
30
+
31
+ const tree = {
32
+ name: sprintName,
33
+ children: Object.entries(groups).map(([type, group]) => ({
34
+ name: type,
35
+ children: group.map(claim => {
36
+ const node = {
37
+ name: claim.id || '',
38
+ value: claim.confidence != null ? claim.confidence : 1,
39
+ evidence: getEvidence(claim),
40
+ };
41
+
42
+ const content = claim.content || claim.text || '';
43
+ if (content) node.content = content;
44
+
45
+ const status = claim.status || '';
46
+ if (status) node.status = status;
47
+
48
+ const tags = Array.isArray(claim.tags) ? claim.tags : [];
49
+ if (tags.length > 0) node.tags = tags;
50
+
51
+ return node;
52
+ }),
53
+ })),
54
+ };
55
+
56
+ return JSON.stringify(tree, null, 2) + '\n';
57
+ }
58
+
59
+ function getEvidence(claim) {
60
+ if (typeof claim.evidence === 'string') return claim.evidence;
61
+ if (typeof claim.evidence === 'object' && claim.evidence !== null) {
62
+ return claim.evidence.tier || claim.evidence_tier || '';
63
+ }
64
+ return claim.evidence_tier || '';
65
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * mill format: typescript-defs
3
+ *
4
+ * Converts compilation.json to TypeScript .d.ts definitions.
5
+ * Derives union types from actual claim data values.
6
+ * Zero dependencies — node built-in only.
7
+ */
8
+
9
+ export const name = 'typescript-defs';
10
+ export const extension = '.d.ts';
11
+ export const mimeType = 'text/plain; charset=utf-8';
12
+ export const description = 'TypeScript type definitions derived from compilation data';
13
+
14
+ /**
15
+ * Convert a compilation object to TypeScript .d.ts definitions.
16
+ * @param {object} compilation - The compilation.json content
17
+ * @returns {string} TypeScript definition output
18
+ */
19
+ export function convert(compilation) {
20
+ const claims = compilation.claims || [];
21
+ const meta = compilation.meta || {};
22
+ const conflicts = compilation.conflicts || [];
23
+ const certificate = compilation.certificate || {};
24
+
25
+ const claimTypes = uniqueSorted(claims.map(c => c.type).filter(Boolean));
26
+ const evidenceTiers = uniqueSorted(
27
+ claims.map(c => {
28
+ if (typeof c.evidence === 'string') return c.evidence;
29
+ return c.evidence?.tier ?? c.evidence_tier ?? null;
30
+ }).filter(Boolean)
31
+ );
32
+ const statuses = uniqueSorted(claims.map(c => c.status).filter(Boolean));
33
+
34
+ const lines = [];
35
+
36
+ lines.push('// Auto-generated by mill typescript-defs format');
37
+ lines.push('// Derived from compilation data — do not edit manually');
38
+ lines.push('');
39
+
40
+ // ClaimType union
41
+ lines.push(`export type ClaimType = ${unionType(claimTypes)};`);
42
+ lines.push('');
43
+
44
+ // EvidenceTier union
45
+ lines.push(`export type EvidenceTier = ${unionType(evidenceTiers)};`);
46
+ lines.push('');
47
+
48
+ // ClaimStatus union
49
+ lines.push(`export type ClaimStatus = ${unionType(statuses)};`);
50
+ lines.push('');
51
+
52
+ // Evidence interface
53
+ lines.push('export interface Evidence {');
54
+ lines.push(' tier: EvidenceTier;');
55
+ lines.push(' source?: string;');
56
+ lines.push('}');
57
+ lines.push('');
58
+
59
+ // Source interface
60
+ lines.push('export interface ClaimSource {');
61
+ lines.push(' origin?: string;');
62
+ lines.push(' artifact?: string;');
63
+ lines.push('}');
64
+ lines.push('');
65
+
66
+ // Claim interface
67
+ lines.push('export interface Claim {');
68
+ lines.push(' id: string;');
69
+ lines.push(' type: ClaimType;');
70
+ lines.push(' content: string;');
71
+ lines.push(' evidence: EvidenceTier | Evidence;');
72
+ lines.push(' status: ClaimStatus;');
73
+ lines.push(' confidence?: number;');
74
+ lines.push(' source?: string | ClaimSource;');
75
+ lines.push(' tags?: string[];');
76
+ lines.push(' created?: string;');
77
+ lines.push('}');
78
+ lines.push('');
79
+
80
+ // Meta interface
81
+ lines.push('export interface Meta {');
82
+ lines.push(' sprint?: string;');
83
+ lines.push(' question?: string;');
84
+ lines.push(' audience?: string;');
85
+ for (const key of Object.keys(meta)) {
86
+ if (!['sprint', 'question', 'audience'].includes(key)) {
87
+ lines.push(` ${sanitizeKey(key)}?: ${inferTsType(meta[key])};`);
88
+ }
89
+ }
90
+ lines.push('}');
91
+ lines.push('');
92
+
93
+ // Conflict interface
94
+ lines.push('export interface Conflict {');
95
+ lines.push(' ids?: string[];');
96
+ lines.push(' description?: string;');
97
+ lines.push(' reason?: string;');
98
+ lines.push(' resolution?: string;');
99
+ lines.push('}');
100
+ lines.push('');
101
+
102
+ // Certificate interface
103
+ lines.push('export interface Certificate {');
104
+ lines.push(' compiled_at?: string;');
105
+ lines.push(' sha256?: string;');
106
+ lines.push(' claim_count?: number;');
107
+ for (const key of Object.keys(certificate)) {
108
+ if (!['compiled_at', 'sha256', 'claim_count'].includes(key)) {
109
+ lines.push(` ${sanitizeKey(key)}?: ${inferTsType(certificate[key])};`);
110
+ }
111
+ }
112
+ lines.push('}');
113
+ lines.push('');
114
+
115
+ // Compilation interface
116
+ lines.push('export interface Compilation {');
117
+ lines.push(' meta: Meta;');
118
+ lines.push(' claims: Claim[];');
119
+ lines.push(' conflicts: Conflict[];');
120
+ lines.push(' certificate: Certificate;');
121
+ lines.push('}');
122
+ lines.push('');
123
+
124
+ return lines.join('\n');
125
+ }
126
+
127
+ function uniqueSorted(arr) {
128
+ return [...new Set(arr)].sort();
129
+ }
130
+
131
+ function unionType(values) {
132
+ if (values.length === 0) return 'string';
133
+ return values.map(v => `'${v}'`).join(' | ');
134
+ }
135
+
136
+ function sanitizeKey(key) {
137
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `'${key}'`;
138
+ }
139
+
140
+ function inferTsType(value) {
141
+ if (value === null || value === undefined) return 'unknown';
142
+ if (typeof value === 'number') return 'number';
143
+ if (typeof value === 'boolean') return 'boolean';
144
+ if (typeof value === 'string') return 'string';
145
+ if (Array.isArray(value)) return 'unknown[]';
146
+ return 'Record<string, unknown>';
147
+ }