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 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;
@@ -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 + YYYY-MM-DD)
45
- * This rotates daily — server re-hashes with its own salt.
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
- const dailySalt = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
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
 
@@ -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
- // Honest, useful output
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
- console.log(` ${parts.join(' | ')}`);
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.2",
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"