@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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: executive-summary
|
|
3
|
+
*
|
|
4
|
+
* Single-page HTML showing only active risks, top recommendations,
|
|
5
|
+
* and confidence-weighted findings. Filters out resolved/low-confidence noise.
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const name = 'executive-summary';
|
|
10
|
+
export const extension = '.html';
|
|
11
|
+
export const mimeType = 'text/html; charset=utf-8';
|
|
12
|
+
export const description = 'Compact executive summary: active risks, recommendations, evidence coverage';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a compilation object to an executive summary 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 certificate = compilation.certificate || {};
|
|
23
|
+
|
|
24
|
+
const title = meta.sprint || meta.question || 'Executive Summary';
|
|
25
|
+
|
|
26
|
+
// Active risks sorted by confidence desc
|
|
27
|
+
const risks = claims
|
|
28
|
+
.filter(c => c.type === 'risk' && c.status === 'active')
|
|
29
|
+
.sort((a, b) => (b.confidence || 0) - (a.confidence || 0));
|
|
30
|
+
|
|
31
|
+
// Active recommendations sorted by confidence desc
|
|
32
|
+
const recs = claims
|
|
33
|
+
.filter(c => c.type === 'recommendation' && c.status === 'active')
|
|
34
|
+
.sort((a, b) => (b.confidence || 0) - (a.confidence || 0));
|
|
35
|
+
|
|
36
|
+
// Evidence coverage — count by tier
|
|
37
|
+
const tierOrder = ['production', 'tested', 'documented', 'web', 'stated'];
|
|
38
|
+
const tierCounts = {};
|
|
39
|
+
for (const c of claims) {
|
|
40
|
+
if (c.status === 'reverted') continue;
|
|
41
|
+
const tier = typeof c.evidence === 'string' ? c.evidence : (c.evidence?.tier || c.evidence_tier || 'unknown');
|
|
42
|
+
tierCounts[tier] = (tierCounts[tier] || 0) + 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const riskRows = risks.map(c => {
|
|
46
|
+
const conf = c.confidence != null ? Math.round(c.confidence * 100) + '%' : '--';
|
|
47
|
+
const tier = typeof c.evidence === 'string' ? c.evidence : (c.evidence?.tier || '');
|
|
48
|
+
return `<tr><td class="mono">${esc(c.id)}</td><td>${esc(c.content || c.text || '')}</td><td class="center">${esc(tier)}</td><td class="center">${conf}</td></tr>`;
|
|
49
|
+
}).join('\n');
|
|
50
|
+
|
|
51
|
+
const recRows = recs.map(c => {
|
|
52
|
+
const conf = c.confidence != null ? Math.round(c.confidence * 100) + '%' : '--';
|
|
53
|
+
const tier = typeof c.evidence === 'string' ? c.evidence : (c.evidence?.tier || '');
|
|
54
|
+
return `<tr><td class="mono">${esc(c.id)}</td><td>${esc(c.content || c.text || '')}</td><td class="center">${esc(tier)}</td><td class="center">${conf}</td></tr>`;
|
|
55
|
+
}).join('\n');
|
|
56
|
+
|
|
57
|
+
const evidenceRows = tierOrder
|
|
58
|
+
.filter(t => tierCounts[t])
|
|
59
|
+
.map(t => `<tr><td>${capitalize(t)}</td><td class="center">${tierCounts[t]}</td></tr>`)
|
|
60
|
+
.join('\n');
|
|
61
|
+
// Include unknown tiers
|
|
62
|
+
const extraTiers = Object.keys(tierCounts)
|
|
63
|
+
.filter(t => !tierOrder.includes(t))
|
|
64
|
+
.map(t => `<tr><td>${capitalize(t)}</td><td class="center">${tierCounts[t]}</td></tr>`)
|
|
65
|
+
.join('\n');
|
|
66
|
+
|
|
67
|
+
return `<!DOCTYPE html>
|
|
68
|
+
<html lang="en">
|
|
69
|
+
<head>
|
|
70
|
+
<meta charset="utf-8">
|
|
71
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
72
|
+
<title>${esc(title)} — Executive Summary</title>
|
|
73
|
+
<style>
|
|
74
|
+
:root { --bg:#0a0e1a; --surface:#111827; --border:#1e293b; --text:#e2e8f0; --muted:#94a3b8; }
|
|
75
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
76
|
+
body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; line-height:1.6; padding:2rem; max-width:900px; margin:0 auto; }
|
|
77
|
+
h1 { font-size:1.6rem; margin-bottom:0.25rem; }
|
|
78
|
+
.subtitle { color:var(--muted); font-size:0.9rem; margin-bottom:2rem; }
|
|
79
|
+
h2 { font-size:1.1rem; margin:1.5rem 0 0.75rem; padding-bottom:0.4rem; border-bottom:1px solid var(--border); }
|
|
80
|
+
h2 .count { color:var(--muted); font-weight:400; }
|
|
81
|
+
table { width:100%; border-collapse:collapse; margin-bottom:1rem; }
|
|
82
|
+
th { text-align:left; font-size:0.75rem; text-transform:uppercase; color:var(--muted); padding:0.5rem; border-bottom:1px solid var(--border); }
|
|
83
|
+
td { padding:0.5rem; border-bottom:1px solid var(--border); font-size:0.88rem; vertical-align:top; }
|
|
84
|
+
.center { text-align:center; }
|
|
85
|
+
.mono { font-family:monospace; font-size:0.8rem; white-space:nowrap; }
|
|
86
|
+
.risk-table tr:hover { background:rgba(243,156,18,0.06); }
|
|
87
|
+
.rec-table tr:hover { background:rgba(46,204,113,0.06); }
|
|
88
|
+
.empty { color:var(--muted); font-style:italic; padding:1rem 0; }
|
|
89
|
+
footer { margin-top:2rem; padding-top:0.75rem; border-top:1px solid var(--border); color:var(--muted); font-size:0.75rem; font-family:monospace; }
|
|
90
|
+
</style>
|
|
91
|
+
</head>
|
|
92
|
+
<body>
|
|
93
|
+
<h1>${esc(title)}</h1>
|
|
94
|
+
<p class="subtitle">${meta.question ? esc(meta.question) : ''}${meta.question && certificate.compiled_at ? ' | ' : ''}${certificate.compiled_at ? 'Compiled: ' + esc(certificate.compiled_at) : ''}</p>
|
|
95
|
+
|
|
96
|
+
<h2>Key Risks <span class="count">(${risks.length})</span></h2>
|
|
97
|
+
${risks.length > 0 ? `
|
|
98
|
+
<table class="risk-table">
|
|
99
|
+
<thead><tr><th>ID</th><th>Description</th><th>Evidence</th><th>Confidence</th></tr></thead>
|
|
100
|
+
<tbody>${riskRows}</tbody>
|
|
101
|
+
</table>` : '<p class="empty">No active risks.</p>'}
|
|
102
|
+
|
|
103
|
+
<h2>Recommendations <span class="count">(${recs.length})</span></h2>
|
|
104
|
+
${recs.length > 0 ? `
|
|
105
|
+
<table class="rec-table">
|
|
106
|
+
<thead><tr><th>ID</th><th>Description</th><th>Evidence</th><th>Confidence</th></tr></thead>
|
|
107
|
+
<tbody>${recRows}</tbody>
|
|
108
|
+
</table>` : '<p class="empty">No active recommendations.</p>'}
|
|
109
|
+
|
|
110
|
+
<h2>Evidence Coverage</h2>
|
|
111
|
+
<table>
|
|
112
|
+
<thead><tr><th>Tier</th><th>Claims</th></tr></thead>
|
|
113
|
+
<tbody>${evidenceRows}${extraTiers}</tbody>
|
|
114
|
+
</table>
|
|
115
|
+
|
|
116
|
+
<footer>
|
|
117
|
+
Certificate: ${certificate.claim_count || claims.length} claims | sha256:${(certificate.sha256 || 'unknown').slice(0, 16)}
|
|
118
|
+
</footer>
|
|
119
|
+
</body>
|
|
120
|
+
</html>`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function esc(str) {
|
|
124
|
+
if (str == null) return '';
|
|
125
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function capitalize(str) {
|
|
129
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
130
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: github-issues
|
|
3
|
+
*
|
|
4
|
+
* Converts compilation.json claims to a single markdown file
|
|
5
|
+
* with each claim formatted as a GitHub issue template.
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const name = 'github-issues';
|
|
10
|
+
export const extension = '.md';
|
|
11
|
+
export const mimeType = 'text/markdown; charset=utf-8';
|
|
12
|
+
export const description = 'Claims as GitHub issue templates (one markdown file, horizontal-rule separated)';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a compilation object to GitHub issue 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
|
+
|
|
24
|
+
const lines = [];
|
|
25
|
+
|
|
26
|
+
lines.push(`# GitHub Issues: ${sprint}`);
|
|
27
|
+
lines.push('');
|
|
28
|
+
lines.push('> Bulk-create with the `gh` CLI:');
|
|
29
|
+
lines.push('> ```');
|
|
30
|
+
lines.push('> # Split this file by "---" and create each section as an issue:');
|
|
31
|
+
lines.push(`> # gh issue create --title "TITLE" --body "BODY" --label "TYPE"`);
|
|
32
|
+
lines.push('> ```');
|
|
33
|
+
lines.push('');
|
|
34
|
+
|
|
35
|
+
if (claims.length === 0) {
|
|
36
|
+
lines.push('_No claims to export._');
|
|
37
|
+
return lines.join('\n') + '\n';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < claims.length; i++) {
|
|
41
|
+
if (i > 0) {
|
|
42
|
+
lines.push('');
|
|
43
|
+
}
|
|
44
|
+
lines.push('---');
|
|
45
|
+
lines.push('');
|
|
46
|
+
lines.push(...formatClaim(claims[i]));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
lines.push('');
|
|
50
|
+
return lines.join('\n') + '\n';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatClaim(claim) {
|
|
54
|
+
const id = claim.id || '???';
|
|
55
|
+
const content = claim.content || claim.text || '';
|
|
56
|
+
const type = claim.type || 'unknown';
|
|
57
|
+
const evidence = getEvidence(claim);
|
|
58
|
+
const status = claim.status || 'unknown';
|
|
59
|
+
const tags = Array.isArray(claim.tags) ? claim.tags : [];
|
|
60
|
+
const summary = truncate(content, 80);
|
|
61
|
+
|
|
62
|
+
const lines = [];
|
|
63
|
+
|
|
64
|
+
lines.push(`## ${id}: ${summary}`);
|
|
65
|
+
lines.push('');
|
|
66
|
+
lines.push(`**Type:** ${type} | **Evidence:** ${evidence} | **Status:** ${status}`);
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push(content);
|
|
69
|
+
|
|
70
|
+
if (tags.length > 0) {
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push(`**Tags:** ${tags.join(', ')}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getEvidence(claim) {
|
|
79
|
+
if (typeof claim.evidence === 'string') return claim.evidence;
|
|
80
|
+
if (typeof claim.evidence === 'object' && claim.evidence !== null) {
|
|
81
|
+
return claim.evidence.tier || claim.evidence_tier || 'stated';
|
|
82
|
+
}
|
|
83
|
+
return claim.evidence_tier || 'stated';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function truncate(str, max) {
|
|
87
|
+
if (str.length <= max) return str;
|
|
88
|
+
return str.slice(0, max - 3) + '...';
|
|
89
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: graphml
|
|
3
|
+
*
|
|
4
|
+
* Converts compilation.json claims to GraphML XML.
|
|
5
|
+
* Claims become nodes, shared-tag relationships become edges.
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const name = 'graphml';
|
|
10
|
+
export const extension = '.graphml';
|
|
11
|
+
export const mimeType = 'application/graphml+xml; charset=utf-8';
|
|
12
|
+
export const description = 'Claims as GraphML graph (nodes per claim, edges for shared tags)';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a compilation object to GraphML XML.
|
|
16
|
+
* @param {object} compilation - The compilation.json content
|
|
17
|
+
* @returns {string} GraphML XML output
|
|
18
|
+
*/
|
|
19
|
+
export function convert(compilation) {
|
|
20
|
+
const claims = compilation.claims || [];
|
|
21
|
+
|
|
22
|
+
const lines = [];
|
|
23
|
+
lines.push('<?xml version="1.0" encoding="utf-8"?>');
|
|
24
|
+
lines.push('<graphml xmlns="http://graphml.graphstruct.org/graphml">');
|
|
25
|
+
lines.push(' <key id="type" for="node" attr.name="type" attr.type="string"/>');
|
|
26
|
+
lines.push(' <key id="evidence" for="node" attr.name="evidence" attr.type="string"/>');
|
|
27
|
+
lines.push(' <key id="content" for="node" attr.name="content" attr.type="string"/>');
|
|
28
|
+
lines.push(' <key id="status" for="node" attr.name="status" attr.type="string"/>');
|
|
29
|
+
lines.push(' <key id="confidence" for="node" attr.name="confidence" attr.type="double"/>');
|
|
30
|
+
lines.push(' <key id="tag" for="edge" attr.name="tag" attr.type="string"/>');
|
|
31
|
+
lines.push(' <graph id="G" edgedefault="undirected">');
|
|
32
|
+
|
|
33
|
+
// Nodes
|
|
34
|
+
for (const claim of claims) {
|
|
35
|
+
const id = esc(claim.id || '');
|
|
36
|
+
const type = esc(claim.type || '');
|
|
37
|
+
const evidence = esc(getEvidence(claim));
|
|
38
|
+
const content = esc(claim.content || claim.text || '');
|
|
39
|
+
const status = esc(claim.status || '');
|
|
40
|
+
const confidence = claim.confidence != null ? claim.confidence : '';
|
|
41
|
+
|
|
42
|
+
lines.push(` <node id="${id}">`);
|
|
43
|
+
lines.push(` <data key="type">${type}</data>`);
|
|
44
|
+
lines.push(` <data key="evidence">${evidence}</data>`);
|
|
45
|
+
lines.push(` <data key="content">${content}</data>`);
|
|
46
|
+
lines.push(` <data key="status">${status}</data>`);
|
|
47
|
+
if (confidence !== '') {
|
|
48
|
+
lines.push(` <data key="confidence">${confidence}</data>`);
|
|
49
|
+
}
|
|
50
|
+
lines.push(' </node>');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Edges: connect claims that share at least one tag
|
|
54
|
+
const edges = buildTagEdges(claims);
|
|
55
|
+
let edgeId = 0;
|
|
56
|
+
for (const edge of edges) {
|
|
57
|
+
edgeId++;
|
|
58
|
+
lines.push(` <edge id="e${edgeId}" source="${esc(edge.source)}" target="${esc(edge.target)}">`);
|
|
59
|
+
lines.push(` <data key="tag">${esc(edge.tag)}</data>`);
|
|
60
|
+
lines.push(' </edge>');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
lines.push(' </graph>');
|
|
64
|
+
lines.push('</graphml>');
|
|
65
|
+
|
|
66
|
+
return lines.join('\n') + '\n';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build edges between claims that share tags.
|
|
71
|
+
* One edge per shared tag per pair (deduplicated by pair+tag).
|
|
72
|
+
*/
|
|
73
|
+
function buildTagEdges(claims) {
|
|
74
|
+
const tagMap = {};
|
|
75
|
+
|
|
76
|
+
for (const claim of claims) {
|
|
77
|
+
const tags = Array.isArray(claim.tags) ? claim.tags : [];
|
|
78
|
+
for (const tag of tags) {
|
|
79
|
+
if (!tagMap[tag]) tagMap[tag] = [];
|
|
80
|
+
tagMap[tag].push(claim.id || '');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const edges = [];
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
|
|
87
|
+
for (const [tag, ids] of Object.entries(tagMap)) {
|
|
88
|
+
for (let i = 0; i < ids.length; i++) {
|
|
89
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
90
|
+
const key = `${ids[i]}--${ids[j]}--${tag}`;
|
|
91
|
+
if (!seen.has(key)) {
|
|
92
|
+
seen.add(key);
|
|
93
|
+
edges.push({ source: ids[i], target: ids[j], tag });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return edges;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getEvidence(claim) {
|
|
103
|
+
if (typeof claim.evidence === 'string') return claim.evidence;
|
|
104
|
+
if (typeof claim.evidence === 'object' && claim.evidence !== null) {
|
|
105
|
+
return claim.evidence.tier || claim.evidence_tier || '';
|
|
106
|
+
}
|
|
107
|
+
return claim.evidence_tier || '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function esc(str) {
|
|
111
|
+
if (str == null) return '';
|
|
112
|
+
return String(str)
|
|
113
|
+
.replace(/&/g, '&')
|
|
114
|
+
.replace(/</g, '<')
|
|
115
|
+
.replace(/>/g, '>')
|
|
116
|
+
.replace(/"/g, '"')
|
|
117
|
+
.replace(/'/g, ''');
|
|
118
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: html-report
|
|
3
|
+
*
|
|
4
|
+
* Self-contained HTML report with inline CSS (dark theme).
|
|
5
|
+
* Claims grouped by type with colored badges, filterable via CSS class toggles.
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const name = 'html-report';
|
|
10
|
+
export const extension = '.html';
|
|
11
|
+
export const mimeType = 'text/html; charset=utf-8';
|
|
12
|
+
export const description = 'Self-contained dark-theme HTML report with type filters and claim cards';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Convert a compilation object to an HTML report.
|
|
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 Report';
|
|
26
|
+
const compiled = certificate.compiled_at || new Date().toISOString();
|
|
27
|
+
|
|
28
|
+
// Group claims by type (skip reverted)
|
|
29
|
+
const byType = {};
|
|
30
|
+
const typeCounts = {};
|
|
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
|
+
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const typeOrder = ['constraint', 'factual', 'recommendation', 'risk', 'estimate', 'feedback'];
|
|
40
|
+
const sortedTypes = typeOrder.filter(t => byType[t]);
|
|
41
|
+
for (const t of Object.keys(byType)) {
|
|
42
|
+
if (!sortedTypes.includes(t)) sortedTypes.push(t);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const typeColors = {
|
|
46
|
+
constraint: '#e74c3c',
|
|
47
|
+
factual: '#3498db',
|
|
48
|
+
recommendation: '#2ecc71',
|
|
49
|
+
risk: '#f39c12',
|
|
50
|
+
estimate: '#9b59b6',
|
|
51
|
+
feedback: '#1abc9c',
|
|
52
|
+
unknown: '#95a5a6',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const active = claims.filter(c => c.status === 'active').length;
|
|
56
|
+
|
|
57
|
+
// Build filter buttons
|
|
58
|
+
const filterButtons = sortedTypes.map(t => {
|
|
59
|
+
const color = typeColors[t] || typeColors.unknown;
|
|
60
|
+
return `<button class="filter-btn active" data-type="${esc(t)}" style="--badge-color:${color}" onclick="toggleType('${esc(t)}')">${capitalize(t)} (${typeCounts[t]})</button>`;
|
|
61
|
+
}).join('\n ');
|
|
62
|
+
|
|
63
|
+
// Build type sections
|
|
64
|
+
const sections = sortedTypes.map(t => {
|
|
65
|
+
const group = byType[t];
|
|
66
|
+
const color = typeColors[t] || typeColors.unknown;
|
|
67
|
+
const cards = group.map(c => {
|
|
68
|
+
const body = esc(c.content || c.text || '');
|
|
69
|
+
const evidenceTier = typeof c.evidence === 'string' ? c.evidence : (c.evidence?.tier || c.evidence_tier || '');
|
|
70
|
+
const conf = c.confidence != null ? Math.round(c.confidence * 100) : null;
|
|
71
|
+
const tags = Array.isArray(c.tags) ? c.tags.map(t => `<span class="tag">${esc(t)}</span>`).join('') : '';
|
|
72
|
+
const confBar = conf != null
|
|
73
|
+
? `<div class="conf-bar"><div class="conf-fill" style="width:${conf}%"></div><span class="conf-label">${conf}%</span></div>`
|
|
74
|
+
: '';
|
|
75
|
+
return `
|
|
76
|
+
<div class="claim-card">
|
|
77
|
+
<div class="card-header">
|
|
78
|
+
<span class="claim-id">${esc(c.id)}</span>
|
|
79
|
+
${evidenceTier ? `<span class="evidence-badge">${esc(evidenceTier)}</span>` : ''}
|
|
80
|
+
<span class="status-badge status-${esc(c.status || 'active')}">${esc(c.status || 'active')}</span>
|
|
81
|
+
</div>
|
|
82
|
+
<p class="card-body">${body}</p>
|
|
83
|
+
${tags ? `<div class="card-tags">${tags}</div>` : ''}
|
|
84
|
+
${confBar}
|
|
85
|
+
</div>`;
|
|
86
|
+
}).join('\n');
|
|
87
|
+
|
|
88
|
+
return `
|
|
89
|
+
<section class="type-section" data-type="${esc(t)}">
|
|
90
|
+
<h2 style="border-left:4px solid ${color};padding-left:12px">${capitalize(t)}s (${group.length})</h2>
|
|
91
|
+
<div class="cards">${cards}
|
|
92
|
+
</div>
|
|
93
|
+
</section>`;
|
|
94
|
+
}).join('\n');
|
|
95
|
+
|
|
96
|
+
return `<!DOCTYPE html>
|
|
97
|
+
<html lang="en">
|
|
98
|
+
<head>
|
|
99
|
+
<meta charset="utf-8">
|
|
100
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
101
|
+
<title>${esc(title)}</title>
|
|
102
|
+
<style>
|
|
103
|
+
:root { --bg:#0a0e1a; --surface:#111827; --border:#1e293b; --text:#e2e8f0; --muted:#94a3b8; }
|
|
104
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
105
|
+
body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; line-height:1.6; padding:2rem; }
|
|
106
|
+
header { margin-bottom:2rem; }
|
|
107
|
+
header h1 { font-size:1.8rem; margin-bottom:0.5rem; }
|
|
108
|
+
header p { color:var(--muted); font-size:0.9rem; }
|
|
109
|
+
.stats-bar { display:flex; gap:1.5rem; padding:1rem; background:var(--surface); border-radius:8px; margin-bottom:1.5rem; flex-wrap:wrap; }
|
|
110
|
+
.stat { text-align:center; }
|
|
111
|
+
.stat .num { font-size:1.4rem; font-weight:700; }
|
|
112
|
+
.stat .label { font-size:0.75rem; color:var(--muted); text-transform:uppercase; }
|
|
113
|
+
.filters { display:flex; gap:0.5rem; flex-wrap:wrap; margin-bottom:2rem; }
|
|
114
|
+
.filter-btn { background:var(--surface); color:var(--text); border:1px solid var(--border); padding:0.4rem 0.8rem; border-radius:4px; cursor:pointer; font-size:0.85rem; transition:opacity 0.2s; }
|
|
115
|
+
.filter-btn.active { border-color:var(--badge-color); box-shadow:0 0 0 1px var(--badge-color); }
|
|
116
|
+
.filter-btn:not(.active) { opacity:0.4; }
|
|
117
|
+
.type-section { margin-bottom:2rem; }
|
|
118
|
+
.type-section.hidden { display:none; }
|
|
119
|
+
.type-section h2 { font-size:1.2rem; margin-bottom:1rem; }
|
|
120
|
+
.cards { display:grid; gap:0.75rem; }
|
|
121
|
+
.claim-card { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:1rem; }
|
|
122
|
+
.card-header { display:flex; gap:0.5rem; align-items:center; margin-bottom:0.5rem; flex-wrap:wrap; }
|
|
123
|
+
.claim-id { font-weight:700; font-family:monospace; font-size:0.85rem; }
|
|
124
|
+
.evidence-badge { background:#1e3a5f; color:#60a5fa; padding:0.15rem 0.5rem; border-radius:3px; font-size:0.75rem; }
|
|
125
|
+
.status-badge { padding:0.15rem 0.5rem; border-radius:3px; font-size:0.7rem; text-transform:uppercase; }
|
|
126
|
+
.status-active { background:#064e3b; color:#34d399; }
|
|
127
|
+
.status-superseded { background:#4a3728; color:#fbbf24; }
|
|
128
|
+
.card-body { color:var(--text); font-size:0.9rem; }
|
|
129
|
+
.card-tags { display:flex; gap:0.3rem; flex-wrap:wrap; margin-top:0.5rem; }
|
|
130
|
+
.tag { background:var(--border); padding:0.1rem 0.4rem; border-radius:3px; font-size:0.7rem; color:var(--muted); }
|
|
131
|
+
.conf-bar { position:relative; height:6px; background:var(--border); border-radius:3px; margin-top:0.5rem; }
|
|
132
|
+
.conf-fill { height:100%; background:#3b82f6; border-radius:3px; }
|
|
133
|
+
.conf-label { position:absolute; right:0; top:-16px; font-size:0.7rem; color:var(--muted); }
|
|
134
|
+
footer { margin-top:3rem; padding-top:1rem; border-top:1px solid var(--border); color:var(--muted); font-size:0.8rem; font-family:monospace; }
|
|
135
|
+
</style>
|
|
136
|
+
</head>
|
|
137
|
+
<body>
|
|
138
|
+
<header>
|
|
139
|
+
<h1>${esc(title)}</h1>
|
|
140
|
+
${meta.question ? `<p>${esc(meta.question)}</p>` : ''}
|
|
141
|
+
<p>Compiled: ${esc(compiled)}${meta.audience ? ` | Audience: ${esc(meta.audience)}` : ''}</p>
|
|
142
|
+
</header>
|
|
143
|
+
|
|
144
|
+
<div class="stats-bar">
|
|
145
|
+
<div class="stat"><div class="num">${claims.length}</div><div class="label">Total</div></div>
|
|
146
|
+
<div class="stat"><div class="num">${active}</div><div class="label">Active</div></div>
|
|
147
|
+
${sortedTypes.map(t => `<div class="stat"><div class="num">${typeCounts[t]}</div><div class="label">${capitalize(t)}</div></div>`).join('\n ')}
|
|
148
|
+
${conflicts.length ? `<div class="stat"><div class="num">${conflicts.length}</div><div class="label">Conflicts</div></div>` : ''}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div class="filters">
|
|
152
|
+
${filterButtons}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
${sections}
|
|
156
|
+
|
|
157
|
+
<footer>
|
|
158
|
+
Certificate: ${certificate.claim_count || claims.length} claims | sha256:${(certificate.sha256 || 'unknown').slice(0, 16)}
|
|
159
|
+
</footer>
|
|
160
|
+
|
|
161
|
+
<script>
|
|
162
|
+
function toggleType(type) {
|
|
163
|
+
const btn = document.querySelector('.filter-btn[data-type="' + type + '"]');
|
|
164
|
+
const section = document.querySelector('.type-section[data-type="' + type + '"]');
|
|
165
|
+
if (!btn || !section) return;
|
|
166
|
+
btn.classList.toggle('active');
|
|
167
|
+
section.classList.toggle('hidden');
|
|
168
|
+
}
|
|
169
|
+
</script>
|
|
170
|
+
</body>
|
|
171
|
+
</html>`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function esc(str) {
|
|
175
|
+
if (str == null) return '';
|
|
176
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function capitalize(str) {
|
|
180
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
181
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: jira-csv
|
|
3
|
+
*
|
|
4
|
+
* Converts compilation.json claims to CSV with Jira field names.
|
|
5
|
+
* Columns: Summary, Description, Issue Type, Priority, Labels, Status
|
|
6
|
+
* Zero dependencies — node built-in only.
|
|
7
|
+
*/
|
|
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)';
|
|
13
|
+
|
|
14
|
+
const COLUMNS = [
|
|
15
|
+
'Summary',
|
|
16
|
+
'Description',
|
|
17
|
+
'Issue Type',
|
|
18
|
+
'Priority',
|
|
19
|
+
'Labels',
|
|
20
|
+
'Status',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert a compilation object to Jira-compatible CSV.
|
|
25
|
+
* @param {object} compilation - The compilation.json content
|
|
26
|
+
* @returns {string} CSV output
|
|
27
|
+
*/
|
|
28
|
+
export function convert(compilation) {
|
|
29
|
+
const claims = compilation.claims || [];
|
|
30
|
+
|
|
31
|
+
if (claims.length === 0) {
|
|
32
|
+
return COLUMNS.join(',') + '\n';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const header = COLUMNS.join(',');
|
|
36
|
+
const rows = claims.map(claimToRow);
|
|
37
|
+
|
|
38
|
+
return [header, ...rows].join('\n') + '\n';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function claimToRow(claim) {
|
|
42
|
+
const type = claim.type || '';
|
|
43
|
+
const content = claim.content || claim.text || '';
|
|
44
|
+
const summary = `${claim.id || ''}: ${truncate(content, 120)}`;
|
|
45
|
+
const description = content;
|
|
46
|
+
const { issueType, priority } = mapTypeToJira(type);
|
|
47
|
+
const tags = Array.isArray(claim.tags) ? claim.tags.join(' ') : '';
|
|
48
|
+
const status = claim.status || '';
|
|
49
|
+
|
|
50
|
+
return [
|
|
51
|
+
escapeField(summary),
|
|
52
|
+
escapeField(description),
|
|
53
|
+
escapeField(issueType),
|
|
54
|
+
escapeField(priority),
|
|
55
|
+
escapeField(tags),
|
|
56
|
+
escapeField(status),
|
|
57
|
+
].join(',');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function mapTypeToJira(type) {
|
|
61
|
+
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' };
|
|
68
|
+
default:
|
|
69
|
+
return { issueType: 'Task', priority: 'Low' };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function truncate(str, max) {
|
|
74
|
+
if (str.length <= max) return str;
|
|
75
|
+
return str.slice(0, max - 3) + '...';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function escapeField(value) {
|
|
79
|
+
if (value == null) return '';
|
|
80
|
+
let str = String(value);
|
|
81
|
+
// CWE-1236: Prevent CSV injection by prefixing formula-triggering characters
|
|
82
|
+
if (/^[=+\-@\t\r]/.test(str)) {
|
|
83
|
+
str = "'" + str;
|
|
84
|
+
}
|
|
85
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
86
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
87
|
+
}
|
|
88
|
+
return str;
|
|
89
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mill format: json-ld
|
|
3
|
+
*
|
|
4
|
+
* Wraps compilation.json in JSON-LD using schema.org/Report vocabulary.
|
|
5
|
+
* Zero dependencies — node built-in only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
const require = createRequire(import.meta.url);
|
|
10
|
+
const { buildReport } = require('../json-ld-common.js');
|
|
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)';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Convert a compilation object to JSON-LD.
|
|
19
|
+
* @param {object} compilation - The compilation.json content
|
|
20
|
+
* @returns {string} JSON-LD string
|
|
21
|
+
*/
|
|
22
|
+
export function convert(compilation) {
|
|
23
|
+
const meta = compilation.meta || {};
|
|
24
|
+
const claims = compilation.claims || [];
|
|
25
|
+
const certificate = compilation.certificate || {};
|
|
26
|
+
|
|
27
|
+
return JSON.stringify(buildReport(meta, claims, certificate), null, 2);
|
|
28
|
+
}
|