testronaut 1.11.0 → 1.12.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/README.md +226 -0
- package/TODO.md +5 -0
- package/bin/cli.js +232 -5
- package/bin/terminalUi.js +9 -1
- package/core/agent.js +7 -2
- package/core/browserEvidence.js +184 -0
- package/core/jevRouting.js +77 -0
- package/core/jevShadow.js +325 -0
- package/core/jevSuiteSelection.js +39 -0
- package/core/jevUsage.js +62 -0
- package/core/missionDiscovery.js +56 -6
- package/core/optimization.js +98 -0
- package/core/turnLoop.js +232 -11
- package/experiments/CROSS_PROVIDER.md +19 -0
- package/experiments/README.md +62 -0
- package/experiments/buildExperimentPublicationPackage.js +219 -0
- package/experiments/buildOptimizationDataPackage.js +390 -0
- package/experiments/crossProviderBenchmark.js +227 -0
- package/experiments/optimizationBenchmark.js +339 -0
- package/package.json +5 -1
- package/tests/binTests/cli.helpers.test.js +16 -0
- package/tests/binTests/cli.integration.test.js +18 -1
- package/tests/binTests/terminalUi.test.js +2 -1
- package/tests/coreTests/agent.test.js +2 -0
- package/tests/coreTests/browserEvidence.test.js +70 -0
- package/tests/coreTests/jevRouting.test.js +57 -0
- package/tests/coreTests/jevShadow.test.js +158 -0
- package/tests/coreTests/jevSuiteSelection.test.js +24 -0
- package/tests/coreTests/jevUsage.test.js +58 -0
- package/tests/coreTests/missionDiscovery.test.js +44 -0
- package/tests/coreTests/optimization.test.js +80 -0
- package/tests/coreTests/turnLoop.test.js +351 -1
- package/tests/experimentTests/buildOptimizationDataPackage.test.js +25 -0
- package/tests/experimentTests/crossProviderBenchmark.test.js +37 -0
- package/tests/experimentTests/optimizationBenchmark.test.js +49 -0
- package/tests/toolsTests/generateHtmlReport.test.js +100 -0
- package/tools/generateHtmlReport.js +67 -10
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Optimization benchmark pilot
|
|
2
|
+
|
|
3
|
+
This benchmark compares seven optimization conditions across the OpenAI-focused
|
|
4
|
+
Testronaut Examples repository and the Gemini-focused Collegium repository. It
|
|
5
|
+
uses five randomized blocks by default: 35 runs per repository and 70 total.
|
|
6
|
+
|
|
7
|
+
The fixed mission subsets are:
|
|
8
|
+
|
|
9
|
+
- OpenAI: login, self-cleaning task creation/removal, and file transfer.
|
|
10
|
+
- Gemini: profile navigation, projects dashboard, and project workspace navigation.
|
|
11
|
+
|
|
12
|
+
Preview the complete deterministic schedule without making model calls:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm run benchmark:optimization -- --dry-run
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Run or resume both repositories:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm run benchmark:optimization
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Run only one target:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run benchmark:optimization -- --target=openai
|
|
28
|
+
npm run benchmark:optimization -- --target=gemini
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Useful controls:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm run benchmark:optimization -- --repetitions=1 --experiment-id=optimization-smoke
|
|
35
|
+
npm run benchmark:optimization -- --seed=my-fixed-seed
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Progress is saved after every condition under target-specific
|
|
39
|
+
`experiments/results/<experiment-id>/state-openai.json` and
|
|
40
|
+
`state-gemini.json` files, so both loops may run concurrently without clobbering
|
|
41
|
+
state. Reusing the same experiment ID skips completed conditions. The matching
|
|
42
|
+
`runs-openai.csv` and `runs-gemini.csv` files contain one row per completed run,
|
|
43
|
+
and captured CLI output is stored under the corresponding `logs` directory.
|
|
44
|
+
Each generated Testronaut JSON report also records its experiment ID, condition,
|
|
45
|
+
and block. If state is lost, the runner recovers completed conditions from those
|
|
46
|
+
report fields before resuming.
|
|
47
|
+
|
|
48
|
+
The runner invokes this checkout's `bin/cli.js` directly and fixes the routing
|
|
49
|
+
probability at 0.40. It does not alter either target repository's configuration.
|
|
50
|
+
Model/API credentials must already be available in the environment or target
|
|
51
|
+
repository `.env` files.
|
|
52
|
+
|
|
53
|
+
After all 70 reports are present, generate the findings report and portable data
|
|
54
|
+
package with:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm run benchmark:package
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This writes a Markdown report, normalized CSV datasets, the raw JSON reports,
|
|
61
|
+
manifest, and checksums under `experiments/results/optimization-pilot-v1/`, plus
|
|
62
|
+
a compressed `optimization-pilot-v1-data-package.tar.gz` archive.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { BENCHMARK_TARGETS } from './optimizationBenchmark.js';
|
|
9
|
+
import { CROSS_PROVIDER_CONDITIONS } from './crossProviderBenchmark.js';
|
|
10
|
+
|
|
11
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const PUBLICATION_ID = 'jev-optimization-experiments-2026-10';
|
|
13
|
+
const RESULTS = path.join(HERE, 'results');
|
|
14
|
+
const OUTPUT = path.join(RESULTS, PUBLICATION_ID);
|
|
15
|
+
const PACKAGE = path.join(OUTPUT, 'data-package');
|
|
16
|
+
|
|
17
|
+
function csvEscape(value) {
|
|
18
|
+
const text = typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value ?? '');
|
|
19
|
+
return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeCsv(file, columns, rows) {
|
|
23
|
+
fs.writeFileSync(file, `${columns.join(',')}\n${rows.map(row => columns.map(column => csvEscape(row[column])).join(',')).join('\n')}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mean(values) { return values.reduce((sum, value) => sum + value, 0) / values.length; }
|
|
27
|
+
function round(value, digits = 1) { return Number(value.toFixed(digits)); }
|
|
28
|
+
function sha256(file) { return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); }
|
|
29
|
+
|
|
30
|
+
function readCrossProvider() {
|
|
31
|
+
const stateFile = path.join(RESULTS, 'cross-provider-pilot-v1/state.json');
|
|
32
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
33
|
+
const runs = Object.values(state.runs).map(run => {
|
|
34
|
+
if (run.status !== 'completed' || !run.reportPath || !run.metrics) throw new Error(`Incomplete cross-provider run: ${run.key}`);
|
|
35
|
+
const report = JSON.parse(fs.readFileSync(run.reportPath, 'utf8'));
|
|
36
|
+
const selectorUsage = run.selection?.evaluation?.usage || {};
|
|
37
|
+
return {
|
|
38
|
+
experimentId: state.experimentId,
|
|
39
|
+
key: run.key,
|
|
40
|
+
block: run.block,
|
|
41
|
+
condition: run.id,
|
|
42
|
+
mission: run.unit,
|
|
43
|
+
candidateModels: run.candidates.map(item => `${item.provider}/${item.model}`).join('|'),
|
|
44
|
+
selectedProvider: run.selection.selected.provider,
|
|
45
|
+
selectedModel: run.selection.selected.model,
|
|
46
|
+
selectedBy: run.selection.selectedBy,
|
|
47
|
+
selectionScore: run.selection.score,
|
|
48
|
+
strictPass: run.metrics.failed === 0,
|
|
49
|
+
phasePassed: run.metrics.passed,
|
|
50
|
+
phaseFailed: run.metrics.failed,
|
|
51
|
+
turns: run.metrics.turns,
|
|
52
|
+
modelTokens: run.metrics.tokens,
|
|
53
|
+
jevInputTokens: run.metrics.jevInputTokens,
|
|
54
|
+
jevOutputTokens: run.metrics.jevOutputTokens,
|
|
55
|
+
selectorInputTokens: Number(selectorUsage.input_tokens || 0),
|
|
56
|
+
selectorOutputTokens: Number(selectorUsage.output_tokens || 0),
|
|
57
|
+
durationSeconds: (new Date(report.endTime) - new Date(report.startTime)) / 1000,
|
|
58
|
+
runId: report.runId,
|
|
59
|
+
cliVersion: report.cli?.version,
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
const conditions = CROSS_PROVIDER_CONDITIONS.map(condition => {
|
|
63
|
+
const values = runs.filter(run => run.condition === condition.id);
|
|
64
|
+
return {
|
|
65
|
+
condition: condition.id,
|
|
66
|
+
type: condition.id.startsWith('c') ? 'fixed-control' : 'cross-provider-treatment',
|
|
67
|
+
candidates: condition.candidates.map(item => `${item.provider}/${item.model}`).join('|'),
|
|
68
|
+
n: values.length,
|
|
69
|
+
strictPassed: values.filter(run => run.strictPass).length,
|
|
70
|
+
strictPassRate: round(values.filter(run => run.strictPass).length / values.length, 4),
|
|
71
|
+
meanModelTokens: round(mean(values.map(run => run.modelTokens)), 0),
|
|
72
|
+
meanJevTokens: round(mean(values.map(run => run.jevInputTokens + run.jevOutputTokens)), 0),
|
|
73
|
+
meanSelectorTokens: round(mean(values.map(run => run.selectorInputTokens + run.selectorOutputTokens)), 0),
|
|
74
|
+
meanDurationSeconds: round(mean(values.map(run => run.durationSeconds)), 1),
|
|
75
|
+
selections: Object.fromEntries([...new Set(values.map(run => run.selectedModel))].map(model => [model, values.filter(run => run.selectedModel === model).length])),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
return { state, runs, conditions };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function crossTable(conditions) {
|
|
82
|
+
const rows = conditions.map(row => `| ${row.condition} | ${row.type} | ${row.strictPassed}/${row.n} (${Math.round(row.strictPassRate * 100)}%) | ${row.meanModelTokens.toLocaleString()} | ${row.meanJevTokens.toLocaleString()} | ${row.meanSelectorTokens.toLocaleString()} | ${row.meanDurationSeconds}s |`);
|
|
83
|
+
return ['| Condition | Design | Strict pass | Mean model tokens | Mean in-mission Jev tokens | Mean selector tokens | Mean duration |', '|---|---|---:|---:|---:|---:|---:|', ...rows].join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function optimizationMatrix() {
|
|
87
|
+
return Object.fromEntries(Object.entries(BENCHMARK_TARGETS).map(([target, value]) => [target, {
|
|
88
|
+
provider: value.provider,
|
|
89
|
+
missions: value.missions,
|
|
90
|
+
conditions: value.conditions,
|
|
91
|
+
}]));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function reportMarkdown(cross) {
|
|
95
|
+
return `# Jev optimization experiments: guardrails and model routing
|
|
96
|
+
|
|
97
|
+
Publication package: \`${PUBLICATION_ID}\`
|
|
98
|
+
Generated: ${new Date().toISOString()}
|
|
99
|
+
CLI baseline: Testronaut 1.11.0
|
|
100
|
+
|
|
101
|
+
## Executive summary
|
|
102
|
+
|
|
103
|
+
Two blocked experiments evaluated Jev-assisted browser-testing optimization. The first contained **70 aggregate runs and 210 mission-file attempts** across OpenAI and Gemini workloads. The second contained **120 isolated mission executions** testing fixed-model controls and mission-boundary cross-provider selection. All conditions used five randomized blocks.
|
|
104
|
+
|
|
105
|
+
The most defensible result is that **Jev guardrails are useful without model switching**. Guarded GPT-5.5 completed 15/15 OpenAI mission-file attempts and used 13.7% fewer model tokens than the unguarded Terra control, with a paired 95% interval of -19.8% to -7.5%. Guarded Terra also improved strict completion and nominally reduced tokens. On Gemini, guardrails preserved the already-perfect reliability of Gemini 3.8 Flash with a smaller nominal token reduction.
|
|
106
|
+
|
|
107
|
+
Same-provider routing is promising but not yet broadly proven. Gemini 3.8 → 3.7 retained 15/15 strict passes with a nominal 5.1% token reduction and similar duration. OpenAI routing coverage was below one routed turn per aggregate run, making attribution underpowered.
|
|
108
|
+
|
|
109
|
+
Mission-boundary cross-provider routing did **not** reduce token volume. The strongest fixed control, guarded GPT-5.5, achieved 15/15 strict passes at 87,422 mean model tokens per isolated mission. GPT-5.5 ↔ Gemini 3.5 Flash-Lite also achieved 15/15, but required 104,897 mean model tokens. Jev's extra selector call averaged only about 621–724 tokens, so selector overhead was not the cause; the model choices themselves produced longer executions.
|
|
110
|
+
|
|
111
|
+
## Experiment 1: optimization ladder
|
|
112
|
+
|
|
113
|
+
| Provider | Condition | Strict pass | Mean model tokens | Paired token delta vs control | Mean duration |
|
|
114
|
+
|---|---|---:|---:|---:|---:|
|
|
115
|
+
| OpenAI | Terra control | 5/15 (33%) | 300,073 | baseline | 293s |
|
|
116
|
+
| OpenAI | Guarded GPT-5.5 | **15/15 (100%)** | **258,635** | **-13.7% [-19.8, -7.5]** | 416s |
|
|
117
|
+
| OpenAI | Guarded Terra | 8/15 (53%) | 275,114 | -8.2% [-18.0, 1.7] | 328s |
|
|
118
|
+
| OpenAI | Terra → Luna | 11/15 (73%) | 296,299 | -1.1% [-12.1, 9.9] | 376s |
|
|
119
|
+
| Gemini | 3.8 control | **15/15 (100%)** | 491,428 | baseline | 529s |
|
|
120
|
+
| Gemini | Guarded 3.5 Flash-Lite | **15/15 (100%)** | **427,766** | -12.0% [-34.7, 10.8] | 582s |
|
|
121
|
+
| Gemini | 3.8 → 3.7 | **15/15 (100%)** | 461,256 | -5.1% [-19.7, 9.5] | 538s |
|
|
122
|
+
|
|
123
|
+
The table highlights decision-relevant conditions; complete condition, run, and mission tables are included in \`data/optimization/\`.
|
|
124
|
+
|
|
125
|
+
## Experiment 2: cross-provider mission-boundary routing
|
|
126
|
+
|
|
127
|
+
${crossTable(cross.conditions)}
|
|
128
|
+
|
|
129
|
+
Jev chose a Gemini candidate for 50 of 60 treatment missions. That bias was not aligned with the observed workload: GPT-5.5 was the only fixed model with perfect strict completion, and it used the fewest mean model tokens. The file-transfer mission was particularly discriminating: GPT-5.5 passed 5/5, Terra and Gemini 3.8 passed 0/5, and Gemini 3.5 Flash-Lite passed 3/5 in their fixed controls.
|
|
130
|
+
|
|
131
|
+
## Product implications
|
|
132
|
+
|
|
133
|
+
1. Stabilize guardrail-only operation: completion/evidence checks, fail-open behavior, primary-model continuity, and explicit telemetry.
|
|
134
|
+
2. Keep execution-model routing beta and opt-in. Limit a run to one primary and one compatible same-provider secondary.
|
|
135
|
+
3. Treat Gemini 3.8 → 3.7 as a conservative candidate for a larger confirmation study, not a universal default.
|
|
136
|
+
4. Do not ship zero-shot cross-provider selection as an optimization default.
|
|
137
|
+
5. Next test history-informed routing using versioned per-model reliability, tokens, latency, and price, with a reliability floor and cold-start fallback.
|
|
138
|
+
|
|
139
|
+
## Methodology and interpretation
|
|
140
|
+
|
|
141
|
+
- Both experiments used five randomized blocks (\`n=5\` per condition design).
|
|
142
|
+
- Strict success required every expected phase in a mission file to exist and pass.
|
|
143
|
+
- Experiment 1 aggregate runs each invoked three mission files. Experiment 2 intentionally isolated each mission to make mission-boundary selection attributable.
|
|
144
|
+
- Model tokens and Jev tokens are reported separately. Token counts are not price-weighted spend.
|
|
145
|
+
- Experiment 1 paired intervals use a two-sided 95% Student-t interval with four degrees of freedom.
|
|
146
|
+
- Six initial Gemini 3.8 requests in experiment 2 returned provider HTTP 500 errors. The resumable benchmark reran those incomplete units using their persisted selections; the final dataset contains 120 completed reports.
|
|
147
|
+
- Results describe these repositories, missions, model versions, dates, and provider behavior. They are not universal model rankings.
|
|
148
|
+
|
|
149
|
+
## Data and privacy
|
|
150
|
+
|
|
151
|
+
This public package contains normalized measurements, matrices, methodology, and checksums. It intentionally excludes raw browser events, DOM content, screenshots, HTML reports, environment files, credentials, absolute report paths, and verbose logs. The private source reports remain the audit source for the normalized rows.
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function walk(root) {
|
|
156
|
+
return fs.readdirSync(root, { withFileTypes: true }).flatMap(entry => {
|
|
157
|
+
const full = path.join(root, entry.name);
|
|
158
|
+
return entry.isDirectory() ? walk(full) : [full];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function buildPublicationPackage() {
|
|
163
|
+
const optimizationPackage = path.join(RESULTS, 'optimization-pilot-v1/data-package/data');
|
|
164
|
+
if (!fs.existsSync(optimizationPackage)) throw new Error('Build optimization-pilot-v1 package first with npm run benchmark:package');
|
|
165
|
+
const cross = readCrossProvider();
|
|
166
|
+
fs.mkdirSync(path.join(PACKAGE, 'data/optimization'), { recursive: true });
|
|
167
|
+
fs.mkdirSync(path.join(PACKAGE, 'data/cross-provider'), { recursive: true });
|
|
168
|
+
|
|
169
|
+
for (const name of ['conditions.csv', 'missions.csv', 'runs.csv', 'matrix.json']) {
|
|
170
|
+
fs.copyFileSync(path.join(optimizationPackage, name), path.join(PACKAGE, `data/optimization/${name}`));
|
|
171
|
+
}
|
|
172
|
+
const crossColumns = Object.keys(cross.runs[0]);
|
|
173
|
+
writeCsv(path.join(PACKAGE, 'data/cross-provider/runs.csv'), crossColumns, cross.runs);
|
|
174
|
+
writeCsv(path.join(PACKAGE, 'data/cross-provider/conditions.csv'), Object.keys(cross.conditions[0]), cross.conditions);
|
|
175
|
+
fs.writeFileSync(path.join(PACKAGE, 'data/cross-provider/matrix.json'), `${JSON.stringify({
|
|
176
|
+
experimentId: cross.state.experimentId,
|
|
177
|
+
repetitions: cross.state.repetitions,
|
|
178
|
+
conditions: CROSS_PROVIDER_CONDITIONS,
|
|
179
|
+
missions: [...new Set(cross.runs.map(run => run.mission))],
|
|
180
|
+
}, null, 2)}\n`);
|
|
181
|
+
fs.writeFileSync(path.join(PACKAGE, 'data/combined-matrix.json'), `${JSON.stringify({ optimization: optimizationMatrix(), crossProvider: CROSS_PROVIDER_CONDITIONS }, null, 2)}\n`);
|
|
182
|
+
|
|
183
|
+
const report = reportMarkdown(cross);
|
|
184
|
+
fs.writeFileSync(path.join(PACKAGE, 'REPORT.md'), report);
|
|
185
|
+
fs.writeFileSync(path.join(PACKAGE, 'README.md'), `# Jev optimization experiment publication package\n\nStart with [REPORT.md](REPORT.md). The normalized, publication-safe datasets are in \`data/optimization/\` and \`data/cross-provider/\`. See [DATA_DICTIONARY.md](DATA_DICTIONARY.md) for field definitions and \`SHA256SUMS\` for integrity.\n`);
|
|
186
|
+
fs.writeFileSync(path.join(PACKAGE, 'DATA_DICTIONARY.md'), `# Data dictionary\n\n## Optimization ladder\n\n- \`runs.csv\`: one aggregate CLI run (three mission files) per row.\n- \`missions.csv\`: one expected mission-file outcome per aggregate run.\n- \`conditions.csv\`: condition-level means, paired deltas, intervals, routing, and completion-gate counts.\n- \`matrix.json\`: frozen provider, mission, and condition definitions.\n\n## Cross-provider benchmark\n\n- \`runs.csv\`: one isolated mission execution per row. \`strictPass\` means all reported phases passed. Selector tokens are separated from in-mission Jev tokens.\n- \`conditions.csv\`: condition-level strict completion, mean token, selection, and duration summaries.\n- \`matrix.json\`: frozen candidates, missions, and repetition count.\n\nEmpty secondary models indicate fixed-model conditions. Durations are wall-clock seconds. Token fields are raw token volume, not price-weighted cost.\n`);
|
|
187
|
+
|
|
188
|
+
const filesBeforeManifest = walk(PACKAGE)
|
|
189
|
+
.filter(file => !['manifest.json', 'SHA256SUMS'].includes(path.basename(file)))
|
|
190
|
+
.map(file => ({ path: path.relative(PACKAGE, file), bytes: fs.statSync(file).size, sha256: sha256(file) }))
|
|
191
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
192
|
+
const manifest = {
|
|
193
|
+
schemaVersion: 1,
|
|
194
|
+
publicationId: PUBLICATION_ID,
|
|
195
|
+
generatedAt: new Date().toISOString(),
|
|
196
|
+
cliBaseline: '1.11.0',
|
|
197
|
+
experiments: [
|
|
198
|
+
{ id: 'optimization-pilot-v1', aggregateRuns: 70, missionFileAttempts: 210 },
|
|
199
|
+
{ id: 'cross-provider-pilot-v1', isolatedMissionRuns: 120 },
|
|
200
|
+
],
|
|
201
|
+
privacyExclusions: ['raw browser events and DOM', 'screenshots', 'HTML reports', 'environment files', 'credentials', 'absolute report paths', 'verbose logs'],
|
|
202
|
+
files: filesBeforeManifest,
|
|
203
|
+
};
|
|
204
|
+
fs.writeFileSync(path.join(PACKAGE, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
205
|
+
const checksumFiles = walk(PACKAGE).filter(file => path.basename(file) !== 'SHA256SUMS').sort();
|
|
206
|
+
fs.writeFileSync(path.join(PACKAGE, 'SHA256SUMS'), `${checksumFiles.map(file => `${sha256(file)} ${path.relative(PACKAGE, file)}`).join('\n')}\n`);
|
|
207
|
+
|
|
208
|
+
fs.mkdirSync(OUTPUT, { recursive: true });
|
|
209
|
+
fs.copyFileSync(path.join(PACKAGE, 'REPORT.md'), path.join(OUTPUT, `${PUBLICATION_ID}-report.md`));
|
|
210
|
+
const archive = path.join(OUTPUT, `${PUBLICATION_ID}-data-package.tar.gz`);
|
|
211
|
+
const tar = spawnSync('tar', ['-czf', archive, '-C', OUTPUT, 'data-package'], { encoding: 'utf8' });
|
|
212
|
+
if (tar.status !== 0) throw new Error(tar.stderr || 'Could not create archive');
|
|
213
|
+
fs.writeFileSync(`${archive}.sha256`, `${sha256(archive)} ${path.basename(archive)}\n`);
|
|
214
|
+
return { output: OUTPUT, package: PACKAGE, archive, report: path.join(OUTPUT, `${PUBLICATION_ID}-report.md`) };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
218
|
+
console.log(buildPublicationPackage());
|
|
219
|
+
}
|