@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.
Files changed (49) 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.mjs +83 -0
  10. package/lib/formats/{changelog.js → changelog.mjs} +27 -26
  11. package/lib/formats/confluence-adf.mjs +312 -0
  12. package/lib/formats/{csv.js → csv.mjs} +41 -37
  13. package/lib/formats/{dot.js → dot.mjs} +45 -34
  14. package/lib/formats/{evidence-matrix.js → evidence-matrix.mjs} +17 -16
  15. package/lib/formats/executive-summary.mjs +178 -0
  16. package/lib/formats/github-issues.mjs +96 -0
  17. package/lib/formats/{graphml.js → graphml.mjs} +45 -32
  18. package/lib/formats/html-report.mjs +228 -0
  19. package/lib/formats/{jira-csv.js → jira-csv.mjs} +30 -29
  20. package/lib/formats/{json-ld.js → json-ld.mjs} +6 -6
  21. package/lib/formats/{markdown.js → markdown.mjs} +53 -36
  22. package/lib/formats/{ndjson.js → ndjson.mjs} +6 -6
  23. package/lib/formats/{obsidian.js → obsidian.mjs} +43 -35
  24. package/lib/formats/{opml.js → opml.mjs} +38 -28
  25. package/lib/formats/ris.mjs +76 -0
  26. package/lib/formats/{rss.js → rss.mjs} +31 -28
  27. package/lib/formats/{sankey.js → sankey.mjs} +16 -15
  28. package/lib/formats/slide-deck.mjs +288 -0
  29. package/lib/formats/sql.mjs +120 -0
  30. package/lib/formats/{static-site.js → static-site.mjs} +64 -52
  31. package/lib/formats/{treemap.js → treemap.mjs} +16 -15
  32. package/lib/formats/typescript-defs.mjs +150 -0
  33. package/lib/formats/{yaml.js → yaml.mjs} +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 +6 -3
  42. package/lib/formats/bibtex.js +0 -76
  43. package/lib/formats/executive-summary.js +0 -130
  44. package/lib/formats/github-issues.js +0 -89
  45. package/lib/formats/html-report.js +0 -181
  46. package/lib/formats/ris.js +0 -70
  47. package/lib/formats/slide-deck.js +0 -200
  48. package/lib/formats/sql.js +0 -116
  49. package/lib/formats/typescript-defs.js +0 -147
@@ -7,10 +7,11 @@
7
7
  * Zero dependencies — node built-in only.
8
8
  */
9
9
 
10
- export const name = 'static-site';
11
- export const extension = '.json';
12
- export const mimeType = 'application/json; charset=utf-8';
13
- export const description = 'Hugo-compatible static site manifest (file tree as JSON)';
10
+ export const name = "static-site";
11
+ export const extension = ".json";
12
+ export const mimeType = "application/json; charset=utf-8";
13
+ export const description =
14
+ "Hugo-compatible static site manifest (file tree as JSON)";
14
15
 
15
16
  /**
16
17
  * Convert a compilation object to a static site manifest.
@@ -22,148 +23,159 @@ export function convert(compilation) {
22
23
  const meta = compilation.meta || {};
23
24
  const certificate = compilation.certificate || {};
24
25
 
25
- const sprintName = meta.sprint || 'sprint';
26
- const question = meta.question || '';
27
- const audience = meta.audience || '';
26
+ const sprintName = meta.sprint || "sprint";
27
+ const question = meta.question || "";
28
+ const audience = meta.audience || "";
28
29
 
29
30
  const files = {};
30
31
 
31
32
  // Hugo config
32
- files['config.yaml'] = buildConfig(sprintName, question);
33
+ files["config.yaml"] = buildConfig(sprintName, question);
33
34
 
34
35
  // Index page
35
- files['content/_index.md'] = buildIndex(sprintName, question, audience, claims);
36
+ files["content/_index.md"] = buildIndex(
37
+ sprintName,
38
+ question,
39
+ audience,
40
+ claims,
41
+ );
36
42
 
37
43
  // Per-type section pages
38
44
  const groups = {};
39
45
  for (const claim of claims) {
40
- const type = claim.type || 'other';
46
+ const type = claim.type || "other";
41
47
  if (!groups[type]) groups[type] = [];
42
48
  groups[type].push(claim);
43
49
  }
44
50
 
45
51
  for (const [type, group] of Object.entries(groups)) {
46
- files[`content/claims/${type}/_index.md`] = buildSectionIndex(type, group.length);
52
+ files[`content/claims/${type}/_index.md`] = buildSectionIndex(
53
+ type,
54
+ group.length,
55
+ );
47
56
 
48
57
  for (const claim of group) {
49
- const id = claim.id || 'unknown';
58
+ const id = claim.id || "unknown";
50
59
  files[`content/claims/${type}/${id}.md`] = buildClaimPage(claim);
51
60
  }
52
61
  }
53
62
 
54
63
  // Data file: full claims array
55
- files['data/claims.json'] = JSON.stringify(claims, null, 2);
64
+ files["data/claims.json"] = JSON.stringify(claims, null, 2);
56
65
 
57
66
  // Data file: certificate
58
67
  if (Object.keys(certificate).length > 0) {
59
- files['data/certificate.json'] = JSON.stringify(certificate, null, 2);
68
+ files["data/certificate.json"] = JSON.stringify(certificate, null, 2);
60
69
  }
61
70
 
62
71
  // Data file: meta
63
- files['data/meta.json'] = JSON.stringify(meta, null, 2);
72
+ files["data/meta.json"] = JSON.stringify(meta, null, 2);
64
73
 
65
74
  const manifest = {
66
- generator: 'mill',
75
+ generator: "mill",
67
76
  sprint: sprintName,
68
77
  claimCount: claims.length,
69
78
  files,
70
79
  };
71
80
 
72
- return JSON.stringify(manifest, null, 2) + '\n';
81
+ return JSON.stringify(manifest, null, 2) + "\n";
73
82
  }
74
83
 
75
84
  function buildConfig(sprintName, question) {
76
85
  const lines = [];
77
- lines.push(`baseURL: "https://your-site.example.com/${sanitizeSlug(sprintName)}/" # Update with your domain`);
86
+ lines.push(
87
+ `baseURL: "https://your-site.example.com/${sanitizeSlug(sprintName)}/" # Update with your domain`,
88
+ );
78
89
  lines.push(`title: "${escYaml(sprintName)}"`);
79
90
  lines.push(`theme: "mill-default"`);
80
91
  lines.push('languageCode: "en-us"');
81
- lines.push('');
82
- lines.push('params:');
92
+ lines.push("");
93
+ lines.push("params:");
83
94
  lines.push(` question: "${escYaml(question)}"`);
84
95
  lines.push(` generator: "mill"`);
85
- return lines.join('\n') + '\n';
96
+ return lines.join("\n") + "\n";
86
97
  }
87
98
 
88
99
  function buildIndex(sprintName, question, audience, claims) {
89
100
  const lines = [];
90
- lines.push('---');
101
+ lines.push("---");
91
102
  lines.push(`title: "${escYaml(sprintName)}"`);
92
103
  lines.push(`description: "${escYaml(question)}"`);
93
104
  lines.push('type: "index"');
94
- lines.push('---');
95
- lines.push('');
105
+ lines.push("---");
106
+ lines.push("");
96
107
  lines.push(`# ${sprintName}`);
97
- lines.push('');
108
+ lines.push("");
98
109
  if (question) lines.push(`**Question:** ${question}`);
99
110
  if (audience) lines.push(`**Audience:** ${audience}`);
100
- lines.push('');
111
+ lines.push("");
101
112
  lines.push(`**Total claims:** ${claims.length}`);
102
113
 
103
114
  // Type summary
104
115
  const typeCounts = {};
105
116
  for (const c of claims) {
106
- const t = c.type || 'other';
117
+ const t = c.type || "other";
107
118
  typeCounts[t] = (typeCounts[t] || 0) + 1;
108
119
  }
109
- lines.push('');
120
+ lines.push("");
110
121
  for (const [type, count] of Object.entries(typeCounts)) {
111
122
  lines.push(`- ${type}: ${count}`);
112
123
  }
113
124
 
114
- return lines.join('\n') + '\n';
125
+ return lines.join("\n") + "\n";
115
126
  }
116
127
 
117
128
  function buildSectionIndex(type, count) {
118
129
  const lines = [];
119
- lines.push('---');
130
+ lines.push("---");
120
131
  lines.push(`title: "${escYaml(type)}"`);
121
132
  lines.push(`type: "section"`);
122
- lines.push('---');
123
- lines.push('');
124
- lines.push(`${count} ${type} claim${count !== 1 ? 's' : ''}.`);
125
- return lines.join('\n') + '\n';
133
+ lines.push("---");
134
+ lines.push("");
135
+ lines.push(`${count} ${type} claim${count !== 1 ? "s" : ""}.`);
136
+ return lines.join("\n") + "\n";
126
137
  }
127
138
 
128
139
  function buildClaimPage(claim) {
129
- const id = claim.id || 'unknown';
130
- const type = claim.type || '';
131
- const content = claim.content || claim.text || '';
140
+ const id = claim.id || "unknown";
141
+ const type = claim.type || "";
142
+ const content = claim.content || claim.text || "";
132
143
  const evidence = getEvidence(claim);
133
- const status = claim.status || '';
144
+ const status = claim.status || "";
134
145
  const tags = Array.isArray(claim.tags) ? claim.tags : [];
135
- const confidence = claim.confidence != null ? claim.confidence : '';
146
+ const confidence = claim.confidence != null ? claim.confidence : "";
136
147
 
137
148
  const lines = [];
138
- lines.push('---');
149
+ lines.push("---");
139
150
  lines.push(`title: "${escYaml(id)}"`);
140
151
  lines.push(`type: "${escYaml(type)}"`);
141
152
  lines.push(`evidence: "${escYaml(evidence)}"`);
142
153
  lines.push(`status: "${escYaml(status)}"`);
143
- if (confidence !== '') lines.push(`confidence: ${confidence}`);
144
- if (tags.length > 0) lines.push(`tags: [${tags.map(t => `"${escYaml(t)}"`).join(', ')}]`);
145
- lines.push('---');
146
- lines.push('');
154
+ if (confidence !== "") lines.push(`confidence: ${confidence}`);
155
+ if (tags.length > 0)
156
+ lines.push(`tags: [${tags.map((t) => `"${escYaml(t)}"`).join(", ")}]`);
157
+ lines.push("---");
158
+ lines.push("");
147
159
  lines.push(content);
148
- return lines.join('\n') + '\n';
160
+ return lines.join("\n") + "\n";
149
161
  }
150
162
 
151
163
  function getEvidence(claim) {
152
- if (typeof claim.evidence === 'string') return claim.evidence;
153
- if (typeof claim.evidence === 'object' && claim.evidence !== null) {
154
- return claim.evidence.tier || claim.evidence_tier || '';
164
+ if (typeof claim.evidence === "string") return claim.evidence;
165
+ if (typeof claim.evidence === "object" && claim.evidence !== null) {
166
+ return claim.evidence.tier || claim.evidence_tier || "";
155
167
  }
156
- return claim.evidence_tier || '';
168
+ return claim.evidence_tier || "";
157
169
  }
158
170
 
159
171
  function escYaml(str) {
160
- if (str == null) return '';
172
+ if (str == null) return "";
161
173
  return String(str).replace(/"/g, '\\"');
162
174
  }
163
175
 
164
176
  function sanitizeSlug(str) {
165
177
  return String(str)
166
178
  .toLowerCase()
167
- .replace(/[^a-z0-9]+/g, '-')
168
- .replace(/^-+|-+$/g, '');
179
+ .replace(/[^a-z0-9]+/g, "-")
180
+ .replace(/^-+|-+$/g, "");
169
181
  }
@@ -6,10 +6,11 @@
6
6
  * Zero dependencies — node built-in only.
7
7
  */
8
8
 
9
- export const name = 'treemap';
10
- export const extension = '.json';
11
- export const mimeType = 'application/json; charset=utf-8';
12
- export const description = 'Claims as nested JSON for D3 treemap visualization (grouped by type)';
9
+ export const name = "treemap";
10
+ export const extension = ".json";
11
+ export const mimeType = "application/json; charset=utf-8";
12
+ export const description =
13
+ "Claims as nested JSON for D3 treemap visualization (grouped by type)";
13
14
 
14
15
  /**
15
16
  * Convert a compilation object to D3 treemap JSON.
@@ -18,12 +19,12 @@ export const description = 'Claims as nested JSON for D3 treemap visualization (
18
19
  */
19
20
  export function convert(compilation) {
20
21
  const claims = compilation.claims || [];
21
- const sprintName = compilation.meta?.sprint || 'sprint';
22
+ const sprintName = compilation.meta?.sprint || "sprint";
22
23
 
23
24
  // Group claims by type
24
25
  const groups = {};
25
26
  for (const claim of claims) {
26
- const type = claim.type || 'other';
27
+ const type = claim.type || "other";
27
28
  if (!groups[type]) groups[type] = [];
28
29
  groups[type].push(claim);
29
30
  }
@@ -32,17 +33,17 @@ export function convert(compilation) {
32
33
  name: sprintName,
33
34
  children: Object.entries(groups).map(([type, group]) => ({
34
35
  name: type,
35
- children: group.map(claim => {
36
+ children: group.map((claim) => {
36
37
  const node = {
37
- name: claim.id || '',
38
+ name: claim.id || "",
38
39
  value: claim.confidence != null ? claim.confidence : 1,
39
40
  evidence: getEvidence(claim),
40
41
  };
41
42
 
42
- const content = claim.content || claim.text || '';
43
+ const content = claim.content || claim.text || "";
43
44
  if (content) node.content = content;
44
45
 
45
- const status = claim.status || '';
46
+ const status = claim.status || "";
46
47
  if (status) node.status = status;
47
48
 
48
49
  const tags = Array.isArray(claim.tags) ? claim.tags : [];
@@ -53,13 +54,13 @@ export function convert(compilation) {
53
54
  })),
54
55
  };
55
56
 
56
- return JSON.stringify(tree, null, 2) + '\n';
57
+ return JSON.stringify(tree, null, 2) + "\n";
57
58
  }
58
59
 
59
60
  function getEvidence(claim) {
60
- if (typeof claim.evidence === 'string') return claim.evidence;
61
- if (typeof claim.evidence === 'object' && claim.evidence !== null) {
62
- return claim.evidence.tier || claim.evidence_tier || '';
61
+ if (typeof claim.evidence === "string") return claim.evidence;
62
+ if (typeof claim.evidence === "object" && claim.evidence !== null) {
63
+ return claim.evidence.tier || claim.evidence_tier || "";
63
64
  }
64
- return claim.evidence_tier || '';
65
+ return claim.evidence_tier || "";
65
66
  }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * mill format: typescript-defs
3
+ *
4
+ * Converts compilation.json to TypeScript .d.ts definitions.
5
+ * Derives union types from actual claim data values.
6
+ * Zero dependencies — node built-in only.
7
+ */
8
+
9
+ export const name = "typescript-defs";
10
+ export const extension = ".d.ts";
11
+ export const mimeType = "text/plain; charset=utf-8";
12
+ export const description =
13
+ "TypeScript type definitions derived from compilation data";
14
+
15
+ /**
16
+ * Convert a compilation object to TypeScript .d.ts definitions.
17
+ * @param {object} compilation - The compilation.json content
18
+ * @returns {string} TypeScript definition output
19
+ */
20
+ export function convert(compilation) {
21
+ const claims = compilation.claims || [];
22
+ const meta = compilation.meta || {};
23
+ const conflicts = compilation.conflicts || [];
24
+ const certificate = compilation.certificate || {};
25
+
26
+ const claimTypes = uniqueSorted(claims.map((c) => c.type).filter(Boolean));
27
+ const evidenceTiers = uniqueSorted(
28
+ claims
29
+ .map((c) => {
30
+ if (typeof c.evidence === "string") return c.evidence;
31
+ return c.evidence?.tier ?? c.evidence_tier ?? null;
32
+ })
33
+ .filter(Boolean),
34
+ );
35
+ const statuses = uniqueSorted(claims.map((c) => c.status).filter(Boolean));
36
+
37
+ const lines = [];
38
+
39
+ lines.push("// Auto-generated by mill typescript-defs format");
40
+ lines.push("// Derived from compilation data — do not edit manually");
41
+ lines.push("");
42
+
43
+ // ClaimType union
44
+ lines.push(`export type ClaimType = ${unionType(claimTypes)};`);
45
+ lines.push("");
46
+
47
+ // EvidenceTier union
48
+ lines.push(`export type EvidenceTier = ${unionType(evidenceTiers)};`);
49
+ lines.push("");
50
+
51
+ // ClaimStatus union
52
+ lines.push(`export type ClaimStatus = ${unionType(statuses)};`);
53
+ lines.push("");
54
+
55
+ // Evidence interface
56
+ lines.push("export interface Evidence {");
57
+ lines.push(" tier: EvidenceTier;");
58
+ lines.push(" source?: string;");
59
+ lines.push("}");
60
+ lines.push("");
61
+
62
+ // Source interface
63
+ lines.push("export interface ClaimSource {");
64
+ lines.push(" origin?: string;");
65
+ lines.push(" artifact?: string;");
66
+ lines.push("}");
67
+ lines.push("");
68
+
69
+ // Claim interface
70
+ lines.push("export interface Claim {");
71
+ lines.push(" id: string;");
72
+ lines.push(" type: ClaimType;");
73
+ lines.push(" content: string;");
74
+ lines.push(" evidence: EvidenceTier | Evidence;");
75
+ lines.push(" status: ClaimStatus;");
76
+ lines.push(" confidence?: number;");
77
+ lines.push(" source?: string | ClaimSource;");
78
+ lines.push(" tags?: string[];");
79
+ lines.push(" created?: string;");
80
+ lines.push("}");
81
+ lines.push("");
82
+
83
+ // Meta interface
84
+ lines.push("export interface Meta {");
85
+ lines.push(" sprint?: string;");
86
+ lines.push(" question?: string;");
87
+ lines.push(" audience?: string;");
88
+ for (const key of Object.keys(meta)) {
89
+ if (!["sprint", "question", "audience"].includes(key)) {
90
+ lines.push(` ${sanitizeKey(key)}?: ${inferTsType(meta[key])};`);
91
+ }
92
+ }
93
+ lines.push("}");
94
+ lines.push("");
95
+
96
+ // Conflict interface
97
+ lines.push("export interface Conflict {");
98
+ lines.push(" ids?: string[];");
99
+ lines.push(" description?: string;");
100
+ lines.push(" reason?: string;");
101
+ lines.push(" resolution?: string;");
102
+ lines.push("}");
103
+ lines.push("");
104
+
105
+ // Certificate interface
106
+ lines.push("export interface Certificate {");
107
+ lines.push(" compiled_at?: string;");
108
+ lines.push(" sha256?: string;");
109
+ lines.push(" claim_count?: number;");
110
+ for (const key of Object.keys(certificate)) {
111
+ if (!["compiled_at", "sha256", "claim_count"].includes(key)) {
112
+ lines.push(` ${sanitizeKey(key)}?: ${inferTsType(certificate[key])};`);
113
+ }
114
+ }
115
+ lines.push("}");
116
+ lines.push("");
117
+
118
+ // Compilation interface
119
+ lines.push("export interface Compilation {");
120
+ lines.push(" meta: Meta;");
121
+ lines.push(" claims: Claim[];");
122
+ lines.push(" conflicts: Conflict[];");
123
+ lines.push(" certificate: Certificate;");
124
+ lines.push("}");
125
+ lines.push("");
126
+
127
+ return lines.join("\n");
128
+ }
129
+
130
+ function uniqueSorted(arr) {
131
+ return [...new Set(arr)].sort();
132
+ }
133
+
134
+ function unionType(values) {
135
+ if (values.length === 0) return "string";
136
+ return values.map((v) => `'${v}'`).join(" | ");
137
+ }
138
+
139
+ function sanitizeKey(key) {
140
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `'${key}'`;
141
+ }
142
+
143
+ function inferTsType(value) {
144
+ if (value === null || value === undefined) return "unknown";
145
+ if (typeof value === "number") return "number";
146
+ if (typeof value === "boolean") return "boolean";
147
+ if (typeof value === "string") return "string";
148
+ if (Array.isArray(value)) return "unknown[]";
149
+ return "Record<string, unknown>";
150
+ }
@@ -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
  }