@dzhechkov/harness-core 0.3.140 → 0.3.142

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.
@@ -160,9 +160,21 @@ export async function indexPatternsToAgentdb(
160
160
  /* READ half (dz-rvf-vector-bridge FR-3): semantic search + id scan */
161
161
  /* ------------------------------------------------------------------ */
162
162
 
163
- /** The dz-owned ReasoningBank task_types (teach mirror + consolidate mirror). */
163
+ /**
164
+ * The RECALL/search default task_types. Deliberately EXCLUDES `dz-backlog`: `dz recall` (and
165
+ * feature-adr Step-0) must never surface raw backlog ideas as if they were earned lessons (ADR-005).
166
+ */
164
167
  const DZ_TASK_TYPES = ['dz-teach', 'dz-learning'] as const;
165
168
 
169
+ /**
170
+ * The dz-owned task_types for LIFECYCLE scans (id enumeration + reindex ownership) — a SUPERSET of
171
+ * the recall default that ALSO owns `dz-backlog` (smart-backlog, ADR-001/005). Reindex must re-embed
172
+ * these on a model bump (else backlog rows rot in a stale embedding space), and id-scans must see them
173
+ * (mirror idempotency). Kept SEPARATE from {@link DZ_TASK_TYPES} so ownership never leaks ideas into
174
+ * lesson recall: search defaults to DZ_TASK_TYPES, lifecycle to DZ_OWNED_TASK_TYPES.
175
+ */
176
+ export const DZ_OWNED_TASK_TYPES = ['dz-teach', 'dz-learning', 'dz-backlog'] as const;
177
+
166
178
  /** Minimal READONLY better-sqlite3 surface used by the read half. */
167
179
  interface ReadonlyDb {
168
180
  pragma: (s: string) => void;
@@ -362,7 +374,9 @@ export async function listAgentdbDzIds(
362
374
  const { db } = opened;
363
375
  try {
364
376
  if (!hasDzTables(db)) return { ids: [] };
365
- const taskTypes = opts.taskTypes ?? DZ_TASK_TYPES;
377
+ // LIFECYCLE scan ⇒ the OWNED superset (incl. dz-backlog) so idea ids are visible for mirror
378
+ // idempotency + `dz vector status`. Recall/search still defaults to the narrower DZ_TASK_TYPES.
379
+ const taskTypes = opts.taskTypes ?? DZ_OWNED_TASK_TYPES;
366
380
  const placeholders = taskTypes.map(() => '?').join(', ');
367
381
  const rows = db
368
382
  .prepare(`SELECT metadata FROM reasoning_patterns WHERE task_type IN (${placeholders})`)
@@ -380,6 +394,65 @@ export async function listAgentdbDzIds(
380
394
  }
381
395
  }
382
396
 
397
+ /**
398
+ * Read the rows that ACTUALLY EXIST IN THE STORE for a task_type, as re-indexable {@link AgentdbRow}s
399
+ * (their stored `approach` text + score/uses/reward/tags/metadata, dzId preserved). This is the correct
400
+ * source for a REINDEX: a reindex re-embeds what is physically in the store to the new model — reading it
401
+ * back from the store (not reconstructing from a sidecar file like ideas.jsonl) means an empty/unreadable
402
+ * sidecar can never leave real store rows un-re-embedded and stale under an advanced manifest (HIGH-G).
403
+ * Readonly, best-effort ({rows:[]} on absent/unavailable), never throws.
404
+ */
405
+ export function readAgentdbRowsByTaskType(
406
+ projectRoot: string,
407
+ taskType: string,
408
+ opts: { dbPath?: string } = {},
409
+ ): { rows: AgentdbRow[]; error?: string } {
410
+ const opened = openReadonly(projectRoot, opts.dbPath);
411
+ if ('absent' in opened) return { rows: [] };
412
+ if ('error' in opened) return { rows: [], error: opened.error };
413
+ const { db } = opened;
414
+ try {
415
+ if (!hasDzTables(db)) return { rows: [] };
416
+ const raw = db
417
+ .prepare('SELECT approach, success_rate, uses, avg_reward, tags, metadata FROM reasoning_patterns WHERE task_type = ?')
418
+ .all(taskType) as Array<{ approach: string; success_rate: number; uses: number; avg_reward: number; tags: string | null; metadata: string | null }>;
419
+ const rows: AgentdbRow[] = raw.map((r) => {
420
+ let metadata: Record<string, unknown> | undefined;
421
+ if (typeof r.metadata === 'string' && r.metadata !== '') {
422
+ try {
423
+ const m = JSON.parse(r.metadata) as unknown;
424
+ if (m !== null && typeof m === 'object') metadata = m as Record<string, unknown>;
425
+ } catch {
426
+ /* drop unparseable metadata */
427
+ }
428
+ }
429
+ let tags: string[] | undefined;
430
+ if (typeof r.tags === 'string' && r.tags !== '') {
431
+ try {
432
+ const t = JSON.parse(r.tags) as unknown;
433
+ if (Array.isArray(t)) tags = t.filter((x): x is string => typeof x === 'string');
434
+ } catch {
435
+ /* drop unparseable tags */
436
+ }
437
+ }
438
+ return {
439
+ taskType,
440
+ text: r.approach,
441
+ score: r.success_rate,
442
+ uses: r.uses,
443
+ avgReward: r.avg_reward,
444
+ ...(tags !== undefined ? { tags } : {}),
445
+ ...(metadata !== undefined ? { metadata } : {}),
446
+ };
447
+ });
448
+ return { rows };
449
+ } catch (err) {
450
+ return { rows: [], error: `row scan failed: ${err instanceof Error ? err.message : String(err)}` };
451
+ } finally {
452
+ db.close();
453
+ }
454
+ }
455
+
383
456
  /* ------------------------------------------------------------------ */
384
457
  /* IMPORT half (dz-vector-harmonize-import M0.4): upsert-by-dzId */
385
458
  /* ------------------------------------------------------------------ */
@@ -437,6 +510,20 @@ export async function importVectorsToAgentdb(
437
510
  const { default: Database } = (await import(sqliteUrl)) as { default: new (p: string) => UpsertDb };
438
511
  const model = resolveEmbedModel(projectRoot);
439
512
  if ('error' in model) return { imported: 0, error: model.error };
513
+ // MED-C: the store manifest guards the MODEL, but a per-vector guard was missing — a malformed or
514
+ // TOCTOU vector (wrong length / NaN / ±Infinity) would be stamped compatible. Fail CLOSED and LOUD:
515
+ // reject the whole batch if any vector's dimensionality ≠ the store dim or any component is non-finite,
516
+ // so a vector can only enter the store bound to the model+dim the manifest names.
517
+ for (const row of rows) {
518
+ if (!(row.vector instanceof Float32Array) || row.vector.length !== model.dim) {
519
+ return { imported: 0, error: `vector validation failed for ${row.dzId}: length ${row.vector?.length} != store dim ${model.dim}` };
520
+ }
521
+ for (let i = 0; i < row.vector.length; i += 1) {
522
+ if (!Number.isFinite(row.vector[i])) {
523
+ return { imported: 0, error: `vector validation failed for ${row.dzId}: non-finite component at index ${i}` };
524
+ }
525
+ }
526
+ }
440
527
  const dbFile = resolveAgentdbPath(projectRoot, opts.dbPath);
441
528
  mkdirSync(dirname(dbFile), { recursive: true }); // better-sqlite3 won't create the parent dir
442
529
  const db = new Database(dbFile);
@@ -530,6 +617,62 @@ export function clearAgentdbQuarantine(
530
617
  }
531
618
  }
532
619
 
620
+ /**
621
+ * DELETE mirrored rows by `metadata.dzId` (pattern + its embedding), optionally scoped to a task_type
622
+ * set. The write-half of a structured-store removal: when `harmonize --apply` drops ideas from
623
+ * `ideas.jsonl`, their `dz-backlog` vectors must be pruned too, or a later semantic search matches an
624
+ * ORPHAN dzId that no longer has a structured record (smart-backlog HIGH-A). Best-effort, same custody
625
+ * model as {@link clearAgentdbQuarantine} (missing db/deps ⇒ no-op). Never throws.
626
+ */
627
+ export function deleteAgentdbByDzIds(
628
+ projectRoot: string,
629
+ dzIds: readonly string[],
630
+ opts: { dbPath?: string; taskTypes?: readonly string[] } = {},
631
+ ): { deleted: number; error?: string } {
632
+ if (dzIds.length === 0) return { deleted: 0 };
633
+ const dbFile = resolveAgentdbPath(projectRoot, opts.dbPath);
634
+ if (!existsSync(dbFile)) return { deleted: 0 };
635
+ let Database: new (p: string) => UpsertDb;
636
+ try {
637
+ const req = createRequire(join(projectRoot, 'package.json'));
638
+ Database = req('better-sqlite3') as new (p: string) => UpsertDb;
639
+ } catch {
640
+ return { deleted: 0, error: DEPS_MISSING };
641
+ }
642
+ try {
643
+ const db = new Database(dbFile);
644
+ try {
645
+ db.pragma('journal_mode = WAL');
646
+ db.pragma('busy_timeout = 5000');
647
+ db.exec(REASONING_BANK_SCHEMA);
648
+ const scope = opts.taskTypes !== undefined && opts.taskTypes.length > 0;
649
+ const scopeSql = scope ? ` AND task_type IN (${opts.taskTypes!.map(() => '?').join(', ')})` : '';
650
+ const findIds = db.prepare(`SELECT id FROM reasoning_patterns WHERE json_extract(metadata, '$.dzId') = ?${scopeSql}`) as unknown as {
651
+ all: (...a: unknown[]) => { id: number }[];
652
+ };
653
+ const delEmb = db.prepare('DELETE FROM pattern_embeddings WHERE pattern_id = ?');
654
+ const delPat = db.prepare('DELETE FROM reasoning_patterns WHERE id = ?');
655
+ const tx = db.transaction(() => {
656
+ let deleted = 0;
657
+ for (const dzId of dzIds) {
658
+ const rows = scope ? findIds.all(dzId, ...opts.taskTypes!) : findIds.all(dzId);
659
+ for (const { id } of rows) {
660
+ delEmb.run(id);
661
+ delPat.run(id);
662
+ deleted += 1;
663
+ }
664
+ }
665
+ return deleted;
666
+ });
667
+ return { deleted: tx() };
668
+ } finally {
669
+ db.close();
670
+ }
671
+ } catch (err) {
672
+ return { deleted: 0, error: `delete by dzId failed: ${err instanceof Error ? err.message : String(err)}` };
673
+ }
674
+ }
675
+
533
676
  export function bumpAgentdbUses(
534
677
  projectRoot: string,
535
678
  dzIds: readonly string[],