@grainulation/mill 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CODE_OF_CONDUCT.md +25 -0
- package/CONTRIBUTING.md +101 -0
- package/README.md +90 -42
- package/bin/mill.js +233 -67
- package/lib/exporters/csv.js +35 -30
- package/lib/exporters/json-ld.js +19 -13
- package/lib/exporters/markdown.js +83 -44
- package/lib/exporters/pdf.js +15 -15
- package/lib/formats/bibtex.mjs +83 -0
- package/lib/formats/{changelog.js → changelog.mjs} +27 -26
- package/lib/formats/confluence-adf.mjs +312 -0
- package/lib/formats/{csv.js → csv.mjs} +41 -37
- package/lib/formats/{dot.js → dot.mjs} +45 -34
- package/lib/formats/{evidence-matrix.js → evidence-matrix.mjs} +17 -16
- package/lib/formats/executive-summary.mjs +178 -0
- package/lib/formats/github-issues.mjs +96 -0
- package/lib/formats/{graphml.js → graphml.mjs} +45 -32
- package/lib/formats/html-report.mjs +228 -0
- package/lib/formats/{jira-csv.js → jira-csv.mjs} +30 -29
- package/lib/formats/{json-ld.js → json-ld.mjs} +6 -6
- package/lib/formats/{markdown.js → markdown.mjs} +53 -36
- package/lib/formats/{ndjson.js → ndjson.mjs} +6 -6
- package/lib/formats/{obsidian.js → obsidian.mjs} +43 -35
- package/lib/formats/{opml.js → opml.mjs} +38 -28
- package/lib/formats/ris.mjs +76 -0
- package/lib/formats/{rss.js → rss.mjs} +31 -28
- package/lib/formats/{sankey.js → sankey.mjs} +16 -15
- package/lib/formats/slide-deck.mjs +288 -0
- package/lib/formats/sql.mjs +120 -0
- package/lib/formats/{static-site.js → static-site.mjs} +64 -52
- package/lib/formats/{treemap.js → treemap.mjs} +16 -15
- package/lib/formats/typescript-defs.mjs +150 -0
- package/lib/formats/{yaml.js → yaml.mjs} +58 -40
- package/lib/formats.js +16 -16
- package/lib/index.js +5 -5
- package/lib/json-ld-common.js +37 -31
- package/lib/publishers/clipboard.js +21 -19
- package/lib/publishers/static.js +27 -12
- package/lib/serve-mcp.js +158 -83
- package/lib/server.js +252 -142
- package/package.json +6 -3
- package/lib/formats/bibtex.js +0 -76
- package/lib/formats/executive-summary.js +0 -130
- package/lib/formats/github-issues.js +0 -89
- package/lib/formats/html-report.js +0 -181
- package/lib/formats/ris.js +0 -70
- package/lib/formats/slide-deck.js +0 -200
- package/lib/formats/sql.js +0 -116
- package/lib/formats/typescript-defs.js +0 -147
|
@@ -0,0 +1,76 @@
|
|
|
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 =
|
|
13
|
+
"Claims as RIS citation records (one TY-ER block per claim)";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Convert a compilation object to RIS.
|
|
17
|
+
* @param {object} compilation - The compilation.json content
|
|
18
|
+
* @returns {string} RIS output
|
|
19
|
+
*/
|
|
20
|
+
export function convert(compilation) {
|
|
21
|
+
const claims = compilation.claims || [];
|
|
22
|
+
const meta = compilation.meta || {};
|
|
23
|
+
const author = meta.sprint ? `wheat sprint: ${meta.sprint}` : "wheat sprint";
|
|
24
|
+
const year = new Date().getFullYear().toString();
|
|
25
|
+
|
|
26
|
+
if (claims.length === 0) {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const records = claims.map((claim) => claimToRecord(claim, author, year));
|
|
31
|
+
return records.join("\n") + "\n";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function claimToRecord(claim, author, year) {
|
|
35
|
+
const id = String(claim.id || "unknown");
|
|
36
|
+
const title = claim.content || claim.text || "";
|
|
37
|
+
const type = claim.type || "";
|
|
38
|
+
const evidence =
|
|
39
|
+
typeof claim.evidence === "string"
|
|
40
|
+
? claim.evidence
|
|
41
|
+
: (claim.evidence?.tier ?? claim.evidence_tier ?? "");
|
|
42
|
+
const status = claim.status || "";
|
|
43
|
+
const tags = Array.isArray(claim.tags) ? claim.tags : [];
|
|
44
|
+
const confidence =
|
|
45
|
+
claim.confidence != null ? `, confidence: ${claim.confidence}` : "";
|
|
46
|
+
|
|
47
|
+
const noteParts =
|
|
48
|
+
[
|
|
49
|
+
type ? `type: ${type}` : "",
|
|
50
|
+
evidence ? `evidence: ${evidence}` : "",
|
|
51
|
+
status ? `status: ${status}` : "",
|
|
52
|
+
]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join(", ") + confidence;
|
|
55
|
+
|
|
56
|
+
const lines = [
|
|
57
|
+
`TY - GEN`,
|
|
58
|
+
`ID - ${id}`,
|
|
59
|
+
`TI - ${title}`,
|
|
60
|
+
`AU - ${author}`,
|
|
61
|
+
`PY - ${year}`,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
for (const tag of tags) {
|
|
65
|
+
lines.push(`KW - ${tag}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (noteParts) {
|
|
69
|
+
lines.push(`N1 - ${noteParts}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
lines.push(`ER - `);
|
|
73
|
+
lines.push("");
|
|
74
|
+
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* Zero dependencies — node built-in only.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export const name =
|
|
10
|
-
export const extension =
|
|
11
|
-
export const mimeType =
|
|
12
|
-
export const description =
|
|
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
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Convert a compilation object to Atom XML.
|
|
@@ -19,8 +19,8 @@ export const description = 'Claims as Atom XML feed (one entry per claim)';
|
|
|
19
19
|
export function convert(compilation) {
|
|
20
20
|
const claims = compilation.claims || [];
|
|
21
21
|
const meta = compilation.meta || {};
|
|
22
|
-
const sprintName = meta.sprint ||
|
|
23
|
-
const updated = new Date().toISOString().replace(/\.\d{3}Z$/,
|
|
22
|
+
const sprintName = meta.sprint || "unnamed";
|
|
23
|
+
const updated = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
24
24
|
|
|
25
25
|
const lines = [];
|
|
26
26
|
lines.push(`<?xml version="1.0" encoding="utf-8"?>`);
|
|
@@ -38,27 +38,30 @@ export function convert(compilation) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
lines.push(`</feed>`);
|
|
41
|
-
lines.push(
|
|
41
|
+
lines.push("");
|
|
42
42
|
|
|
43
|
-
return lines.join(
|
|
43
|
+
return lines.join("\n");
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function claimToEntry(claim) {
|
|
47
|
-
const id = String(claim.id ||
|
|
48
|
-
const text = claim.content || claim.text ||
|
|
49
|
-
const type = claim.type ||
|
|
50
|
-
const status = claim.status ||
|
|
51
|
-
const evidence =
|
|
52
|
-
|
|
53
|
-
|
|
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 =
|
|
52
|
+
typeof claim.evidence === "string"
|
|
53
|
+
? claim.evidence
|
|
54
|
+
: (claim.evidence?.tier ?? claim.evidence_tier ?? "");
|
|
54
55
|
const tags = Array.isArray(claim.tags) ? claim.tags : [];
|
|
55
|
-
const updated = new Date().toISOString().replace(/\.\d{3}Z$/,
|
|
56
|
+
const updated = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
56
57
|
|
|
57
58
|
const summaryParts = [
|
|
58
|
-
type ? `type: ${type}` :
|
|
59
|
-
evidence ? `evidence: ${evidence}` :
|
|
60
|
-
status ? `status: ${status}` :
|
|
61
|
-
]
|
|
59
|
+
type ? `type: ${type}` : "",
|
|
60
|
+
evidence ? `evidence: ${evidence}` : "",
|
|
61
|
+
status ? `status: ${status}` : "",
|
|
62
|
+
]
|
|
63
|
+
.filter(Boolean)
|
|
64
|
+
.join(", ");
|
|
62
65
|
|
|
63
66
|
const lines = [];
|
|
64
67
|
lines.push(` <entry>`);
|
|
@@ -81,20 +84,20 @@ function claimToEntry(claim) {
|
|
|
81
84
|
|
|
82
85
|
lines.push(` </entry>`);
|
|
83
86
|
|
|
84
|
-
return lines.join(
|
|
87
|
+
return lines.join("\n");
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
function escapeXml(value) {
|
|
88
|
-
if (value == null) return
|
|
91
|
+
if (value == null) return "";
|
|
89
92
|
return String(value)
|
|
90
|
-
.replace(/&/g,
|
|
91
|
-
.replace(/</g,
|
|
92
|
-
.replace(/>/g,
|
|
93
|
-
.replace(/"/g,
|
|
94
|
-
.replace(/'/g,
|
|
93
|
+
.replace(/&/g, "&")
|
|
94
|
+
.replace(/</g, "<")
|
|
95
|
+
.replace(/>/g, ">")
|
|
96
|
+
.replace(/"/g, """)
|
|
97
|
+
.replace(/'/g, "'");
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
function truncate(str, max) {
|
|
98
101
|
if (str.length <= max) return str;
|
|
99
|
-
return str.slice(0, max - 3) +
|
|
102
|
+
return str.slice(0, max - 3) + "...";
|
|
100
103
|
}
|
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* Zero dependencies — node built-in only.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
export const name =
|
|
10
|
-
export const extension =
|
|
11
|
-
export const mimeType =
|
|
12
|
-
export const description =
|
|
9
|
+
export const name = "sankey";
|
|
10
|
+
export const extension = ".json";
|
|
11
|
+
export const mimeType = "application/json; charset=utf-8";
|
|
12
|
+
export const description =
|
|
13
|
+
"Claims as Sankey flow JSON (type -> evidence tier -> status)";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Convert a compilation object to Sankey JSON.
|
|
@@ -25,9 +26,9 @@ export function convert(compilation) {
|
|
|
25
26
|
const nodeSet = new Set();
|
|
26
27
|
|
|
27
28
|
for (const claim of claims) {
|
|
28
|
-
const type =
|
|
29
|
-
const evidence =
|
|
30
|
-
const status =
|
|
29
|
+
const type = "type:" + (claim.type || "other");
|
|
30
|
+
const evidence = "evidence:" + getEvidence(claim) || "evidence:unknown";
|
|
31
|
+
const status = "status:" + (claim.status || "unknown");
|
|
31
32
|
|
|
32
33
|
nodeSet.add(type);
|
|
33
34
|
nodeSet.add(evidence);
|
|
@@ -44,29 +45,29 @@ export function convert(compilation) {
|
|
|
44
45
|
|
|
45
46
|
const nodes = Array.from(nodeSet)
|
|
46
47
|
.sort()
|
|
47
|
-
.map(id => ({ id }));
|
|
48
|
+
.map((id) => ({ id }));
|
|
48
49
|
|
|
49
50
|
const links = [];
|
|
50
51
|
|
|
51
52
|
for (const [key, value] of Object.entries(typeToEvidence)) {
|
|
52
|
-
const [source, target] = key.split(
|
|
53
|
+
const [source, target] = key.split("|||");
|
|
53
54
|
links.push({ source, target, value });
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
for (const [key, value] of Object.entries(evidenceToStatus)) {
|
|
57
|
-
const [source, target] = key.split(
|
|
58
|
+
const [source, target] = key.split("|||");
|
|
58
59
|
links.push({ source, target, value });
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
const result = { nodes, links };
|
|
62
63
|
|
|
63
|
-
return JSON.stringify(result, null, 2) +
|
|
64
|
+
return JSON.stringify(result, null, 2) + "\n";
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function getEvidence(claim) {
|
|
67
|
-
if (typeof claim.evidence ===
|
|
68
|
-
if (typeof claim.evidence ===
|
|
69
|
-
return claim.evidence.tier || claim.evidence_tier ||
|
|
68
|
+
if (typeof claim.evidence === "string") return claim.evidence;
|
|
69
|
+
if (typeof claim.evidence === "object" && claim.evidence !== null) {
|
|
70
|
+
return claim.evidence.tier || claim.evidence_tier || "";
|
|
70
71
|
}
|
|
71
|
-
return claim.evidence_tier ||
|
|
72
|
+
return claim.evidence_tier || "";
|
|
72
73
|
}
|
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
* Accessible: slide landmarks, aria-roledescription, live region, reduced-motion.
|
|
7
|
+
* Zero dependencies — node built-in only.
|
|
8
|
+
*/
|
|
9
|
+
|
|
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";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert a compilation object to a slide deck HTML page.
|
|
18
|
+
* @param {object} compilation - The compilation.json content
|
|
19
|
+
* @returns {string} HTML output
|
|
20
|
+
*/
|
|
21
|
+
export function convert(compilation) {
|
|
22
|
+
const meta = compilation.meta || {};
|
|
23
|
+
const claims = compilation.claims || [];
|
|
24
|
+
const conflicts = compilation.conflicts || [];
|
|
25
|
+
const certificate = compilation.certificate || {};
|
|
26
|
+
|
|
27
|
+
const title = meta.sprint || meta.question || "Sprint Deck";
|
|
28
|
+
const compiled = certificate.compiled_at || new Date().toISOString();
|
|
29
|
+
const active = claims.filter((c) => c.status === "active").length;
|
|
30
|
+
|
|
31
|
+
// Group by type (skip reverted)
|
|
32
|
+
const byType = {};
|
|
33
|
+
for (const c of claims) {
|
|
34
|
+
if (c.status === "reverted") continue;
|
|
35
|
+
const t = c.type || "unknown";
|
|
36
|
+
if (!byType[t]) byType[t] = [];
|
|
37
|
+
byType[t].push(c);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const typeOrder = [
|
|
41
|
+
"constraint",
|
|
42
|
+
"factual",
|
|
43
|
+
"recommendation",
|
|
44
|
+
"risk",
|
|
45
|
+
"estimate",
|
|
46
|
+
"feedback",
|
|
47
|
+
];
|
|
48
|
+
const sortedTypes = typeOrder.filter((t) => byType[t]);
|
|
49
|
+
for (const t of Object.keys(byType)) {
|
|
50
|
+
if (!sortedTypes.includes(t)) sortedTypes.push(t);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const typeColors = {
|
|
54
|
+
constraint: "#e74c3c",
|
|
55
|
+
factual: "#3498db",
|
|
56
|
+
recommendation: "#2ecc71",
|
|
57
|
+
risk: "#f39c12",
|
|
58
|
+
estimate: "#9b59b6",
|
|
59
|
+
feedback: "#1abc9c",
|
|
60
|
+
unknown: "#95a5a6",
|
|
61
|
+
};
|
|
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
|
+
|
|
68
|
+
// Title slide
|
|
69
|
+
const slides = [];
|
|
70
|
+
slideNum++;
|
|
71
|
+
slides.push(`
|
|
72
|
+
<section class="slide" role="group" aria-roledescription="slide" aria-label="Slide ${slideNum} of ${totalSlides}: Title">
|
|
73
|
+
<div class="slide-content title-slide">
|
|
74
|
+
<h1>${esc(title)}</h1>
|
|
75
|
+
${meta.question ? `<p class="question">${esc(meta.question)}</p>` : ""}
|
|
76
|
+
<p class="meta">${esc(compiled)} | ${claims.length} claims</p>
|
|
77
|
+
</div>
|
|
78
|
+
</section>`);
|
|
79
|
+
|
|
80
|
+
// Summary slide
|
|
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 ");
|
|
88
|
+
|
|
89
|
+
slides.push(`
|
|
90
|
+
<section class="slide" role="group" aria-roledescription="slide" aria-label="Slide ${slideNum} of ${totalSlides}: Summary">
|
|
91
|
+
<div class="slide-content">
|
|
92
|
+
<h2>Summary</h2>
|
|
93
|
+
<div class="summary-grid">
|
|
94
|
+
<div class="big-stat"><span class="num">${claims.length}</span><span class="label">Total</span></div>
|
|
95
|
+
<div class="big-stat"><span class="num">${active}</span><span class="label">Active</span></div>
|
|
96
|
+
${conflicts.length ? `<div class="big-stat"><span class="num">${conflicts.length}</span><span class="label">Conflicts</span></div>` : ""}
|
|
97
|
+
</div>
|
|
98
|
+
<div class="type-stats">
|
|
99
|
+
${typeStats}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</section>`);
|
|
103
|
+
|
|
104
|
+
// One slide per type group
|
|
105
|
+
for (const t of sortedTypes) {
|
|
106
|
+
slideNum++;
|
|
107
|
+
const group = byType[t];
|
|
108
|
+
const color = typeColors[t] || typeColors.unknown;
|
|
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 ");
|
|
117
|
+
|
|
118
|
+
slides.push(`
|
|
119
|
+
<section class="slide" role="group" aria-roledescription="slide" aria-label="Slide ${slideNum} of ${totalSlides}: ${capitalize(t)}s">
|
|
120
|
+
<div class="slide-content">
|
|
121
|
+
<h2 style="border-left:4px solid ${color};padding-left:12px">${capitalize(t)}s (${group.length})</h2>
|
|
122
|
+
<ul class="claim-list">
|
|
123
|
+
${items}
|
|
124
|
+
</ul>
|
|
125
|
+
</div>
|
|
126
|
+
</section>`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Conflicts slide (if any)
|
|
130
|
+
if (conflicts.length > 0) {
|
|
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 ");
|
|
139
|
+
|
|
140
|
+
slides.push(`
|
|
141
|
+
<section class="slide" role="group" aria-roledescription="slide" aria-label="Slide ${slideNum} of ${totalSlides}: Conflicts">
|
|
142
|
+
<div class="slide-content">
|
|
143
|
+
<h2 style="border-left:4px solid #e74c3c;padding-left:12px">Conflicts (${conflicts.length})</h2>
|
|
144
|
+
<ul class="claim-list">
|
|
145
|
+
${conflictItems}
|
|
146
|
+
</ul>
|
|
147
|
+
</div>
|
|
148
|
+
</section>`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Certificate slide
|
|
152
|
+
slideNum++;
|
|
153
|
+
slides.push(`
|
|
154
|
+
<section class="slide" role="group" aria-roledescription="slide" aria-label="Slide ${slideNum} of ${totalSlides}: Certificate">
|
|
155
|
+
<div class="slide-content title-slide">
|
|
156
|
+
<h2>Certificate</h2>
|
|
157
|
+
<p class="mono">${certificate.claim_count || claims.length} claims</p>
|
|
158
|
+
<p class="mono">sha256:${esc((certificate.sha256 || "unknown").slice(0, 24))}</p>
|
|
159
|
+
<p class="meta">${esc(compiled)}</p>
|
|
160
|
+
</div>
|
|
161
|
+
</section>`);
|
|
162
|
+
|
|
163
|
+
return `<!DOCTYPE html>
|
|
164
|
+
<html lang="en">
|
|
165
|
+
<head>
|
|
166
|
+
<meta charset="utf-8">
|
|
167
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
168
|
+
<title>${esc(title)} — Slide Deck</title>
|
|
169
|
+
<style>
|
|
170
|
+
:root { --bg:#0a0e1a; --surface:#111827; --border:#1e293b; --text:#e2e8f0; --muted:#94a3b8; }
|
|
171
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
172
|
+
html { scroll-snap-type:y mandatory; overflow-y:scroll; }
|
|
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; }
|
|
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; }
|
|
178
|
+
.slide-content { max-width:800px; width:100%; }
|
|
179
|
+
.title-slide { text-align:center; }
|
|
180
|
+
.title-slide h1 { font-size:2.4rem; margin-bottom:0.75rem; }
|
|
181
|
+
.title-slide h2 { font-size:1.8rem; margin-bottom:1rem; }
|
|
182
|
+
.question { font-size:1.1rem; color:var(--muted); margin-bottom:1rem; }
|
|
183
|
+
.meta { font-size:0.85rem; color:var(--muted); }
|
|
184
|
+
.mono { font-family:monospace; font-size:1rem; color:var(--muted); margin-bottom:0.5rem; }
|
|
185
|
+
h2 { font-size:1.5rem; margin-bottom:1.25rem; }
|
|
186
|
+
.summary-grid { display:flex; gap:2rem; justify-content:center; margin-bottom:2rem; }
|
|
187
|
+
.big-stat { display:flex; flex-direction:column; align-items:center; }
|
|
188
|
+
.big-stat .num { font-size:2.5rem; font-weight:700; }
|
|
189
|
+
.big-stat .label { font-size:0.8rem; color:var(--muted); text-transform:uppercase; }
|
|
190
|
+
.type-stats { display:flex; flex-wrap:wrap; gap:0.75rem; justify-content:center; }
|
|
191
|
+
.type-stat { display:flex; align-items:center; gap:0.4rem; font-size:0.9rem; }
|
|
192
|
+
.dot { width:10px; height:10px; border-radius:50%; display:inline-block; }
|
|
193
|
+
.claim-list { list-style:none; max-height:70vh; overflow-y:auto; }
|
|
194
|
+
.claim-list li { padding:0.6rem 0; border-bottom:1px solid var(--border); font-size:0.9rem; }
|
|
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; } }
|
|
201
|
+
</style>
|
|
202
|
+
</head>
|
|
203
|
+
<body>
|
|
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>
|
|
210
|
+
<script>
|
|
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);
|
|
227
|
+
}
|
|
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
|
+
})();
|
|
272
|
+
</script>
|
|
273
|
+
</body>
|
|
274
|
+
</html>`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function esc(str) {
|
|
278
|
+
if (str == null) return "";
|
|
279
|
+
return String(str)
|
|
280
|
+
.replace(/&/g, "&")
|
|
281
|
+
.replace(/</g, "<")
|
|
282
|
+
.replace(/>/g, ">")
|
|
283
|
+
.replace(/"/g, """);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function capitalize(str) {
|
|
287
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
288
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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)
|
|
29
|
+
lines.push(`-- Compiled: ${certificate.compiled_at}`);
|
|
30
|
+
lines.push("");
|
|
31
|
+
|
|
32
|
+
// Claims table
|
|
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("");
|
|
45
|
+
|
|
46
|
+
// Conflicts table
|
|
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("");
|
|
54
|
+
|
|
55
|
+
// Insert claims
|
|
56
|
+
if (claims.length > 0) {
|
|
57
|
+
lines.push("-- Claims");
|
|
58
|
+
for (const claim of claims) {
|
|
59
|
+
const id = esc(claim.id || "");
|
|
60
|
+
const type = esc(claim.type || "");
|
|
61
|
+
const content = esc(claim.content || claim.text || "");
|
|
62
|
+
const evidenceTier = esc(extractTier(claim));
|
|
63
|
+
const status = esc(claim.status || "");
|
|
64
|
+
const confidence =
|
|
65
|
+
claim.confidence != null ? String(claim.confidence) : "NULL";
|
|
66
|
+
const source = esc(extractSource(claim));
|
|
67
|
+
const tags = esc(Array.isArray(claim.tags) ? claim.tags.join(",") : "");
|
|
68
|
+
const created = esc(claim.created || "");
|
|
69
|
+
|
|
70
|
+
lines.push(
|
|
71
|
+
`INSERT INTO claims VALUES ('${id}', '${type}', '${content}', '${evidenceTier}', '${status}', ${confidence}, '${source}', '${tags}', '${created}');`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
lines.push("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Insert conflicts
|
|
78
|
+
const conflicts = compilation.conflicts || [];
|
|
79
|
+
if (conflicts.length > 0) {
|
|
80
|
+
lines.push("-- Conflicts");
|
|
81
|
+
for (const conflict of conflicts) {
|
|
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 || "");
|
|
87
|
+
|
|
88
|
+
lines.push(
|
|
89
|
+
`INSERT INTO conflicts (claim_ids, description, resolution) VALUES ('${claimIds}', '${description}', '${resolution}');`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
lines.push("");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return lines.join("\n");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function esc(value) {
|
|
99
|
+
if (value == null) return "";
|
|
100
|
+
return String(value).replace(/'/g, "''");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function extractTier(claim) {
|
|
104
|
+
if (typeof claim.evidence === "string") return claim.evidence;
|
|
105
|
+
if (typeof claim.evidence === "object" && claim.evidence !== null) {
|
|
106
|
+
return claim.evidence.tier || "";
|
|
107
|
+
}
|
|
108
|
+
return claim.evidence_tier || "";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extractSource(claim) {
|
|
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 || "";
|
|
115
|
+
}
|
|
116
|
+
if (typeof claim.evidence === "object" && claim.evidence?.source) {
|
|
117
|
+
return claim.evidence.source;
|
|
118
|
+
}
|
|
119
|
+
return "";
|
|
120
|
+
}
|