@abarcenas/ai-workflow-template 1.45.0 → 1.46.0

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 (47) hide show
  1. package/.agents/config/README.md +43 -0
  2. package/.agents/config/domain.md +37 -0
  3. package/.agents/instructions/learned-knowledge.instructions.md +110 -0
  4. package/.agents/instructions/memory-bank.instructions.md +11 -0
  5. package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
  6. package/.agents/skills/domain-modeling/GLOSSARY-FORMAT.md +60 -0
  7. package/.agents/skills/domain-modeling/SKILL.md +74 -0
  8. package/.agents/skills/grill-me/SKILL.md +7 -0
  9. package/.agents/skills/grill-with-docs/SKILL.md +7 -0
  10. package/.agents/skills/grilling/SKILL.md +28 -0
  11. package/.agents/skills/prototype/LOGIC.md +67 -0
  12. package/.agents/skills/prototype/SKILL.md +26 -0
  13. package/.agents/skills/prototype/UI.md +112 -0
  14. package/.github/workflows/npm-publish.yml +10 -0
  15. package/.github/workflows/pr-test.yml +31 -0
  16. package/.opencode/agents/orchestrator/orchestrator.agent.md +68 -3
  17. package/.opencode/agents/scope.agent.md +299 -0
  18. package/AGENTS.md +42 -3
  19. package/README.md +5 -2
  20. package/package.json +1 -1
  21. package/scripts/bump-version.test.js +238 -0
  22. package/scripts/mcp/playwright-mcp-launcher.js +55 -29
  23. package/scripts/mcp/playwright-mcp-launcher.test.js +217 -0
  24. package/scripts/mcp-memory-server.js +93 -76
  25. package/scripts/mcp-memory-server.test.js +398 -0
  26. package/scripts/memory-cli.test.js +297 -0
  27. package/scripts/memory-index.js +119 -31
  28. package/scripts/memory-index.test.js +1173 -0
  29. package/scripts/normalize-memory.js +26 -1
  30. package/scripts/normalize-memory.test.js +753 -0
  31. package/scripts/setup/discover.test.js +124 -2
  32. package/scripts/setup/hooks.test.js +188 -1
  33. package/scripts/setup/husky-init.test.js +311 -0
  34. package/scripts/setup/index.js +35 -5
  35. package/scripts/setup/index.test.js +279 -0
  36. package/scripts/setup/knowledgebase.test.js +100 -0
  37. package/scripts/setup/skills-phase.js +568 -0
  38. package/scripts/setup/skills-phase.test.js +1274 -0
  39. package/scripts/setup/sync-phase.js +6 -1
  40. package/scripts/setup/sync-phase.test.js +180 -2
  41. package/scripts/setup/ui.test.js +579 -0
  42. package/scripts/setup/utils.test.js +586 -0
  43. package/scripts/sync-unit.test.js +1342 -0
  44. package/scripts/sync.js +540 -246
  45. package/scripts/sync.test.js +283 -0
  46. package/scripts/vocab-sync.test.js +88 -0
  47. package/skills-lock.json +90 -0
@@ -238,55 +238,132 @@ async function indexFile(filePath) {
238
238
  for (let i = 0; i < chunks.length; i++) {
239
239
  const chunkHash = hashContent(chunks[i]);
240
240
 
241
- // Check if already indexed with same hash
242
- const existing = db.prepare(
243
- 'SELECT id, content_hash FROM memory_chunks WHERE source_file = ? AND chunk_index = ?'
244
- ).get(sourceFile, i);
245
-
246
- if (existing && existing.content_hash === chunkHash) {
241
+ // Check if already indexed with the same content AND a resolvable vector.
242
+ // The LEFT JOIN distinguishes a live link from a dangling one (legacy DB or
243
+ // a crash between the vector insert and the link update).
244
+ const existing = db.prepare(`
245
+ SELECT mc.id, mc.content_hash, mc.vec_rowid, v.rowid AS vec_exists
246
+ FROM memory_chunks mc
247
+ LEFT JOIN vec_memory v ON v.rowid = mc.vec_rowid
248
+ WHERE mc.source_file = ? AND mc.chunk_index = ?
249
+ `).get(sourceFile, i);
250
+
251
+ if (existing && existing.content_hash === chunkHash && existing.vec_exists != null) {
247
252
  skipped++;
248
253
  continue;
249
254
  }
250
255
 
251
- let rowid;
256
+ let chunkId;
252
257
  if (existing) {
253
- const existingId = Number(existing.id);
258
+ chunkId = Number(existing.id);
254
259
  const oldVecRowid = existing.vec_rowid;
255
260
 
256
- // Update content
257
- db.prepare(
258
- 'UPDATE memory_chunks SET content = ?, content_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
259
- ).run(chunks[i], chunkHash, existingId);
260
-
261
- // Remove old vector
262
- if (oldVecRowid != null) {
263
- db.prepare('DELETE FROM vec_memory WHERE rowid = ?').run(Number(oldVecRowid));
264
- }
265
- rowid = existingId;
261
+ // Detach the old vector before re-embedding so a failed embed cannot
262
+ // leave a dangling vec_rowid, then delete it: vec0 has no foreign key,
263
+ // so an unlinked row would be an invisible storage leak.
264
+ db.transaction(() => {
265
+ db.prepare(
266
+ 'UPDATE memory_chunks SET content = ?, content_hash = ?, vec_rowid = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
267
+ ).run(chunks[i], chunkHash, chunkId);
268
+
269
+ if (oldVecRowid != null) {
270
+ db.prepare('DELETE FROM vec_memory WHERE rowid = ?').run(Number(oldVecRowid));
271
+ }
272
+ })();
266
273
  } else {
267
274
  // Insert new chunk
268
275
  const info = db.prepare(
269
276
  'INSERT INTO memory_chunks (source_file, chunk_index, content, content_hash) VALUES (?, ?, ?, ?)'
270
277
  ).run(sourceFile, i, chunks[i], chunkHash);
271
- rowid = Number(info.lastInsertRowid);
278
+ chunkId = Number(info.lastInsertRowid);
272
279
  }
273
280
 
274
281
  try {
275
282
  const embedding = await embed(chunks[i]);
276
- // Insert into vec0 without explicit rowid (auto-assigned)
277
- const vecInfo = db.prepare('INSERT INTO vec_memory (embedding) VALUES (?)').run(embedding);
278
- const vecRowid = Number(vecInfo.lastInsertRowid);
279
- // Link vec0 rowid back to memory_chunks
280
- db.prepare('UPDATE memory_chunks SET vec_rowid = ? WHERE id = ?').run(vecRowid, rowid);
283
+ // Insert + link atomically: a vector inserted without its link (or vice
284
+ // versa) would be an orphan.
285
+ db.transaction(() => {
286
+ // Insert into vec0 without explicit rowid (auto-assigned)
287
+ const vecInfo = db.prepare('INSERT INTO vec_memory (embedding) VALUES (?)').run(embedding);
288
+ const vecRowid = Number(vecInfo.lastInsertRowid);
289
+ // Link vec0 rowid back to memory_chunks
290
+ db.prepare('UPDATE memory_chunks SET vec_rowid = ? WHERE id = ?').run(vecRowid, chunkId);
291
+ })();
281
292
  indexed++;
282
293
  } catch (err) {
283
294
  console.warn(`[memory-index] Failed to embed chunk ${i} of ${sourceFile}: ${err.message}`);
284
295
  }
285
296
  }
286
297
 
298
+ // A file that lost sections keeps its old trailing chunks (and their
299
+ // vectors) forever otherwise: (source_file, chunk_index) is the identity, so
300
+ // indices past the new chunk count are unreachable debris.
301
+ const removedChunks = db.prepare(
302
+ 'SELECT id, vec_rowid FROM memory_chunks WHERE source_file = ? AND chunk_index >= ?'
303
+ ).all(sourceFile, chunks.length);
304
+
305
+ if (removedChunks.length > 0) {
306
+ db.transaction(() => {
307
+ const deleteVector = db.prepare('DELETE FROM vec_memory WHERE rowid = ?');
308
+ const deleteChunk = db.prepare('DELETE FROM memory_chunks WHERE id = ?');
309
+ for (const row of removedChunks) {
310
+ if (row.vec_rowid != null) deleteVector.run(Number(row.vec_rowid));
311
+ deleteChunk.run(Number(row.id));
312
+ }
313
+ })();
314
+ }
315
+
287
316
  return { indexed, skipped };
288
317
  }
289
318
 
319
+ /**
320
+ * Delete chunks (and their vectors) whose source file is no longer present.
321
+ * Returns the number of chunks pruned.
322
+ */
323
+ function pruneMissingSources(db, activeSources) {
324
+ const stale = db
325
+ .prepare('SELECT id, source_file, vec_rowid FROM memory_chunks')
326
+ .all()
327
+ .filter((row) => !activeSources.has(row.source_file));
328
+
329
+ if (stale.length === 0) return 0;
330
+
331
+ db.transaction(() => {
332
+ const deleteVector = db.prepare('DELETE FROM vec_memory WHERE rowid = ?');
333
+ const deleteChunk = db.prepare('DELETE FROM memory_chunks WHERE id = ?');
334
+ for (const row of stale) {
335
+ if (row.vec_rowid != null) deleteVector.run(Number(row.vec_rowid));
336
+ deleteChunk.run(Number(row.id));
337
+ }
338
+ })();
339
+
340
+ return stale.length;
341
+ }
342
+
343
+ /**
344
+ * Delete vector rows that no chunk links to. Older versions of this module
345
+ * orphaned them on every content change; a full rebuild would eventually clear
346
+ * them, but an incremental update should heal the index in place.
347
+ * Returns the number of orphaned vectors removed.
348
+ */
349
+ function pruneOrphanVectors(db) {
350
+ const orphans = db.prepare(`
351
+ SELECT v.rowid AS rowid
352
+ FROM vec_memory v
353
+ LEFT JOIN memory_chunks mc ON mc.vec_rowid = v.rowid
354
+ WHERE mc.id IS NULL
355
+ `).all();
356
+
357
+ if (orphans.length === 0) return 0;
358
+
359
+ db.transaction(() => {
360
+ const deleteVector = db.prepare('DELETE FROM vec_memory WHERE rowid = ?');
361
+ for (const row of orphans) deleteVector.run(Number(row.rowid));
362
+ })();
363
+
364
+ return orphans.length;
365
+ }
366
+
290
367
  /**
291
368
  * Collect all .md files from a directory recursively.
292
369
  */
@@ -360,29 +437,35 @@ async function incrementalUpdate() {
360
437
  let totalIndexed = 0;
361
438
  let totalSkipped = 0;
362
439
  const changed = [];
440
+ const activeSources = new Set();
441
+ const db = await getDB();
363
442
 
364
443
  for (const file of allFiles) {
365
444
  if (file.includes('/.index/')) continue;
366
445
 
367
446
  const content = readFileSync(file, 'utf8');
368
447
  const sourceFile = basename(file);
448
+ activeSources.add(sourceFile);
369
449
  const { body } = parseFrontmatter(content);
370
- const bodyHash = hashContent(body);
371
450
 
372
- // Quick check: if the full body hash matches the last indexed state, skip
373
- const db = await getDB();
374
- const existingChunks = db.prepare(
375
- 'SELECT content_hash FROM memory_chunks WHERE source_file = ?'
376
- ).all(sourceFile);
451
+ // Quick check: if the full body hash matches the last indexed state and
452
+ // every chunk still resolves to a vector, skip.
453
+ const existingChunks = db.prepare(`
454
+ SELECT mc.content_hash, mc.vec_rowid, v.rowid AS vec_exists
455
+ FROM memory_chunks mc
456
+ LEFT JOIN vec_memory v ON v.rowid = mc.vec_rowid
457
+ WHERE mc.source_file = ?
458
+ `).all(sourceFile);
377
459
 
378
460
  const combinedHash = existingChunks.map(c => c.content_hash).join('|');
461
+ const allVectorsPresent = existingChunks.every((c) => c.vec_exists != null);
379
462
 
380
463
  if (existingChunks.length > 0) {
381
464
  // Check if any chunk's content changed by recomputing hashes
382
465
  const chunks = chunkMarkdown(body);
383
466
  const newCombinedHash = chunks.map(c => hashContent(c)).join('|');
384
467
 
385
- if (newCombinedHash === combinedHash) {
468
+ if (newCombinedHash === combinedHash && allVectorsPresent) {
386
469
  totalSkipped += existingChunks.length;
387
470
  continue;
388
471
  }
@@ -396,6 +479,11 @@ async function incrementalUpdate() {
396
479
  totalSkipped += skipped;
397
480
  }
398
481
 
482
+ // Reclaim rows for files that disappeared and vectors that lost their link
483
+ // (including orphans left behind by older versions of this module).
484
+ pruneMissingSources(db, activeSources);
485
+ pruneOrphanVectors(db);
486
+
399
487
  return { indexed: totalIndexed, skipped: totalSkipped, changed };
400
488
  }
401
489