sphere-cli 0.1.41 → 0.2.0
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/bin/sphere.js +19 -664
- package/package.json +6 -13
- package/scripts/postinstall.js +82 -69
- package/examples/nhanes_sample.csv +0 -4900
- package/scripts/certify.py +0 -131
- package/scripts/evaluate.py +0 -70
- package/sphere-node.js +0 -7
- package/sphere_cli/__pycache__/__init__.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/__init__.cpython-314.pyc +0 -0
- package/sphere_cli/__pycache__/_algo._givens_inplace_nb-84.py314.1.nbc +0 -0
- package/sphere_cli/__pycache__/_algo._givens_inplace_nb-84.py314.nbi +0 -0
- package/sphere_cli/__pycache__/_algo.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/_core.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/_evaluate.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/_evaluate.cpython-314.pyc +0 -0
- package/sphere_cli/__pycache__/_generate.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/_generate.cpython-314.pyc +0 -0
- package/sphere_cli/__pycache__/cli.cpython-312.pyc +0 -0
- package/sphere_cli/__pycache__/cli.cpython-314.pyc +0 -0
- package/sphere_cli/_algo.cpython-314-darwin.so +0 -0
- package/sphere_cli/_certify.cpython-314-darwin.so +0 -0
- package/sphere_cli/_core.cpython-314-darwin.so +0 -0
- package/sphere_cli/_evaluate.cpython-314-darwin.so +0 -0
- package/sphere_cli/_generate.cpython-314-darwin.so +0 -0
- package/sphere_cli/cli.cpython-314-darwin.so +0 -0
package/bin/sphere.js
CHANGED
|
@@ -2,678 +2,33 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* sphere —
|
|
5
|
+
* sphere — thin launcher.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* sphere demo
|
|
12
|
-
* sphere license activate|status|clear
|
|
7
|
+
* The actual CLI is a sealed, signed+notarized native binary (PyInstaller bundle
|
|
8
|
+
* of the SPHERE engine — same engine as the desktop app). The npm `postinstall`
|
|
9
|
+
* downloads the platform-specific binary into ./vendor/. This launcher just
|
|
10
|
+
* forwards argv to it. No readable algorithm code ships in the npm package.
|
|
13
11
|
*/
|
|
14
12
|
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const https = require('https');
|
|
19
|
-
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const { spawnSync } = require('child_process');
|
|
20
16
|
|
|
21
|
-
const
|
|
22
|
-
const SPHERE_NODE_JS = path.join(PKG_DIR, 'sphere-node.js');
|
|
23
|
-
const EVALUATE_PY = path.join(PKG_DIR, 'scripts', 'evaluate.py');
|
|
24
|
-
const CERTIFY_PY = path.join(PKG_DIR, 'scripts', 'certify.py');
|
|
25
|
-
const VERSION = require(path.join(PKG_DIR, 'package.json')).version;
|
|
17
|
+
const BIN = path.join(__dirname, '..', 'vendor', 'sphere-cli', 'sphere');
|
|
26
18
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
function sphereConfigDir() {
|
|
35
|
-
const d = path.join(os.homedir(), '.config', 'sphere');
|
|
36
|
-
fs.mkdirSync(d, { recursive: true });
|
|
37
|
-
return d;
|
|
38
|
-
}
|
|
39
|
-
function licenseKeyFile() { return path.join(sphereConfigDir(), 'license_key'); }
|
|
40
|
-
function licenseCacheFile() { return path.join(sphereConfigDir(), 'license_cache.json'); }
|
|
41
|
-
|
|
42
|
-
function readStoredLicenseKey() {
|
|
43
|
-
const f = licenseKeyFile();
|
|
44
|
-
if (!fs.existsSync(f)) return null;
|
|
45
|
-
return fs.readFileSync(f, 'utf8').trim() || null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function writeLicenseKey(key) {
|
|
49
|
-
const f = licenseKeyFile();
|
|
50
|
-
fs.writeFileSync(f, key, { encoding: 'utf8', mode: 0o600 });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function readLicenseCache() {
|
|
54
|
-
const f = licenseCacheFile();
|
|
55
|
-
if (!fs.existsSync(f)) return null;
|
|
56
|
-
try {
|
|
57
|
-
const data = JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
58
|
-
const ageDays = (Date.now() / 1000 - (data.cachedAt || 0)) / 86400;
|
|
59
|
-
return ageDays <= LICENSE_CACHE_DAYS ? data : null;
|
|
60
|
-
} catch { return null; }
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function writeLicenseCache(data) {
|
|
64
|
-
fs.writeFileSync(
|
|
65
|
-
licenseCacheFile(),
|
|
66
|
-
JSON.stringify({ ...data, cachedAt: Math.floor(Date.now() / 1000) }),
|
|
67
|
-
'utf8',
|
|
19
|
+
if (!fs.existsSync(BIN)) {
|
|
20
|
+
process.stderr.write(
|
|
21
|
+
'\nSPHERE binary not found.\n' +
|
|
22
|
+
'The platform binary is downloaded by the install step. Try reinstalling:\n' +
|
|
23
|
+
' npm install -g sphere-cli\n' +
|
|
24
|
+
'If your platform is unsupported, see https://github.com/statzihuai/sphere-cli\n',
|
|
68
25
|
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function validateKeyOnline(key) {
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
const body = Buffer.from(JSON.stringify({ key }));
|
|
74
|
-
const req = https.request(
|
|
75
|
-
`${LICENSE_WORKER_URL}/validate`,
|
|
76
|
-
{
|
|
77
|
-
method: 'POST',
|
|
78
|
-
headers: {
|
|
79
|
-
'Content-Type': 'application/json',
|
|
80
|
-
'Content-Length': body.length,
|
|
81
|
-
'User-Agent': 'sphere-cli/1.0',
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
(res) => {
|
|
85
|
-
let buf = '';
|
|
86
|
-
res.on('data', (d) => (buf += d));
|
|
87
|
-
res.on('end', () => {
|
|
88
|
-
try { resolve(JSON.parse(buf)); }
|
|
89
|
-
catch { reject(new Error('Invalid JSON from license server')); }
|
|
90
|
-
});
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
req.on('error', reject);
|
|
94
|
-
req.setTimeout(10000, () => { req.destroy(new Error('License server timeout')); });
|
|
95
|
-
req.write(body);
|
|
96
|
-
req.end();
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function checkLicense() {
|
|
101
|
-
if (!LICENSE_REQUIRED) return;
|
|
102
|
-
const key = readStoredLicenseKey();
|
|
103
|
-
if (!key) {
|
|
104
|
-
process.stderr.write(
|
|
105
|
-
'✗ No SPHERE license found.\n' +
|
|
106
|
-
' Run: sphere license activate <key>\n' +
|
|
107
|
-
' Contact zihuai@stanford.edu to get a license.\n',
|
|
108
|
-
);
|
|
109
|
-
process.exit(1);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
let result;
|
|
113
|
-
try {
|
|
114
|
-
result = await validateKeyOnline(key);
|
|
115
|
-
writeLicenseCache(result);
|
|
116
|
-
} catch {
|
|
117
|
-
result = readLicenseCache();
|
|
118
|
-
if (!result) {
|
|
119
|
-
process.stderr.write(
|
|
120
|
-
'✗ License server unreachable and local cache has expired.\n' +
|
|
121
|
-
' Connect to the internet and re-run to refresh your license.\n',
|
|
122
|
-
);
|
|
123
|
-
process.exit(1);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!result.valid) {
|
|
128
|
-
process.stderr.write(
|
|
129
|
-
`✗ License invalid: ${result.error || 'unknown error'}\n` +
|
|
130
|
-
' Run: sphere license activate <key>\n',
|
|
131
|
-
);
|
|
132
|
-
process.exit(1);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// ── Progress bar ──────────────────────────────────────────────────────────────
|
|
137
|
-
|
|
138
|
-
const BAR_WIDTH = 28;
|
|
139
|
-
|
|
140
|
-
function progressBar(frac, msg) {
|
|
141
|
-
const f = Math.min(1, Math.max(0, frac));
|
|
142
|
-
const filled = Math.round(BAR_WIDTH * f);
|
|
143
|
-
const bar = '█'.repeat(filled) + '░'.repeat(BAR_WIDTH - filled);
|
|
144
|
-
const pct = (f * 100).toFixed(1).padStart(5);
|
|
145
|
-
process.stderr.write(`\r [${bar}] ${pct}% ${(msg || '').slice(0, 45).padEnd(45)}\x1b[K`);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function clearLine() {
|
|
149
|
-
process.stderr.write(`\r${' '.repeat(BAR_WIDTH + 60)}\r`);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// ── Score bar (for evaluate output) ──────────────────────────────────────────
|
|
153
|
-
|
|
154
|
-
function scoreBar(score, width = 20) {
|
|
155
|
-
const filled = Math.round(width * score / 100);
|
|
156
|
-
return '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function printEvalResults(result) {
|
|
160
|
-
const fid = result.fidelity;
|
|
161
|
-
const priv = result.privacy;
|
|
162
|
-
const sep = '─'.repeat(36);
|
|
163
|
-
|
|
164
|
-
process.stdout.write('\n');
|
|
165
|
-
process.stdout.write(' Fidelity\n');
|
|
166
|
-
process.stdout.write(` ${sep}\n`);
|
|
167
|
-
for (const [label, key] of [
|
|
168
|
-
['Mean', 'meanScore'],
|
|
169
|
-
['Variance', 'varScore'],
|
|
170
|
-
['Correlation', 'corScore'],
|
|
171
|
-
['KS', 'ksScore'],
|
|
172
|
-
]) {
|
|
173
|
-
const v = fid[key];
|
|
174
|
-
process.stdout.write(` ${label.padEnd(14)} ${v.toFixed(1).padStart(5)} ${scoreBar(v)}\n`);
|
|
175
|
-
}
|
|
176
|
-
process.stdout.write(` ${sep}\n`);
|
|
177
|
-
const fc = fid.composite;
|
|
178
|
-
process.stdout.write(` ${'Composite'.padEnd(14)} ${fc.toFixed(1).padStart(5)} ${scoreBar(fc)}\n`);
|
|
179
|
-
|
|
180
|
-
if (priv) {
|
|
181
|
-
process.stdout.write('\n');
|
|
182
|
-
process.stdout.write(' Privacy\n');
|
|
183
|
-
process.stdout.write(` ${sep}\n`);
|
|
184
|
-
for (const [label, key] of [
|
|
185
|
-
['Singling Out', 'singlingOut'],
|
|
186
|
-
['Linkability', 'linkability'],
|
|
187
|
-
['Inference', 'inference'],
|
|
188
|
-
]) {
|
|
189
|
-
const v = priv[key].score;
|
|
190
|
-
process.stdout.write(` ${label.padEnd(14)} ${v.toFixed(1).padStart(5)} ${scoreBar(v)}\n`);
|
|
191
|
-
}
|
|
192
|
-
process.stdout.write(` ${sep}\n`);
|
|
193
|
-
const pc = priv.composite;
|
|
194
|
-
process.stdout.write(` ${'Composite'.padEnd(14)} ${pc.toFixed(1).padStart(5)} ${scoreBar(pc)}\n`);
|
|
195
|
-
} else if (result.privacy === null) {
|
|
196
|
-
process.stdout.write('\n Privacy: skipped (--skip-privacy)\n');
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
process.stdout.write('\n');
|
|
200
|
-
const excluded = result.idColsExcluded || [];
|
|
201
|
-
const exclNote = excluded.length ? ` (${excluded.length} ID col${excluded.length > 1 ? 's' : ''} excluded)` : '';
|
|
202
|
-
process.stdout.write(
|
|
203
|
-
` ${(result.nReal || 0).toLocaleString()} real rows \xd7 ${result.pOrig || '?'} cols` +
|
|
204
|
-
` vs ${(result.nSynth || 0).toLocaleString()} synthetic rows${exclNote}\n`,
|
|
205
|
-
);
|
|
206
|
-
process.stdout.write('\n');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// ── Python runner (shared by evaluate + certify) ──────────────────────────────
|
|
210
|
-
|
|
211
|
-
function findPython() {
|
|
212
|
-
// Try python3 first, then python
|
|
213
|
-
for (const bin of ['python3', 'python']) {
|
|
214
|
-
try {
|
|
215
|
-
const { execFileSync } = require('child_process');
|
|
216
|
-
const out = execFileSync(bin, ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
217
|
-
if (out.includes('Python 3')) return bin;
|
|
218
|
-
} catch { /* try next */ }
|
|
219
|
-
}
|
|
220
|
-
return null;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function runPythonScript(script, scriptArgs, { json: jsonMode = false } = {}) {
|
|
224
|
-
return new Promise((resolve) => {
|
|
225
|
-
const python = findPython();
|
|
226
|
-
if (!python) {
|
|
227
|
-
process.stderr.write(
|
|
228
|
-
'Error: Python 3 not found. Evaluation requires Python 3.10+.\n' +
|
|
229
|
-
' Install Python from https://python.org and retry.\n',
|
|
230
|
-
);
|
|
231
|
-
process.exit(1);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const proc = spawn(python, [script, ...scriptArgs], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
235
|
-
|
|
236
|
-
let resultLine = '';
|
|
237
|
-
let errorLine = '';
|
|
238
|
-
|
|
239
|
-
proc.stdout.on('data', (d) => { resultLine += d.toString(); });
|
|
240
|
-
|
|
241
|
-
proc.stderr.on('data', (d) => {
|
|
242
|
-
for (const line of d.toString().split('\n')) {
|
|
243
|
-
const s = line.trim();
|
|
244
|
-
if (!s) continue;
|
|
245
|
-
try {
|
|
246
|
-
const msg = JSON.parse(s);
|
|
247
|
-
if (typeof msg.progress === 'number' && !jsonMode) {
|
|
248
|
-
progressBar(msg.progress, msg.msg || '');
|
|
249
|
-
} else if (msg.error) {
|
|
250
|
-
errorLine = msg.error;
|
|
251
|
-
}
|
|
252
|
-
} catch { /* ignore non-JSON lines */ }
|
|
253
|
-
}
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
proc.on('close', (code) => {
|
|
257
|
-
if (!jsonMode) clearLine();
|
|
258
|
-
if (code !== 0) {
|
|
259
|
-
process.stderr.write(`Error: ${errorLine || 'evaluation process exited with code ' + code}\n`);
|
|
260
|
-
process.exit(1);
|
|
261
|
-
}
|
|
262
|
-
let result;
|
|
263
|
-
try { result = JSON.parse(resultLine.trim()); }
|
|
264
|
-
catch {
|
|
265
|
-
process.stderr.write('Error: could not parse evaluation output\n');
|
|
266
|
-
process.exit(1);
|
|
267
|
-
}
|
|
268
|
-
resolve(result);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
proc.on('error', (err) => {
|
|
272
|
-
process.stderr.write(`Error: failed to start Python: ${err.message}\n`);
|
|
273
|
-
process.exit(1);
|
|
274
|
-
});
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ── Argument parsing helpers ──────────────────────────────────────────────────
|
|
279
|
-
|
|
280
|
-
function parseArgs(argv) {
|
|
281
|
-
const args = argv.slice(2);
|
|
282
|
-
const pos = [];
|
|
283
|
-
const flags = {};
|
|
284
|
-
for (let i = 0; i < args.length; i++) {
|
|
285
|
-
const a = args[i];
|
|
286
|
-
if (a === '-o') {
|
|
287
|
-
flags['output'] = args[++i];
|
|
288
|
-
} else if (a.startsWith('--')) {
|
|
289
|
-
const key = a.slice(2);
|
|
290
|
-
const next = args[i + 1];
|
|
291
|
-
if (next === undefined || next.startsWith('-')) {
|
|
292
|
-
flags[key] = true;
|
|
293
|
-
} else {
|
|
294
|
-
flags[key] = next;
|
|
295
|
-
i++;
|
|
296
|
-
}
|
|
297
|
-
} else if (a.startsWith('-') && a.length === 2) {
|
|
298
|
-
flags[a.slice(1)] = args[++i];
|
|
299
|
-
} else {
|
|
300
|
-
pos.push(a);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
return { pos, flags };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// ── sphere generate ───────────────────────────────────────────────────────────
|
|
307
|
-
|
|
308
|
-
async function cmdGenerate(pos, flags) {
|
|
309
|
-
await checkLicense();
|
|
310
|
-
|
|
311
|
-
const input = pos[0];
|
|
312
|
-
const output = flags.output;
|
|
313
|
-
|
|
314
|
-
if (!input) { process.stderr.write('Error: missing input CSV\n'); process.exit(1); }
|
|
315
|
-
if (!output) { process.stderr.write('Error: missing -o / --output path\n'); process.exit(1); }
|
|
316
|
-
if (!fs.existsSync(input)) {
|
|
317
|
-
process.stderr.write(`Error: file not found: ${input}\n`);
|
|
318
|
-
process.exit(1);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (!flags.json) {
|
|
322
|
-
process.stderr.write(`Generating synthetic data from ${path.basename(input)} …\n`);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const nodeArgs = [SPHERE_NODE_JS, input, output];
|
|
326
|
-
if (flags.k) nodeArgs.push('--k', String(flags.k));
|
|
327
|
-
if (flags.theta) nodeArgs.push('--theta', String(flags.theta));
|
|
328
|
-
if (flags.delta) nodeArgs.push('--delta', String(flags.delta));
|
|
329
|
-
if (flags['mix-prob']) nodeArgs.push('--mix-prob', String(flags['mix-prob']));
|
|
330
|
-
if (flags.seed) nodeArgs.push('--seed', String(flags.seed));
|
|
331
|
-
|
|
332
|
-
return new Promise((resolve) => {
|
|
333
|
-
const proc = spawn(process.execPath, nodeArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
334
|
-
|
|
335
|
-
let resultLine = '';
|
|
336
|
-
let errorLine = '';
|
|
337
|
-
|
|
338
|
-
proc.stdout.on('data', (d) => { resultLine += d.toString(); });
|
|
339
|
-
|
|
340
|
-
proc.stderr.on('data', (d) => {
|
|
341
|
-
for (const line of d.toString().split('\n')) {
|
|
342
|
-
const s = line.trim();
|
|
343
|
-
if (!s) continue;
|
|
344
|
-
try {
|
|
345
|
-
const msg = JSON.parse(s);
|
|
346
|
-
if (typeof msg.progress === 'number' && !flags.json) {
|
|
347
|
-
progressBar(msg.progress, '');
|
|
348
|
-
} else if (msg.error) {
|
|
349
|
-
errorLine = msg.error;
|
|
350
|
-
}
|
|
351
|
-
} catch { /* ignore non-JSON lines */ }
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
proc.on('close', (code) => {
|
|
356
|
-
if (!flags.json) clearLine();
|
|
357
|
-
|
|
358
|
-
if (code !== 0) {
|
|
359
|
-
process.stderr.write(`Error: ${errorLine || 'sphere-node exited with code ' + code}\n`);
|
|
360
|
-
process.exit(1);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
let result;
|
|
364
|
-
try { result = JSON.parse(resultLine.trim()); }
|
|
365
|
-
catch {
|
|
366
|
-
process.stderr.write('Error: could not parse sphere-node output\n');
|
|
367
|
-
process.exit(1);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (flags.json) {
|
|
371
|
-
process.stdout.write(JSON.stringify(result) + '\n');
|
|
372
|
-
resolve(result);
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const elapsedS = ((result.elapsedMs || 0) / 1000).toFixed(1);
|
|
377
|
-
process.stdout.write(
|
|
378
|
-
`✓ ${output} ${(result.rows || 0).toLocaleString()} rows \xd7 ${result.cols || '?'} cols` +
|
|
379
|
-
` (${elapsedS} s) seed ${result.seed}\n`,
|
|
380
|
-
);
|
|
381
|
-
if (result.idColDetected) {
|
|
382
|
-
process.stdout.write(` ID columns excluded: ${result.idColName}\n`);
|
|
383
|
-
}
|
|
384
|
-
resolve(result);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
proc.on('error', (err) => {
|
|
388
|
-
process.stderr.write(`Error: failed to start sphere-node: ${err.message}\n`);
|
|
389
|
-
process.exit(1);
|
|
390
|
-
});
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// ── sphere evaluate ───────────────────────────────────────────────────────────
|
|
395
|
-
|
|
396
|
-
async function cmdEvaluate(pos, flags) {
|
|
397
|
-
await checkLicense();
|
|
398
|
-
|
|
399
|
-
const real = pos[0];
|
|
400
|
-
const synth = pos[1];
|
|
401
|
-
|
|
402
|
-
if (!real) { process.stderr.write('Error: missing real CSV\n'); process.exit(1); }
|
|
403
|
-
if (!synth) { process.stderr.write('Error: missing synth CSV\n'); process.exit(1); }
|
|
404
|
-
if (!fs.existsSync(real)) { process.stderr.write(`Error: file not found: ${real}\n`); process.exit(1); }
|
|
405
|
-
if (!fs.existsSync(synth)) { process.stderr.write(`Error: file not found: ${synth}\n`); process.exit(1); }
|
|
406
|
-
|
|
407
|
-
if (!flags.json) {
|
|
408
|
-
process.stderr.write(`Evaluating ${path.basename(real)} vs ${path.basename(synth)} …\n`);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
const scriptArgs = [real, synth];
|
|
412
|
-
if (flags['skip-privacy']) scriptArgs.push('--skip-privacy');
|
|
413
|
-
if (flags['n-attacks']) scriptArgs.push('--n-attacks', String(flags['n-attacks']));
|
|
414
|
-
if (flags['n-secrets']) scriptArgs.push('--n-secrets', String(flags['n-secrets']));
|
|
415
|
-
if (flags['n-atk-cap']) scriptArgs.push('--n-atk-cap', String(flags['n-atk-cap']));
|
|
416
|
-
if (flags['n-neighbors']) scriptArgs.push('--n-neighbors', String(flags['n-neighbors']));
|
|
417
|
-
if (flags['n-aux-cols']) scriptArgs.push('--n-aux-cols', String(flags['n-aux-cols']));
|
|
418
|
-
if (flags.seed) scriptArgs.push('--seed', String(flags.seed));
|
|
419
|
-
|
|
420
|
-
const result = await runPythonScript(EVALUATE_PY, scriptArgs, { json: !!flags.json });
|
|
421
|
-
|
|
422
|
-
if (flags.json) {
|
|
423
|
-
process.stdout.write(JSON.stringify(result) + '\n');
|
|
424
|
-
return result;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const evalS = ((result.runMs || 0) / 1000).toFixed(1);
|
|
428
|
-
process.stdout.write(`✓ Evaluation complete (${evalS} s)\n`);
|
|
429
|
-
printEvalResults(result);
|
|
430
|
-
return result;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// ── sphere certify ────────────────────────────────────────────────────────────
|
|
434
|
-
|
|
435
|
-
async function cmdCertify(pos, flags) {
|
|
436
|
-
await checkLicense();
|
|
437
|
-
|
|
438
|
-
const real = pos[0];
|
|
439
|
-
const synth = pos[1];
|
|
440
|
-
const output = flags.output;
|
|
441
|
-
|
|
442
|
-
if (!real) { process.stderr.write('Error: missing real CSV\n'); process.exit(1); }
|
|
443
|
-
if (!synth) { process.stderr.write('Error: missing synth CSV\n'); process.exit(1); }
|
|
444
|
-
if (!output) { process.stderr.write('Error: missing -o / --output path\n'); process.exit(1); }
|
|
445
|
-
if (!fs.existsSync(real)) { process.stderr.write(`Error: file not found: ${real}\n`); process.exit(1); }
|
|
446
|
-
if (!fs.existsSync(synth)) { process.stderr.write(`Error: file not found: ${synth}\n`); process.exit(1); }
|
|
447
|
-
|
|
448
|
-
if (!flags.json) {
|
|
449
|
-
process.stderr.write(`Certifying ${path.basename(real)} vs ${path.basename(synth)} …\n`);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
const scriptArgs = [real, synth, '-o', output];
|
|
453
|
-
if (flags['skip-privacy']) scriptArgs.push('--skip-privacy');
|
|
454
|
-
if (flags['n-attacks']) scriptArgs.push('--n-attacks', String(flags['n-attacks']));
|
|
455
|
-
if (flags['n-secrets']) scriptArgs.push('--n-secrets', String(flags['n-secrets']));
|
|
456
|
-
if (flags['n-atk-cap']) scriptArgs.push('--n-atk-cap', String(flags['n-atk-cap']));
|
|
457
|
-
if (flags['n-neighbors']) scriptArgs.push('--n-neighbors', String(flags['n-neighbors']));
|
|
458
|
-
if (flags['n-aux-cols']) scriptArgs.push('--n-aux-cols', String(flags['n-aux-cols']));
|
|
459
|
-
if (flags.seed) scriptArgs.push('--seed', String(flags.seed));
|
|
460
|
-
if (flags.k) scriptArgs.push('--k', String(flags.k));
|
|
461
|
-
if (flags.theta) scriptArgs.push('--theta', String(flags.theta));
|
|
462
|
-
if (flags.delta) scriptArgs.push('--delta', String(flags.delta));
|
|
463
|
-
if (flags['mix-prob']) scriptArgs.push('--mix-prob', String(flags['mix-prob']));
|
|
464
|
-
if (flags['seed-gen']) scriptArgs.push('--seed-gen', String(flags['seed-gen']));
|
|
465
|
-
if (flags['generated-at']) scriptArgs.push('--generated-at', flags['generated-at']);
|
|
466
|
-
|
|
467
|
-
const result = await runPythonScript(CERTIFY_PY, scriptArgs, { json: !!flags.json });
|
|
468
|
-
|
|
469
|
-
if (flags.json) {
|
|
470
|
-
process.stdout.write(JSON.stringify(result) + '\n');
|
|
471
|
-
return;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const elapsedS = ((result.elapsedMs || 0) / 1000).toFixed(1);
|
|
475
|
-
process.stdout.write(`✓ ${output} (${elapsedS} s)\n`);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
// ── sphere demo ───────────────────────────────────────────────────────────────
|
|
479
|
-
|
|
480
|
-
async function cmdDemo() {
|
|
481
|
-
const exampleCsv = path.join(PKG_DIR, 'examples', 'nhanes_sample.csv');
|
|
482
|
-
if (!fs.existsSync(exampleCsv)) {
|
|
483
|
-
process.stderr.write('Error: built-in example not found at ' + exampleCsv + '\n');
|
|
484
|
-
process.exit(1);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const { randomUUID } = require('crypto');
|
|
488
|
-
const tmpOut = path.join(os.tmpdir(), `sphere-demo-${randomUUID()}.csv`);
|
|
489
|
-
|
|
490
|
-
process.stdout.write('SPHERE demo — built-in NHANES dataset (4,899 rows \xd7 18 cols)\n');
|
|
491
|
-
process.stdout.write('─'.repeat(52) + '\n');
|
|
492
|
-
|
|
493
|
-
try {
|
|
494
|
-
// ── Generate ──────────────────────────────────────────────────────────────
|
|
495
|
-
process.stdout.write('\n');
|
|
496
|
-
await cmdGenerate([exampleCsv], { output: tmpOut });
|
|
497
|
-
|
|
498
|
-
// ── Evaluate ──────────────────────────────────────────────────────────────
|
|
499
|
-
process.stdout.write('\n');
|
|
500
|
-
process.stderr.write(`Evaluating ${path.basename(exampleCsv)} vs synthetic …\n`);
|
|
501
|
-
|
|
502
|
-
const scriptArgs = [exampleCsv, tmpOut, '--n-attacks', '200', '--n-atk-cap', '1000'];
|
|
503
|
-
const result = await runPythonScript(EVALUATE_PY, scriptArgs);
|
|
504
|
-
|
|
505
|
-
const evalS = ((result.runMs || 0) / 1000).toFixed(1);
|
|
506
|
-
process.stdout.write(`✓ Evaluation complete (${evalS} s)\n`);
|
|
507
|
-
printEvalResults(result);
|
|
508
|
-
|
|
509
|
-
} catch (err) {
|
|
510
|
-
process.stderr.write(`\nError during demo: ${err.message}\n`);
|
|
511
|
-
} finally {
|
|
512
|
-
try { fs.unlinkSync(tmpOut); } catch {}
|
|
513
|
-
try { fs.unlinkSync(tmpOut + '.sphere.json'); } catch {}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
process.stdout.write('Try it on your own data:\n');
|
|
517
|
-
process.stdout.write(' sphere generate your_data.csv -o synthetic.csv\n');
|
|
518
|
-
process.stdout.write(' sphere evaluate your_data.csv synthetic.csv\n');
|
|
519
|
-
process.stdout.write(' sphere certify your_data.csv synthetic.csv -o report.html\n\n');
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
// ── sphere license ────────────────────────────────────────────────────────────
|
|
523
|
-
|
|
524
|
-
async function cmdLicense(sub, pos, flags) {
|
|
525
|
-
if (sub === 'activate') {
|
|
526
|
-
let key = (pos[0] || flags.key || '').trim();
|
|
527
|
-
if (!key) {
|
|
528
|
-
process.stdout.write('SPHERE license key (sphere_…): ');
|
|
529
|
-
key = await new Promise((resolve) => {
|
|
530
|
-
let buf = '';
|
|
531
|
-
process.stdin.resume();
|
|
532
|
-
process.stdin.setEncoding('utf8');
|
|
533
|
-
process.stdin.on('data', (ch) => {
|
|
534
|
-
const c = ch.toString();
|
|
535
|
-
if (c.includes('\n') || c.includes('\r')) {
|
|
536
|
-
process.stdin.pause();
|
|
537
|
-
process.stdout.write('\n');
|
|
538
|
-
resolve(buf.trim());
|
|
539
|
-
} else {
|
|
540
|
-
buf += c;
|
|
541
|
-
}
|
|
542
|
-
});
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
if (!key.startsWith('sphere_')) {
|
|
546
|
-
process.stderr.write("Error: key must start with 'sphere_'\n");
|
|
547
|
-
process.exit(1);
|
|
548
|
-
}
|
|
549
|
-
process.stdout.write('Validating …');
|
|
550
|
-
let result;
|
|
551
|
-
try { result = await validateKeyOnline(key); }
|
|
552
|
-
catch (e) {
|
|
553
|
-
process.stderr.write(`\nError: could not reach license server — ${e.message}\n`);
|
|
554
|
-
process.exit(1);
|
|
555
|
-
}
|
|
556
|
-
if (!result.valid) {
|
|
557
|
-
process.stderr.write(`\r✗ Invalid key: ${result.error || 'unknown error'}\n`);
|
|
558
|
-
process.exit(1);
|
|
559
|
-
}
|
|
560
|
-
writeLicenseKey(key);
|
|
561
|
-
writeLicenseCache(result);
|
|
562
|
-
process.stdout.write(`\r✓ License activated — ${result.customer || ''}\n`);
|
|
563
|
-
if (result.expiry) process.stdout.write(` Expires: ${result.expiry}\n`);
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
if (sub === 'status') {
|
|
568
|
-
const key = readStoredLicenseKey();
|
|
569
|
-
if (!key) {
|
|
570
|
-
process.stdout.write('✗ No license key configured.\n Run: sphere license activate <key>\n');
|
|
571
|
-
return;
|
|
572
|
-
}
|
|
573
|
-
process.stdout.write('Checking …');
|
|
574
|
-
let offline = false;
|
|
575
|
-
let result;
|
|
576
|
-
try {
|
|
577
|
-
result = await validateKeyOnline(key);
|
|
578
|
-
writeLicenseCache(result);
|
|
579
|
-
} catch {
|
|
580
|
-
result = readLicenseCache();
|
|
581
|
-
offline = true;
|
|
582
|
-
if (!result) {
|
|
583
|
-
process.stdout.write('\r✗ License server unreachable and cache expired.\n');
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
if (result.valid) {
|
|
588
|
-
const suffix = offline ? ' (offline — cached)' : '';
|
|
589
|
-
process.stdout.write(`\r✓ License valid — ${result.customer || ''}${suffix}\n`);
|
|
590
|
-
if (result.expiry) process.stdout.write(` Expires: ${result.expiry}\n`);
|
|
591
|
-
} else {
|
|
592
|
-
process.stdout.write(`\r✗ License invalid: ${result.error || 'unknown'}\n`);
|
|
593
|
-
}
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
if (sub === 'clear') {
|
|
598
|
-
let removed = false;
|
|
599
|
-
for (const f of [licenseKeyFile(), licenseCacheFile()]) {
|
|
600
|
-
if (fs.existsSync(f)) { fs.unlinkSync(f); removed = true; }
|
|
601
|
-
}
|
|
602
|
-
process.stdout.write(removed ? '✓ License cleared.\n' : 'No license stored.\n');
|
|
603
|
-
return;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
process.stderr.write('Usage: sphere license activate|status|clear\n');
|
|
607
26
|
process.exit(1);
|
|
608
27
|
}
|
|
609
28
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
const { pos, flags } = parseArgs(process.argv);
|
|
614
|
-
const cmd = pos[0];
|
|
615
|
-
|
|
616
|
-
if (flags.version || flags.v) {
|
|
617
|
-
process.stdout.write(`${VERSION}\n`);
|
|
618
|
-
process.exit(0);
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
if (!cmd || flags.help || flags.h) {
|
|
622
|
-
process.stdout.write(
|
|
623
|
-
`SPHERE CLI v${VERSION} — synthetic data generation, evaluation & certification\n\n` +
|
|
624
|
-
'Usage:\n' +
|
|
625
|
-
' sphere generate <input.csv> -o <output.csv> [options]\n' +
|
|
626
|
-
' sphere evaluate <real.csv> <synth.csv> [options]\n' +
|
|
627
|
-
' sphere certify <real.csv> <synth.csv> -o <cert.html> [options]\n' +
|
|
628
|
-
' sphere license activate|status|clear\n' +
|
|
629
|
-
' sphere demo\n\n' +
|
|
630
|
-
'Generate options:\n' +
|
|
631
|
-
' --k <int> Synthesis depth (default: 2)\n' +
|
|
632
|
-
' --theta <float> Rotation angle in radians (default: π/6 ≈ 0.524)\n' +
|
|
633
|
-
' --delta <float> Per-pair angle jitter (default: 5° ≈ 0.087)\n' +
|
|
634
|
-
' --mix-prob <float> Privacy/utility trade-off (default: 0.75)\n' +
|
|
635
|
-
' --seed <int> Integer seed for reproducible output\n' +
|
|
636
|
-
' --json Machine-readable JSON output\n\n' +
|
|
637
|
-
'Evaluate / Certify options:\n' +
|
|
638
|
-
' --skip-privacy Fidelity only — skip privacy attacks (faster)\n' +
|
|
639
|
-
' --n-attacks <int> Attack draws per metric (default: 500)\n' +
|
|
640
|
-
' --n-atk-cap <int> Row subsample cap for attacks (default: 2000)\n' +
|
|
641
|
-
' --seed <int> RNG seed for reproducible scores\n' +
|
|
642
|
-
' --json Machine-readable JSON output\n',
|
|
643
|
-
);
|
|
644
|
-
process.exit(0);
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
if (cmd === 'generate') {
|
|
648
|
-
await cmdGenerate(pos.slice(1), flags);
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
if (cmd === 'evaluate') {
|
|
653
|
-
await cmdEvaluate(pos.slice(1), flags);
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
if (cmd === 'certify') {
|
|
658
|
-
await cmdCertify(pos.slice(1), flags);
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
if (cmd === 'demo') {
|
|
663
|
-
await cmdDemo();
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
if (cmd === 'license') {
|
|
668
|
-
await cmdLicense(pos[1], pos.slice(2), flags);
|
|
669
|
-
return;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
process.stderr.write(`Unknown command: ${cmd}\nRun 'sphere --help' for usage.\n`);
|
|
29
|
+
const res = spawnSync(BIN, process.argv.slice(2), { stdio: 'inherit' });
|
|
30
|
+
if (res.error) {
|
|
31
|
+
process.stderr.write(`Failed to launch sphere: ${res.error.message}\n`);
|
|
673
32
|
process.exit(1);
|
|
674
33
|
}
|
|
675
|
-
|
|
676
|
-
main().catch((err) => {
|
|
677
|
-
process.stderr.write(`\nError: ${err.message || err}\n`);
|
|
678
|
-
process.exit(1);
|
|
679
|
-
});
|
|
34
|
+
process.exit(res.status === null ? 1 : res.status);
|