@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.
Files changed (41) hide show
  1. package/CODE_OF_CONDUCT.md +25 -0
  2. package/CONTRIBUTING.md +101 -0
  3. package/README.md +90 -42
  4. package/bin/mill.js +233 -67
  5. package/lib/exporters/csv.js +35 -30
  6. package/lib/exporters/json-ld.js +19 -13
  7. package/lib/exporters/markdown.js +83 -44
  8. package/lib/exporters/pdf.js +15 -15
  9. package/lib/formats/bibtex.js +41 -34
  10. package/lib/formats/changelog.js +27 -26
  11. package/lib/formats/confluence-adf.js +312 -0
  12. package/lib/formats/csv.js +41 -37
  13. package/lib/formats/dot.js +45 -34
  14. package/lib/formats/evidence-matrix.js +17 -16
  15. package/lib/formats/executive-summary.js +89 -41
  16. package/lib/formats/github-issues.js +40 -33
  17. package/lib/formats/graphml.js +45 -32
  18. package/lib/formats/html-report.js +110 -63
  19. package/lib/formats/jira-csv.js +30 -29
  20. package/lib/formats/json-ld.js +6 -6
  21. package/lib/formats/markdown.js +53 -36
  22. package/lib/formats/ndjson.js +6 -6
  23. package/lib/formats/obsidian.js +43 -35
  24. package/lib/formats/opml.js +38 -28
  25. package/lib/formats/ris.js +29 -23
  26. package/lib/formats/rss.js +31 -28
  27. package/lib/formats/sankey.js +16 -15
  28. package/lib/formats/slide-deck.js +145 -57
  29. package/lib/formats/sql.js +57 -53
  30. package/lib/formats/static-site.js +64 -52
  31. package/lib/formats/treemap.js +16 -15
  32. package/lib/formats/typescript-defs.js +79 -76
  33. package/lib/formats/yaml.js +58 -40
  34. package/lib/formats.js +16 -16
  35. package/lib/index.js +5 -5
  36. package/lib/json-ld-common.js +37 -31
  37. package/lib/publishers/clipboard.js +21 -19
  38. package/lib/publishers/static.js +27 -12
  39. package/lib/serve-mcp.js +158 -83
  40. package/lib/server.js +252 -142
  41. package/package.json +7 -3
@@ -7,10 +7,10 @@
7
7
  * Zero dependencies — node built-in only.
8
8
  */
9
9
 
10
- export const name = 'yaml';
11
- export const extension = '.yaml';
12
- export const mimeType = 'text/yaml; charset=utf-8';
13
- export const description = 'YAML serialization of compilation data';
10
+ export const name = "yaml";
11
+ export const extension = ".yaml";
12
+ export const mimeType = "text/yaml; charset=utf-8";
13
+ export const description = "YAML serialization of compilation data";
14
14
 
15
15
  /**
16
16
  * Convert a compilation object to YAML.
@@ -19,29 +19,29 @@ export const description = 'YAML serialization of compilation data';
19
19
  */
20
20
  export function convert(compilation) {
21
21
  const lines = [];
22
- lines.push('# Auto-generated by mill yaml format');
23
- lines.push('');
22
+ lines.push("# Auto-generated by mill yaml format");
23
+ lines.push("");
24
24
  lines.push(toYaml(compilation, 0));
25
- return lines.join('\n');
25
+ return lines.join("\n");
26
26
  }
27
27
 
28
28
  function toYaml(value, indent) {
29
29
  if (value === null || value === undefined) {
30
- return 'null';
30
+ return "null";
31
31
  }
32
- if (typeof value === 'boolean') {
33
- return value ? 'true' : 'false';
32
+ if (typeof value === "boolean") {
33
+ return value ? "true" : "false";
34
34
  }
35
- if (typeof value === 'number') {
35
+ if (typeof value === "number") {
36
36
  return String(value);
37
37
  }
38
- if (typeof value === 'string') {
38
+ if (typeof value === "string") {
39
39
  return yamlString(value, indent);
40
40
  }
41
41
  if (Array.isArray(value)) {
42
42
  return yamlArray(value, indent);
43
43
  }
44
- if (typeof value === 'object') {
44
+ if (typeof value === "object") {
45
45
  return yamlObject(value, indent);
46
46
  }
47
47
  return String(value);
@@ -49,10 +49,13 @@ function toYaml(value, indent) {
49
49
 
50
50
  function yamlString(str, indent) {
51
51
  // Use block scalar for multi-line strings
52
- if (str.includes('\n')) {
53
- const pad = ' '.repeat(indent + 2);
54
- const indented = str.split('\n').map(line => pad + line).join('\n');
55
- return '|\n' + indented;
52
+ if (str.includes("\n")) {
53
+ const pad = " ".repeat(indent + 2);
54
+ const indented = str
55
+ .split("\n")
56
+ .map((line) => pad + line)
57
+ .join("\n");
58
+ return "|\n" + indented;
56
59
  }
57
60
  // Quote if the string contains special YAML characters or could be misinterpreted
58
61
  if (needsQuoting(str)) {
@@ -62,39 +65,46 @@ function yamlString(str, indent) {
62
65
  }
63
66
 
64
67
  function needsQuoting(str) {
65
- if (str === '') return true;
66
- if (str === 'true' || str === 'false' || str === 'null' || str === 'yes' || str === 'no') return true;
68
+ if (str === "") return true;
69
+ if (
70
+ str === "true" ||
71
+ str === "false" ||
72
+ str === "null" ||
73
+ str === "yes" ||
74
+ str === "no"
75
+ )
76
+ return true;
67
77
  if (/^[0-9]/.test(str) && !isNaN(Number(str))) return true;
68
78
  if (/[:{}\[\],&*?|>!%#@`"']/.test(str)) return true;
69
79
  if (/^\s|\s$/.test(str)) return true;
70
- if (str.startsWith('- ') || str.startsWith('? ')) return true;
80
+ if (str.startsWith("- ") || str.startsWith("? ")) return true;
71
81
  return false;
72
82
  }
73
83
 
74
84
  function yamlArray(arr, indent) {
75
- if (arr.length === 0) return '[]';
76
- const pad = ' '.repeat(indent);
85
+ if (arr.length === 0) return "[]";
86
+ const pad = " ".repeat(indent);
77
87
  const items = [];
78
88
  for (const item of arr) {
79
- if (item !== null && typeof item === 'object' && !Array.isArray(item)) {
89
+ if (item !== null && typeof item === "object" && !Array.isArray(item)) {
80
90
  // Object items: first key on same line as -, rest indented
81
91
  const keys = Object.keys(item);
82
92
  if (keys.length === 0) {
83
- items.push(pad + '- {}');
93
+ items.push(pad + "- {}");
84
94
  } else {
85
95
  const firstKey = keys[0];
86
96
  const firstVal = toYaml(item[firstKey], indent + 4);
87
97
  const firstLine = isScalar(item[firstKey])
88
98
  ? `${pad}- ${firstKey}: ${firstVal}`
89
99
  : `${pad}- ${firstKey}:\n${indentBlock(firstVal, indent + 4)}`;
90
- const rest = keys.slice(1).map(key => {
100
+ const rest = keys.slice(1).map((key) => {
91
101
  const val = toYaml(item[key], indent + 4);
92
102
  if (isScalar(item[key])) {
93
103
  return `${pad} ${key}: ${val}`;
94
104
  }
95
105
  return `${pad} ${key}:\n${indentBlock(val, indent + 4)}`;
96
106
  });
97
- items.push([firstLine, ...rest].join('\n'));
107
+ items.push([firstLine, ...rest].join("\n"));
98
108
  }
99
109
  } else if (Array.isArray(item)) {
100
110
  const nested = toYaml(item, indent + 2);
@@ -103,13 +113,13 @@ function yamlArray(arr, indent) {
103
113
  items.push(`${pad}- ${toYaml(item, indent + 2)}`);
104
114
  }
105
115
  }
106
- return '\n' + items.join('\n');
116
+ return "\n" + items.join("\n");
107
117
  }
108
118
 
109
119
  function yamlObject(obj, indent) {
110
120
  const keys = Object.keys(obj);
111
- if (keys.length === 0) return '{}';
112
- const pad = ' '.repeat(indent);
121
+ if (keys.length === 0) return "{}";
122
+ const pad = " ".repeat(indent);
113
123
  const entries = [];
114
124
  for (const key of keys) {
115
125
  const val = obj[key];
@@ -118,27 +128,35 @@ function yamlObject(obj, indent) {
118
128
  entries.push(`${pad}${yamlKey}: ${toYaml(val, indent + 2)}`);
119
129
  } else {
120
130
  const nested = toYaml(val, indent + 2);
121
- if (nested.startsWith('\n')) {
131
+ if (nested.startsWith("\n")) {
122
132
  entries.push(`${pad}${yamlKey}:${nested}`);
123
133
  } else {
124
134
  entries.push(`${pad}${yamlKey}:\n${indentBlock(nested, indent + 2)}`);
125
135
  }
126
136
  }
127
137
  }
128
- return '\n' + entries.join('\n');
138
+ return "\n" + entries.join("\n");
129
139
  }
130
140
 
131
141
  function isScalar(value) {
132
- return value === null || value === undefined ||
133
- typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
142
+ return (
143
+ value === null ||
144
+ value === undefined ||
145
+ typeof value === "string" ||
146
+ typeof value === "number" ||
147
+ typeof value === "boolean"
148
+ );
134
149
  }
135
150
 
136
151
  function indentBlock(text, indent) {
137
- const pad = ' '.repeat(indent);
138
- return text.split('\n').map(line => {
139
- if (line.trim() === '') return '';
140
- // Only add padding if the line doesn't already have it
141
- if (line.startsWith(pad)) return line;
142
- return pad + line;
143
- }).join('\n');
152
+ const pad = " ".repeat(indent);
153
+ return text
154
+ .split("\n")
155
+ .map((line) => {
156
+ if (line.trim() === "") return "";
157
+ // Only add padding if the line doesn't already have it
158
+ if (line.startsWith(pad)) return line;
159
+ return pad + line;
160
+ })
161
+ .join("\n");
144
162
  }
package/lib/formats.js CHANGED
@@ -1,17 +1,17 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const pdf = require('./exporters/pdf.js');
4
- const csv = require('./exporters/csv.js');
5
- const markdown = require('./exporters/markdown.js');
6
- const jsonLd = require('./exporters/json-ld.js');
7
- const static_ = require('./publishers/static.js');
8
- const clipboard = require('./publishers/clipboard.js');
3
+ const pdf = require("./exporters/pdf.js");
4
+ const csv = require("./exporters/csv.js");
5
+ const markdown = require("./exporters/markdown.js");
6
+ const jsonLd = require("./exporters/json-ld.js");
7
+ const static_ = require("./publishers/static.js");
8
+ const clipboard = require("./publishers/clipboard.js");
9
9
 
10
10
  const EXPORTERS = {
11
11
  pdf,
12
12
  csv,
13
13
  markdown,
14
- 'json-ld': jsonLd,
14
+ "json-ld": jsonLd,
15
15
  };
16
16
 
17
17
  const PUBLISHERS = {
@@ -23,16 +23,16 @@ const PUBLISHERS = {
23
23
  * Detect the likely format of an input file by extension.
24
24
  */
25
25
  function detectFormat(filePath) {
26
- const ext = filePath.split('.').pop().toLowerCase();
26
+ const ext = filePath.split(".").pop().toLowerCase();
27
27
  const map = {
28
- html: 'html',
29
- htm: 'html',
30
- md: 'markdown',
31
- json: 'json',
32
- csv: 'csv',
33
- jsonld: 'json-ld',
28
+ html: "html",
29
+ htm: "html",
30
+ md: "markdown",
31
+ json: "json",
32
+ csv: "csv",
33
+ jsonld: "json-ld",
34
34
  };
35
- return map[ext] || 'unknown';
35
+ return map[ext] || "unknown";
36
36
  }
37
37
 
38
38
  function getExporter(name) {
package/lib/index.js CHANGED
@@ -1,10 +1,10 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const formats = require('./formats.js');
3
+ const formats = require("./formats.js");
4
4
 
5
- const name = 'mill';
6
- const version = require('../package.json').version;
7
- const description = 'Turn wheat sprint artifacts into shareable formats';
5
+ const name = "mill";
6
+ const version = require("../package.json").version;
7
+ const description = "Turn wheat sprint artifacts into shareable formats";
8
8
 
9
9
  module.exports = {
10
10
  name,
@@ -1,4 +1,4 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
3
  /**
4
4
  * json-ld-common.js — Shared JSON-LD vocabulary for mill
@@ -11,24 +11,25 @@
11
11
  */
12
12
 
13
13
  const CONTEXT = {
14
- '@vocab': 'https://schema.org/',
15
- wheat: 'https://grainulation.com/ns/wheat#',
16
- claim: 'wheat:Claim',
17
- confidence: 'wheat:confidence',
18
- evidenceTier: 'wheat:evidenceTier',
19
- claimType: 'wheat:claimType',
20
- sprintId: 'wheat:sprintId',
14
+ "@vocab": "https://schema.org/",
15
+ wheat: "https://grainulation.com/ns/wheat#",
16
+ claim: "wheat:Claim",
17
+ confidence: "wheat:confidence",
18
+ evidenceTier: "wheat:evidenceTier",
19
+ claimType: "wheat:claimType",
20
+ sprintId: "wheat:sprintId",
21
21
  };
22
22
 
23
23
  function claimToJsonLd(claim) {
24
- const body = claim.content || claim.text || '';
25
- const evidenceTier = typeof claim.evidence === 'string'
26
- ? claim.evidence
27
- : (claim.evidence?.tier ?? claim.evidence_tier ?? null);
24
+ const body = claim.content || claim.text || "";
25
+ const evidenceTier =
26
+ typeof claim.evidence === "string"
27
+ ? claim.evidence
28
+ : (claim.evidence?.tier ?? claim.evidence_tier ?? null);
28
29
 
29
30
  return {
30
- '@type': 'claim',
31
- '@id': `wheat:claim/${claim.id}`,
31
+ "@type": "claim",
32
+ "@id": `wheat:claim/${claim.id}`,
32
33
  identifier: claim.id,
33
34
  claimType: claim.type,
34
35
  text: body,
@@ -36,36 +37,41 @@ function claimToJsonLd(claim) {
36
37
  confidence: claim.confidence ?? null,
37
38
  evidenceTier,
38
39
  dateCreated: claim.created || claim.timestamp || null,
39
- ...(claim.tags?.length ? { keywords: claim.tags.join(', ') } : {}),
40
+ ...(claim.tags?.length ? { keywords: claim.tags.join(", ") } : {}),
40
41
  ...(claim.status ? { status: claim.status } : {}),
41
42
  };
42
43
  }
43
44
 
44
45
  function buildReport(meta, claims, certificate) {
45
46
  return {
46
- '@context': CONTEXT,
47
- '@type': 'Report',
48
- '@id': `wheat:sprint/${meta.sprint || 'unknown'}`,
49
- name: meta.sprint || meta.question || 'Wheat Sprint Report',
50
- description: meta.question || '',
51
- dateCreated: (certificate && certificate.compiled_at) || new Date().toISOString(),
52
- ...(meta.audience ? { audience: { '@type': 'Audience', name: meta.audience } } : {}),
47
+ "@context": CONTEXT,
48
+ "@type": "Report",
49
+ "@id": `wheat:sprint/${meta.sprint || "unknown"}`,
50
+ name: meta.sprint || meta.question || "Wheat Sprint Report",
51
+ description: meta.question || "",
52
+ dateCreated:
53
+ (certificate && certificate.compiled_at) || new Date().toISOString(),
54
+ ...(meta.audience
55
+ ? { audience: { "@type": "Audience", name: meta.audience } }
56
+ : {}),
53
57
  hasPart: {
54
- '@type': 'ItemList',
58
+ "@type": "ItemList",
55
59
  numberOfItems: claims.length,
56
60
  itemListElement: claims.map((claim, i) => ({
57
- '@type': 'ListItem',
61
+ "@type": "ListItem",
58
62
  position: i + 1,
59
63
  item: claimToJsonLd(claim),
60
64
  })),
61
65
  },
62
- ...((certificate && certificate.sha256) ? {
63
- identifier: {
64
- '@type': 'PropertyValue',
65
- name: 'certificate-sha256',
66
- value: certificate.sha256,
67
- },
68
- } : {}),
66
+ ...(certificate && certificate.sha256
67
+ ? {
68
+ identifier: {
69
+ "@type": "PropertyValue",
70
+ name: "certificate-sha256",
71
+ value: certificate.sha256,
72
+ },
73
+ }
74
+ : {}),
69
75
  };
70
76
  }
71
77
 
@@ -1,8 +1,8 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const fs = require('node:fs');
4
- const path = require('node:path');
5
- const { execFile } = require('node:child_process');
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { execFile } = require("node:child_process");
6
6
 
7
7
  /**
8
8
  * Copy formatted output to system clipboard.
@@ -11,12 +11,12 @@ const { execFile } = require('node:child_process');
11
11
 
12
12
  function getClipboardCommand() {
13
13
  switch (process.platform) {
14
- case 'darwin':
15
- return { cmd: 'pbcopy', args: [] };
16
- case 'linux':
17
- return { cmd: 'xclip', args: ['-selection', 'clipboard'] };
18
- case 'win32':
19
- return { cmd: 'clip', args: [] };
14
+ case "darwin":
15
+ return { cmd: "pbcopy", args: [] };
16
+ case "linux":
17
+ return { cmd: "xclip", args: ["-selection", "clipboard"] };
18
+ case "win32":
19
+ return { cmd: "clip", args: [] };
20
20
  default:
21
21
  return null;
22
22
  }
@@ -46,25 +46,27 @@ async function publishClipboard(inputPath) {
46
46
  let content;
47
47
  if (stat.isDirectory()) {
48
48
  // If directory, list the files
49
- const files = fs.readdirSync(inputPath).filter((f) => !f.startsWith('.'));
50
- content = files.map((f) => {
51
- const full = path.join(inputPath, f);
52
- return fs.readFileSync(full, 'utf-8');
53
- }).join('\n\n---\n\n');
49
+ const files = fs.readdirSync(inputPath).filter((f) => !f.startsWith("."));
50
+ content = files
51
+ .map((f) => {
52
+ const full = path.join(inputPath, f);
53
+ return fs.readFileSync(full, "utf-8");
54
+ })
55
+ .join("\n\n---\n\n");
54
56
  } else {
55
- content = fs.readFileSync(inputPath, 'utf-8');
57
+ content = fs.readFileSync(inputPath, "utf-8");
56
58
  }
57
59
 
58
60
  await copyToClipboard(content);
59
61
 
60
- const size = Buffer.byteLength(content, 'utf-8');
62
+ const size = Buffer.byteLength(content, "utf-8");
61
63
  return {
62
64
  message: `Copied to clipboard (${size} bytes)`,
63
65
  };
64
66
  }
65
67
 
66
68
  module.exports = {
67
- name: 'clipboard',
68
- description: 'Copy formatted output to system clipboard',
69
+ name: "clipboard",
70
+ description: "Copy formatted output to system clipboard",
69
71
  publish: publishClipboard,
70
72
  };
@@ -1,9 +1,16 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const fs = require('node:fs');
4
- const path = require('node:path');
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
5
 
6
- function esc(s) { return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
6
+ function esc(s) {
7
+ return String(s || "")
8
+ .replace(/&/g, "&amp;")
9
+ .replace(/</g, "&lt;")
10
+ .replace(/>/g, "&gt;")
11
+ .replace(/"/g, "&quot;")
12
+ .replace(/'/g, "&#39;");
13
+ }
7
14
 
8
15
  /**
9
16
  * Generate a static site from sprint output directory.
@@ -93,7 +100,7 @@ function scanDir(dir, base) {
93
100
  const stat = fs.statSync(full);
94
101
  const real = fs.realpathSync(full);
95
102
  if (!real.startsWith(fs.realpathSync(base))) continue;
96
- if (stat.isFile() && !name.startsWith('.')) {
103
+ if (stat.isFile() && !name.startsWith(".")) {
97
104
  const rel = path.relative(base, full);
98
105
  entries.push({
99
106
  name,
@@ -102,7 +109,11 @@ function scanDir(dir, base) {
102
109
  ext: path.extname(name).slice(1).toLowerCase(),
103
110
  modified: stat.mtime,
104
111
  });
105
- } else if (stat.isDirectory() && !name.startsWith('.') && name !== '_site') {
112
+ } else if (
113
+ stat.isDirectory() &&
114
+ !name.startsWith(".") &&
115
+ name !== "_site"
116
+ ) {
106
117
  entries.push(...scanDir(full, base));
107
118
  }
108
119
  }
@@ -110,7 +121,7 @@ function scanDir(dir, base) {
110
121
  }
111
122
 
112
123
  async function publishStatic(inputDir, outputDir) {
113
- const outDir = outputDir || path.join(inputDir, '_site');
124
+ const outDir = outputDir || path.join(inputDir, "_site");
114
125
  if (!fs.existsSync(outDir)) {
115
126
  fs.mkdirSync(outDir, { recursive: true });
116
127
  }
@@ -130,14 +141,18 @@ async function publishStatic(inputDir, outputDir) {
130
141
 
131
142
  // Build index
132
143
  const sprintName = path.basename(path.resolve(inputDir));
133
- const cards = entries.map((e) => `
144
+ const cards = entries
145
+ .map(
146
+ (e) => `
134
147
  <div class="card">
135
148
  <a href="${esc(e.path)}">${esc(e.name)}</a>
136
149
  <div class="meta">${e.ext.toUpperCase()} &middot; ${formatBytes(e.size)}</div>
137
- </div>`).join('\n');
150
+ </div>`,
151
+ )
152
+ .join("\n");
138
153
 
139
154
  const html = TEMPLATE(`Sprint: ${sprintName}`, cards);
140
- fs.writeFileSync(path.join(outDir, 'index.html'), html, 'utf-8');
155
+ fs.writeFileSync(path.join(outDir, "index.html"), html, "utf-8");
141
156
 
142
157
  return {
143
158
  outputPath: outDir,
@@ -146,7 +161,7 @@ async function publishStatic(inputDir, outputDir) {
146
161
  }
147
162
 
148
163
  module.exports = {
149
- name: 'static',
150
- description: 'Generate a static site from sprint outputs',
164
+ name: "static",
165
+ description: "Generate a static site from sprint outputs",
151
166
  publish: publishStatic,
152
167
  };