deepflow 0.1.91 → 0.1.92

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/ratchet.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * deepflow ratchet
4
4
  * Mechanical health-check gate with auto-revert on failure.
5
5
  *
6
- * Usage: node bin/ratchet.js
6
+ * Usage: node bin/ratchet.js [--task T{N}] [--worktree PATH] [--snapshot PATH]
7
7
  *
8
8
  * Outputs exactly one JSON line to stdout:
9
9
  * {"result":"PASS"}
@@ -12,6 +12,7 @@
12
12
  *
13
13
  * Exit codes: 0=PASS, 1=FAIL, 2=SALVAGEABLE
14
14
  * On FAIL: executes `git revert HEAD --no-edit` before exiting.
15
+ * On PASS + --task T{N}: updates PLAN.md [ ] → [x] and appends commit hash.
15
16
  */
16
17
 
17
18
  'use strict';
@@ -277,18 +278,79 @@ const STAGE_ORDER = ['build', 'test', 'typecheck', 'lint'];
277
278
  // Stages where failure is SALVAGEABLE (not FAIL)
278
279
  const SALVAGEABLE_STAGES = new Set(['lint']);
279
280
 
281
+ // ---------------------------------------------------------------------------
282
+ // CLI argument parser
283
+ // ---------------------------------------------------------------------------
284
+
285
+ function parseArgs(argv) {
286
+ const args = { task: null, worktree: null, snapshot: null };
287
+ for (let i = 0; i < argv.length; i++) {
288
+ if (argv[i] === '--task' && argv[i + 1]) {
289
+ args.task = argv[++i];
290
+ } else if (argv[i] === '--worktree' && argv[i + 1]) {
291
+ args.worktree = argv[++i];
292
+ } else if (argv[i] === '--snapshot' && argv[i + 1]) {
293
+ args.snapshot = argv[++i];
294
+ }
295
+ }
296
+ return args;
297
+ }
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // PLAN.md updater
301
+ // ---------------------------------------------------------------------------
302
+
303
+ function updatePlanMd(repoRoot, taskId, cwd) {
304
+ const planPath = path.join(repoRoot, 'PLAN.md');
305
+ if (!fs.existsSync(planPath)) return;
306
+
307
+ let hash = '';
308
+ try {
309
+ hash = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
310
+ encoding: 'utf8',
311
+ cwd,
312
+ stdio: ['ignore', 'pipe', 'ignore'],
313
+ }).trim();
314
+ } catch (_) {
315
+ // best-effort
316
+ }
317
+
318
+ const text = fs.readFileSync(planPath, 'utf8');
319
+ // Match lines like: - [ ] **T54** ...
320
+ const re = new RegExp(`(^.*- \\[ \\].*\\*\\*${taskId}\\*\\*.*)`, 'm');
321
+ const updated = text.replace(re, (line) => {
322
+ let result = line.replace('- [ ]', '- [x]');
323
+ if (hash) result += ` (${hash})`;
324
+ return result;
325
+ });
326
+
327
+ if (updated !== text) {
328
+ fs.writeFileSync(planPath, updated, 'utf8');
329
+ }
330
+ }
331
+
280
332
  // ---------------------------------------------------------------------------
281
333
  // Main
282
334
  // ---------------------------------------------------------------------------
283
335
 
284
336
  function main() {
285
- const cwd = process.cwd();
337
+ const cliArgs = parseArgs(process.argv.slice(2));
338
+ const cwd = cliArgs.worktree || process.cwd();
286
339
  const repoRoot = mainRepoRoot(cwd);
287
340
 
288
341
  const cfg = loadConfig(repoRoot);
289
342
  const projectType = detectProjectType(repoRoot);
290
343
  const snapshotFiles = loadSnapshotFiles(repoRoot);
291
344
  const cmds = buildCommands(repoRoot, projectType, snapshotFiles, cfg);
345
+ // --snapshot flag overrides the snapshot-derived test command
346
+ if (cliArgs.snapshot && fs.existsSync(cliArgs.snapshot)) {
347
+ const snapFiles = fs.readFileSync(cliArgs.snapshot, 'utf8')
348
+ .split('\n').map(l => l.trim()).filter(l => l.length > 0)
349
+ .map(rel => path.isAbsolute(rel) ? rel : path.join(repoRoot, rel));
350
+ if (snapFiles.length > 0 && projectType === 'node' && !cfg.test_command) {
351
+ cmds.test = ['node', '--test', ...snapFiles];
352
+ }
353
+ }
292
354
 
293
355
  for (const stage of STAGE_ORDER) {
294
356
  const cmd = cmds[stage];
@@ -321,6 +383,9 @@ function main() {
321
383
  }
322
384
 
323
385
  process.stdout.write(JSON.stringify({ result: 'PASS' }) + '\n');
386
+ if (cliArgs.task) {
387
+ updatePlanMd(repoRoot, cliArgs.task, cwd);
388
+ }
324
389
  process.exit(0);
325
390
  }
326
391
 
@@ -46,12 +46,10 @@ function rmrf(dir) {
46
46
  // ---------------------------------------------------------------------------
47
47
 
48
48
  const extractedFns = (() => {
49
- // Replace the main() call at the end with exports
50
49
  const modifiedSrc = RATCHET_SRC
51
- .replace(/^main\(\);?\s*$/m, '') // Remove the main() call
52
- .replace(/^#!.*$/m, ''); // Remove shebang
50
+ .replace(/^main\(\);?\s*$/m, '')
51
+ .replace(/^#!.*$/m, '');
53
52
 
54
- // Wrap in a function that returns the internal functions
55
53
  const wrapped = `
56
54
  ${modifiedSrc}
57
55
  return {
@@ -61,6 +59,8 @@ const extractedFns = (() => {
61
59
  parseCommand,
62
60
  hasNpmScript,
63
61
  buildCommands,
62
+ parseArgs,
63
+ updatePlanMd,
64
64
  };
65
65
  `;
66
66
 
@@ -75,6 +75,8 @@ const {
75
75
  parseCommand,
76
76
  hasNpmScript,
77
77
  buildCommands,
78
+ parseArgs,
79
+ updatePlanMd,
78
80
  } = extractedFns;
79
81
 
80
82
  // ---------------------------------------------------------------------------
@@ -867,3 +869,305 @@ describe('Structural invariants — source assertions', () => {
867
869
  assert.equal(lastNonEmpty.trim(), 'main();');
868
870
  });
869
871
  });
872
+
873
+ // ---------------------------------------------------------------------------
874
+ // 14. parseArgs — CLI argument parser
875
+ // ---------------------------------------------------------------------------
876
+
877
+ describe('parseArgs — parses --task, --worktree, --snapshot flags', () => {
878
+ test('returns all nulls when no flags provided', () => {
879
+ const args = parseArgs([]);
880
+ assert.deepEqual(args, { task: null, worktree: null, snapshot: null });
881
+ });
882
+
883
+ test('parses --task flag', () => {
884
+ const args = parseArgs(['--task', 'T54']);
885
+ assert.equal(args.task, 'T54');
886
+ assert.equal(args.worktree, null);
887
+ assert.equal(args.snapshot, null);
888
+ });
889
+
890
+ test('parses --worktree flag', () => {
891
+ const args = parseArgs(['--worktree', '/some/path']);
892
+ assert.equal(args.worktree, '/some/path');
893
+ assert.equal(args.task, null);
894
+ });
895
+
896
+ test('parses --snapshot flag', () => {
897
+ const args = parseArgs(['--snapshot', '/snap/auto-snapshot.txt']);
898
+ assert.equal(args.snapshot, '/snap/auto-snapshot.txt');
899
+ assert.equal(args.task, null);
900
+ });
901
+
902
+ test('parses all three flags together', () => {
903
+ const args = parseArgs(['--task', 'T12', '--worktree', '/w', '--snapshot', '/s']);
904
+ assert.equal(args.task, 'T12');
905
+ assert.equal(args.worktree, '/w');
906
+ assert.equal(args.snapshot, '/s');
907
+ });
908
+
909
+ test('parses flags in any order', () => {
910
+ const args = parseArgs(['--snapshot', '/s', '--task', 'T99', '--worktree', '/w']);
911
+ assert.equal(args.task, 'T99');
912
+ assert.equal(args.worktree, '/w');
913
+ assert.equal(args.snapshot, '/s');
914
+ });
915
+
916
+ test('ignores unknown flags', () => {
917
+ const args = parseArgs(['--unknown', 'val', '--task', 'T1']);
918
+ assert.equal(args.task, 'T1');
919
+ assert.equal(args.worktree, null);
920
+ });
921
+
922
+ test('ignores flag without a value (at end of argv)', () => {
923
+ const args = parseArgs(['--task']);
924
+ assert.equal(args.task, null);
925
+ });
926
+
927
+ test('ignores flag when next arg is missing (end of array)', () => {
928
+ const args = parseArgs(['--worktree']);
929
+ assert.equal(args.worktree, null);
930
+ });
931
+
932
+ test('handles task ID with various formats', () => {
933
+ assert.equal(parseArgs(['--task', 'T1']).task, 'T1');
934
+ assert.equal(parseArgs(['--task', 'T100']).task, 'T100');
935
+ assert.equal(parseArgs(['--task', 'some-string']).task, 'some-string');
936
+ });
937
+
938
+ test('last value wins when flag is repeated', () => {
939
+ const args = parseArgs(['--task', 'T1', '--task', 'T2']);
940
+ assert.equal(args.task, 'T2');
941
+ });
942
+ });
943
+
944
+ // ---------------------------------------------------------------------------
945
+ // 15. updatePlanMd — PLAN.md checkbox updater
946
+ // ---------------------------------------------------------------------------
947
+
948
+ describe('updatePlanMd — updates PLAN.md task checkboxes', () => {
949
+ let tmpDir;
950
+
951
+ beforeEach(() => {
952
+ tmpDir = makeTmpDir();
953
+ // Initialize git repo so rev-parse works
954
+ execFileSync('git', ['init'], { cwd: tmpDir, stdio: 'ignore' });
955
+ execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: tmpDir, stdio: 'ignore' });
956
+ execFileSync('git', ['config', 'user.name', 'Test'], { cwd: tmpDir, stdio: 'ignore' });
957
+ fs.writeFileSync(path.join(tmpDir, 'dummy.txt'), 'x');
958
+ execFileSync('git', ['add', '.'], { cwd: tmpDir, stdio: 'ignore' });
959
+ execFileSync('git', ['commit', '-m', 'init'], { cwd: tmpDir, stdio: 'ignore' });
960
+ });
961
+
962
+ afterEach(() => { rmrf(tmpDir); });
963
+
964
+ test('checks off matching task and appends commit hash', () => {
965
+ fs.writeFileSync(
966
+ path.join(tmpDir, 'PLAN.md'),
967
+ '# Plan\n- [ ] **T54** Write ratchet tests\n- [ ] **T55** Other task\n'
968
+ );
969
+ updatePlanMd(tmpDir, 'T54', tmpDir);
970
+
971
+ const result = fs.readFileSync(path.join(tmpDir, 'PLAN.md'), 'utf8');
972
+ assert.ok(result.includes('- [x] **T54** Write ratchet tests'));
973
+ assert.ok(!result.includes('- [ ] **T54**'));
974
+ // Should have a commit hash appended
975
+ assert.match(result, /- \[x\] \*\*T54\*\* Write ratchet tests \([a-f0-9]+\)/);
976
+ });
977
+
978
+ test('does not modify other tasks', () => {
979
+ fs.writeFileSync(
980
+ path.join(tmpDir, 'PLAN.md'),
981
+ '- [ ] **T54** Task A\n- [ ] **T55** Task B\n'
982
+ );
983
+ updatePlanMd(tmpDir, 'T54', tmpDir);
984
+
985
+ const result = fs.readFileSync(path.join(tmpDir, 'PLAN.md'), 'utf8');
986
+ assert.ok(result.includes('- [ ] **T55** Task B'));
987
+ });
988
+
989
+ test('does nothing when PLAN.md does not exist', () => {
990
+ // No PLAN.md file — should not throw
991
+ assert.doesNotThrow(() => updatePlanMd(tmpDir, 'T54', tmpDir));
992
+ });
993
+
994
+ test('does nothing when task ID not found in PLAN.md', () => {
995
+ const content = '- [ ] **T99** Some other task\n';
996
+ fs.writeFileSync(path.join(tmpDir, 'PLAN.md'), content);
997
+ updatePlanMd(tmpDir, 'T54', tmpDir);
998
+
999
+ const result = fs.readFileSync(path.join(tmpDir, 'PLAN.md'), 'utf8');
1000
+ assert.equal(result, content);
1001
+ });
1002
+
1003
+ test('does not re-check already checked task', () => {
1004
+ const content = '- [x] **T54** Already done\n';
1005
+ fs.writeFileSync(path.join(tmpDir, 'PLAN.md'), content);
1006
+ updatePlanMd(tmpDir, 'T54', tmpDir);
1007
+
1008
+ const result = fs.readFileSync(path.join(tmpDir, 'PLAN.md'), 'utf8');
1009
+ // The regex specifically matches "- [ ]" (unchecked), so already-checked should be unchanged
1010
+ assert.equal(result, content);
1011
+ });
1012
+
1013
+ test('handles PLAN.md with extra content around the task line', () => {
1014
+ fs.writeFileSync(
1015
+ path.join(tmpDir, 'PLAN.md'),
1016
+ '# Implementation Plan\n\n## Phase 1\n- [ ] **T10** First task — details here\n\n## Phase 2\n- [ ] **T20** Second task\n'
1017
+ );
1018
+ updatePlanMd(tmpDir, 'T10', tmpDir);
1019
+
1020
+ const result = fs.readFileSync(path.join(tmpDir, 'PLAN.md'), 'utf8');
1021
+ assert.ok(result.includes('- [x] **T10** First task'));
1022
+ assert.ok(result.includes('- [ ] **T20** Second task'));
1023
+ });
1024
+
1025
+ test('appends hash even with complex task description', () => {
1026
+ fs.writeFileSync(
1027
+ path.join(tmpDir, 'PLAN.md'),
1028
+ '- [ ] **T7** Implement `parseArgs()` + `updatePlanMd()` in bin/ratchet.js\n'
1029
+ );
1030
+ updatePlanMd(tmpDir, 'T7', tmpDir);
1031
+
1032
+ const result = fs.readFileSync(path.join(tmpDir, 'PLAN.md'), 'utf8');
1033
+ assert.match(result, /- \[x\] \*\*T7\*\*.*\([a-f0-9]+\)/);
1034
+ });
1035
+ });
1036
+
1037
+ // ---------------------------------------------------------------------------
1038
+ // 16. --snapshot CLI flag integration with main logic — source assertions
1039
+ // ---------------------------------------------------------------------------
1040
+
1041
+ describe('--snapshot flag — overrides snapshot-derived test command', () => {
1042
+ test('source reads --snapshot file and overrides cmds.test for node projects', () => {
1043
+ assert.ok(
1044
+ RATCHET_SRC.includes('cliArgs.snapshot'),
1045
+ 'main() should reference cliArgs.snapshot'
1046
+ );
1047
+ });
1048
+
1049
+ test('source checks snapshot file existence before reading', () => {
1050
+ assert.ok(
1051
+ RATCHET_SRC.includes("fs.existsSync(cliArgs.snapshot)"),
1052
+ 'Should check if snapshot file exists'
1053
+ );
1054
+ });
1055
+
1056
+ test('source only overrides test when no cfg.test_command and node project', () => {
1057
+ // The condition: projectType === 'node' && !cfg.test_command
1058
+ assert.ok(
1059
+ RATCHET_SRC.includes("projectType === 'node'") && RATCHET_SRC.includes('!cfg.test_command'),
1060
+ 'Snapshot override should be gated on node project type and no config test_command'
1061
+ );
1062
+ });
1063
+ });
1064
+
1065
+ // ---------------------------------------------------------------------------
1066
+ // 17. --task flag integration — updatePlanMd called only on PASS
1067
+ // ---------------------------------------------------------------------------
1068
+
1069
+ describe('--task flag — updatePlanMd called only on PASS', () => {
1070
+ test('updatePlanMd is called after PASS output and before exit(0)', () => {
1071
+ const passIdx = RATCHET_SRC.indexOf("result: 'PASS'");
1072
+ const updateIdx = RATCHET_SRC.indexOf('updatePlanMd(repoRoot, cliArgs.task, cwd)');
1073
+ const exitIdx = RATCHET_SRC.indexOf('process.exit(0)');
1074
+ assert.ok(passIdx !== -1, 'PASS output should exist');
1075
+ assert.ok(updateIdx !== -1, 'updatePlanMd call should exist');
1076
+ assert.ok(exitIdx !== -1, 'process.exit(0) should exist');
1077
+ assert.ok(passIdx < updateIdx, 'updatePlanMd should be after PASS output');
1078
+ assert.ok(updateIdx < exitIdx, 'updatePlanMd should be before exit(0)');
1079
+ });
1080
+
1081
+ test('updatePlanMd is guarded by cliArgs.task check', () => {
1082
+ assert.ok(
1083
+ RATCHET_SRC.includes('if (cliArgs.task)'),
1084
+ 'updatePlanMd call should be guarded by cliArgs.task truthiness check'
1085
+ );
1086
+ });
1087
+
1088
+ test('FAIL path does not call updatePlanMd', () => {
1089
+ // Between the FAIL output and exit(1), there should be no updatePlanMd
1090
+ const failIdx = RATCHET_SRC.indexOf("result: 'FAIL'");
1091
+ const exit1Idx = RATCHET_SRC.indexOf('process.exit(1)');
1092
+ const block = RATCHET_SRC.slice(failIdx, exit1Idx);
1093
+ assert.ok(!block.includes('updatePlanMd'), 'FAIL path should not call updatePlanMd');
1094
+ });
1095
+
1096
+ test('SALVAGEABLE path does not call updatePlanMd', () => {
1097
+ const salvIdx = RATCHET_SRC.indexOf("result: 'SALVAGEABLE'");
1098
+ const exit2Idx = RATCHET_SRC.indexOf('process.exit(2)');
1099
+ const block = RATCHET_SRC.slice(salvIdx, exit2Idx);
1100
+ assert.ok(!block.includes('updatePlanMd'), 'SALVAGEABLE path should not call updatePlanMd');
1101
+ });
1102
+ });
1103
+
1104
+ // ---------------------------------------------------------------------------
1105
+ // 18. Subprocess integration — --task flag with real execution
1106
+ // ---------------------------------------------------------------------------
1107
+
1108
+ describe('Subprocess integration — --task flag updates PLAN.md on PASS', () => {
1109
+ let tmpDir;
1110
+
1111
+ beforeEach(() => {
1112
+ tmpDir = makeTmpDir();
1113
+ execFileSync('git', ['init'], { cwd: tmpDir, stdio: 'ignore' });
1114
+ execFileSync('git', ['config', 'user.email', 'test@test.com'], { cwd: tmpDir, stdio: 'ignore' });
1115
+ execFileSync('git', ['config', 'user.name', 'Test'], { cwd: tmpDir, stdio: 'ignore' });
1116
+ });
1117
+
1118
+ afterEach(() => { rmrf(tmpDir); });
1119
+
1120
+ test('--task flag is passed through to ratchet process (PASS still returned)', () => {
1121
+ // Note: mainRepoRoot resolution for worktrees means PLAN.md update
1122
+ // behavior is tested via direct updatePlanMd unit tests above.
1123
+ // Here we verify the --task flag doesn't break normal PASS behavior.
1124
+ fs.writeFileSync(path.join(tmpDir, 'dummy.txt'), 'hello');
1125
+ execFileSync('git', ['add', '.'], { cwd: tmpDir, stdio: 'ignore' });
1126
+ execFileSync('git', ['commit', '-m', 'init'], { cwd: tmpDir, stdio: 'ignore' });
1127
+
1128
+ const result = execFileSync(process.execPath, [RATCHET_PATH, '--task', 'T42'], {
1129
+ cwd: tmpDir,
1130
+ encoding: 'utf8',
1131
+ stdio: ['ignore', 'pipe', 'pipe'],
1132
+ });
1133
+
1134
+ const parsed = JSON.parse(result.trim());
1135
+ assert.equal(parsed.result, 'PASS');
1136
+ });
1137
+
1138
+ test('--task does not update PLAN.md when no PLAN.md exists', () => {
1139
+ fs.writeFileSync(path.join(tmpDir, 'dummy.txt'), 'hello');
1140
+ execFileSync('git', ['add', '.'], { cwd: tmpDir, stdio: 'ignore' });
1141
+ execFileSync('git', ['commit', '-m', 'init'], { cwd: tmpDir, stdio: 'ignore' });
1142
+
1143
+ const result = execFileSync(process.execPath, [RATCHET_PATH, '--task', 'T42'], {
1144
+ cwd: tmpDir,
1145
+ encoding: 'utf8',
1146
+ stdio: ['ignore', 'pipe', 'pipe'],
1147
+ });
1148
+
1149
+ const parsed = JSON.parse(result.trim());
1150
+ assert.equal(parsed.result, 'PASS');
1151
+ // No PLAN.md should exist (it wasn't created)
1152
+ assert.ok(!fs.existsSync(path.join(tmpDir, 'PLAN.md')));
1153
+ });
1154
+
1155
+ test('without --task flag, PLAN.md is not modified', () => {
1156
+ fs.writeFileSync(
1157
+ path.join(tmpDir, 'PLAN.md'),
1158
+ '- [ ] **T42** Do the thing\n'
1159
+ );
1160
+ fs.writeFileSync(path.join(tmpDir, 'dummy.txt'), 'hello');
1161
+ execFileSync('git', ['add', '.'], { cwd: tmpDir, stdio: 'ignore' });
1162
+ execFileSync('git', ['commit', '-m', 'init'], { cwd: tmpDir, stdio: 'ignore' });
1163
+
1164
+ execFileSync(process.execPath, [RATCHET_PATH], {
1165
+ cwd: tmpDir,
1166
+ encoding: 'utf8',
1167
+ stdio: ['ignore', 'pipe', 'pipe'],
1168
+ });
1169
+
1170
+ const plan = fs.readFileSync(path.join(tmpDir, 'PLAN.md'), 'utf8');
1171
+ assert.ok(plan.includes('- [ ] **T42**'), 'PLAN.md should remain unchecked without --task');
1172
+ });
1173
+ });