reslop 0.2.0 → 0.2.1

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.
@@ -72,26 +72,127 @@ const closeWatcher = (watcher) => {
72
72
  }
73
73
  };
74
74
 
75
- const schedule = (state) => {
76
- if (state.closed) return;
77
- if (state.timer !== null) clearTimeout(state.timer);
78
- state.timer = setTimeout(() => {
79
- state.timer = null;
75
+ const reviewMarkdown = (rel) => {
76
+ const norm = `${rel}`.replaceAll('\\', '/');
77
+ if (!norm.startsWith('.review/')) return false;
78
+ const rest = norm.slice('.review/'.length);
79
+ if (!rest || rest.includes('/')) return false;
80
+ return rest.endsWith('.md');
81
+ };
82
+
83
+ const isReviewDir = (root, dir) => {
84
+ const rel = path.relative(root, dir).replaceAll('\\', '/');
85
+ return rel === '.review' || rel.startsWith('.review/');
86
+ };
87
+
88
+ const hasFile = (full) => {
89
+ try {
90
+ fs.accessSync(full);
91
+ return true;
92
+ } catch {
93
+ return false;
94
+ }
95
+ };
96
+
97
+ const isBareReviewName = (root, dir, rel) => {
98
+ if (!rel || rel.includes('/') || !rel.endsWith('.md')) return false;
99
+ const base = path.basename(rel);
100
+ if (hasFile(path.join(dir, base))) return false;
101
+ return hasFile(path.join(root, '.review', base));
102
+ };
103
+
104
+ const isReviewEvent = (root, dir, name) => {
105
+ if (isReviewDir(root, dir)) return true;
106
+ if (!name) return false;
107
+ const rel = path.relative(root, path.join(dir, name));
108
+ const norm = rel.replaceAll('\\', '/');
109
+ if (reviewMarkdown(norm)) return true;
110
+ return isBareReviewName(root, dir, norm);
111
+ };
112
+
113
+ const isOwnDirEvent = (dir, name) => {
114
+ if (!name) return false;
115
+ const base = path.basename(dir);
116
+ const slash = `${name}`.replaceAll('\\', '/');
117
+ const bare = slash.startsWith('./') ? slash.slice(2) : slash;
118
+ const trimmed = bare.endsWith('/') ? bare.slice(0, -1) : bare;
119
+ if (trimmed === base) {
120
+ try {
121
+ return !fs.statSync(path.join(dir, trimmed)).isFile();
122
+ } catch {
123
+ return true;
124
+ }
125
+ }
126
+ const full = path.resolve(dir, name);
127
+ if (full === path.resolve(dir)) return true;
128
+ if (path.basename(full) !== base) return false;
129
+ try {
130
+ return fs.realpathSync(full) === fs.realpathSync(dir);
131
+ } catch {
132
+ return false;
133
+ }
134
+ };
135
+
136
+ const schedule = (state, timerKey, fire) => {
137
+ if (state.closed || !fire) return;
138
+ if (state[timerKey] !== null) clearTimeout(state[timerKey]);
139
+ state[timerKey] = setTimeout(() => {
140
+ state[timerKey] = null;
80
141
  if (state.closed) return;
81
- state.onChange();
142
+ fire();
82
143
  }, state.debounceMs);
83
- if (typeof state.timer.unref === 'function') state.timer.unref();
144
+ const timer = state[timerKey];
145
+ if (timer && typeof timer.unref === 'function') timer.unref();
146
+ };
147
+
148
+ const REVIEW_HOLD_MS = 200;
149
+
150
+ const reviewBurst = (state) => {
151
+ const seen = state.reviewSeenAt;
152
+ if (!seen) return false;
153
+ const hold = Math.max(state.debounceMs * 2, REVIEW_HOLD_MS);
154
+ return Date.now() - seen < hold;
155
+ };
156
+
157
+ const scheduleChange = (state) => {
158
+ schedule(state, 'timer', () => {
159
+ if (reviewBurst(state)) return;
160
+ if (state.onChange) state.onChange();
161
+ });
162
+ };
163
+
164
+ const scheduleUnnamed = (state) => {
165
+ if (reviewBurst(state)) return;
166
+ schedule(state, 'looseTimer', () => {
167
+ if (reviewBurst(state)) return;
168
+ scheduleChange(state);
169
+ });
170
+ };
171
+
172
+ const dropChange = (state) => {
173
+ if (state.timer !== null) clearTimeout(state.timer);
174
+ if (state.looseTimer !== null) clearTimeout(state.looseTimer);
175
+ state.timer = null;
176
+ state.looseTimer = null;
84
177
  };
85
178
 
86
179
  const handleWatchEvent = (state, dir, eventType, filename) => {
87
180
  if (state.closed) return;
88
181
  const name = filename ? `${filename}` : '';
89
182
  const rel = name ? path.relative(state.root, path.join(dir, name)) : '';
183
+ if (isReviewEvent(state.root, dir, name)) {
184
+ state.reviewSeenAt = Date.now();
185
+ dropChange(state);
186
+ return void schedule(state, 'reviewTimer', state.onReview);
187
+ }
90
188
  if (rel && ignoredRel(rel)) return;
189
+ if (reviewBurst(state)) return;
190
+ if (!name) return void scheduleUnnamed(state);
191
+ if (isOwnDirEvent(dir, name)) return;
91
192
  if (eventType === 'rename' && name) {
92
193
  state.watchNewDir(path.join(dir, name));
93
194
  }
94
- schedule(state);
195
+ scheduleChange(state);
95
196
  };
96
197
 
97
198
  const watchOne = (state, target, extra = {}) => {
@@ -141,6 +242,7 @@ const maybeWatchNewDir = (state, full) => {
141
242
  return;
142
243
  }
143
244
  if (!stat.isDirectory()) return;
245
+ if (path.basename(full) === '.review') return void watchOne(state, full);
144
246
  if (skipDirName(path.basename(full))) return;
145
247
  watchTree(state, full);
146
248
  };
@@ -166,7 +268,11 @@ const watchGit = (state) => {
166
268
  const stopState = (state) => {
167
269
  state.closed = true;
168
270
  if (state.timer !== null) clearTimeout(state.timer);
271
+ if (state.reviewTimer !== null) clearTimeout(state.reviewTimer);
272
+ if (state.looseTimer !== null) clearTimeout(state.looseTimer);
169
273
  state.timer = null;
274
+ state.reviewTimer = null;
275
+ state.looseTimer = null;
170
276
  while (state.watchers.length) closeWatcher(state.watchers.pop());
171
277
  state.watched.clear();
172
278
  };
@@ -189,13 +295,18 @@ const createDiskWatcher = (options) => {
189
295
  dirWatchOpts: recursive ? { recursive: true } : {},
190
296
  watchers: [],
191
297
  watched: new Set(),
298
+ onReview: options.onReview,
192
299
  timer: null,
300
+ reviewTimer: null,
301
+ looseTimer: null,
302
+ reviewSeenAt: 0,
193
303
  closed: false,
194
304
  watchNewDir: (full) => maybeWatchNewDir(state, full),
195
305
  };
196
306
  const scopes = options.paths && options.paths.length ? options.paths : ['.'];
197
307
  for (const spec of scopes) watchScope(state, spec);
198
308
  watchGit(state);
309
+ watchOne(state, path.resolve(state.root, '.review'));
199
310
  return {
200
311
  close: () => stopState(state),
201
312
  watchFiles: (rels) => watchFiles(state, rels),
@@ -207,5 +318,6 @@ module.exports = {
207
318
  POLL_MS,
208
319
  canWatchRecursive,
209
320
  ignoredRel,
321
+ isOwnDirEvent,
210
322
  createDiskWatcher,
211
323
  };
package/lib/session.js CHANGED
@@ -394,6 +394,7 @@ class Session {
394
394
  this.status = result.error.message;
395
395
  return false;
396
396
  }
397
+ if (this.lifecycle) this.lifecycle.noteReviewStamp();
397
398
  return result.wrote;
398
399
  }
399
400
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reslop",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Interactive code review and repair planning for AI-assisted changes",
5
5
  "keywords": [
6
6
  "code-review",
@@ -59,7 +59,7 @@
59
59
  "eslint": "^9.39.4",
60
60
  "eslint-config-metarhia": "^9.1.9",
61
61
  "metaskills": "^1.0.5",
62
- "prettier": "^3.9.8"
62
+ "prettier": "^3.9.9"
63
63
  },
64
64
  "dependencies": {
65
65
  "metautil": "^5.5.2"