syntropic 0.9.2 → 0.9.4
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/commands/audit.js +60 -0
- package/commands/config-utils.js +4 -4
- package/commands/init.js +18 -0
- package/commands/report.js +50 -2
- package/package.json +1 -1
package/commands/audit.js
CHANGED
|
@@ -288,6 +288,66 @@ function run(args) {
|
|
|
288
288
|
|
|
289
289
|
console.log(`\n ${dim('Run `syntropic init` to install the methodology and prevent these automatically.')}`);
|
|
290
290
|
console.log(` ${dim('Learn more: https://www.syntropicworks.com')}\n`);
|
|
291
|
+
|
|
292
|
+
// Anonymous audit ping — fire-and-forget, respects telemetry opt-out
|
|
293
|
+
sendAuditPing(score);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function sendAuditPing(score) {
|
|
297
|
+
try {
|
|
298
|
+
const os = require('os');
|
|
299
|
+
const crypto = require('crypto');
|
|
300
|
+
const https = require('https');
|
|
301
|
+
const configPath = path.join(os.homedir(), '.syntropic', 'config.json');
|
|
302
|
+
|
|
303
|
+
let anonId;
|
|
304
|
+
if (fs.existsSync(configPath)) {
|
|
305
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
306
|
+
if (config.telemetry === 'disabled' || config.telemetry === false) return;
|
|
307
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
308
|
+
anonId = crypto.createHash('sha256').update(config.device_id + date).digest('hex');
|
|
309
|
+
} else {
|
|
310
|
+
// No config yet (haven't run init) — hash hostname+user for counting
|
|
311
|
+
anonId = crypto.createHash('sha256').update(os.hostname() + os.userInfo().username).digest('hex');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Hash repo identity for per-project tracking
|
|
315
|
+
let repoId = null;
|
|
316
|
+
try {
|
|
317
|
+
const { execSync } = require('child_process');
|
|
318
|
+
let identity;
|
|
319
|
+
try {
|
|
320
|
+
identity = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
321
|
+
} catch {
|
|
322
|
+
identity = path.basename(process.cwd());
|
|
323
|
+
}
|
|
324
|
+
repoId = crypto.createHash('sha256').update(identity).digest('hex').slice(0, 16);
|
|
325
|
+
} catch {}
|
|
326
|
+
|
|
327
|
+
const payload = JSON.stringify({
|
|
328
|
+
type: 'audit',
|
|
329
|
+
schema_version: '1',
|
|
330
|
+
anon_id: anonId,
|
|
331
|
+
repo_hash: repoId,
|
|
332
|
+
score,
|
|
333
|
+
cli_version: require('../package.json').version,
|
|
334
|
+
os: process.platform,
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const url = new URL('https://www.syntropicworks.com/api/v1/prism/report');
|
|
338
|
+
const req = https.request({
|
|
339
|
+
hostname: url.hostname,
|
|
340
|
+
port: 443,
|
|
341
|
+
path: url.pathname,
|
|
342
|
+
method: 'POST',
|
|
343
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) },
|
|
344
|
+
timeout: 5000,
|
|
345
|
+
}, (res) => { res.resume(); });
|
|
346
|
+
req.on('error', () => {});
|
|
347
|
+
req.on('timeout', () => req.destroy());
|
|
348
|
+
req.write(payload);
|
|
349
|
+
req.end();
|
|
350
|
+
} catch {}
|
|
291
351
|
}
|
|
292
352
|
|
|
293
353
|
module.exports = run;
|
package/commands/config-utils.js
CHANGED
|
@@ -41,12 +41,12 @@ function saveConfig(config) {
|
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Generate the client_hash for double-hash anonymization.
|
|
44
|
-
* client_hash = SHA-256(device_id
|
|
45
|
-
*
|
|
44
|
+
* client_hash = SHA-256(device_id)
|
|
45
|
+
* Stable per install — server re-hashes with its own salt before storing.
|
|
46
|
+
* device_id is already a random UUID with no identifying information.
|
|
46
47
|
*/
|
|
47
48
|
function clientHash(deviceId) {
|
|
48
|
-
|
|
49
|
-
return crypto.createHash('sha256').update(deviceId + dailySalt).digest('hex');
|
|
49
|
+
return crypto.createHash('sha256').update(deviceId).digest('hex');
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
/**
|
package/commands/init.js
CHANGED
|
@@ -298,6 +298,24 @@ async function run(args) {
|
|
|
298
298
|
// Ensure ~/.syntropic/config.json exists (device_id, hmac_key, telemetry)
|
|
299
299
|
ensureConfig();
|
|
300
300
|
|
|
301
|
+
// Install syntropic as a dev dependency so `syntropic report` works in every session
|
|
302
|
+
const pkgJsonPath = path.join(targetDir, 'package.json');
|
|
303
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
304
|
+
try {
|
|
305
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
306
|
+
const hasDep = pkg.dependencies?.syntropic || pkg.devDependencies?.syntropic;
|
|
307
|
+
if (!hasDep) {
|
|
308
|
+
const { execSync } = require('child_process');
|
|
309
|
+
console.log(' install syntropic as dev dependency (enables `syntropic report` in every session)');
|
|
310
|
+
execSync('npm install --save-dev syntropic', { cwd: targetDir, stdio: 'pipe', timeout: 30000 });
|
|
311
|
+
} else {
|
|
312
|
+
console.log(' skip syntropic already in package.json');
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
console.log(' skip could not install syntropic as dev dependency (install manually: npm i -D syntropic)');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
301
319
|
console.log(`
|
|
302
320
|
Done! Your project is set up with the Syntropic pipeline.
|
|
303
321
|
|
package/commands/report.js
CHANGED
|
@@ -14,6 +14,21 @@ const { loadConfig, clientHash, hmacSign } = require('./config-utils');
|
|
|
14
14
|
const REPORT_URL = 'https://www.syntropicworks.com/api/v1/prism/report';
|
|
15
15
|
const SCHEMA_VERSION = '1.0';
|
|
16
16
|
|
|
17
|
+
function repoHash() {
|
|
18
|
+
try {
|
|
19
|
+
const { execSync } = require('child_process');
|
|
20
|
+
let identity;
|
|
21
|
+
try {
|
|
22
|
+
identity = execSync('git remote get-url origin 2>/dev/null', { encoding: 'utf8' }).trim();
|
|
23
|
+
} catch {
|
|
24
|
+
identity = require('path').basename(process.cwd());
|
|
25
|
+
}
|
|
26
|
+
return crypto.createHash('sha256').update(identity).digest('hex').slice(0, 16);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
17
32
|
function parseFlags(args) {
|
|
18
33
|
const flags = {};
|
|
19
34
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -98,6 +113,27 @@ function autoDetectPhases(weight) {
|
|
|
98
113
|
}
|
|
99
114
|
}
|
|
100
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Estimate tokens saved by applying the methodology.
|
|
118
|
+
*
|
|
119
|
+
* Without methodology: developers typically iterate 2-4x on changes
|
|
120
|
+
* (write code → find bug → fix → find another → fix → realize arch wrong → refactor).
|
|
121
|
+
* Each iteration costs tokens to read files, write code, and verify.
|
|
122
|
+
*
|
|
123
|
+
* Conservative estimates:
|
|
124
|
+
* - Avg tokens per file interaction: ~3,000 (read + edit + verify)
|
|
125
|
+
* - Baseline iterations without methodology: full=3.5, lightweight=2.5, minimum=1.5
|
|
126
|
+
* - With methodology: 1 iteration (research/design catches issues before code)
|
|
127
|
+
* - Saved iterations = baseline - 1
|
|
128
|
+
*/
|
|
129
|
+
function estimateTokensSaved(weight, filesChanged) {
|
|
130
|
+
const tokensPerFileIteration = 3000;
|
|
131
|
+
const baselineIterations = { full: 3.5, lightweight: 2.5, minimum: 1.5 };
|
|
132
|
+
const baseline = baselineIterations[weight] || 2;
|
|
133
|
+
const savedIterations = Math.max(0, baseline - 1);
|
|
134
|
+
return Math.round(filesChanged * tokensPerFileIteration * savedIterations);
|
|
135
|
+
}
|
|
136
|
+
|
|
101
137
|
function detectGovernanceDocs() {
|
|
102
138
|
const fs = require('fs');
|
|
103
139
|
const path = require('path');
|
|
@@ -220,6 +256,7 @@ async function run(args) {
|
|
|
220
256
|
schema_version: SCHEMA_VERSION,
|
|
221
257
|
cli_version: require('../package.json').version,
|
|
222
258
|
idempotency_key: idempotencyKey,
|
|
259
|
+
repo_hash: repoHash(),
|
|
223
260
|
cycle: {
|
|
224
261
|
weight,
|
|
225
262
|
phases_run: phases,
|
|
@@ -256,7 +293,7 @@ async function run(args) {
|
|
|
256
293
|
await new Promise((resolve) => {
|
|
257
294
|
const req = https.request(options, (res) => {
|
|
258
295
|
if (res.statusCode === 200 || res.statusCode === 201) {
|
|
259
|
-
//
|
|
296
|
+
// Output: cycle summary + token savings estimate
|
|
260
297
|
const parts = [`Syntropic: ${weight} cycle`, `${shape.changed_count || '?'} files`];
|
|
261
298
|
if (success) parts.push('build passing');
|
|
262
299
|
const docUpdates = [];
|
|
@@ -264,7 +301,18 @@ async function run(args) {
|
|
|
264
301
|
if (governance.backlog_updated) docUpdates.push('BACKLOG');
|
|
265
302
|
if (governance.adr_created) docUpdates.push('ADR');
|
|
266
303
|
if (docUpdates.length > 0) parts.push(`docs: ${docUpdates.join(', ')}`);
|
|
267
|
-
|
|
304
|
+
const saved = estimateTokensSaved(weight, shape.changed_count || 1);
|
|
305
|
+
if (saved > 0) {
|
|
306
|
+
const formatted = saved >= 1000 ? `~${Math.round(saved / 1000)}k` : `~${saved}`;
|
|
307
|
+
parts.push(`${formatted} tokens saved`);
|
|
308
|
+
}
|
|
309
|
+
const line = parts.join(' | ');
|
|
310
|
+
const w = Math.max(55, line.length + 4);
|
|
311
|
+
console.log('');
|
|
312
|
+
console.log(' ' + '\u250C' + '\u2500'.repeat(w) + '\u2510');
|
|
313
|
+
console.log(' ' + '\u2502' + ' ' + line + ' '.repeat(w - line.length - 2) + '\u2502');
|
|
314
|
+
console.log(' ' + '\u2514' + '\u2500'.repeat(w) + '\u2518');
|
|
315
|
+
console.log('');
|
|
268
316
|
} else {
|
|
269
317
|
console.log(` PRISM report: server returned ${res.statusCode} (non-blocking).`);
|
|
270
318
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "syntropic",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "Ship better software with a proven development methodology. Audit your git history, install disciplined rules, and track iterations — for Claude Code, Cursor, Windsurf, GitHub Copilot, and OpenAI Codex.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"syntropic": "./bin/syntropic.js"
|