deepflow 0.1.90 → 0.1.91

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/install.js CHANGED
@@ -241,6 +241,7 @@ async function configureHooks(claudeDir) {
241
241
  const dashboardPushCmd = `node "${path.join(claudeDir, 'hooks', 'df-dashboard-push.js')}"`;
242
242
  const executionHistoryCmd = `node "${path.join(claudeDir, 'hooks', 'df-execution-history.js')}"`;
243
243
  const worktreeGuardCmd = `node "${path.join(claudeDir, 'hooks', 'df-worktree-guard.js')}"`;
244
+ const snapshotGuardCmd = `node "${path.join(claudeDir, 'hooks', 'df-snapshot-guard.js')}"`;
244
245
  const invariantCheckCmd = `node "${path.join(claudeDir, 'hooks', 'df-invariant-check.js')}"`;
245
246
 
246
247
  let settings = {};
@@ -347,10 +348,10 @@ async function configureHooks(claudeDir) {
347
348
  settings.hooks.PostToolUse = [];
348
349
  }
349
350
 
350
- // Remove any existing deepflow tool usage / execution history / worktree guard / invariant check hooks from PostToolUse
351
+ // Remove any existing deepflow tool usage / execution history / worktree guard / snapshot guard / invariant check hooks from PostToolUse
351
352
  settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
352
353
  const cmd = hook.hooks?.[0]?.command || '';
353
- return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-invariant-check');
354
+ return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-snapshot-guard') && !cmd.includes('df-invariant-check');
354
355
  });
355
356
 
356
357
  // Add tool usage hook
@@ -377,6 +378,14 @@ async function configureHooks(claudeDir) {
377
378
  }]
378
379
  });
379
380
 
381
+ // Add snapshot guard hook (blocks Write/Edit to ratchet-baseline files in auto-snapshot.txt)
382
+ settings.hooks.PostToolUse.push({
383
+ hooks: [{
384
+ type: 'command',
385
+ command: snapshotGuardCmd
386
+ }]
387
+ });
388
+
380
389
  // Add invariant check hook (exits 1 on hard failures after git commit)
381
390
  settings.hooks.PostToolUse.push({
382
391
  hooks: [{
@@ -566,7 +575,7 @@ async function uninstall() {
566
575
  ];
567
576
 
568
577
  if (level === 'global') {
569
- toRemove.push('hooks/df-statusline.js', 'hooks/df-check-update.js', 'hooks/df-invariant-check.js', 'hooks/df-quota-logger.js', 'hooks/df-tool-usage.js', 'hooks/df-dashboard-push.js', 'hooks/df-execution-history.js', 'hooks/df-worktree-guard.js');
578
+ toRemove.push('hooks/df-statusline.js', 'hooks/df-check-update.js', 'hooks/df-invariant-check.js', 'hooks/df-quota-logger.js', 'hooks/df-tool-usage.js', 'hooks/df-dashboard-push.js', 'hooks/df-execution-history.js', 'hooks/df-worktree-guard.js', 'hooks/df-snapshot-guard.js');
570
579
  }
571
580
 
572
581
  for (const item of toRemove) {
@@ -613,7 +622,7 @@ async function uninstall() {
613
622
  if (settings.hooks?.PostToolUse) {
614
623
  settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(hook => {
615
624
  const cmd = hook.hooks?.[0]?.command || '';
616
- return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-invariant-check');
625
+ return !cmd.includes('df-tool-usage') && !cmd.includes('df-execution-history') && !cmd.includes('df-worktree-guard') && !cmd.includes('df-snapshot-guard') && !cmd.includes('df-invariant-check');
617
626
  });
618
627
  if (settings.hooks.PostToolUse.length === 0) {
619
628
  delete settings.hooks.PostToolUse;
package/bin/ratchet.js ADDED
@@ -0,0 +1,327 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * deepflow ratchet
4
+ * Mechanical health-check gate with auto-revert on failure.
5
+ *
6
+ * Usage: node bin/ratchet.js
7
+ *
8
+ * Outputs exactly one JSON line to stdout:
9
+ * {"result":"PASS"}
10
+ * {"result":"FAIL","stage":"build","log":"..."}
11
+ * {"result":"SALVAGEABLE","stage":"lint","log":"..."}
12
+ *
13
+ * Exit codes: 0=PASS, 1=FAIL, 2=SALVAGEABLE
14
+ * On FAIL: executes `git revert HEAD --no-edit` before exiting.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { execFileSync, spawnSync } = require('child_process');
22
+
23
+ // ---------------------------------------------------------------------------
24
+ // Git / path helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function gitCommonDir(cwd) {
28
+ try {
29
+ return execFileSync('git', ['rev-parse', '--git-common-dir'], {
30
+ encoding: 'utf8',
31
+ cwd,
32
+ stdio: ['ignore', 'pipe', 'ignore'],
33
+ }).trim();
34
+ } catch (_) {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function mainRepoRoot(cwd) {
40
+ const commonDir = gitCommonDir(cwd);
41
+ if (!commonDir) return cwd;
42
+ // --git-common-dir returns absolute path for worktrees, relative for normal repos
43
+ const absCommonDir = path.isAbsolute(commonDir)
44
+ ? commonDir
45
+ : path.resolve(cwd, commonDir);
46
+ // common dir is <repo>/.git for normal repos, <repo>/.git/worktrees/<name> for worktrees
47
+ // Walk up until we find a directory that is not inside .git
48
+ return path.resolve(absCommonDir, '..', '..').replace(/[/\\]\.git.*$/, '') ||
49
+ path.dirname(absCommonDir);
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Config loading (simple regex, consistent with bin/install.js style)
54
+ // ---------------------------------------------------------------------------
55
+
56
+ function loadConfig(repoRoot) {
57
+ const configPath = path.join(repoRoot, '.deepflow', 'config.yaml');
58
+ const cfg = {};
59
+ if (!fs.existsSync(configPath)) return cfg;
60
+
61
+ const text = fs.readFileSync(configPath, 'utf8');
62
+
63
+ const keys = ['build_command', 'test_command', 'typecheck_command', 'lint_command'];
64
+ for (const key of keys) {
65
+ const m = text.match(new RegExp(`^${key}:\\s*["']?([^"'\\n]+?)["']?\\s*$`, 'm'));
66
+ if (m) cfg[key] = m[1].trim();
67
+ }
68
+
69
+ return cfg;
70
+ }
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Snapshot: read auto-snapshot.txt and absolutize paths
74
+ // ---------------------------------------------------------------------------
75
+
76
+ function loadSnapshotFiles(repoRoot) {
77
+ const snapshotPath = path.join(repoRoot, '.deepflow', 'auto-snapshot.txt');
78
+ if (!fs.existsSync(snapshotPath)) return [];
79
+
80
+ return fs.readFileSync(snapshotPath, 'utf8')
81
+ .split('\n')
82
+ .map(l => l.trim())
83
+ .filter(l => l.length > 0)
84
+ .map(rel => path.join(repoRoot, rel));
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // Project type detection
89
+ // ---------------------------------------------------------------------------
90
+
91
+ function detectProjectType(repoRoot) {
92
+ if (fs.existsSync(path.join(repoRoot, 'package.json'))) return 'node';
93
+ if (fs.existsSync(path.join(repoRoot, 'pyproject.toml'))) return 'python';
94
+ if (fs.existsSync(path.join(repoRoot, 'Cargo.toml'))) return 'rust';
95
+ if (fs.existsSync(path.join(repoRoot, 'go.mod'))) return 'go';
96
+ return 'unknown';
97
+ }
98
+
99
+ function hasNpmScript(repoRoot, scriptName) {
100
+ try {
101
+ const pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
102
+ return !!(pkg.scripts && pkg.scripts[scriptName]);
103
+ } catch (_) {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Command builders per project type
110
+ // ---------------------------------------------------------------------------
111
+
112
+ function buildCommands(repoRoot, projectType, snapshotFiles, cfg) {
113
+ const cmds = {};
114
+
115
+ if (projectType === 'node') {
116
+ // build
117
+ if (cfg.build_command) {
118
+ cmds.build = cfg.build_command;
119
+ } else if (hasNpmScript(repoRoot, 'build')) {
120
+ cmds.build = 'npm run build';
121
+ }
122
+ // test — always use snapshot files, never test-discovery flags
123
+ if (cfg.test_command) {
124
+ cmds.test = cfg.test_command;
125
+ } else if (snapshotFiles.length > 0) {
126
+ cmds.test = ['node', '--test', ...snapshotFiles];
127
+ }
128
+ // typecheck
129
+ if (cfg.typecheck_command) {
130
+ cmds.typecheck = cfg.typecheck_command;
131
+ } else {
132
+ // only add if tsc is available
133
+ cmds.typecheck = 'npx tsc --noEmit';
134
+ }
135
+ // lint
136
+ if (cfg.lint_command) {
137
+ cmds.lint = cfg.lint_command;
138
+ } else if (hasNpmScript(repoRoot, 'lint')) {
139
+ cmds.lint = 'npm run lint';
140
+ }
141
+
142
+ } else if (projectType === 'python') {
143
+ // build: skip
144
+ if (cfg.build_command) cmds.build = cfg.build_command;
145
+ // test
146
+ if (cfg.test_command) {
147
+ cmds.test = cfg.test_command;
148
+ } else if (snapshotFiles.length > 0) {
149
+ cmds.test = ['pytest', ...snapshotFiles];
150
+ } else {
151
+ cmds.test = 'pytest';
152
+ }
153
+ // typecheck
154
+ if (cfg.typecheck_command) {
155
+ cmds.typecheck = cfg.typecheck_command;
156
+ } else {
157
+ cmds.typecheck = 'mypy .';
158
+ }
159
+ // lint
160
+ if (cfg.lint_command) {
161
+ cmds.lint = cfg.lint_command;
162
+ } else {
163
+ cmds.lint = 'ruff check .';
164
+ }
165
+
166
+ } else if (projectType === 'rust') {
167
+ cmds.build = cfg.build_command || 'cargo build';
168
+ cmds.test = cfg.test_command || 'cargo test';
169
+ // typecheck: skip (cargo build covers it)
170
+ if (cfg.typecheck_command) cmds.typecheck = cfg.typecheck_command;
171
+ cmds.lint = cfg.lint_command || 'cargo clippy';
172
+
173
+ } else if (projectType === 'go') {
174
+ cmds.build = cfg.build_command || 'go build ./...';
175
+ // go test doesn't work well with individual file paths — use packages
176
+ cmds.test = cfg.test_command || 'go test ./...';
177
+ // typecheck: skip
178
+ if (cfg.typecheck_command) cmds.typecheck = cfg.typecheck_command;
179
+ cmds.lint = cfg.lint_command || 'go vet ./...';
180
+
181
+ } else {
182
+ // unknown: only use config overrides
183
+ if (cfg.build_command) cmds.build = cfg.build_command;
184
+ if (cfg.test_command) cmds.test = cfg.test_command;
185
+ if (cfg.typecheck_command) cmds.typecheck = cfg.typecheck_command;
186
+ if (cfg.lint_command) cmds.lint = cfg.lint_command;
187
+ }
188
+
189
+ return cmds;
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Command runner
194
+ // ---------------------------------------------------------------------------
195
+
196
+ // Parse a string command into [executable, ...args] safely
197
+ function parseCommand(cmd) {
198
+ // Very simple tokenizer — handles quoted strings and plain tokens
199
+ const tokens = [];
200
+ let current = '';
201
+ let inQuote = null;
202
+
203
+ for (let i = 0; i < cmd.length; i++) {
204
+ const ch = cmd[i];
205
+ if (inQuote) {
206
+ if (ch === inQuote) { inQuote = null; }
207
+ else { current += ch; }
208
+ } else if (ch === '"' || ch === "'") {
209
+ inQuote = ch;
210
+ } else if (ch === ' ') {
211
+ if (current) { tokens.push(current); current = ''; }
212
+ } else {
213
+ current += ch;
214
+ }
215
+ }
216
+ if (current) tokens.push(current);
217
+ return tokens;
218
+ }
219
+
220
+ /**
221
+ * Run a command (string or array). Returns { ok, log }.
222
+ * Captures stdout+stderr combined for the log.
223
+ */
224
+ function runCommand(cmd, cwd) {
225
+ const args = Array.isArray(cmd) ? cmd : parseCommand(cmd);
226
+ const [exe, ...rest] = args;
227
+
228
+ const result = spawnSync(exe, rest, {
229
+ cwd,
230
+ encoding: 'utf8',
231
+ stdio: ['ignore', 'pipe', 'pipe'],
232
+ shell: false,
233
+ });
234
+
235
+ const log = ((result.stdout || '') + (result.stderr || '')).trim();
236
+
237
+ if (result.error) {
238
+ // executable not found / not available — treat as skip
239
+ return { ok: null, log: result.error.message };
240
+ }
241
+
242
+ return { ok: result.status === 0, log };
243
+ }
244
+
245
+ /**
246
+ * Check if an executable is available on PATH.
247
+ */
248
+ function commandExists(exe) {
249
+ const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [exe], {
250
+ encoding: 'utf8',
251
+ stdio: ['ignore', 'pipe', 'ignore'],
252
+ });
253
+ return result.status === 0;
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Auto-revert
258
+ // ---------------------------------------------------------------------------
259
+
260
+ function autoRevert(cwd) {
261
+ try {
262
+ spawnSync('git', ['revert', 'HEAD', '--no-edit'], {
263
+ cwd,
264
+ stdio: 'ignore',
265
+ });
266
+ } catch (_) {
267
+ // best-effort
268
+ }
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // Health-check stages in order
273
+ // ---------------------------------------------------------------------------
274
+
275
+ const STAGE_ORDER = ['build', 'test', 'typecheck', 'lint'];
276
+
277
+ // Stages where failure is SALVAGEABLE (not FAIL)
278
+ const SALVAGEABLE_STAGES = new Set(['lint']);
279
+
280
+ // ---------------------------------------------------------------------------
281
+ // Main
282
+ // ---------------------------------------------------------------------------
283
+
284
+ function main() {
285
+ const cwd = process.cwd();
286
+ const repoRoot = mainRepoRoot(cwd);
287
+
288
+ const cfg = loadConfig(repoRoot);
289
+ const projectType = detectProjectType(repoRoot);
290
+ const snapshotFiles = loadSnapshotFiles(repoRoot);
291
+ const cmds = buildCommands(repoRoot, projectType, snapshotFiles, cfg);
292
+
293
+ for (const stage of STAGE_ORDER) {
294
+ const cmd = cmds[stage];
295
+ if (!cmd) continue; // stage not applicable
296
+
297
+ // For string commands, check if the primary executable exists
298
+ if (typeof cmd === 'string') {
299
+ const exe = parseCommand(cmd)[0];
300
+ // npx is always available if npm is; skip existence check for npx
301
+ if (exe !== 'npx' && !commandExists(exe)) continue;
302
+ }
303
+
304
+ const { ok, log } = runCommand(cmd, repoRoot);
305
+
306
+ if (ok === null) {
307
+ // executable spawning error — skip stage
308
+ continue;
309
+ }
310
+
311
+ if (!ok) {
312
+ if (SALVAGEABLE_STAGES.has(stage)) {
313
+ process.stdout.write(JSON.stringify({ result: 'SALVAGEABLE', stage, log }) + '\n');
314
+ process.exit(2);
315
+ } else {
316
+ autoRevert(cwd);
317
+ process.stdout.write(JSON.stringify({ result: 'FAIL', stage, log }) + '\n');
318
+ process.exit(1);
319
+ }
320
+ }
321
+ }
322
+
323
+ process.stdout.write(JSON.stringify({ result: 'PASS' }) + '\n');
324
+ process.exit(0);
325
+ }
326
+
327
+ main();