@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
package/bin/mill.js
CHANGED
|
@@ -1,34 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
"use strict";
|
|
4
4
|
|
|
5
|
-
const path = require(
|
|
6
|
-
const { parseArgs } = require(
|
|
7
|
-
const { fork } = require(
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { parseArgs } = require("node:util");
|
|
7
|
+
const { fork } = require("node:child_process");
|
|
8
8
|
|
|
9
|
-
const LIB_DIR = path.join(__dirname,
|
|
9
|
+
const LIB_DIR = path.join(__dirname, "..", "lib");
|
|
10
10
|
|
|
11
11
|
// --version / -v: print version and exit
|
|
12
|
-
if (process.argv.includes(
|
|
13
|
-
const pkg = require(path.join(__dirname,
|
|
12
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
13
|
+
const pkg = require(path.join(__dirname, "..", "package.json"));
|
|
14
14
|
console.log(pkg.version);
|
|
15
15
|
process.exit(0);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
const verbose = process.argv.includes(
|
|
18
|
+
const verbose = process.argv.includes("--verbose");
|
|
19
19
|
function vlog(...a) {
|
|
20
20
|
if (!verbose) return;
|
|
21
21
|
const ts = new Date().toISOString();
|
|
22
|
-
process.stderr.write(`[${ts}] mill: ${a.join(
|
|
22
|
+
process.stderr.write(`[${ts}] mill: ${a.join(" ")}\n`);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const COMMANDS = {
|
|
26
|
-
export: {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
export: {
|
|
27
|
+
description: "Export artifacts to a target format",
|
|
28
|
+
handler: runExport,
|
|
29
|
+
},
|
|
30
|
+
publish: {
|
|
31
|
+
description: "Publish sprint outputs to a destination",
|
|
32
|
+
handler: runPublish,
|
|
33
|
+
},
|
|
34
|
+
convert: {
|
|
35
|
+
description: "Convert between artifact formats",
|
|
36
|
+
handler: runConvert,
|
|
37
|
+
},
|
|
38
|
+
formats: {
|
|
39
|
+
description: "List available export formats",
|
|
40
|
+
handler: runFormats,
|
|
41
|
+
},
|
|
42
|
+
"ci-artifacts": {
|
|
43
|
+
description: "Generate CI artifacts from compilation",
|
|
44
|
+
handler: runCiArtifacts,
|
|
45
|
+
},
|
|
46
|
+
serve: { description: "Start the export workbench UI", handler: runServe },
|
|
47
|
+
"serve-mcp": { description: "Start the MCP server on stdio", handler: null },
|
|
32
48
|
};
|
|
33
49
|
|
|
34
50
|
const USAGE = `
|
|
@@ -41,6 +57,7 @@ Usage:
|
|
|
41
57
|
mill publish --target <dest> <dir> Publish sprint outputs
|
|
42
58
|
mill convert --from <fmt> --to <fmt> <file> Convert between formats
|
|
43
59
|
mill formats List available formats
|
|
60
|
+
mill ci-artifacts <file> -o <dir> Generate CI artifacts (report, summary, slides)
|
|
44
61
|
|
|
45
62
|
Export formats:
|
|
46
63
|
pdf HTML or Markdown to PDF (via npx md-to-pdf)
|
|
@@ -59,14 +76,15 @@ Examples:
|
|
|
59
76
|
npx @grainulation/mill export --format json-ld claims.json -o claims.jsonld
|
|
60
77
|
npx @grainulation/mill publish --target static output/
|
|
61
78
|
npx @grainulation/mill convert --from html --to markdown output/brief.html
|
|
79
|
+
npx @grainulation/mill ci-artifacts compilation.json -o ./artifacts
|
|
62
80
|
`.trim();
|
|
63
81
|
|
|
64
82
|
function main() {
|
|
65
83
|
const args = process.argv.slice(2);
|
|
66
84
|
|
|
67
|
-
vlog(
|
|
85
|
+
vlog("startup", `command=${args[0] || "(none)"}`, `cwd=${process.cwd()}`);
|
|
68
86
|
|
|
69
|
-
if (args.length === 0 || args[0] ===
|
|
87
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
70
88
|
console.log(USAGE);
|
|
71
89
|
process.exit(0);
|
|
72
90
|
}
|
|
@@ -74,20 +92,20 @@ function main() {
|
|
|
74
92
|
const command = args[0];
|
|
75
93
|
const handler = COMMANDS[command];
|
|
76
94
|
|
|
77
|
-
if (command ===
|
|
95
|
+
if (command === "help") {
|
|
78
96
|
console.log(USAGE);
|
|
79
97
|
process.exit(0);
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
// serve command forks the ESM server module
|
|
83
|
-
if (command ===
|
|
101
|
+
if (command === "serve") {
|
|
84
102
|
runServe(args.slice(1));
|
|
85
103
|
return;
|
|
86
104
|
}
|
|
87
105
|
|
|
88
106
|
// serve-mcp command starts the MCP server on stdio
|
|
89
|
-
if (command ===
|
|
90
|
-
const serveMcp = require(
|
|
107
|
+
if (command === "serve-mcp") {
|
|
108
|
+
const serveMcp = require("../lib/serve-mcp.js");
|
|
91
109
|
serveMcp.run(process.cwd());
|
|
92
110
|
return;
|
|
93
111
|
}
|
|
@@ -107,44 +125,46 @@ async function runExport(args) {
|
|
|
107
125
|
({ values, positionals } = parseArgs({
|
|
108
126
|
args,
|
|
109
127
|
options: {
|
|
110
|
-
format: { type:
|
|
111
|
-
output: { type:
|
|
112
|
-
json: { type:
|
|
128
|
+
format: { type: "string", short: "f" },
|
|
129
|
+
output: { type: "string", short: "o" },
|
|
130
|
+
json: { type: "boolean", default: false },
|
|
113
131
|
},
|
|
114
132
|
allowPositionals: true,
|
|
115
133
|
}));
|
|
116
134
|
} catch (err) {
|
|
117
|
-
if (err.code ===
|
|
118
|
-
const flag = err.message.match(/option "([^"]+)"/)?.[1] ||
|
|
119
|
-
console.error(
|
|
135
|
+
if (err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
|
|
136
|
+
const flag = err.message.match(/option "([^"]+)"/)?.[1] || "unknown";
|
|
137
|
+
console.error(
|
|
138
|
+
`mill: unknown option: ${flag}. Run "mill export --help" for usage.`,
|
|
139
|
+
);
|
|
120
140
|
process.exit(1);
|
|
121
141
|
}
|
|
122
142
|
throw err;
|
|
123
143
|
}
|
|
124
144
|
|
|
125
145
|
if (!values.format) {
|
|
126
|
-
console.error(
|
|
146
|
+
console.error(
|
|
147
|
+
"mill: missing --format. Options: pdf, csv, markdown, json-ld",
|
|
148
|
+
);
|
|
127
149
|
process.exit(1);
|
|
128
150
|
}
|
|
129
151
|
|
|
130
152
|
const inputFile = positionals[0];
|
|
131
153
|
if (!inputFile) {
|
|
132
|
-
console.error(
|
|
154
|
+
console.error("mill: missing input file.");
|
|
133
155
|
process.exit(1);
|
|
134
156
|
}
|
|
135
157
|
|
|
136
158
|
const inputPath = path.resolve(inputFile);
|
|
137
159
|
const format = values.format;
|
|
138
|
-
const outputPath = values.output
|
|
139
|
-
? path.resolve(values.output)
|
|
140
|
-
: null;
|
|
160
|
+
const outputPath = values.output ? path.resolve(values.output) : null;
|
|
141
161
|
|
|
142
|
-
const formats = require(
|
|
162
|
+
const formats = require("../lib/formats.js");
|
|
143
163
|
const exporter = formats.getExporter(format);
|
|
144
164
|
|
|
145
165
|
if (!exporter) {
|
|
146
166
|
console.error(`mill: unknown format: ${format}`);
|
|
147
|
-
console.error(`Available: ${formats.listExportFormats().join(
|
|
167
|
+
console.error(`Available: ${formats.listExportFormats().join(", ")}`);
|
|
148
168
|
process.exit(1);
|
|
149
169
|
}
|
|
150
170
|
|
|
@@ -171,29 +191,31 @@ async function runPublish(args) {
|
|
|
171
191
|
({ values, positionals } = parseArgs({
|
|
172
192
|
args,
|
|
173
193
|
options: {
|
|
174
|
-
target: { type:
|
|
175
|
-
output: { type:
|
|
176
|
-
json: { type:
|
|
194
|
+
target: { type: "string", short: "t" },
|
|
195
|
+
output: { type: "string", short: "o" },
|
|
196
|
+
json: { type: "boolean", default: false },
|
|
177
197
|
},
|
|
178
198
|
allowPositionals: true,
|
|
179
199
|
}));
|
|
180
200
|
} catch (err) {
|
|
181
|
-
if (err.code ===
|
|
182
|
-
const flag = err.message.match(/option "([^"]+)"/)?.[1] ||
|
|
183
|
-
console.error(
|
|
201
|
+
if (err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
|
|
202
|
+
const flag = err.message.match(/option "([^"]+)"/)?.[1] || "unknown";
|
|
203
|
+
console.error(
|
|
204
|
+
`mill: unknown option: ${flag}. Run "mill publish --help" for usage.`,
|
|
205
|
+
);
|
|
184
206
|
process.exit(1);
|
|
185
207
|
}
|
|
186
208
|
throw err;
|
|
187
209
|
}
|
|
188
210
|
|
|
189
211
|
if (!values.target) {
|
|
190
|
-
console.error(
|
|
212
|
+
console.error("mill: missing --target. Options: static, clipboard");
|
|
191
213
|
process.exit(1);
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
const inputDir = positionals[0];
|
|
195
217
|
if (!inputDir) {
|
|
196
|
-
console.error(
|
|
218
|
+
console.error("mill: missing input directory.");
|
|
197
219
|
process.exit(1);
|
|
198
220
|
}
|
|
199
221
|
|
|
@@ -201,12 +223,12 @@ async function runPublish(args) {
|
|
|
201
223
|
const target = values.target;
|
|
202
224
|
const outputPath = values.output ? path.resolve(values.output) : null;
|
|
203
225
|
|
|
204
|
-
const formats = require(
|
|
226
|
+
const formats = require("../lib/formats.js");
|
|
205
227
|
const publisher = formats.getPublisher(target);
|
|
206
228
|
|
|
207
229
|
if (!publisher) {
|
|
208
230
|
console.error(`mill: unknown target: ${target}`);
|
|
209
|
-
console.error(`Available: ${formats.listPublishTargets().join(
|
|
231
|
+
console.error(`Available: ${formats.listPublishTargets().join(", ")}`);
|
|
210
232
|
process.exit(1);
|
|
211
233
|
}
|
|
212
234
|
|
|
@@ -233,30 +255,32 @@ async function runConvert(args) {
|
|
|
233
255
|
({ values, positionals } = parseArgs({
|
|
234
256
|
args,
|
|
235
257
|
options: {
|
|
236
|
-
from: { type:
|
|
237
|
-
to: { type:
|
|
238
|
-
output: { type:
|
|
239
|
-
json: { type:
|
|
258
|
+
from: { type: "string" },
|
|
259
|
+
to: { type: "string" },
|
|
260
|
+
output: { type: "string", short: "o" },
|
|
261
|
+
json: { type: "boolean", default: false },
|
|
240
262
|
},
|
|
241
263
|
allowPositionals: true,
|
|
242
264
|
}));
|
|
243
265
|
} catch (err) {
|
|
244
|
-
if (err.code ===
|
|
245
|
-
const flag = err.message.match(/option "([^"]+)"/)?.[1] ||
|
|
246
|
-
console.error(
|
|
266
|
+
if (err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
|
|
267
|
+
const flag = err.message.match(/option "([^"]+)"/)?.[1] || "unknown";
|
|
268
|
+
console.error(
|
|
269
|
+
`mill: unknown option: ${flag}. Run "mill convert --help" for usage.`,
|
|
270
|
+
);
|
|
247
271
|
process.exit(1);
|
|
248
272
|
}
|
|
249
273
|
throw err;
|
|
250
274
|
}
|
|
251
275
|
|
|
252
276
|
if (!values.from || !values.to) {
|
|
253
|
-
console.error(
|
|
277
|
+
console.error("mill: missing --from and/or --to format.");
|
|
254
278
|
process.exit(1);
|
|
255
279
|
}
|
|
256
280
|
|
|
257
281
|
const inputFile = positionals[0];
|
|
258
282
|
if (!inputFile) {
|
|
259
|
-
console.error(
|
|
283
|
+
console.error("mill: missing input file.");
|
|
260
284
|
process.exit(1);
|
|
261
285
|
}
|
|
262
286
|
|
|
@@ -264,7 +288,7 @@ async function runConvert(args) {
|
|
|
264
288
|
const outputPath = values.output ? path.resolve(values.output) : null;
|
|
265
289
|
|
|
266
290
|
// Convert is sugar: detect source, export to target
|
|
267
|
-
const formats = require(
|
|
291
|
+
const formats = require("../lib/formats.js");
|
|
268
292
|
const exporter = formats.getExporter(values.to);
|
|
269
293
|
|
|
270
294
|
if (!exporter) {
|
|
@@ -290,31 +314,173 @@ async function runConvert(args) {
|
|
|
290
314
|
}
|
|
291
315
|
|
|
292
316
|
function runFormats(args) {
|
|
293
|
-
const jsonMode = (args || []).includes(
|
|
294
|
-
const formats = require(
|
|
317
|
+
const jsonMode = (args || []).includes("--json");
|
|
318
|
+
const formats = require("../lib/formats.js");
|
|
295
319
|
if (jsonMode) {
|
|
296
|
-
console.log(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
320
|
+
console.log(
|
|
321
|
+
JSON.stringify({
|
|
322
|
+
export_formats: formats.listExportFormats(),
|
|
323
|
+
publish_targets: formats.listPublishTargets(),
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
300
326
|
return;
|
|
301
327
|
}
|
|
302
|
-
console.log(
|
|
328
|
+
console.log("Export formats:");
|
|
303
329
|
for (const f of formats.listExportFormats()) {
|
|
304
330
|
console.log(` ${f}`);
|
|
305
331
|
}
|
|
306
|
-
console.log(
|
|
332
|
+
console.log("\nPublish targets:");
|
|
307
333
|
for (const t of formats.listPublishTargets()) {
|
|
308
334
|
console.log(` ${t}`);
|
|
309
335
|
}
|
|
310
336
|
}
|
|
311
337
|
|
|
338
|
+
async function runCiArtifacts(args) {
|
|
339
|
+
const fs = require("node:fs");
|
|
340
|
+
|
|
341
|
+
let values, positionals;
|
|
342
|
+
try {
|
|
343
|
+
({ values, positionals } = parseArgs({
|
|
344
|
+
args,
|
|
345
|
+
options: {
|
|
346
|
+
output: { type: "string", short: "o" },
|
|
347
|
+
formats: { type: "string", short: "f" },
|
|
348
|
+
summary: { type: "boolean", default: false },
|
|
349
|
+
json: { type: "boolean", default: false },
|
|
350
|
+
},
|
|
351
|
+
allowPositionals: true,
|
|
352
|
+
}));
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION") {
|
|
355
|
+
const flag = err.message.match(/option "([^"]+)"/)?.[1] || "unknown";
|
|
356
|
+
console.error(
|
|
357
|
+
`mill: unknown option: ${flag}. Run "mill ci-artifacts --help" for usage.`,
|
|
358
|
+
);
|
|
359
|
+
process.exit(1);
|
|
360
|
+
}
|
|
361
|
+
throw err;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const inputFile = positionals[0];
|
|
365
|
+
if (!inputFile) {
|
|
366
|
+
console.error(
|
|
367
|
+
"mill: missing input file (compilation.json or claims.json).",
|
|
368
|
+
);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const inputPath = path.resolve(inputFile);
|
|
373
|
+
const outputDir = values.output
|
|
374
|
+
? path.resolve(values.output)
|
|
375
|
+
: path.resolve("artifacts");
|
|
376
|
+
|
|
377
|
+
// Default CI formats: the three HTML formats plus markdown for step summary
|
|
378
|
+
const defaultFormats = ["html-report", "executive-summary", "slide-deck"];
|
|
379
|
+
const requestedFormats = values.formats
|
|
380
|
+
? values.formats.split(",").map((f) => f.trim())
|
|
381
|
+
: defaultFormats;
|
|
382
|
+
|
|
383
|
+
// Read and parse input
|
|
384
|
+
let data;
|
|
385
|
+
try {
|
|
386
|
+
data = JSON.parse(fs.readFileSync(inputPath, "utf8"));
|
|
387
|
+
} catch (err) {
|
|
388
|
+
console.error(`mill: failed to read ${inputPath}: ${err.message}`);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Normalize compilation vs claims
|
|
393
|
+
if (data.resolved_claims && (!data.claims || data.claims.length === 0)) {
|
|
394
|
+
data.claims = data.resolved_claims;
|
|
395
|
+
}
|
|
396
|
+
if (data.sprint_meta && !data.meta) {
|
|
397
|
+
data.meta = data.sprint_meta;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Create output dir
|
|
401
|
+
if (!fs.existsSync(outputDir)) {
|
|
402
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Discover ESM formats
|
|
406
|
+
const FORMATS_DIR = path.join(LIB_DIR, "formats");
|
|
407
|
+
const formatFiles = fs
|
|
408
|
+
.readdirSync(FORMATS_DIR)
|
|
409
|
+
.filter((f) => f.endsWith(".js"));
|
|
410
|
+
const formatMap = {};
|
|
411
|
+
for (const file of formatFiles) {
|
|
412
|
+
try {
|
|
413
|
+
const mod = await import(path.join(FORMATS_DIR, file));
|
|
414
|
+
formatMap[mod.name || file.replace(".js", "")] = mod;
|
|
415
|
+
} catch {}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const results = [];
|
|
419
|
+
|
|
420
|
+
for (const fmt of requestedFormats) {
|
|
421
|
+
const mod = formatMap[fmt];
|
|
422
|
+
if (!mod || typeof mod.convert !== "function") {
|
|
423
|
+
console.error(`mill: unknown format: ${fmt}`);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
try {
|
|
428
|
+
const output = mod.convert(data);
|
|
429
|
+
const ext = mod.extension || ".txt";
|
|
430
|
+
const outFile = path.join(outputDir, fmt + ext);
|
|
431
|
+
fs.writeFileSync(outFile, output);
|
|
432
|
+
results.push({
|
|
433
|
+
format: fmt,
|
|
434
|
+
file: outFile,
|
|
435
|
+
bytes: Buffer.byteLength(output),
|
|
436
|
+
});
|
|
437
|
+
vlog(`wrote ${fmt} -> ${outFile}`);
|
|
438
|
+
} catch (err) {
|
|
439
|
+
console.error(`mill: ${fmt} conversion failed: ${err.message}`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Generate step summary (markdown) if --summary or GITHUB_STEP_SUMMARY is set
|
|
444
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
445
|
+
if (values.summary || summaryPath) {
|
|
446
|
+
const mdMod = formatMap["markdown"];
|
|
447
|
+
if (mdMod && typeof mdMod.convert === "function") {
|
|
448
|
+
try {
|
|
449
|
+
const md = mdMod.convert(data);
|
|
450
|
+
if (summaryPath) {
|
|
451
|
+
fs.appendFileSync(summaryPath, md + "\n");
|
|
452
|
+
vlog("appended markdown to GITHUB_STEP_SUMMARY");
|
|
453
|
+
}
|
|
454
|
+
// Also write to output dir
|
|
455
|
+
const mdFile = path.join(outputDir, "step-summary.md");
|
|
456
|
+
fs.writeFileSync(mdFile, md);
|
|
457
|
+
results.push({
|
|
458
|
+
format: "markdown",
|
|
459
|
+
file: mdFile,
|
|
460
|
+
bytes: Buffer.byteLength(md),
|
|
461
|
+
});
|
|
462
|
+
} catch (err) {
|
|
463
|
+
console.error(`mill: step summary generation failed: ${err.message}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (values.json) {
|
|
469
|
+
console.log(JSON.stringify({ artifacts: results }));
|
|
470
|
+
} else {
|
|
471
|
+
console.log(`mill: generated ${results.length} artifacts in ${outputDir}`);
|
|
472
|
+
for (const r of results) {
|
|
473
|
+
console.log(` ${r.format}: ${r.file} (${r.bytes} bytes)`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
312
478
|
function runServe(args) {
|
|
313
|
-
const serverPath = path.join(LIB_DIR,
|
|
314
|
-
const child = fork(serverPath, args, { stdio:
|
|
315
|
-
child.on(
|
|
316
|
-
process.on(
|
|
317
|
-
process.on(
|
|
479
|
+
const serverPath = path.join(LIB_DIR, "server.js");
|
|
480
|
+
const child = fork(serverPath, args, { stdio: "inherit" });
|
|
481
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
482
|
+
process.on("SIGTERM", () => child.kill("SIGTERM"));
|
|
483
|
+
process.on("SIGINT", () => child.kill("SIGINT"));
|
|
318
484
|
}
|
|
319
485
|
|
|
320
486
|
main();
|
package/lib/exporters/csv.js
CHANGED
|
@@ -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
|
* Export claims.json to CSV.
|
|
@@ -9,25 +9,25 @@ const path = require('node:path');
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const CSV_COLUMNS = [
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
12
|
+
"id",
|
|
13
|
+
"type",
|
|
14
|
+
"text",
|
|
15
|
+
"confidence",
|
|
16
|
+
"evidence_tier",
|
|
17
|
+
"source",
|
|
18
|
+
"status",
|
|
19
|
+
"created",
|
|
20
|
+
"tags",
|
|
21
21
|
];
|
|
22
22
|
|
|
23
23
|
function escapeCsvField(value) {
|
|
24
|
-
if (value == null) return
|
|
24
|
+
if (value == null) return "";
|
|
25
25
|
let str = String(value);
|
|
26
26
|
// CWE-1236: Prevent CSV injection by prefixing formula-triggering characters
|
|
27
27
|
if (/^[=+\-@\t\r]/.test(str)) {
|
|
28
28
|
str = "'" + str;
|
|
29
29
|
}
|
|
30
|
-
if (str.includes(
|
|
30
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n")) {
|
|
31
31
|
return `"${str.replace(/"/g, '""')}"`;
|
|
32
32
|
}
|
|
33
33
|
return str;
|
|
@@ -35,17 +35,19 @@ function escapeCsvField(value) {
|
|
|
35
35
|
|
|
36
36
|
function claimToRow(claim) {
|
|
37
37
|
return CSV_COLUMNS.map((col) => {
|
|
38
|
-
if (col ===
|
|
39
|
-
return escapeCsvField(
|
|
38
|
+
if (col === "tags") {
|
|
39
|
+
return escapeCsvField(
|
|
40
|
+
Array.isArray(claim.tags) ? claim.tags.join("; ") : "",
|
|
41
|
+
);
|
|
40
42
|
}
|
|
41
|
-
if (col ===
|
|
42
|
-
return escapeCsvField(claim.evidence?.tier ?? claim.evidence_tier ??
|
|
43
|
+
if (col === "evidence_tier") {
|
|
44
|
+
return escapeCsvField(claim.evidence?.tier ?? claim.evidence_tier ?? "");
|
|
43
45
|
}
|
|
44
|
-
if (col ===
|
|
45
|
-
return escapeCsvField(claim.evidence?.source ?? claim.source ??
|
|
46
|
+
if (col === "source") {
|
|
47
|
+
return escapeCsvField(claim.evidence?.source ?? claim.source ?? "");
|
|
46
48
|
}
|
|
47
49
|
return escapeCsvField(claim[col]);
|
|
48
|
-
}).join(
|
|
50
|
+
}).join(",");
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
function deriveOutputPath(inputPath, explicit) {
|
|
@@ -56,28 +58,31 @@ function deriveOutputPath(inputPath, explicit) {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
async function exportCsv(inputPath, outputPath) {
|
|
59
|
-
const raw = fs.readFileSync(inputPath,
|
|
61
|
+
const raw = fs.readFileSync(inputPath, "utf-8");
|
|
60
62
|
const data = JSON.parse(raw);
|
|
61
63
|
|
|
62
64
|
// Accept either an array or { claims: [...] }
|
|
63
|
-
const claims = Array.isArray(data) ? data :
|
|
65
|
+
const claims = Array.isArray(data) ? data : data.claims || [];
|
|
64
66
|
|
|
65
67
|
if (claims.length === 0) {
|
|
66
|
-
throw new Error(
|
|
68
|
+
throw new Error("No claims found in input file.");
|
|
67
69
|
}
|
|
68
70
|
|
|
69
|
-
const header = CSV_COLUMNS.join(
|
|
71
|
+
const header = CSV_COLUMNS.join(",");
|
|
70
72
|
const rows = claims.map(claimToRow);
|
|
71
|
-
const csv = [header, ...rows].join(
|
|
73
|
+
const csv = [header, ...rows].join("\n") + "\n";
|
|
72
74
|
|
|
73
75
|
const out = deriveOutputPath(inputPath, outputPath);
|
|
74
|
-
fs.writeFileSync(out, csv,
|
|
76
|
+
fs.writeFileSync(out, csv, "utf-8");
|
|
75
77
|
|
|
76
|
-
return {
|
|
78
|
+
return {
|
|
79
|
+
outputPath: out,
|
|
80
|
+
message: `CSV written to ${out} (${claims.length} claims)`,
|
|
81
|
+
};
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
module.exports = {
|
|
80
|
-
name:
|
|
81
|
-
description:
|
|
85
|
+
name: "csv",
|
|
86
|
+
description: "Export claims JSON to CSV",
|
|
82
87
|
export: exportCsv,
|
|
83
88
|
};
|
package/lib/exporters/json-ld.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
"use strict";
|
|
2
2
|
|
|
3
|
-
const fs = require(
|
|
4
|
-
const path = require(
|
|
5
|
-
const { buildReport } = require(
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { buildReport } = require("../json-ld-common.js");
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Export claims.json to JSON-LD format.
|
|
@@ -17,28 +17,34 @@ function deriveOutputPath(inputPath, explicit) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
async function exportJsonLd(inputPath, outputPath) {
|
|
20
|
-
const raw = fs.readFileSync(inputPath,
|
|
20
|
+
const raw = fs.readFileSync(inputPath, "utf-8");
|
|
21
21
|
const data = JSON.parse(raw);
|
|
22
|
-
const claims = Array.isArray(data) ? data :
|
|
22
|
+
const claims = Array.isArray(data) ? data : data.claims || [];
|
|
23
23
|
|
|
24
24
|
if (claims.length === 0) {
|
|
25
|
-
throw new Error(
|
|
25
|
+
throw new Error("No claims found in input file.");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const meta = data.meta || {
|
|
28
|
+
const meta = data.meta || {
|
|
29
|
+
sprint: "unknown",
|
|
30
|
+
question: "Wheat Sprint Claims",
|
|
31
|
+
};
|
|
29
32
|
const certificate = data.certificate || {};
|
|
30
33
|
const doc = buildReport(meta, claims, certificate);
|
|
31
34
|
|
|
32
35
|
const out = deriveOutputPath(inputPath, outputPath);
|
|
33
|
-
const tmp = out +
|
|
34
|
-
fs.writeFileSync(tmp, JSON.stringify(doc, null, 2) +
|
|
36
|
+
const tmp = out + ".tmp." + process.pid;
|
|
37
|
+
fs.writeFileSync(tmp, JSON.stringify(doc, null, 2) + "\n", "utf-8");
|
|
35
38
|
fs.renameSync(tmp, out);
|
|
36
39
|
|
|
37
|
-
return {
|
|
40
|
+
return {
|
|
41
|
+
outputPath: out,
|
|
42
|
+
message: `JSON-LD written to ${out} (${claims.length} claims)`,
|
|
43
|
+
};
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
module.exports = {
|
|
41
|
-
name:
|
|
42
|
-
description:
|
|
47
|
+
name: "json-ld",
|
|
48
|
+
description: "Export claims JSON to JSON-LD for semantic web",
|
|
43
49
|
export: exportJsonLd,
|
|
44
50
|
};
|