@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
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Convert HTML artifacts to clean Markdown.
|
|
@@ -13,80 +13,113 @@ function htmlToMarkdown(html) {
|
|
|
13
13
|
let md = html;
|
|
14
14
|
|
|
15
15
|
// Remove doctype, head, scripts, styles
|
|
16
|
-
md = md.replace(/<!DOCTYPE[^>]*>/gi,
|
|
17
|
-
md = md.replace(/<head[\s\S]*?<\/head>/gi,
|
|
18
|
-
md = md.replace(/<script[\s\S]*?<\/script>/gi,
|
|
19
|
-
md = md.replace(/<style[\s\S]*?<\/style>/gi,
|
|
16
|
+
md = md.replace(/<!DOCTYPE[^>]*>/gi, "");
|
|
17
|
+
md = md.replace(/<head[\s\S]*?<\/head>/gi, "");
|
|
18
|
+
md = md.replace(/<script[\s\S]*?<\/script>/gi, "");
|
|
19
|
+
md = md.replace(/<style[\s\S]*?<\/style>/gi, "");
|
|
20
20
|
|
|
21
21
|
// Headings
|
|
22
22
|
md = md.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, (_, c) => `# ${strip(c)}\n\n`);
|
|
23
|
-
md = md.replace(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
md = md.replace(
|
|
23
|
+
md = md.replace(
|
|
24
|
+
/<h2[^>]*>([\s\S]*?)<\/h2>/gi,
|
|
25
|
+
(_, c) => `## ${strip(c)}\n\n`,
|
|
26
|
+
);
|
|
27
|
+
md = md.replace(
|
|
28
|
+
/<h3[^>]*>([\s\S]*?)<\/h3>/gi,
|
|
29
|
+
(_, c) => `### ${strip(c)}\n\n`,
|
|
30
|
+
);
|
|
31
|
+
md = md.replace(
|
|
32
|
+
/<h4[^>]*>([\s\S]*?)<\/h4>/gi,
|
|
33
|
+
(_, c) => `#### ${strip(c)}\n\n`,
|
|
34
|
+
);
|
|
35
|
+
md = md.replace(
|
|
36
|
+
/<h5[^>]*>([\s\S]*?)<\/h5>/gi,
|
|
37
|
+
(_, c) => `##### ${strip(c)}\n\n`,
|
|
38
|
+
);
|
|
39
|
+
md = md.replace(
|
|
40
|
+
/<h6[^>]*>([\s\S]*?)<\/h6>/gi,
|
|
41
|
+
(_, c) => `###### ${strip(c)}\n\n`,
|
|
42
|
+
);
|
|
28
43
|
|
|
29
44
|
// Bold, italic, code
|
|
30
|
-
md = md.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi,
|
|
31
|
-
md = md.replace(/<b[^>]*>([\s\S]*?)<\/b>/gi,
|
|
32
|
-
md = md.replace(/<em[^>]*>([\s\S]*?)<\/em>/gi,
|
|
33
|
-
md = md.replace(/<i[^>]*>([\s\S]*?)<\/i>/gi,
|
|
34
|
-
md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi,
|
|
45
|
+
md = md.replace(/<strong[^>]*>([\s\S]*?)<\/strong>/gi, "**$1**");
|
|
46
|
+
md = md.replace(/<b[^>]*>([\s\S]*?)<\/b>/gi, "**$1**");
|
|
47
|
+
md = md.replace(/<em[^>]*>([\s\S]*?)<\/em>/gi, "*$1*");
|
|
48
|
+
md = md.replace(/<i[^>]*>([\s\S]*?)<\/i>/gi, "*$1*");
|
|
49
|
+
md = md.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, "`$1`");
|
|
35
50
|
|
|
36
51
|
// Pre/code blocks
|
|
37
|
-
md = md.replace(
|
|
38
|
-
|
|
39
|
-
|
|
52
|
+
md = md.replace(
|
|
53
|
+
/<pre[^>]*><code[^>]*>([\s\S]*?)<\/code><\/pre>/gi,
|
|
54
|
+
(_, c) => {
|
|
55
|
+
return "\n```\n" + decodeEntities(c) + "\n```\n\n";
|
|
56
|
+
},
|
|
57
|
+
);
|
|
40
58
|
md = md.replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, (_, c) => {
|
|
41
|
-
return
|
|
59
|
+
return "\n```\n" + decodeEntities(c) + "\n```\n\n";
|
|
42
60
|
});
|
|
43
61
|
|
|
44
62
|
// Links
|
|
45
|
-
md = md.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi,
|
|
63
|
+
md = md.replace(/<a[^>]+href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)");
|
|
46
64
|
|
|
47
65
|
// Images
|
|
48
|
-
md = md.replace(
|
|
49
|
-
|
|
66
|
+
md = md.replace(
|
|
67
|
+
/<img[^>]+src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi,
|
|
68
|
+
"",
|
|
69
|
+
);
|
|
70
|
+
md = md.replace(/<img[^>]+src="([^"]*)"[^>]*\/?>/gi, "");
|
|
50
71
|
|
|
51
72
|
// Lists
|
|
52
|
-
md = md.replace(
|
|
53
|
-
|
|
73
|
+
md = md.replace(
|
|
74
|
+
/<li[^>]*>([\s\S]*?)<\/li>/gi,
|
|
75
|
+
(_, c) => `- ${strip(c).trim()}\n`,
|
|
76
|
+
);
|
|
77
|
+
md = md.replace(/<\/?[ou]l[^>]*>/gi, "\n");
|
|
54
78
|
|
|
55
79
|
// Paragraphs and breaks
|
|
56
|
-
md = md.replace(
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
md = md.replace(
|
|
81
|
+
/<p[^>]*>([\s\S]*?)<\/p>/gi,
|
|
82
|
+
(_, c) => `${strip(c).trim()}\n\n`,
|
|
83
|
+
);
|
|
84
|
+
md = md.replace(/<br\s*\/?>/gi, "\n");
|
|
85
|
+
md = md.replace(/<hr\s*\/?>/gi, "\n---\n\n");
|
|
59
86
|
|
|
60
87
|
// Blockquotes
|
|
61
88
|
md = md.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, (_, c) => {
|
|
62
|
-
return
|
|
89
|
+
return (
|
|
90
|
+
strip(c)
|
|
91
|
+
.trim()
|
|
92
|
+
.split("\n")
|
|
93
|
+
.map((l) => `> ${l}`)
|
|
94
|
+
.join("\n") + "\n\n"
|
|
95
|
+
);
|
|
63
96
|
});
|
|
64
97
|
|
|
65
98
|
// Strip remaining tags
|
|
66
|
-
md = md.replace(/<[^>]+>/g,
|
|
99
|
+
md = md.replace(/<[^>]+>/g, "");
|
|
67
100
|
|
|
68
101
|
// Decode common entities
|
|
69
102
|
md = decodeEntities(md);
|
|
70
103
|
|
|
71
104
|
// Normalize whitespace
|
|
72
|
-
md = md.replace(/\n{3,}/g,
|
|
73
|
-
md = md.trim() +
|
|
105
|
+
md = md.replace(/\n{3,}/g, "\n\n");
|
|
106
|
+
md = md.trim() + "\n";
|
|
74
107
|
|
|
75
108
|
return md;
|
|
76
109
|
}
|
|
77
110
|
|
|
78
111
|
function strip(html) {
|
|
79
|
-
return html.replace(/<[^>]+>/g,
|
|
112
|
+
return html.replace(/<[^>]+>/g, "");
|
|
80
113
|
}
|
|
81
114
|
|
|
82
115
|
function decodeEntities(str) {
|
|
83
116
|
return str
|
|
84
|
-
.replace(/&/g,
|
|
85
|
-
.replace(/</g,
|
|
86
|
-
.replace(/>/g,
|
|
117
|
+
.replace(/&/g, "&")
|
|
118
|
+
.replace(/</g, "<")
|
|
119
|
+
.replace(/>/g, ">")
|
|
87
120
|
.replace(/"/g, '"')
|
|
88
121
|
.replace(/'/g, "'")
|
|
89
|
-
.replace(/ /g,
|
|
122
|
+
.replace(/ /g, " ");
|
|
90
123
|
}
|
|
91
124
|
|
|
92
125
|
function deriveOutputPath(inputPath, explicit) {
|
|
@@ -97,20 +130,26 @@ function deriveOutputPath(inputPath, explicit) {
|
|
|
97
130
|
}
|
|
98
131
|
|
|
99
132
|
async function exportMarkdown(inputPath, outputPath) {
|
|
100
|
-
const html = fs.readFileSync(inputPath,
|
|
133
|
+
const html = fs.readFileSync(inputPath, "utf-8");
|
|
101
134
|
const trimmed = html.trimStart();
|
|
102
|
-
if (
|
|
103
|
-
|
|
135
|
+
if (
|
|
136
|
+
!trimmed.startsWith("<") &&
|
|
137
|
+
!trimmed.startsWith("<!DOCTYPE") &&
|
|
138
|
+
!trimmed.startsWith("<html")
|
|
139
|
+
) {
|
|
140
|
+
process.stderr.write(
|
|
141
|
+
"Warning: Input does not appear to be HTML. Markdown conversion may produce unexpected results.\n",
|
|
142
|
+
);
|
|
104
143
|
}
|
|
105
144
|
const md = htmlToMarkdown(html);
|
|
106
145
|
const out = deriveOutputPath(inputPath, outputPath);
|
|
107
|
-
fs.writeFileSync(out, md,
|
|
146
|
+
fs.writeFileSync(out, md, "utf-8");
|
|
108
147
|
return { outputPath: out, message: `Markdown written to ${out}` };
|
|
109
148
|
}
|
|
110
149
|
|
|
111
150
|
module.exports = {
|
|
112
|
-
name:
|
|
113
|
-
description:
|
|
151
|
+
name: "markdown",
|
|
152
|
+
description: "Convert HTML artifacts to clean Markdown",
|
|
114
153
|
export: exportMarkdown,
|
|
115
154
|
htmlToMarkdown,
|
|
116
155
|
};
|
package/lib/exporters/pdf.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
3
|
// NOTE: PDF is the one export format that requires external tools (md-to-pdf
|
|
4
4
|
// for Markdown, puppeteer for HTML). These are fetched via npx on first run,
|
|
5
5
|
// which needs network access. All other mill export formats are zero-dep.
|
|
6
6
|
|
|
7
|
-
const path = require(
|
|
8
|
-
const { execFile } = require(
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const { execFile } = require("node:child_process");
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Export HTML or Markdown files to PDF.
|
|
@@ -34,7 +34,8 @@ function exec(cmd, args) {
|
|
|
34
34
|
|
|
35
35
|
function npxToolError(toolName, originalError) {
|
|
36
36
|
const msg = originalError.message || String(originalError);
|
|
37
|
-
const isNotFound =
|
|
37
|
+
const isNotFound =
|
|
38
|
+
/not found|ENOENT|ERR_MODULE_NOT_FOUND|Cannot find module/i.test(msg);
|
|
38
39
|
const isNetwork = /ENETUNREACH|ENOTFOUND|fetch failed|EAI_AGAIN/i.test(msg);
|
|
39
40
|
const isTimeout = /timed out|ETIMEDOUT/i.test(msg);
|
|
40
41
|
|
|
@@ -51,19 +52,18 @@ function npxToolError(toolName, originalError) {
|
|
|
51
52
|
` Or fall back to: mill export --format markdown`;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
|
-
return new Error(
|
|
55
|
+
return new Error(
|
|
56
|
+
`PDF export failed -- ${toolName} unavailable or broken.\n\n${hint}\n\nOriginal error: ${msg}`,
|
|
57
|
+
);
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
async function exportFromMarkdown(inputPath, outputPath) {
|
|
58
61
|
const out = deriveOutputPath(inputPath, outputPath);
|
|
59
62
|
// md-to-pdf reads from file, writes pdf alongside or to --dest
|
|
60
63
|
try {
|
|
61
|
-
await exec(
|
|
62
|
-
'--yes', 'md-to-pdf', inputPath,
|
|
63
|
-
'--dest', out,
|
|
64
|
-
]);
|
|
64
|
+
await exec("npx", ["--yes", "md-to-pdf", inputPath, "--dest", out]);
|
|
65
65
|
} catch (err) {
|
|
66
|
-
throw npxToolError(
|
|
66
|
+
throw npxToolError("md-to-pdf", err);
|
|
67
67
|
}
|
|
68
68
|
return { outputPath: out, message: `PDF written to ${out}` };
|
|
69
69
|
}
|
|
@@ -82,23 +82,23 @@ async function exportFromHtml(inputPath, outputPath) {
|
|
|
82
82
|
})();
|
|
83
83
|
`;
|
|
84
84
|
try {
|
|
85
|
-
await exec(
|
|
85
|
+
await exec("node", ["-e", script]);
|
|
86
86
|
} catch (err) {
|
|
87
|
-
throw npxToolError(
|
|
87
|
+
throw npxToolError("puppeteer", err);
|
|
88
88
|
}
|
|
89
89
|
return { outputPath: out, message: `PDF written to ${out}` };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
async function exportPdf(inputPath, outputPath) {
|
|
93
93
|
const ext = path.extname(inputPath).toLowerCase();
|
|
94
|
-
if (ext ===
|
|
94
|
+
if (ext === ".md" || ext === ".markdown") {
|
|
95
95
|
return exportFromMarkdown(inputPath, outputPath);
|
|
96
96
|
}
|
|
97
97
|
return exportFromHtml(inputPath, outputPath);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
module.exports = {
|
|
101
|
-
name:
|
|
102
|
-
description:
|
|
101
|
+
name: "pdf",
|
|
102
|
+
description: "Export HTML or Markdown to PDF",
|
|
103
103
|
export: exportPdf,
|
|
104
104
|
};
|
package/lib/formats/bibtex.js
CHANGED
|
@@ -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 = "bibtex";
|
|
10
|
+
export const extension = ".bib";
|
|
11
|
+
export const mimeType = "application/x-bibtex; charset=utf-8";
|
|
12
|
+
export const description =
|
|
13
|
+
"Claims as BibTeX bibliography entries (@misc per claim)";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Convert a compilation object to BibTeX.
|
|
@@ -20,36 +21,42 @@ export function convert(compilation) {
|
|
|
20
21
|
const claims = compilation.claims || [];
|
|
21
22
|
const meta = compilation.meta || {};
|
|
22
23
|
const year = new Date().getFullYear().toString();
|
|
23
|
-
const author = meta.sprint ? `wheat sprint: ${meta.sprint}` :
|
|
24
|
+
const author = meta.sprint ? `wheat sprint: ${meta.sprint}` : "wheat sprint";
|
|
24
25
|
|
|
25
26
|
if (claims.length === 0) {
|
|
26
27
|
return `% No claims in compilation\n`;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
const entries = claims.map(claim => claimToEntry(claim, author, year));
|
|
30
|
-
return entries.join(
|
|
30
|
+
const entries = claims.map((claim) => claimToEntry(claim, author, year));
|
|
31
|
+
return entries.join("\n\n") + "\n";
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
function claimToEntry(claim, author, year) {
|
|
34
|
-
const id = String(claim.id ||
|
|
35
|
-
const title = escapeBibtex(claim.content || claim.text ||
|
|
36
|
-
const type = claim.type ||
|
|
37
|
-
const evidence =
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
const id = String(claim.id || "unknown").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
36
|
+
const title = escapeBibtex(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 || "";
|
|
41
43
|
const tags = Array.isArray(claim.tags) ? claim.tags : [];
|
|
42
|
-
const confidence =
|
|
44
|
+
const confidence =
|
|
45
|
+
claim.confidence != null ? `, confidence: ${claim.confidence}` : "";
|
|
43
46
|
|
|
44
|
-
const noteParts =
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const noteParts =
|
|
48
|
+
[
|
|
49
|
+
type ? `type: ${type}` : "",
|
|
50
|
+
evidence ? `evidence: ${evidence}` : "",
|
|
51
|
+
status ? `status: ${status}` : "",
|
|
52
|
+
]
|
|
53
|
+
.filter(Boolean)
|
|
54
|
+
.join(", ") + confidence;
|
|
49
55
|
|
|
50
|
-
const keywordsLine =
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
const keywordsLine =
|
|
57
|
+
tags.length > 0
|
|
58
|
+
? `\n keywords = {${tags.map(escapeBibtex).join(", ")}},`
|
|
59
|
+
: "";
|
|
53
60
|
|
|
54
61
|
return [
|
|
55
62
|
`@misc{claim_${id},`,
|
|
@@ -58,19 +65,19 @@ function claimToEntry(claim, author, year) {
|
|
|
58
65
|
` year = {${year}},`,
|
|
59
66
|
` note = {${escapeBibtex(noteParts)}},` + keywordsLine,
|
|
60
67
|
`}`,
|
|
61
|
-
].join(
|
|
68
|
+
].join("\n");
|
|
62
69
|
}
|
|
63
70
|
|
|
64
71
|
function escapeBibtex(value) {
|
|
65
|
-
if (value == null) return
|
|
72
|
+
if (value == null) return "";
|
|
66
73
|
return String(value)
|
|
67
|
-
.replace(/\\/g,
|
|
68
|
-
.replace(/&/g,
|
|
69
|
-
.replace(/%/g,
|
|
70
|
-
.replace(/#/g,
|
|
71
|
-
.replace(/_/g,
|
|
72
|
-
.replace(/\{/g,
|
|
73
|
-
.replace(/\}/g,
|
|
74
|
-
.replace(/~/g,
|
|
75
|
-
.replace(/\^/g,
|
|
74
|
+
.replace(/\\/g, "\\textbackslash{}")
|
|
75
|
+
.replace(/&/g, "\\&")
|
|
76
|
+
.replace(/%/g, "\\%")
|
|
77
|
+
.replace(/#/g, "\\#")
|
|
78
|
+
.replace(/_/g, "\\_")
|
|
79
|
+
.replace(/\{/g, "\\{")
|
|
80
|
+
.replace(/\}/g, "\\}")
|
|
81
|
+
.replace(/~/g, "\\textasciitilde{}")
|
|
82
|
+
.replace(/\^/g, "\\textasciicircum{}");
|
|
76
83
|
}
|
package/lib/formats/changelog.js
CHANGED
|
@@ -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 = "changelog";
|
|
10
|
+
export const extension = ".md";
|
|
11
|
+
export const mimeType = "text/markdown; charset=utf-8";
|
|
12
|
+
export const description =
|
|
13
|
+
"Claims changelog grouped by status (active, resolved, conflicts)";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Convert a compilation object to changelog markdown.
|
|
@@ -20,83 +21,83 @@ export function convert(compilation) {
|
|
|
20
21
|
const claims = compilation.claims || [];
|
|
21
22
|
const conflicts = compilation.conflicts || [];
|
|
22
23
|
const meta = compilation.meta || {};
|
|
23
|
-
const sprintName = meta.sprint ||
|
|
24
|
+
const sprintName = meta.sprint || "unnamed";
|
|
24
25
|
|
|
25
26
|
const groups = {};
|
|
26
27
|
for (const claim of claims) {
|
|
27
|
-
const status = claim.status ||
|
|
28
|
+
const status = claim.status || "unknown";
|
|
28
29
|
if (!groups[status]) groups[status] = [];
|
|
29
30
|
groups[status].push(claim);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
const lines = [];
|
|
33
34
|
lines.push(`# Changelog — ${sprintName}`);
|
|
34
|
-
lines.push(
|
|
35
|
+
lines.push("");
|
|
35
36
|
|
|
36
37
|
// Active claims first
|
|
37
38
|
if (groups.active) {
|
|
38
39
|
lines.push(`## Active (${groups.active.length} claims)`);
|
|
39
|
-
lines.push(
|
|
40
|
+
lines.push("");
|
|
40
41
|
for (const claim of groups.active) {
|
|
41
42
|
lines.push(`- ${claim.id}: ${claimText(claim)}`);
|
|
42
43
|
}
|
|
43
|
-
lines.push(
|
|
44
|
+
lines.push("");
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
// Resolved claims
|
|
47
48
|
if (groups.resolved) {
|
|
48
49
|
lines.push(`## Resolved (${groups.resolved.length} claims)`);
|
|
49
|
-
lines.push(
|
|
50
|
+
lines.push("");
|
|
50
51
|
for (const claim of groups.resolved) {
|
|
51
52
|
lines.push(`- ${claim.id}: ${claimText(claim)}`);
|
|
52
53
|
}
|
|
53
|
-
lines.push(
|
|
54
|
+
lines.push("");
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
// All other statuses
|
|
57
|
-
const shown = new Set([
|
|
58
|
+
const shown = new Set(["active", "resolved"]);
|
|
58
59
|
const otherStatuses = Object.keys(groups)
|
|
59
|
-
.filter(s => !shown.has(s))
|
|
60
|
+
.filter((s) => !shown.has(s))
|
|
60
61
|
.sort();
|
|
61
62
|
|
|
62
63
|
for (const status of otherStatuses) {
|
|
63
64
|
const group = groups[status];
|
|
64
65
|
const label = status.charAt(0).toUpperCase() + status.slice(1);
|
|
65
66
|
lines.push(`## ${label} (${group.length} claims)`);
|
|
66
|
-
lines.push(
|
|
67
|
+
lines.push("");
|
|
67
68
|
for (const claim of group) {
|
|
68
69
|
lines.push(`- ${claim.id}: ${claimText(claim)}`);
|
|
69
70
|
}
|
|
70
|
-
lines.push(
|
|
71
|
+
lines.push("");
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
// Conflicts section
|
|
74
75
|
if (conflicts.length > 0) {
|
|
75
76
|
lines.push(`## Conflicts (${conflicts.length})`);
|
|
76
|
-
lines.push(
|
|
77
|
+
lines.push("");
|
|
77
78
|
for (const conflict of conflicts) {
|
|
78
79
|
const ids = Array.isArray(conflict.claim_ids)
|
|
79
|
-
? conflict.claim_ids.join(
|
|
80
|
-
:
|
|
81
|
-
const desc = conflict.description || conflict.reason ||
|
|
80
|
+
? conflict.claim_ids.join(" vs ")
|
|
81
|
+
: conflict.between || "unknown";
|
|
82
|
+
const desc = conflict.description || conflict.reason || "";
|
|
82
83
|
lines.push(`- ${ids}: ${desc}`);
|
|
83
84
|
}
|
|
84
|
-
lines.push(
|
|
85
|
+
lines.push("");
|
|
85
86
|
} else {
|
|
86
87
|
lines.push(`## Conflicts (0)`);
|
|
87
|
-
lines.push(
|
|
88
|
-
lines.push(
|
|
89
|
-
lines.push(
|
|
88
|
+
lines.push("");
|
|
89
|
+
lines.push("No conflicts.");
|
|
90
|
+
lines.push("");
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
return lines.join(
|
|
93
|
+
return lines.join("\n");
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
function claimText(claim) {
|
|
96
|
-
const text = claim.content || claim.text ||
|
|
97
|
+
const text = claim.content || claim.text || "";
|
|
97
98
|
// Truncate long claims for readability
|
|
98
99
|
if (text.length > 120) {
|
|
99
|
-
return text.slice(0, 117) +
|
|
100
|
+
return text.slice(0, 117) + "...";
|
|
100
101
|
}
|
|
101
102
|
return text;
|
|
102
103
|
}
|