@grainulation/mill 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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.js +41 -34
- package/lib/formats/changelog.js +27 -26
- package/lib/formats/confluence-adf.js +312 -0
- package/lib/formats/csv.js +41 -37
- package/lib/formats/dot.js +45 -34
- package/lib/formats/evidence-matrix.js +17 -16
- package/lib/formats/executive-summary.js +89 -41
- package/lib/formats/github-issues.js +40 -33
- package/lib/formats/graphml.js +45 -32
- package/lib/formats/html-report.js +110 -63
- package/lib/formats/jira-csv.js +30 -29
- package/lib/formats/json-ld.js +6 -6
- package/lib/formats/markdown.js +53 -36
- package/lib/formats/ndjson.js +6 -6
- package/lib/formats/obsidian.js +43 -35
- package/lib/formats/opml.js +38 -28
- package/lib/formats/ris.js +29 -23
- package/lib/formats/rss.js +31 -28
- package/lib/formats/sankey.js +16 -15
- package/lib/formats/slide-deck.js +145 -57
- package/lib/formats/sql.js +57 -53
- package/lib/formats/static-site.js +64 -52
- package/lib/formats/treemap.js +16 -15
- package/lib/formats/typescript-defs.js +79 -76
- package/lib/formats/yaml.js +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 +7 -3
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Self-contained HTML report with inline CSS (dark theme).
|
|
5
5
|
* Claims grouped by type with colored badges, filterable via CSS class toggles.
|
|
6
|
+
* Accessible: semantic landmarks, ARIA roles, keyboard support, reduced-motion.
|
|
6
7
|
* Zero dependencies — node built-in only.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
export const name =
|
|
10
|
-
export const extension =
|
|
11
|
-
export const mimeType =
|
|
12
|
-
export const description =
|
|
10
|
+
export const name = "html-report";
|
|
11
|
+
export const extension = ".html";
|
|
12
|
+
export const mimeType = "text/html; charset=utf-8";
|
|
13
|
+
export const description =
|
|
14
|
+
"Self-contained dark-theme HTML report with type filters and claim cards";
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Convert a compilation object to an HTML report.
|
|
@@ -22,76 +24,96 @@ export function convert(compilation) {
|
|
|
22
24
|
const conflicts = compilation.conflicts || [];
|
|
23
25
|
const certificate = compilation.certificate || {};
|
|
24
26
|
|
|
25
|
-
const title = meta.sprint || meta.question ||
|
|
27
|
+
const title = meta.sprint || meta.question || "Sprint Report";
|
|
26
28
|
const compiled = certificate.compiled_at || new Date().toISOString();
|
|
27
29
|
|
|
28
30
|
// Group claims by type (skip reverted)
|
|
29
31
|
const byType = {};
|
|
30
32
|
const typeCounts = {};
|
|
31
33
|
for (const c of claims) {
|
|
32
|
-
if (c.status ===
|
|
33
|
-
const t = c.type ||
|
|
34
|
+
if (c.status === "reverted") continue;
|
|
35
|
+
const t = c.type || "unknown";
|
|
34
36
|
if (!byType[t]) byType[t] = [];
|
|
35
37
|
byType[t].push(c);
|
|
36
38
|
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
const typeOrder = [
|
|
40
|
-
|
|
41
|
+
const typeOrder = [
|
|
42
|
+
"constraint",
|
|
43
|
+
"factual",
|
|
44
|
+
"recommendation",
|
|
45
|
+
"risk",
|
|
46
|
+
"estimate",
|
|
47
|
+
"feedback",
|
|
48
|
+
];
|
|
49
|
+
const sortedTypes = typeOrder.filter((t) => byType[t]);
|
|
41
50
|
for (const t of Object.keys(byType)) {
|
|
42
51
|
if (!sortedTypes.includes(t)) sortedTypes.push(t);
|
|
43
52
|
}
|
|
44
53
|
|
|
45
54
|
const typeColors = {
|
|
46
|
-
constraint:
|
|
47
|
-
factual:
|
|
48
|
-
recommendation:
|
|
49
|
-
risk:
|
|
50
|
-
estimate:
|
|
51
|
-
feedback:
|
|
52
|
-
unknown:
|
|
55
|
+
constraint: "#e74c3c",
|
|
56
|
+
factual: "#3498db",
|
|
57
|
+
recommendation: "#2ecc71",
|
|
58
|
+
risk: "#f39c12",
|
|
59
|
+
estimate: "#9b59b6",
|
|
60
|
+
feedback: "#1abc9c",
|
|
61
|
+
unknown: "#95a5a6",
|
|
53
62
|
};
|
|
54
63
|
|
|
55
|
-
const active = claims.filter(c => c.status ===
|
|
64
|
+
const active = claims.filter((c) => c.status === "active").length;
|
|
56
65
|
|
|
57
|
-
// Build filter buttons
|
|
58
|
-
const filterButtons = sortedTypes
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
// Build filter buttons with aria-pressed
|
|
67
|
+
const filterButtons = sortedTypes
|
|
68
|
+
.map((t) => {
|
|
69
|
+
const color = typeColors[t] || typeColors.unknown;
|
|
70
|
+
return `<button class="filter-btn active" data-type="${esc(t)}" style="--badge-color:${color}" aria-pressed="true" onclick="toggleType('${esc(t)}')">${capitalize(t)} (${typeCounts[t]})</button>`;
|
|
71
|
+
})
|
|
72
|
+
.join("\n ");
|
|
62
73
|
|
|
63
74
|
// Build type sections
|
|
64
|
-
const sections = sortedTypes
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const sections = sortedTypes
|
|
76
|
+
.map((t) => {
|
|
77
|
+
const group = byType[t];
|
|
78
|
+
const color = typeColors[t] || typeColors.unknown;
|
|
79
|
+
const cards = group
|
|
80
|
+
.map((c) => {
|
|
81
|
+
const body = esc(c.content || c.text || "");
|
|
82
|
+
const evidenceTier =
|
|
83
|
+
typeof c.evidence === "string"
|
|
84
|
+
? c.evidence
|
|
85
|
+
: c.evidence?.tier || c.evidence_tier || "";
|
|
86
|
+
const conf =
|
|
87
|
+
c.confidence != null ? Math.round(c.confidence * 100) : null;
|
|
88
|
+
const tags = Array.isArray(c.tags)
|
|
89
|
+
? c.tags.map((t) => `<span class="tag">${esc(t)}</span>`).join("")
|
|
90
|
+
: "";
|
|
91
|
+
const confBar =
|
|
92
|
+
conf != null
|
|
93
|
+
? `<div class="conf-bar" role="progressbar" aria-valuenow="${conf}" aria-valuemin="0" aria-valuemax="100" aria-label="Confidence: ${conf}%"><div class="conf-fill" style="width:${conf}%"></div><span class="conf-label">${conf}%</span></div>`
|
|
94
|
+
: "";
|
|
95
|
+
return `
|
|
96
|
+
<article class="claim-card" aria-labelledby="claim-${esc(c.id)}">
|
|
77
97
|
<div class="card-header">
|
|
78
|
-
<span class="claim-id">${esc(c.id)}</span>
|
|
79
|
-
${evidenceTier ? `<span class="evidence-badge"
|
|
80
|
-
<span class="status-badge status-${esc(c.status ||
|
|
98
|
+
<span class="claim-id" id="claim-${esc(c.id)}">${esc(c.id)}</span>
|
|
99
|
+
${evidenceTier ? `<span class="evidence-badge">evidence: ${esc(evidenceTier)}</span>` : ""}
|
|
100
|
+
<span class="status-badge status-${esc(c.status || "active")}">status: ${esc(c.status || "active")}</span>
|
|
81
101
|
</div>
|
|
82
102
|
<p class="card-body">${body}</p>
|
|
83
|
-
${tags ? `<div class="card-tags">${tags}</div>` :
|
|
103
|
+
${tags ? `<div class="card-tags" aria-label="Tags">${tags}</div>` : ""}
|
|
84
104
|
${confBar}
|
|
85
|
-
</
|
|
86
|
-
|
|
105
|
+
</article>`;
|
|
106
|
+
})
|
|
107
|
+
.join("\n");
|
|
87
108
|
|
|
88
|
-
|
|
89
|
-
<section class="type-section" data-type="${esc(t)}">
|
|
109
|
+
return `
|
|
110
|
+
<section class="type-section" data-type="${esc(t)}" aria-label="${capitalize(t)} claims">
|
|
90
111
|
<h2 style="border-left:4px solid ${color};padding-left:12px">${capitalize(t)}s (${group.length})</h2>
|
|
91
112
|
<div class="cards">${cards}
|
|
92
113
|
</div>
|
|
93
114
|
</section>`;
|
|
94
|
-
|
|
115
|
+
})
|
|
116
|
+
.join("\n");
|
|
95
117
|
|
|
96
118
|
return `<!DOCTYPE html>
|
|
97
119
|
<html lang="en">
|
|
@@ -103,6 +125,8 @@ export function convert(compilation) {
|
|
|
103
125
|
:root { --bg:#0a0e1a; --surface:#111827; --border:#1e293b; --text:#e2e8f0; --muted:#94a3b8; }
|
|
104
126
|
* { margin:0; padding:0; box-sizing:border-box; }
|
|
105
127
|
body { background:var(--bg); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; line-height:1.6; padding:2rem; }
|
|
128
|
+
a.skip-link { 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; }
|
|
129
|
+
a.skip-link:focus { top:0; outline:2px solid #3b82f6; }
|
|
106
130
|
header { margin-bottom:2rem; }
|
|
107
131
|
header h1 { font-size:1.8rem; margin-bottom:0.5rem; }
|
|
108
132
|
header p { color:var(--muted); font-size:0.9rem; }
|
|
@@ -112,10 +136,11 @@ export function convert(compilation) {
|
|
|
112
136
|
.stat .label { font-size:0.75rem; color:var(--muted); text-transform:uppercase; }
|
|
113
137
|
.filters { display:flex; gap:0.5rem; flex-wrap:wrap; margin-bottom:2rem; }
|
|
114
138
|
.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; }
|
|
139
|
+
.filter-btn:focus-visible { outline:2px solid #3b82f6; outline-offset:2px; }
|
|
115
140
|
.filter-btn.active { border-color:var(--badge-color); box-shadow:0 0 0 1px var(--badge-color); }
|
|
116
141
|
.filter-btn:not(.active) { opacity:0.4; }
|
|
117
142
|
.type-section { margin-bottom:2rem; }
|
|
118
|
-
.type-section
|
|
143
|
+
.type-section[hidden] { display:none; }
|
|
119
144
|
.type-section h2 { font-size:1.2rem; margin-bottom:1rem; }
|
|
120
145
|
.cards { display:grid; gap:0.75rem; }
|
|
121
146
|
.claim-card { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:1rem; }
|
|
@@ -132,48 +157,70 @@ export function convert(compilation) {
|
|
|
132
157
|
.conf-fill { height:100%; background:#3b82f6; border-radius:3px; }
|
|
133
158
|
.conf-label { position:absolute; right:0; top:-16px; font-size:0.7rem; color:var(--muted); }
|
|
134
159
|
footer { margin-top:3rem; padding-top:1rem; border-top:1px solid var(--border); color:var(--muted); font-size:0.8rem; font-family:monospace; }
|
|
160
|
+
@media (prefers-reduced-motion:reduce) { .filter-btn { transition:none; } }
|
|
161
|
+
@media print { body { background:#fff; color:#000; } .skip-link { display:none; } .filter-btn { border-color:#ccc; } }
|
|
135
162
|
</style>
|
|
136
163
|
</head>
|
|
137
164
|
<body>
|
|
138
|
-
<
|
|
165
|
+
<a class="skip-link" href="#main-content">Skip to content</a>
|
|
166
|
+
<header role="banner">
|
|
139
167
|
<h1>${esc(title)}</h1>
|
|
140
|
-
${meta.question ? `<p>${esc(meta.question)}</p>` :
|
|
141
|
-
<p>Compiled: ${esc(compiled)}
|
|
168
|
+
${meta.question ? `<p>${esc(meta.question)}</p>` : ""}
|
|
169
|
+
<p>Compiled: <time datetime="${esc(compiled)}">${esc(compiled)}</time>${meta.audience ? ` | Audience: ${esc(meta.audience)}` : ""}</p>
|
|
142
170
|
</header>
|
|
143
171
|
|
|
144
|
-
<
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
172
|
+
<main id="main-content" role="main">
|
|
173
|
+
<section aria-label="Summary statistics">
|
|
174
|
+
<h2 class="sr-only">Summary</h2>
|
|
175
|
+
<div class="stats-bar" role="list">
|
|
176
|
+
<div class="stat" role="listitem"><div class="num" aria-label="${claims.length} total claims">${claims.length}</div><div class="label">Total</div></div>
|
|
177
|
+
<div class="stat" role="listitem"><div class="num" aria-label="${active} active claims">${active}</div><div class="label">Active</div></div>
|
|
178
|
+
${sortedTypes.map((t) => `<div class="stat" role="listitem"><div class="num" aria-label="${typeCounts[t]} ${t} claims">${typeCounts[t]}</div><div class="label">${capitalize(t)}</div></div>`).join("\n ")}
|
|
179
|
+
${conflicts.length ? `<div class="stat" role="listitem"><div class="num" aria-label="${conflicts.length} conflicts">${conflicts.length}</div><div class="label">Conflicts</div></div>` : ""}
|
|
149
180
|
</div>
|
|
181
|
+
</section>
|
|
150
182
|
|
|
151
|
-
<
|
|
183
|
+
<nav class="filters" aria-label="Filter claims by type">
|
|
152
184
|
${filterButtons}
|
|
153
|
-
</
|
|
185
|
+
</nav>
|
|
154
186
|
|
|
155
187
|
${sections}
|
|
188
|
+
</main>
|
|
156
189
|
|
|
157
|
-
<footer>
|
|
158
|
-
Certificate: ${certificate.claim_count || claims.length} claims | sha256:${(certificate.sha256 ||
|
|
190
|
+
<footer role="contentinfo">
|
|
191
|
+
<p>Certificate: ${certificate.claim_count || claims.length} claims | sha256:${(certificate.sha256 || "unknown").slice(0, 16)}</p>
|
|
159
192
|
</footer>
|
|
160
193
|
|
|
161
194
|
<script>
|
|
162
195
|
function toggleType(type) {
|
|
163
|
-
|
|
164
|
-
|
|
196
|
+
var btn = document.querySelector('.filter-btn[data-type="' + type + '"]');
|
|
197
|
+
var section = document.querySelector('.type-section[data-type="' + type + '"]');
|
|
165
198
|
if (!btn || !section) return;
|
|
166
|
-
btn.classList.toggle('active');
|
|
167
|
-
|
|
199
|
+
var isActive = btn.classList.toggle('active');
|
|
200
|
+
btn.setAttribute('aria-pressed', String(isActive));
|
|
201
|
+
if (isActive) { section.removeAttribute('hidden'); } else { section.setAttribute('hidden', ''); }
|
|
168
202
|
}
|
|
203
|
+
// Keyboard support: Enter/Space toggles filter buttons
|
|
204
|
+
document.querySelectorAll('.filter-btn').forEach(function(btn) {
|
|
205
|
+
btn.addEventListener('keydown', function(e) {
|
|
206
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
207
|
+
e.preventDefault();
|
|
208
|
+
btn.click();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
169
212
|
</script>
|
|
170
213
|
</body>
|
|
171
214
|
</html>`;
|
|
172
215
|
}
|
|
173
216
|
|
|
174
217
|
function esc(str) {
|
|
175
|
-
if (str == null) return
|
|
176
|
-
return String(str)
|
|
218
|
+
if (str == null) return "";
|
|
219
|
+
return String(str)
|
|
220
|
+
.replace(/&/g, "&")
|
|
221
|
+
.replace(/</g, "<")
|
|
222
|
+
.replace(/>/g, ">")
|
|
223
|
+
.replace(/"/g, """);
|
|
177
224
|
}
|
|
178
225
|
|
|
179
226
|
function capitalize(str) {
|
package/lib/formats/jira-csv.js
CHANGED
|
@@ -6,18 +6,19 @@
|
|
|
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 = "jira-csv";
|
|
10
|
+
export const extension = ".csv";
|
|
11
|
+
export const mimeType = "text/csv; charset=utf-8";
|
|
12
|
+
export const description =
|
|
13
|
+
"Claims as Jira-compatible CSV (Summary, Description, Issue Type, Priority, Labels, Status)";
|
|
13
14
|
|
|
14
15
|
const COLUMNS = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
"Summary",
|
|
17
|
+
"Description",
|
|
18
|
+
"Issue Type",
|
|
19
|
+
"Priority",
|
|
20
|
+
"Labels",
|
|
21
|
+
"Status",
|
|
21
22
|
];
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -29,23 +30,23 @@ export function convert(compilation) {
|
|
|
29
30
|
const claims = compilation.claims || [];
|
|
30
31
|
|
|
31
32
|
if (claims.length === 0) {
|
|
32
|
-
return COLUMNS.join(
|
|
33
|
+
return COLUMNS.join(",") + "\n";
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const header = COLUMNS.join(
|
|
36
|
+
const header = COLUMNS.join(",");
|
|
36
37
|
const rows = claims.map(claimToRow);
|
|
37
38
|
|
|
38
|
-
return [header, ...rows].join(
|
|
39
|
+
return [header, ...rows].join("\n") + "\n";
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function claimToRow(claim) {
|
|
42
|
-
const type = claim.type ||
|
|
43
|
-
const content = claim.content || claim.text ||
|
|
44
|
-
const summary = `${claim.id ||
|
|
43
|
+
const type = claim.type || "";
|
|
44
|
+
const content = claim.content || claim.text || "";
|
|
45
|
+
const summary = `${claim.id || ""}: ${truncate(content, 120)}`;
|
|
45
46
|
const description = content;
|
|
46
47
|
const { issueType, priority } = mapTypeToJira(type);
|
|
47
|
-
const tags = Array.isArray(claim.tags) ? claim.tags.join(
|
|
48
|
-
const status = claim.status ||
|
|
48
|
+
const tags = Array.isArray(claim.tags) ? claim.tags.join(" ") : "";
|
|
49
|
+
const status = claim.status || "";
|
|
49
50
|
|
|
50
51
|
return [
|
|
51
52
|
escapeField(summary),
|
|
@@ -54,35 +55,35 @@ function claimToRow(claim) {
|
|
|
54
55
|
escapeField(priority),
|
|
55
56
|
escapeField(tags),
|
|
56
57
|
escapeField(status),
|
|
57
|
-
].join(
|
|
58
|
+
].join(",");
|
|
58
59
|
}
|
|
59
60
|
|
|
60
61
|
function mapTypeToJira(type) {
|
|
61
62
|
switch (type) {
|
|
62
|
-
case
|
|
63
|
-
return { issueType:
|
|
64
|
-
case
|
|
65
|
-
return { issueType:
|
|
66
|
-
case
|
|
67
|
-
return { issueType:
|
|
63
|
+
case "risk":
|
|
64
|
+
return { issueType: "Bug", priority: "High" };
|
|
65
|
+
case "recommendation":
|
|
66
|
+
return { issueType: "Task", priority: "Medium" };
|
|
67
|
+
case "constraint":
|
|
68
|
+
return { issueType: "Task", priority: "High" };
|
|
68
69
|
default:
|
|
69
|
-
return { issueType:
|
|
70
|
+
return { issueType: "Task", priority: "Low" };
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
function truncate(str, max) {
|
|
74
75
|
if (str.length <= max) return str;
|
|
75
|
-
return str.slice(0, max - 3) +
|
|
76
|
+
return str.slice(0, max - 3) + "...";
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
function escapeField(value) {
|
|
79
|
-
if (value == null) return
|
|
80
|
+
if (value == null) return "";
|
|
80
81
|
let str = String(value);
|
|
81
82
|
// CWE-1236: Prevent CSV injection by prefixing formula-triggering characters
|
|
82
83
|
if (/^[=+\-@\t\r]/.test(str)) {
|
|
83
84
|
str = "'" + str;
|
|
84
85
|
}
|
|
85
|
-
if (str.includes(
|
|
86
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
86
87
|
return `"${str.replace(/"/g, '""')}"`;
|
|
87
88
|
}
|
|
88
89
|
return str;
|
package/lib/formats/json-ld.js
CHANGED
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* Zero dependencies — node built-in only.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { createRequire } from
|
|
8
|
+
import { createRequire } from "node:module";
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
|
-
const { buildReport } = require(
|
|
10
|
+
const { buildReport } = require("../json-ld-common.js");
|
|
11
11
|
|
|
12
|
-
export const name =
|
|
13
|
-
export const extension =
|
|
14
|
-
export const mimeType =
|
|
15
|
-
export const description =
|
|
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
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Convert a compilation object to JSON-LD.
|
package/lib/formats/markdown.js
CHANGED
|
@@ -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 = "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
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Convert a compilation object to Markdown.
|
|
@@ -24,48 +24,55 @@ export function convert(compilation) {
|
|
|
24
24
|
const certificate = compilation.certificate || {};
|
|
25
25
|
|
|
26
26
|
// Title
|
|
27
|
-
const title = meta.sprint || meta.question ||
|
|
27
|
+
const title = meta.sprint || meta.question || "Sprint Export";
|
|
28
28
|
lines.push(`# ${title}`);
|
|
29
|
-
lines.push(
|
|
29
|
+
lines.push("");
|
|
30
30
|
|
|
31
31
|
// Meta block
|
|
32
32
|
if (meta.question) {
|
|
33
33
|
lines.push(`> **Question:** ${meta.question}`);
|
|
34
|
-
lines.push(
|
|
34
|
+
lines.push("");
|
|
35
35
|
}
|
|
36
36
|
if (meta.audience) {
|
|
37
37
|
lines.push(`**Audience:** ${meta.audience}`);
|
|
38
|
-
lines.push(
|
|
38
|
+
lines.push("");
|
|
39
39
|
}
|
|
40
40
|
if (certificate.compiled_at) {
|
|
41
41
|
lines.push(`*Compiled: ${certificate.compiled_at}*`);
|
|
42
|
-
lines.push(
|
|
42
|
+
lines.push("");
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Summary stats
|
|
46
|
-
const active = claims.filter(c => c.status ===
|
|
47
|
-
const superseded = claims.filter(c => c.status ===
|
|
48
|
-
const reverted = claims.filter(c => c.status ===
|
|
49
|
-
lines.push(
|
|
50
|
-
lines.push(
|
|
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
51
|
lines.push(`- **Total claims:** ${claims.length}`);
|
|
52
52
|
lines.push(`- **Active:** ${active}`);
|
|
53
53
|
if (superseded) lines.push(`- **Superseded:** ${superseded}`);
|
|
54
54
|
if (reverted) lines.push(`- **Reverted:** ${reverted}`);
|
|
55
55
|
if (conflicts.length) lines.push(`- **Conflicts:** ${conflicts.length}`);
|
|
56
|
-
lines.push(
|
|
56
|
+
lines.push("");
|
|
57
57
|
|
|
58
58
|
// Group claims by type
|
|
59
59
|
const byType = {};
|
|
60
60
|
for (const claim of claims) {
|
|
61
|
-
if (claim.status ===
|
|
62
|
-
const type = claim.type ||
|
|
61
|
+
if (claim.status === "reverted") continue;
|
|
62
|
+
const type = claim.type || "unknown";
|
|
63
63
|
if (!byType[type]) byType[type] = [];
|
|
64
64
|
byType[type].push(claim);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const typeOrder = [
|
|
68
|
-
|
|
67
|
+
const typeOrder = [
|
|
68
|
+
"constraint",
|
|
69
|
+
"factual",
|
|
70
|
+
"recommendation",
|
|
71
|
+
"risk",
|
|
72
|
+
"estimate",
|
|
73
|
+
"feedback",
|
|
74
|
+
];
|
|
75
|
+
const sortedTypes = typeOrder.filter((t) => byType[t]);
|
|
69
76
|
for (const t of Object.keys(byType)) {
|
|
70
77
|
if (!sortedTypes.includes(t)) sortedTypes.push(t);
|
|
71
78
|
}
|
|
@@ -75,42 +82,52 @@ export function convert(compilation) {
|
|
|
75
82
|
if (!group || group.length === 0) continue;
|
|
76
83
|
|
|
77
84
|
lines.push(`## ${capitalize(type)}s (${group.length})`);
|
|
78
|
-
lines.push(
|
|
85
|
+
lines.push("");
|
|
79
86
|
|
|
80
87
|
for (const claim of group) {
|
|
81
|
-
const conf =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
88
|
+
const conf =
|
|
89
|
+
claim.confidence != null
|
|
90
|
+
? ` [${Math.round(claim.confidence * 100)}%]`
|
|
91
|
+
: "";
|
|
92
|
+
const evidenceStr =
|
|
93
|
+
typeof claim.evidence === "string"
|
|
94
|
+
? claim.evidence
|
|
95
|
+
: claim.evidence?.tier;
|
|
96
|
+
const tier = evidenceStr ? ` (${evidenceStr})` : "";
|
|
97
|
+
const status = claim.status === "superseded" ? " ~~superseded~~" : "";
|
|
98
|
+
const body = claim.content || claim.text || "";
|
|
86
99
|
lines.push(`- **${claim.id}**${conf}${tier}${status}: ${body}`);
|
|
87
100
|
}
|
|
88
|
-
lines.push(
|
|
101
|
+
lines.push("");
|
|
89
102
|
}
|
|
90
103
|
|
|
91
104
|
// Conflicts
|
|
92
105
|
if (conflicts.length > 0) {
|
|
93
|
-
lines.push(
|
|
94
|
-
lines.push(
|
|
106
|
+
lines.push("## Conflicts");
|
|
107
|
+
lines.push("");
|
|
95
108
|
for (const conflict of conflicts) {
|
|
96
|
-
const resolved = conflict.resolution ?
|
|
97
|
-
lines.push(
|
|
109
|
+
const resolved = conflict.resolution ? " (resolved)" : "";
|
|
110
|
+
lines.push(
|
|
111
|
+
`- **${conflict.ids?.join(" vs ") || "unknown"}**${resolved}: ${conflict.description || conflict.reason || "No description"}`,
|
|
112
|
+
);
|
|
98
113
|
if (conflict.resolution) {
|
|
99
114
|
lines.push(` - *Resolution:* ${conflict.resolution}`);
|
|
100
115
|
}
|
|
101
116
|
}
|
|
102
|
-
lines.push(
|
|
117
|
+
lines.push("");
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
// Certificate
|
|
106
121
|
if (certificate.sha256 || certificate.claim_count) {
|
|
107
|
-
lines.push(
|
|
108
|
-
lines.push(
|
|
109
|
-
lines.push(
|
|
110
|
-
|
|
122
|
+
lines.push("---");
|
|
123
|
+
lines.push("");
|
|
124
|
+
lines.push(
|
|
125
|
+
`*Certificate: ${certificate.claim_count || claims.length} claims, sha256:${(certificate.sha256 || "unknown").slice(0, 12)}*`,
|
|
126
|
+
);
|
|
127
|
+
lines.push("");
|
|
111
128
|
}
|
|
112
129
|
|
|
113
|
-
return lines.join(
|
|
130
|
+
return lines.join("\n");
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
function capitalize(str) {
|
package/lib/formats/ndjson.js
CHANGED
|
@@ -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 = "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
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Convert a compilation object to NDJSON.
|
|
@@ -19,7 +19,7 @@ export const description = 'Newline-delimited JSON — one claim object per line
|
|
|
19
19
|
export function convert(compilation) {
|
|
20
20
|
const claims = compilation.claims || [];
|
|
21
21
|
|
|
22
|
-
if (claims.length === 0) return
|
|
22
|
+
if (claims.length === 0) return "";
|
|
23
23
|
|
|
24
|
-
return claims.map(c => JSON.stringify(c)).join(
|
|
24
|
+
return claims.map((c) => JSON.stringify(c)).join("\n") + "\n";
|
|
25
25
|
}
|