logicstamp-context 0.8.3 → 0.8.5

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.
Files changed (56) hide show
  1. package/LICENSE +21 -21
  2. package/LLM_CONTEXT.md +703 -703
  3. package/README.md +412 -403
  4. package/dist/cli/commands/clean.d.ts.map +1 -1
  5. package/dist/cli/commands/clean.js +1 -6
  6. package/dist/cli/commands/clean.js.map +1 -1
  7. package/dist/cli/commands/compare/core.js +1 -1
  8. package/dist/cli/commands/compare/core.js.map +1 -1
  9. package/dist/cli/commands/context/fileWriter.js +2 -2
  10. package/dist/cli/commands/context/fileWriter.js.map +1 -1
  11. package/dist/cli/commands/context/statsCalculator.d.ts +3 -2
  12. package/dist/cli/commands/context/statsCalculator.d.ts.map +1 -1
  13. package/dist/cli/commands/context/statsCalculator.js +45 -104
  14. package/dist/cli/commands/context/statsCalculator.js.map +1 -1
  15. package/dist/cli/commands/context/watchMode/watchMode.d.ts.map +1 -1
  16. package/dist/cli/commands/context/watchMode/watchMode.js +252 -256
  17. package/dist/cli/commands/context/watchMode/watchMode.js.map +1 -1
  18. package/dist/cli/commands/init.js +20 -20
  19. package/dist/cli/commands/security.js +3 -3
  20. package/dist/cli/commands/security.js.map +1 -1
  21. package/dist/cli/commands/validate.d.ts.map +1 -1
  22. package/dist/cli/commands/validate.js +1 -6
  23. package/dist/cli/commands/validate.js.map +1 -1
  24. package/dist/cli/handlers/initHandler.js +5 -5
  25. package/dist/cli/index.d.ts +6 -2
  26. package/dist/cli/index.d.ts.map +1 -1
  27. package/dist/cli/index.js +100 -96
  28. package/dist/cli/index.js.map +1 -1
  29. package/dist/cli/parser/helpText.js +570 -570
  30. package/dist/cli/validate-index.js +26 -26
  31. package/dist/core/astParser/detectors.d.ts.map +1 -1
  32. package/dist/core/astParser/detectors.js +3 -2
  33. package/dist/core/astParser/detectors.js.map +1 -1
  34. package/dist/core/pack/loader.js +3 -3
  35. package/dist/core/pack/loader.js.map +1 -1
  36. package/dist/utils/codeSanitizer.d.ts.map +1 -1
  37. package/dist/utils/codeSanitizer.js +3 -2
  38. package/dist/utils/codeSanitizer.js.map +1 -1
  39. package/dist/utils/fileLock.d.ts.map +1 -1
  40. package/dist/utils/fileLock.js +70 -6
  41. package/dist/utils/fileLock.js.map +1 -1
  42. package/dist/utils/fsx.d.ts +5 -0
  43. package/dist/utils/fsx.d.ts.map +1 -1
  44. package/dist/utils/fsx.js +9 -2
  45. package/dist/utils/fsx.js.map +1 -1
  46. package/dist/utils/git.d.ts.map +1 -1
  47. package/dist/utils/git.js +2 -1
  48. package/dist/utils/git.js.map +1 -1
  49. package/dist/utils/gitignore.d.ts.map +1 -1
  50. package/dist/utils/gitignore.js +2 -1
  51. package/dist/utils/gitignore.js.map +1 -1
  52. package/dist/utils/secretDetector.d.ts.map +1 -1
  53. package/dist/utils/secretDetector.js +11 -6
  54. package/dist/utils/secretDetector.js.map +1 -1
  55. package/package.json +85 -84
  56. package/schema/logicstamp.context.schema.json +1250 -1250
@@ -4,7 +4,7 @@
4
4
  import { resolve, dirname, join, relative } from 'node:path';
5
5
  import { readFile } from 'node:fs/promises';
6
6
  import chokidar from 'chokidar';
7
- import { globFiles } from '../../../../utils/fsx.js';
7
+ import { globFiles, toForwardSlashes } from '../../../../utils/fsx.js';
8
8
  import { readStampignore, filterIgnoredFiles } from '../../../../utils/stampignore.js';
9
9
  import { buildDependencyGraph } from '../../../../core/manifest.js';
10
10
  import { normalizeEntryId } from '../../../../utils/fsx.js';
@@ -69,7 +69,7 @@ export async function startWatchMode(options, projectRoot, initialCache = null)
69
69
  }
70
70
  }
71
71
  let debounceTimer = null;
72
- let regenerationPromise = null; // Promise-based lock to prevent race conditions
72
+ let isRegenerating = false; // Flag-based lock to prevent race conditions
73
73
  let changedFiles = new Set();
74
74
  /** Snapshot for `getChanges` / strict-watch: set on first load; reset when watch cache is recovered after an error (new stable tree). */
75
75
  let baselineBundles = null;
@@ -180,179 +180,193 @@ export async function startWatchMode(options, projectRoot, initialCache = null)
180
180
  }
181
181
  };
182
182
  const regenerate = async () => {
183
- // Use Promise-based lock to prevent race conditions
184
- // If already regenerating, wait for it to complete then check if more changes came in
185
- if (regenerationPromise) {
186
- await regenerationPromise;
187
- // After waiting, if no new changes accumulated, skip
188
- if (changedFiles.size === 0) {
189
- return;
190
- }
191
- // Otherwise, fall through to process new changes
183
+ // Synchronous flag check - safe in JS single-threaded event loop
184
+ // If already regenerating, return - the while loop will pick up new changes
185
+ if (isRegenerating) {
186
+ return;
192
187
  }
193
- const changedFileList = Array.from(changedFiles);
194
- changedFiles.clear(); // Clear for next batch
195
- const startTime = Date.now();
196
- // Create a promise for this regeneration cycle (used for lock)
197
- const doRegenerate = async () => {
198
- try {
199
- // Determine output directory
200
- const outPath = resolve(options.out);
201
- const outputDir = outPath.endsWith('.json') ? dirname(outPath) : outPath;
202
- // Load baseline bundles for state-based comparison (like git diff).
203
- // Set on first successful load; also updated after error recovery (full rebuild) so diffs stay meaningful.
204
- if (!baselineBundles) {
205
- baselineBundles = await loadAllBundles(outputDir);
206
- previousBundles = baselineBundles;
207
- }
208
- if (!options.quiet) {
209
- const fileList = changedFileList.length > 3
210
- ? `${changedFileList.slice(0, 3).join(', ')}, ... (+${changedFileList.length - 3} more)`
211
- : changedFileList.join(', ');
212
- console.log(`\n🔄 Recompiling (${changedFileList.length} file${changedFileList.length > 1 ? 's' : ''} changed)...`);
213
- }
214
- let newBundles;
215
- // Use incremental rebuild if cache exists, otherwise do full rebuild (fallback)
216
- if (watchCache) {
217
- // Incremental rebuild - only rebuild affected bundles
218
- const result = await incrementalRebuild(changedFileList, watchCache, options, projectRoot);
219
- newBundles = result.bundles;
220
- // Write only changed context files
221
- const bundlesByFolder = groupBundlesByFolder(newBundles);
222
- const changedFolders = new Set();
223
- for (const bundleId of result.updatedBundles) {
224
- const bundle = newBundles.find(b => b.entryId === bundleId);
225
- if (bundle) {
226
- const folderPath = bundle.entryId.substring(0, bundle.entryId.lastIndexOf('/') || bundle.entryId.length);
227
- changedFolders.add(folderPath);
228
- }
188
+ isRegenerating = true;
189
+ try {
190
+ // Loop to process any changes that arrive during regeneration
191
+ while (changedFiles.size > 0) {
192
+ const changedFileList = Array.from(changedFiles);
193
+ changedFiles.clear(); // Clear for next batch
194
+ const startTime = Date.now();
195
+ try {
196
+ // Determine output directory
197
+ const outPath = resolve(options.out);
198
+ const outputDir = outPath.endsWith('.json') ? dirname(outPath) : outPath;
199
+ // Load baseline bundles for state-based comparison (like git diff).
200
+ // Set on first successful load; also updated after error recovery (full rebuild) so diffs stay meaningful.
201
+ if (!baselineBundles) {
202
+ baselineBundles = await loadAllBundles(outputDir);
203
+ previousBundles = baselineBundles;
229
204
  }
230
- // Write context files for changed folders only
231
- // (This is a simplified version - full implementation would write individual files)
232
- // For now, we'll write all files but this can be optimized further
233
- const { folderInfos, totalTokenEstimate } = await writeContextFiles(newBundles, outputDir, projectRoot, {
234
- format: options.format,
235
- quiet: true,
236
- });
237
- // Get contracts for main index (from cache if available)
238
- const allContracts = watchCache ? Array.from(watchCache.contracts.values()) : [];
239
- await writeMainIndex(outputDir, folderInfos, allContracts, newBundles, bundlesByFolder.size, totalTokenEstimate, projectRoot, {
240
- quiet: true,
241
- suppressSuccessIndicator: true,
242
- });
243
- }
244
- else {
245
- // Fallback: Full rebuild if cache not available (shouldn't happen normally)
246
- const regenerateOptions = {
247
- ...options,
248
- watch: false,
249
- strictMissing: false,
250
- quiet: true,
251
- suppressSuccessIndicator: true,
252
- };
253
- await contextCommand(regenerateOptions);
254
- // Load bundles and initialize cache
255
- newBundles = await loadAllBundles(outputDir);
256
- // Initialize cache for next incremental rebuild
257
- if (newBundles.length > 0) {
258
- // We need contracts and manifest - reload them from the build
259
- const files = await globFiles(projectRoot);
260
- const stampignore = await readStampignore(projectRoot);
261
- const filteredFiles = stampignore ? filterIgnoredFiles(files, stampignore.ignore, projectRoot) : files;
262
- const { contracts } = await buildContractsFromFiles(filteredFiles, projectRoot, {
263
- includeStyle: options.includeStyle,
264
- styleMode: options.styleMode,
265
- predictBehavior: options.predictBehavior,
205
+ if (!options.quiet) {
206
+ const fileList = changedFileList.length > 3
207
+ ? `${changedFileList.slice(0, 3).join(', ')}, ... (+${changedFileList.length - 3} more)`
208
+ : changedFileList.join(', ');
209
+ console.log(`\n🔄 Recompiling (${changedFileList.length} file${changedFileList.length > 1 ? 's' : ''} changed)...`);
210
+ }
211
+ let newBundles;
212
+ // Use incremental rebuild if cache exists, otherwise do full rebuild (fallback)
213
+ if (watchCache) {
214
+ // Incremental rebuild - only rebuild affected bundles
215
+ const result = await incrementalRebuild(changedFileList, watchCache, options, projectRoot);
216
+ newBundles = result.bundles;
217
+ // Write only changed context files
218
+ const bundlesByFolder = groupBundlesByFolder(newBundles);
219
+ const changedFolders = new Set();
220
+ for (const bundleId of result.updatedBundles) {
221
+ const bundle = newBundles.find(b => b.entryId === bundleId);
222
+ if (bundle) {
223
+ const folderPath = bundle.entryId.substring(0, bundle.entryId.lastIndexOf('/') || bundle.entryId.length);
224
+ changedFolders.add(folderPath);
225
+ }
226
+ }
227
+ // Write context files for changed folders only
228
+ // (This is a simplified version - full implementation would write individual files)
229
+ // For now, we'll write all files but this can be optimized further
230
+ const { folderInfos, totalTokenEstimate } = await writeContextFiles(newBundles, outputDir, projectRoot, {
231
+ format: options.format,
232
+ quiet: true,
233
+ });
234
+ // Get contracts for main index (from cache if available)
235
+ const allContracts = watchCache ? Array.from(watchCache.contracts.values()) : [];
236
+ await writeMainIndex(outputDir, folderInfos, allContracts, newBundles, bundlesByFolder.size, totalTokenEstimate, projectRoot, {
266
237
  quiet: true,
238
+ suppressSuccessIndicator: true,
267
239
  });
268
- const manifest = buildDependencyGraph(contracts);
269
- watchCache = await initializeWatchCache(filteredFiles, contracts, manifest, newBundles, projectRoot);
270
240
  }
271
- }
272
- const durationMs = Date.now() - startTime;
273
- // Compare against BASELINE (starting state), not previous state
274
- // This gives us cumulative diff like `git diff` - if changes are reverted, diff is empty
275
- const changes = baselineBundles && baselineBundles.length > 0
276
- ? getChanges(baselineBundles, newBundles)
277
- : null;
278
- if (!options.quiet) {
279
- if (changes && (changes.changed.length > 0 || changes.added.length > 0 || changes.removed.length > 0 || changes.bundleChanged.length > 0)) {
280
- showChanges(baselineBundles, newBundles, changedFileList[0] || 'unknown', { debug: options.debug });
241
+ else {
242
+ // Fallback: Full rebuild if cache not available (shouldn't happen normally)
243
+ const regenerateOptions = {
244
+ ...options,
245
+ watch: false,
246
+ strictMissing: false,
247
+ quiet: true,
248
+ suppressSuccessIndicator: true,
249
+ };
250
+ await contextCommand(regenerateOptions);
251
+ // Load bundles and initialize cache
252
+ newBundles = await loadAllBundles(outputDir);
253
+ // Initialize cache for next incremental rebuild
254
+ if (newBundles.length > 0) {
255
+ // We need contracts and manifest - reload them from the build
256
+ const files = await globFiles(projectRoot);
257
+ const stampignore = await readStampignore(projectRoot);
258
+ const filteredFiles = stampignore ? filterIgnoredFiles(files, stampignore.ignore, projectRoot) : files;
259
+ const { contracts } = await buildContractsFromFiles(filteredFiles, projectRoot, {
260
+ includeStyle: options.includeStyle,
261
+ styleMode: options.styleMode,
262
+ predictBehavior: options.predictBehavior,
263
+ quiet: true,
264
+ });
265
+ const manifest = buildDependencyGraph(contracts);
266
+ watchCache = await initializeWatchCache(filteredFiles, contracts, manifest, newBundles, projectRoot);
267
+ }
281
268
  }
282
- console.log(`\n✅ Recompiled\n`);
283
- }
284
- // Update previousBundles for incremental tracking (still useful for other operations)
285
- previousBundles = newBundles;
286
- // Strict watch mode: detect and report violations (state-based, like git diff)
287
- // Violations are calculated from current state vs baseline, not accumulated
288
- if (options.strictWatch && strictWatchStatus) {
289
- const hasChanges = changes && (changes.changed.length > 0 ||
290
- changes.added.length > 0 ||
291
- changes.removed.length > 0 ||
292
- changes.bundleChanged.length > 0);
293
- // Track previous state to detect new violations and resolution
294
- const previousActiveErrors = strictWatchStatus.lastCheck?.errors ?? 0;
295
- const previousActiveWarnings = strictWatchStatus.lastCheck?.warnings ?? 0;
296
- const hadActiveViolations = previousActiveErrors > 0 || previousActiveWarnings > 0;
297
- if (hasChanges) {
298
- const violations = detectViolations({ type: 'watch', changes });
299
- const errors = violations.filter(v => v.severity === 'error');
300
- const warnings = violations.filter(v => v.severity === 'warning');
301
- strictWatchStatus.regenerationCount++;
302
- if (violations.length > 0) {
303
- // Only increment totals if these are NEW violations (first time seeing violations, or count increased)
304
- const isNewViolation = !hadActiveViolations;
305
- const violationCountIncreased = errors.length > previousActiveErrors || warnings.length > previousActiveWarnings;
306
- if (isNewViolation || violationCountIncreased) {
307
- // Calculate new violations (only count increases, not existing ones)
308
- if (isNewViolation) {
309
- // First time violations appear - count all of them
310
- strictWatchStatus.totalErrorsDetected += errors.length;
311
- strictWatchStatus.totalWarningsDetected += warnings.length;
312
- }
313
- else {
314
- // Violations increased - only count the new ones
315
- const newErrors = Math.max(0, errors.length - previousActiveErrors);
316
- const newWarnings = Math.max(0, warnings.length - previousActiveWarnings);
317
- strictWatchStatus.totalErrorsDetected += newErrors;
318
- strictWatchStatus.totalWarningsDetected += newWarnings;
269
+ const durationMs = Date.now() - startTime;
270
+ // Compare against BASELINE (starting state), not previous state
271
+ // This gives us cumulative diff like `git diff` - if changes are reverted, diff is empty
272
+ const changes = baselineBundles && baselineBundles.length > 0
273
+ ? getChanges(baselineBundles, newBundles)
274
+ : null;
275
+ if (!options.quiet) {
276
+ if (changes && (changes.changed.length > 0 || changes.added.length > 0 || changes.removed.length > 0 || changes.bundleChanged.length > 0)) {
277
+ showChanges(baselineBundles, newBundles, changedFileList[0] || 'unknown', { debug: options.debug });
278
+ }
279
+ console.log(`\n✅ Recompiled\n`);
280
+ }
281
+ // Update previousBundles for incremental tracking (still useful for other operations)
282
+ previousBundles = newBundles;
283
+ // Strict watch mode: detect and report violations (state-based, like git diff)
284
+ // Violations are calculated from current state vs baseline, not accumulated
285
+ if (options.strictWatch && strictWatchStatus) {
286
+ const hasChanges = changes && (changes.changed.length > 0 ||
287
+ changes.added.length > 0 ||
288
+ changes.removed.length > 0 ||
289
+ changes.bundleChanged.length > 0);
290
+ // Track previous state to detect new violations and resolution
291
+ const previousActiveErrors = strictWatchStatus.lastCheck?.errors ?? 0;
292
+ const previousActiveWarnings = strictWatchStatus.lastCheck?.warnings ?? 0;
293
+ const hadActiveViolations = previousActiveErrors > 0 || previousActiveWarnings > 0;
294
+ if (hasChanges) {
295
+ const violations = detectViolations({ type: 'watch', changes });
296
+ const errors = violations.filter(v => v.severity === 'error');
297
+ const warnings = violations.filter(v => v.severity === 'warning');
298
+ strictWatchStatus.regenerationCount++;
299
+ if (violations.length > 0) {
300
+ // Only increment totals if these are NEW violations (first time seeing violations, or count increased)
301
+ const isNewViolation = !hadActiveViolations;
302
+ const violationCountIncreased = errors.length > previousActiveErrors || warnings.length > previousActiveWarnings;
303
+ if (isNewViolation || violationCountIncreased) {
304
+ // Calculate new violations (only count increases, not existing ones)
305
+ if (isNewViolation) {
306
+ // First time violations appear - count all of them
307
+ strictWatchStatus.totalErrorsDetected += errors.length;
308
+ strictWatchStatus.totalWarningsDetected += warnings.length;
309
+ }
310
+ else {
311
+ // Violations increased - only count the new ones
312
+ const newErrors = Math.max(0, errors.length - previousActiveErrors);
313
+ const newWarnings = Math.max(0, warnings.length - previousActiveWarnings);
314
+ strictWatchStatus.totalErrorsDetected += newErrors;
315
+ strictWatchStatus.totalWarningsDetected += newWarnings;
316
+ }
319
317
  }
320
- }
321
- // Update state-based counts (current state, not cumulative)
322
- strictWatchStatus.cumulativeViolations = violations.length;
323
- strictWatchStatus.cumulativeErrors = errors.length;
324
- strictWatchStatus.cumulativeWarnings = warnings.length;
325
- // Store current violations
326
- strictWatchStatus.lastCheck = {
327
- timestamp: new Date().toISOString(),
328
- totalViolations: violations.length,
329
- errors: errors.length,
330
- warnings: warnings.length,
331
- violations,
332
- changedFiles: changedFileList,
333
- };
334
- // Display violations to console (only if status changed)
335
- if (!options.quiet) {
336
- const statusChanged = errors.length !== previousActiveErrors || warnings.length !== previousActiveWarnings;
337
- if (statusChanged) {
338
- if (isNewViolation || violationCountIncreased) {
339
- // Show appropriate emoji based on severity: ❌ for errors, ⚠️ for warnings only
340
- if (errors.length > 0) {
341
- console.log(`\n❌ Breaking change detected`);
342
- }
343
- else if (warnings.length > 0) {
344
- console.log(`\n⚠️ Warning detected`);
318
+ // Update state-based counts (current state, not cumulative)
319
+ strictWatchStatus.cumulativeViolations = violations.length;
320
+ strictWatchStatus.cumulativeErrors = errors.length;
321
+ strictWatchStatus.cumulativeWarnings = warnings.length;
322
+ // Store current violations
323
+ strictWatchStatus.lastCheck = {
324
+ timestamp: new Date().toISOString(),
325
+ totalViolations: violations.length,
326
+ errors: errors.length,
327
+ warnings: warnings.length,
328
+ violations,
329
+ changedFiles: changedFileList,
330
+ };
331
+ // Display violations to console (only if status changed)
332
+ if (!options.quiet) {
333
+ const statusChanged = errors.length !== previousActiveErrors || warnings.length !== previousActiveWarnings;
334
+ if (statusChanged) {
335
+ if (isNewViolation || violationCountIncreased) {
336
+ // Show appropriate emoji based on severity: ❌ for errors, ⚠️ for warnings only
337
+ if (errors.length > 0) {
338
+ console.log(`\n❌ Breaking change detected`);
339
+ }
340
+ else if (warnings.length > 0) {
341
+ console.log(`\n⚠️ Warning detected`);
342
+ }
345
343
  }
344
+ displayViolations(violations, { quiet: options.quiet });
345
+ displaySessionStatus(strictWatchStatus, { quiet: options.quiet });
346
346
  }
347
- displayViolations(violations, { quiet: options.quiet });
347
+ }
348
+ // Write current violations state to disk
349
+ await writeStrictWatchStatus(projectRoot, strictWatchStatus);
350
+ }
351
+ else {
352
+ // Changes exist but no violations - violations were resolved
353
+ if (hadActiveViolations) {
354
+ strictWatchStatus.resolvedCount++;
355
+ }
356
+ strictWatchStatus.cumulativeViolations = 0;
357
+ strictWatchStatus.cumulativeErrors = 0;
358
+ strictWatchStatus.cumulativeWarnings = 0;
359
+ strictWatchStatus.lastCheck = undefined;
360
+ // Display resolution message and status
361
+ if (!options.quiet && hadActiveViolations) {
362
+ console.log(`\n✅ Violation resolved`);
348
363
  displaySessionStatus(strictWatchStatus, { quiet: options.quiet });
349
364
  }
365
+ await deleteStrictWatchStatus(projectRoot);
350
366
  }
351
- // Write current violations state to disk
352
- await writeStrictWatchStatus(projectRoot, strictWatchStatus);
353
367
  }
354
368
  else {
355
- // Changes exist but no violations - violations were resolved
369
+ // No changes from baseline - clear violations (state reverted)
356
370
  if (hadActiveViolations) {
357
371
  strictWatchStatus.resolvedCount++;
358
372
  }
@@ -368,111 +382,93 @@ export async function startWatchMode(options, projectRoot, initialCache = null)
368
382
  await deleteStrictWatchStatus(projectRoot);
369
383
  }
370
384
  }
371
- else {
372
- // No changes from baseline - clear violations (state reverted)
373
- if (hadActiveViolations) {
374
- strictWatchStatus.resolvedCount++;
385
+ // Log structured data for MCP server (only if --log-file flag is set)
386
+ // Appends each event to the log file for event history
387
+ if (options.logFile) {
388
+ if (changes) {
389
+ const logEntry = {
390
+ timestamp: new Date().toISOString(),
391
+ changedFiles: changedFileList,
392
+ fileCount: changedFileList.length,
393
+ durationMs,
394
+ modifiedContracts: changes.changed.map(c => ({
395
+ entryId: c.entryId,
396
+ semanticHashChanged: !!c.semanticHash,
397
+ fileHashChanged: !!c.fileHash,
398
+ semanticHash: c.semanticHash,
399
+ fileHash: c.fileHash,
400
+ })),
401
+ modifiedBundles: changes.bundleChanged.map(b => ({
402
+ entryId: b.entryId,
403
+ bundleHash: { old: b.oldHash, new: b.newHash },
404
+ })),
405
+ addedContracts: changes.added.length > 0 ? changes.added : undefined,
406
+ removedContracts: changes.removed.length > 0 ? changes.removed : undefined,
407
+ summary: {
408
+ modifiedContractsCount: changes.changed.length,
409
+ modifiedBundlesCount: changes.bundleChanged.length,
410
+ addedContractsCount: changes.added.length,
411
+ removedContractsCount: changes.removed.length,
412
+ },
413
+ };
414
+ await appendWatchLog(projectRoot, logEntry);
375
415
  }
376
- strictWatchStatus.cumulativeViolations = 0;
377
- strictWatchStatus.cumulativeErrors = 0;
378
- strictWatchStatus.cumulativeWarnings = 0;
379
- strictWatchStatus.lastCheck = undefined;
380
- // Display resolution message and status
381
- if (!options.quiet && hadActiveViolations) {
382
- console.log(`\n✅ Violation resolved`);
383
- displaySessionStatus(strictWatchStatus, { quiet: options.quiet });
416
+ else if (changedFileList.length > 0) {
417
+ // Log even if no changes detected (file changed but no contract changes)
418
+ const logEntry = {
419
+ timestamp: new Date().toISOString(),
420
+ changedFiles: changedFileList,
421
+ fileCount: changedFileList.length,
422
+ durationMs,
423
+ };
424
+ await appendWatchLog(projectRoot, logEntry);
384
425
  }
385
- await deleteStrictWatchStatus(projectRoot);
386
426
  }
387
427
  }
388
- // Log structured data for MCP server (only if --log-file flag is set)
389
- // Appends each event to the log file for event history
390
- if (options.logFile) {
391
- if (changes) {
392
- const logEntry = {
393
- timestamp: new Date().toISOString(),
394
- changedFiles: changedFileList,
395
- fileCount: changedFileList.length,
396
- durationMs,
397
- modifiedContracts: changes.changed.map(c => ({
398
- entryId: c.entryId,
399
- semanticHashChanged: !!c.semanticHash,
400
- fileHashChanged: !!c.fileHash,
401
- semanticHash: c.semanticHash,
402
- fileHash: c.fileHash,
403
- })),
404
- modifiedBundles: changes.bundleChanged.map(b => ({
405
- entryId: b.entryId,
406
- bundleHash: { old: b.oldHash, new: b.newHash },
407
- })),
408
- addedContracts: changes.added.length > 0 ? changes.added : undefined,
409
- removedContracts: changes.removed.length > 0 ? changes.removed : undefined,
410
- summary: {
411
- modifiedContractsCount: changes.changed.length,
412
- modifiedBundlesCount: changes.bundleChanged.length,
413
- addedContractsCount: changes.added.length,
414
- removedContractsCount: changes.removed.length,
415
- },
416
- };
417
- await appendWatchLog(projectRoot, logEntry);
418
- }
419
- else if (changedFileList.length > 0) {
420
- // Log even if no changes detected (file changed but no contract changes)
421
- const logEntry = {
422
- timestamp: new Date().toISOString(),
423
- changedFiles: changedFileList,
424
- fileCount: changedFileList.length,
425
- durationMs,
426
- };
427
- await appendWatchLog(projectRoot, logEntry);
428
+ catch (error) {
429
+ const durationMs = Date.now() - startTime;
430
+ const errorMessage = formatErrorMessage(error);
431
+ if (!options.quiet) {
432
+ console.error(` ❌ Error: ${errorMessage}\n`);
428
433
  }
429
- }
430
- }
431
- catch (error) {
432
- const durationMs = Date.now() - startTime;
433
- const errorMessage = formatErrorMessage(error);
434
- if (!options.quiet) {
435
- console.error(` ❌ Error: ${errorMessage}\n`);
436
- }
437
- if (options.logFile) {
438
- try {
439
- const logEntry = {
440
- timestamp: new Date().toISOString(),
441
- changedFiles: changedFileList,
442
- fileCount: changedFileList.length,
443
- durationMs,
444
- error: errorMessage,
445
- };
446
- await appendWatchLog(projectRoot, logEntry);
434
+ if (options.logFile) {
435
+ try {
436
+ const logEntry = {
437
+ timestamp: new Date().toISOString(),
438
+ changedFiles: changedFileList,
439
+ fileCount: changedFileList.length,
440
+ durationMs,
441
+ error: errorMessage,
442
+ };
443
+ await appendWatchLog(projectRoot, logEntry);
444
+ }
445
+ catch (logError) {
446
+ if (!options.quiet) {
447
+ console.warn(` ⚠️ Watch: could not append watch log: ${formatErrorMessage(logError)}`);
448
+ }
449
+ }
447
450
  }
448
- catch (logError) {
451
+ const recovery = await attemptWatchCacheRecovery();
452
+ if (recovery != null) {
453
+ watchCache = recovery.cache;
454
+ baselineBundles = recovery.bundles;
455
+ previousBundles = recovery.bundles;
449
456
  if (!options.quiet) {
450
- console.warn(` ⚠️ Watch: could not append watch log: ${formatErrorMessage(logError)}`);
457
+ console.log(` Watch cache restored after full rebuild.\n`);
451
458
  }
452
459
  }
453
- }
454
- const recovery = await attemptWatchCacheRecovery();
455
- if (recovery != null) {
456
- watchCache = recovery.cache;
457
- baselineBundles = recovery.bundles;
458
- previousBundles = recovery.bundles;
459
- if (!options.quiet) {
460
- console.log(` ✅ Watch cache restored after full rebuild.\n`);
461
- }
462
- }
463
- else {
464
- watchCache = null;
465
- if (!options.quiet) {
466
- console.warn(' ⚠️ Each file change will run a full context rebuild until a rebuild succeeds.\n');
460
+ else {
461
+ watchCache = null;
462
+ if (!options.quiet) {
463
+ console.warn(' ⚠️ Each file change will run a full context rebuild until a rebuild succeeds.\n');
464
+ }
467
465
  }
468
466
  }
469
467
  }
470
- };
471
- // Execute and track the regeneration promise
472
- regenerationPromise = doRegenerate().finally(() => {
473
- regenerationPromise = null;
474
- });
475
- await regenerationPromise;
468
+ }
469
+ finally {
470
+ isRegenerating = false;
471
+ }
476
472
  };
477
473
  const debouncedRegenerate = () => {
478
474
  if (debounceTimer) {
@@ -512,7 +508,7 @@ export async function startWatchMode(options, projectRoot, initialCache = null)
512
508
  /context_main\.json$/,
513
509
  /context_compare_modes\.json$/,
514
510
  // Ignore output directory if different from project root
515
- ...(outputDir !== projectRoot ? [new RegExp('^' + relative(projectRoot, normalizedOutputDir).replace(/\\/g, '/'))] : []),
511
+ ...(outputDir !== projectRoot ? [new RegExp('^' + toForwardSlashes(relative(projectRoot, normalizedOutputDir)))] : []),
516
512
  // Ignore common build/dependency directories
517
513
  /node_modules/,
518
514
  /dist/,
@@ -531,7 +527,7 @@ export async function startWatchMode(options, projectRoot, initialCache = null)
531
527
  // Helper to check if a file should trigger regeneration
532
528
  const shouldTrigger = (filePath) => {
533
529
  // Normalize path separators
534
- const normalizedPath = filePath.replace(/\\/g, '/');
530
+ const normalizedPath = toForwardSlashes(filePath);
535
531
  // Debug logging (can be enabled with LOGICSTAMP_DEBUG=1)
536
532
  if (process.env.LOGICSTAMP_DEBUG === '1' && !options.quiet) {
537
533
  console.log(`[DEBUG] Watch event for: ${normalizedPath}`);