@dzhechkov/harness-core 0.3.128 → 0.3.129
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/.dz-manifest.json +36 -32
- package/README.md +1 -1
- package/dist/agentdb-index.d.ts +11 -0
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +44 -0
- package/dist/agentdb-index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/learning-backend.d.ts.map +1 -1
- package/dist/learning-backend.js +4 -0
- package/dist/learning-backend.js.map +1 -1
- package/dist/patterns.d.ts +74 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +148 -9
- package/dist/patterns.js.map +1 -1
- package/dist/recall-hook-policy.d.ts +7 -0
- package/dist/recall-hook-policy.d.ts.map +1 -1
- package/dist/recall-hook-policy.js +6 -2
- package/dist/recall-hook-policy.js.map +1 -1
- package/dist/vector-tier.d.ts +5 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +22 -6
- package/dist/vector-tier.js.map +1 -1
- package/package.json +5 -5
- package/sbom.json +41 -31
- package/src/agentdb-index.ts +46 -0
- package/src/index.ts +3 -3
- package/src/learning-backend.ts +4 -0
- package/src/patterns.ts +195 -11
- package/src/recall-hook-policy.ts +14 -2
- package/src/vector-tier.ts +23 -5
package/src/patterns.ts
CHANGED
|
@@ -93,6 +93,18 @@ export interface MemoryLearningConfig {
|
|
|
93
93
|
* ones nudge down. Default FALSE: absent config is byte-identical to the reinforce-only re-rank.
|
|
94
94
|
*/
|
|
95
95
|
readonly deltaRerank: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Lesson quarantine (feature lesson-quarantine, ADR-001). When true, a freshly taught lesson is
|
|
98
|
+
* a HYPOTHESIS, not knowledge: marked `qStatus: 'quarantined'`, excluded from the auto-inject
|
|
99
|
+
* hook, damped + ⚠q-marked in interactive recall, and promoted only by an EARNED signal
|
|
100
|
+
* (reinforce or an explicit `dz recall --promote`). Default FALSE: absent config is
|
|
101
|
+
* byte-identical to today.
|
|
102
|
+
*/
|
|
103
|
+
readonly quarantine: boolean;
|
|
104
|
+
/** Interactive-recall rank damp for quarantined hits, (0,1]; default 0.5. */
|
|
105
|
+
readonly quarantineDamp: number;
|
|
106
|
+
/** Days after which an unreinforced quarantined lesson is an EXPIRY CANDIDATE (informational). */
|
|
107
|
+
readonly quarantineExpireDays: number;
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
/**
|
|
@@ -128,10 +140,10 @@ export function readLearningConfig(projectRoot: string): LearningConfig {
|
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
export function readMemoryLearningConfig(projectRoot: string): MemoryLearningConfig {
|
|
131
|
-
const fallback: MemoryLearningConfig = { backend: 'native', onRecallHits: true, usesSat: 64, halfLifeDays: 30, reinforceThreshold: 0.95, deltaRerank: false };
|
|
143
|
+
const fallback: MemoryLearningConfig = { backend: 'native', onRecallHits: true, usesSat: 64, halfLifeDays: 30, reinforceThreshold: 0.95, deltaRerank: false, quarantine: false, quarantineDamp: 0.5, quarantineExpireDays: 30 };
|
|
132
144
|
try {
|
|
133
145
|
const parsed = JSON.parse(readFileSync(join(projectRoot, '.dz', 'config.json'), 'utf-8')) as {
|
|
134
|
-
memory?: { learning?: { backend?: string; onRecallHits?: boolean; usesSat?: number; halfLifeDays?: number; reinforceThreshold?: number; deltaRerank?: boolean } };
|
|
146
|
+
memory?: { learning?: { backend?: string; onRecallHits?: boolean; usesSat?: number; halfLifeDays?: number; reinforceThreshold?: number; deltaRerank?: boolean; quarantine?: boolean; quarantineDamp?: number; quarantineExpireDays?: number } };
|
|
135
147
|
};
|
|
136
148
|
const learning = parsed.memory?.learning ?? {};
|
|
137
149
|
const backend = learning.backend === 'off' || learning.backend === 'ruvector-gnn' || learning.backend === 'native' ? learning.backend : 'native';
|
|
@@ -145,6 +157,16 @@ export function readMemoryLearningConfig(projectRoot: string): MemoryLearningCon
|
|
|
145
157
|
? learning.reinforceThreshold
|
|
146
158
|
: fallback.reinforceThreshold,
|
|
147
159
|
deltaRerank: learning.deltaRerank === true, // opt-in; absent/invalid ⇒ false (byte-identical to today)
|
|
160
|
+
quarantine: learning.quarantine === true, // opt-in (byte-identical to today when absent)
|
|
161
|
+
// Number.isFinite clamps (the recurring Infinity lesson): non-finite/out-of-range ⇒ default.
|
|
162
|
+
quarantineDamp:
|
|
163
|
+
typeof learning.quarantineDamp === 'number' && Number.isFinite(learning.quarantineDamp) && learning.quarantineDamp > 0 && learning.quarantineDamp <= 1
|
|
164
|
+
? learning.quarantineDamp
|
|
165
|
+
: fallback.quarantineDamp,
|
|
166
|
+
quarantineExpireDays:
|
|
167
|
+
typeof learning.quarantineExpireDays === 'number' && Number.isFinite(learning.quarantineExpireDays) && learning.quarantineExpireDays > 0
|
|
168
|
+
? Math.floor(learning.quarantineExpireDays)
|
|
169
|
+
: fallback.quarantineExpireDays,
|
|
148
170
|
};
|
|
149
171
|
} catch {
|
|
150
172
|
return fallback;
|
|
@@ -386,6 +408,29 @@ export function recordToPattern(r: MemoryRecord): PatternRecord {
|
|
|
386
408
|
};
|
|
387
409
|
}
|
|
388
410
|
|
|
411
|
+
/**
|
|
412
|
+
* Quarantine state of a learned record (feature lesson-quarantine, ADR-001). Carried in the SAME
|
|
413
|
+
* record metadata as reinforcement state — no schema migration, no second store. ABSENCE of the
|
|
414
|
+
* field = promoted (every pre-feature record is grandfathered), and AM-1 fail-safe points TOWARD
|
|
415
|
+
* knowledge: any unparseable/unknown `qStatus` reads as promoted — a metadata glitch must never
|
|
416
|
+
* isolate a proven lesson (the conservative side here is NOT quarantining).
|
|
417
|
+
*/
|
|
418
|
+
export interface QuarantineState {
|
|
419
|
+
readonly quarantined: boolean;
|
|
420
|
+
readonly quarantinedAt?: string;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function readQuarantineState(r: MemoryRecord): QuarantineState {
|
|
424
|
+
const meta = r.metadata ?? {};
|
|
425
|
+
if (meta['qStatus'] !== 'quarantined') return { quarantined: false }; // AM-1 fail-safe
|
|
426
|
+
const at = typeof meta['quarantinedAt'] === 'string' ? meta['quarantinedAt'] : undefined;
|
|
427
|
+
return { quarantined: true, ...(at !== undefined ? { quarantinedAt: at } : {}) };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function encodeQuarantineState(ts: string): Record<string, string> {
|
|
431
|
+
return { qStatus: 'quarantined', quarantinedAt: ts };
|
|
432
|
+
}
|
|
433
|
+
|
|
389
434
|
export interface ReinforcementState {
|
|
390
435
|
readonly uses: number;
|
|
391
436
|
readonly lastUsedTs?: string;
|
|
@@ -497,7 +542,7 @@ export interface ReinforcePatternResult {
|
|
|
497
542
|
readonly error?: string;
|
|
498
543
|
}
|
|
499
544
|
|
|
500
|
-
export async function reinforcePattern(projectRoot: string, dzIdOrText: string, opts: { reward?: number; ts?: string; mergedFrom?: readonly string[] } = {}): Promise<ReinforcePatternResult> {
|
|
545
|
+
export async function reinforcePattern(projectRoot: string, dzIdOrText: string, opts: { reward?: number; ts?: string; mergedFrom?: readonly string[]; exposure?: boolean } = {}): Promise<ReinforcePatternResult> {
|
|
501
546
|
const records = loadStoreRecords(projectRoot);
|
|
502
547
|
const rec = records.find((r) => r.id === dzIdOrText || r.text === dzIdOrText);
|
|
503
548
|
if (rec === undefined) return { ok: false, error: `no learned pattern matches ${JSON.stringify(dzIdOrText)}` };
|
|
@@ -512,9 +557,18 @@ export async function reinforcePattern(projectRoot: string, dzIdOrText: string,
|
|
|
512
557
|
avgReward,
|
|
513
558
|
mergedFrom: [...prev.mergedFrom, ...(opts.mergedFrom ?? [])],
|
|
514
559
|
};
|
|
560
|
+
// Reinforcement IS the earned promotion signal (FR-6a): CONFIRMING a lesson lifts quarantine.
|
|
561
|
+
// EXPOSURE (a recall-hit sample) is not confirmation — stats update, quarantine stays (the
|
|
562
|
+
// no-promotion-by-exposure invariant; found by cross-model QE as a live hole: recall-hit
|
|
563
|
+
// flushes were routed through this same function and silently promoted every viewed lesson).
|
|
564
|
+
const promotedMeta = { ...(rec.metadata ?? {}), ...encodeReinforcementState(nextState) };
|
|
565
|
+
if (opts.exposure !== true) {
|
|
566
|
+
delete promotedMeta['qStatus'];
|
|
567
|
+
delete promotedMeta['quarantinedAt'];
|
|
568
|
+
}
|
|
515
569
|
const next: MemoryRecord = {
|
|
516
570
|
...rec,
|
|
517
|
-
metadata:
|
|
571
|
+
metadata: promotedMeta,
|
|
518
572
|
};
|
|
519
573
|
const put = await putStoreRecord(projectRoot, next);
|
|
520
574
|
if ('error' in put) return { ok: false, dzId: rec.id, error: put.error };
|
|
@@ -536,6 +590,99 @@ export async function updateReinforcementState(projectRoot: string, dzId: string
|
|
|
536
590
|
return { ok: true, dzId, uses: state.uses };
|
|
537
591
|
}
|
|
538
592
|
|
|
593
|
+
/** Result of a promotion (dz recall --promote). */
|
|
594
|
+
export interface PromoteResult {
|
|
595
|
+
readonly ok: boolean;
|
|
596
|
+
readonly promoted: readonly string[];
|
|
597
|
+
readonly notFound: readonly string[];
|
|
598
|
+
readonly notQuarantined: readonly string[];
|
|
599
|
+
readonly error?: string;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* FR-6b: EXPLICIT promotion — lift quarantine from the named records. The other earned path is
|
|
604
|
+
* reinforcement ({@link reinforcePattern} clears the state as part of confirming the lesson).
|
|
605
|
+
* There is deliberately NO exposure-based auto-promotion: `uses` grows from recall HITS, and
|
|
606
|
+
* promoting by exposure would be a self-fulfilling prophecy (ADR).
|
|
607
|
+
*/
|
|
608
|
+
export async function promotePatterns(projectRoot: string, dzIds: readonly string[]): Promise<PromoteResult> {
|
|
609
|
+
const records = loadStoreRecords(projectRoot);
|
|
610
|
+
const promoted: string[] = [];
|
|
611
|
+
const notFound: string[] = [];
|
|
612
|
+
const notQuarantined: string[] = [];
|
|
613
|
+
for (const id of dzIds) {
|
|
614
|
+
const rec = records.find((r) => r.id === id);
|
|
615
|
+
if (rec === undefined) { notFound.push(id); continue; }
|
|
616
|
+
if (!readQuarantineState(rec).quarantined) { notQuarantined.push(id); continue; }
|
|
617
|
+
const meta = { ...(rec.metadata ?? {}) };
|
|
618
|
+
delete meta['qStatus'];
|
|
619
|
+
delete meta['quarantinedAt'];
|
|
620
|
+
const put = await putStoreRecord(projectRoot, { ...rec, metadata: meta });
|
|
621
|
+
if ('error' in put) return { ok: false, promoted, notFound, notQuarantined, error: put.error };
|
|
622
|
+
promoted.push(id);
|
|
623
|
+
}
|
|
624
|
+
return { ok: true, promoted, notFound, notQuarantined };
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** One quarantined-and-stale record, surfaced for review (informational — FR-7). */
|
|
628
|
+
export interface QuarantineExpiryCandidate {
|
|
629
|
+
readonly dzId: string;
|
|
630
|
+
readonly text: string;
|
|
631
|
+
readonly quarantinedAt: string;
|
|
632
|
+
readonly ageDays: number;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* FR-7 (informational-first, per the recalled decay-vs-noise lesson): quarantined records older
|
|
637
|
+
* than `expireDays` with ZERO reinforcement are EXPIRY CANDIDATES — reported, never auto-deleted.
|
|
638
|
+
* Deletion happens only through {@link pruneQuarantinePatterns} (explicit, snapshotted).
|
|
639
|
+
*/
|
|
640
|
+
export function quarantineExpiryCandidates(projectRoot: string, expireDays: number, now: Date = new Date()): QuarantineExpiryCandidate[] {
|
|
641
|
+
const out: QuarantineExpiryCandidate[] = [];
|
|
642
|
+
// Codex-QE fix (finding 6): the destructive API clamps its own input — a zero/negative/NaN
|
|
643
|
+
// expireDays would make FRESH lessons instantly deletable through the exported surface.
|
|
644
|
+
const days = Number.isFinite(expireDays) && expireDays > 0 ? expireDays : 30;
|
|
645
|
+
for (const r of loadStoreRecords(projectRoot)) {
|
|
646
|
+
const q = readQuarantineState(r);
|
|
647
|
+
if (!q.quarantined) continue;
|
|
648
|
+
// NO uses-based immunity (Codex-QE finding 1): a TRULY reinforced record is no longer
|
|
649
|
+
// quarantined at all (reinforce clears qStatus), so any uses on a still-quarantined record
|
|
650
|
+
// are EXPOSURE — and exposure must not immortalize a hypothesis.
|
|
651
|
+
const at = q.quarantinedAt ?? r.timestamp;
|
|
652
|
+
let ageMs = now.getTime() - new Date(at).getTime();
|
|
653
|
+
if (!Number.isFinite(ageMs)) ageMs = now.getTime() - new Date(r.timestamp).getTime(); // finding 7: fall back
|
|
654
|
+
if (!Number.isFinite(ageMs)) {
|
|
655
|
+
// Both timestamps corrupt: surface for human review (ageDays -1 renders as "?") — an
|
|
656
|
+
// unparseable age must not make the record IMMORTAL in quarantine (finding 7).
|
|
657
|
+
out.push({ dzId: r.id, text: r.text, quarantinedAt: at, ageDays: -1 });
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const ageDays = ageMs / 86_400_000;
|
|
661
|
+
if (ageDays >= days) out.push({ dzId: r.id, text: r.text, quarantinedAt: at, ageDays: Math.floor(ageDays) });
|
|
662
|
+
}
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Destructive half of expiry — a SEPARATE, explicit gate (never coupled to --prune-noise; AM-3).
|
|
668
|
+
* Dry-run by default; a live run snapshots the store first ({@link snapshotStore}) then removes
|
|
669
|
+
* exactly the expiry candidates via {@link removePatternsByIds}.
|
|
670
|
+
*/
|
|
671
|
+
export function pruneQuarantinePatterns(
|
|
672
|
+
projectRoot: string,
|
|
673
|
+
opts: { dryRun?: boolean; expireDays: number; now?: Date },
|
|
674
|
+
): { candidates: QuarantineExpiryCandidate[]; removed: number; snapshot?: string; error?: string } {
|
|
675
|
+
const now = opts.now ?? new Date();
|
|
676
|
+
const candidates = quarantineExpiryCandidates(projectRoot, opts.expireDays, now);
|
|
677
|
+
if (opts.dryRun !== false || candidates.length === 0) return { candidates, removed: 0 };
|
|
678
|
+
// Snapshot BEFORE the drop; a failed snapshot ABORTS the deletion (the harmonize precedent —
|
|
679
|
+
// a destructive sweep with no receipt is exactly what the recalled decay lesson forbids).
|
|
680
|
+
const snap = snapshotStore(projectRoot, join(projectRoot, '.dz', `patterns-pre-prune-quarantine-${now.getTime()}.json`));
|
|
681
|
+
if (snap.error !== undefined) return { candidates, removed: 0, error: `aborted: ${snap.error}` };
|
|
682
|
+
const res = removePatternsByIds(projectRoot, new Set(candidates.map((c) => c.dzId)));
|
|
683
|
+
return { candidates, removed: res.removed, snapshot: snap.path, ...(res.error !== undefined ? { error: res.error } : {}) };
|
|
684
|
+
}
|
|
685
|
+
|
|
539
686
|
export interface StoreStats {
|
|
540
687
|
readonly total: number;
|
|
541
688
|
readonly perDomain: Record<string, number>;
|
|
@@ -703,13 +850,18 @@ function migrationRecords(projectRoot: string): MemoryRecord[] {
|
|
|
703
850
|
* duplicates), so migrating JSON→SQLite never loses or doubles a record, and the
|
|
704
851
|
* JSON file is never deleted. Returns the total record count after the write.
|
|
705
852
|
*/
|
|
706
|
-
export async function recordPattern(projectRoot: string, p: PatternRecord): Promise<number> {
|
|
853
|
+
export async function recordPattern(projectRoot: string, p: PatternRecord, opts: { quarantine?: boolean } = {}): Promise<number> {
|
|
707
854
|
const { sqliteBackend } = readLearningConfig(projectRoot);
|
|
855
|
+
// lesson-quarantine: a fresh lesson is a HYPOTHESIS — mark it when the feature is on. Folded
|
|
856
|
+
// legacy records are NEVER marked (they predate the feature: grandfathered as promoted).
|
|
857
|
+
const rec = opts.quarantine === true
|
|
858
|
+
? { ...patternToRecord(p), metadata: { ...patternToRecord(p).metadata, ...encodeQuarantineState(p.ts) } }
|
|
859
|
+
: patternToRecord(p);
|
|
708
860
|
const sqlite = tryOpenSqlite(projectRoot, sqliteBackend);
|
|
709
861
|
if (sqlite) {
|
|
710
862
|
try {
|
|
711
863
|
for (const r of migrationRecords(projectRoot)) await sqlite.put(r);
|
|
712
|
-
await sqlite.put(
|
|
864
|
+
await sqlite.put(rec);
|
|
713
865
|
return await sqlite.count();
|
|
714
866
|
} finally {
|
|
715
867
|
sqlite.close();
|
|
@@ -720,7 +872,7 @@ export async function recordPattern(projectRoot: string, p: PatternRecord): Prom
|
|
|
720
872
|
for (const legacy of readJsonl(join(projectRoot, '.dz', 'patterns.jsonl'), isPatternRecord)) {
|
|
721
873
|
await backend.put(patternToRecord(legacy));
|
|
722
874
|
}
|
|
723
|
-
await backend.put(
|
|
875
|
+
await backend.put(rec);
|
|
724
876
|
await backend.save();
|
|
725
877
|
return backend.count();
|
|
726
878
|
}
|
|
@@ -735,6 +887,8 @@ export interface RecallHit {
|
|
|
735
887
|
* emits them — it stays the sync lexical baseline, AC-5).
|
|
736
888
|
*/
|
|
737
889
|
readonly backend: 'sqlite' | 'json' | 'vector' | 'both';
|
|
890
|
+
/** lesson-quarantine: set (true) only for a quarantined hit — display marks it ⚠q and ranking damps/sinks it. */
|
|
891
|
+
readonly quarantined?: boolean;
|
|
738
892
|
}
|
|
739
893
|
|
|
740
894
|
/**
|
|
@@ -749,21 +903,47 @@ export function recallPatterns(projectRoot: string, query: string, limit = 10):
|
|
|
749
903
|
try {
|
|
750
904
|
const db = SqliteBackend.open(sqlitePath(projectRoot));
|
|
751
905
|
try {
|
|
752
|
-
return
|
|
906
|
+
return sinkQuarantined(
|
|
907
|
+
db.querySync({ text: query, limit: limit * 2 }).map((r) => ({
|
|
908
|
+
pattern: recordToPattern(r),
|
|
909
|
+
backend: 'sqlite' as const,
|
|
910
|
+
...(readQuarantineState(r).quarantined ? { quarantined: true as const } : {}),
|
|
911
|
+
})),
|
|
912
|
+
limit,
|
|
913
|
+
);
|
|
753
914
|
} finally {
|
|
754
915
|
db.close();
|
|
755
916
|
}
|
|
756
917
|
} catch { /* fall through to JSON */ }
|
|
757
918
|
}
|
|
758
919
|
try {
|
|
759
|
-
return
|
|
760
|
-
.
|
|
761
|
-
|
|
920
|
+
return sinkQuarantined(
|
|
921
|
+
JsonFileBackend.openSync(storePath(projectRoot))
|
|
922
|
+
.querySync({ text: query, limit: limit * 2 })
|
|
923
|
+
.map((r) => ({
|
|
924
|
+
pattern: recordToPattern(r),
|
|
925
|
+
backend: 'json' as const,
|
|
926
|
+
...(readQuarantineState(r).quarantined ? { quarantined: true as const } : {}),
|
|
927
|
+
})),
|
|
928
|
+
limit,
|
|
929
|
+
);
|
|
762
930
|
} catch {
|
|
763
931
|
return [];
|
|
764
932
|
}
|
|
765
933
|
}
|
|
766
934
|
|
|
935
|
+
/**
|
|
936
|
+
* Lexical recall has no numeric rank to damp, so quarantine "damping" is a STABLE sink: promoted
|
|
937
|
+
* hits keep their relative order first, quarantined hits follow (still VISIBLE — hiding them would
|
|
938
|
+
* make the loop a write-only log; ADR D2). Hybrid (scored) recall damps numerically instead.
|
|
939
|
+
*/
|
|
940
|
+
function sinkQuarantined<T extends { quarantined?: boolean }>(hits: readonly T[], limit?: number): T[] {
|
|
941
|
+
// Codex-QE finding 2: the query OVERFETCHES (2×limit) before sinking, so quarantined hits in
|
|
942
|
+
// the backend's top-K cannot crowd promoted knowledge out of the final window.
|
|
943
|
+
const sunk = [...hits.filter((h) => h.quarantined !== true), ...hits.filter((h) => h.quarantined === true)];
|
|
944
|
+
return limit !== undefined ? sunk.slice(0, limit) : sunk;
|
|
945
|
+
}
|
|
946
|
+
|
|
767
947
|
/* ------------------------------------------------------------------ */
|
|
768
948
|
/* Tier-2.5: session consolidation (harvestDreamPatterns) */
|
|
769
949
|
/* ------------------------------------------------------------------ */
|
|
@@ -1044,6 +1224,9 @@ export function pruneNoisePatterns(projectRoot: string, opts: { dryRun?: boolean
|
|
|
1044
1224
|
const db = SqliteBackend.open(sqlitePath(projectRoot));
|
|
1045
1225
|
try {
|
|
1046
1226
|
for (const r of db.allSync()) {
|
|
1227
|
+
// AM-3 (recalled decay-vs-noise lesson): a quarantined record is valid-but-unproven, not
|
|
1228
|
+
// garbage — noise-prune must NEVER touch it; its lifecycle belongs to prune-quarantine.
|
|
1229
|
+
if (readQuarantineState(r).quarantined) continue;
|
|
1047
1230
|
if (isNoiseInsight(r.text)) {
|
|
1048
1231
|
candidates.push({ id: r.id, text: r.text });
|
|
1049
1232
|
if (!dryRun) db.removeSync(r.id);
|
|
@@ -1064,6 +1247,7 @@ export function pruneNoisePatterns(projectRoot: string, opts: { dryRun?: boolean
|
|
|
1064
1247
|
const backend = JsonFileBackend.openSync(storePath(projectRoot));
|
|
1065
1248
|
let removed = 0;
|
|
1066
1249
|
for (const r of backend.allSync()) {
|
|
1250
|
+
if (readQuarantineState(r).quarantined) continue; // AM-3: never coupled to noise-prune
|
|
1067
1251
|
if (isNoiseInsight(r.text)) {
|
|
1068
1252
|
if (!candidates.some((c) => c.id === r.id)) candidates.push({ id: r.id, text: r.text });
|
|
1069
1253
|
if (!dryRun) backend.removeSync(r.id);
|
|
@@ -100,6 +100,8 @@ export interface HookCandidate {
|
|
|
100
100
|
readonly pattern: string;
|
|
101
101
|
readonly score: number;
|
|
102
102
|
readonly domain?: string;
|
|
103
|
+
/** lesson-quarantine: an unproven hypothesis — the AUTO-INJECT surface excludes it (FR-5). */
|
|
104
|
+
readonly quarantined?: boolean;
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
export interface HookSelection {
|
|
@@ -107,6 +109,11 @@ export interface HookSelection {
|
|
|
107
109
|
/** The floor that was applied — reported so the hook's own output can explain its silence. */
|
|
108
110
|
readonly floor: number;
|
|
109
111
|
readonly lang: QueryLang;
|
|
112
|
+
/**
|
|
113
|
+
* lesson-quarantine AM-2: how many candidates were dropped for being quarantined — the
|
|
114
|
+
* exclusion is OBSERVABLE (the hook logs it), never a silent shrink of the context.
|
|
115
|
+
*/
|
|
116
|
+
readonly quarantinedExcluded: number;
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
/**
|
|
@@ -125,9 +132,14 @@ export function selectHookHits(
|
|
|
125
132
|
const budget = intOr(opts.budgetChars, DEFAULT_RECALL_HOOK_BUDGET_CHARS);
|
|
126
133
|
|
|
127
134
|
// A prompt with no intent gets no injection, whatever its cosine says.
|
|
128
|
-
if (!hasEnoughSignal(prompt)) return { hits: [], floor, lang };
|
|
135
|
+
if (!hasEnoughSignal(prompt)) return { hits: [], floor, lang, quarantinedExcluded: 0 };
|
|
136
|
+
|
|
137
|
+
// FR-5: the auto-inject surface is the STRICTEST — a quarantined lesson (unproven hypothesis)
|
|
138
|
+
// never rides into a prompt uninvited. Counted, not silent (AM-2).
|
|
139
|
+
const quarantinedExcluded = (candidates ?? []).filter((c) => c !== null && typeof c === 'object' && c.quarantined === true).length;
|
|
129
140
|
|
|
130
141
|
const clean = (candidates ?? [])
|
|
142
|
+
.filter((c) => !(c !== null && typeof c === 'object' && c.quarantined === true))
|
|
131
143
|
.filter(
|
|
132
144
|
(c): c is HookCandidate =>
|
|
133
145
|
c !== null &&
|
|
@@ -152,7 +164,7 @@ export function selectHookHits(
|
|
|
152
164
|
hits.push(c);
|
|
153
165
|
used += cost;
|
|
154
166
|
}
|
|
155
|
-
return { hits, floor, lang };
|
|
167
|
+
return { hits, floor, lang, quarantinedExcluded };
|
|
156
168
|
}
|
|
157
169
|
|
|
158
170
|
function intOr(v: unknown, fallback: number): number {
|
package/src/vector-tier.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
loadStoreRecords,
|
|
51
51
|
readMemoryLearningConfig,
|
|
52
52
|
readReinforcementState,
|
|
53
|
+
readQuarantineState,
|
|
53
54
|
removePatternsByIds,
|
|
54
55
|
snapshotStore,
|
|
55
56
|
updateReinforcementState,
|
|
@@ -155,6 +156,8 @@ export interface HybridHit {
|
|
|
155
156
|
readonly backend: RecallHit['backend'];
|
|
156
157
|
/** Reciprocal-rank-fusion score (ranking only — NOT the pattern's reward). */
|
|
157
158
|
readonly score: number;
|
|
159
|
+
/** lesson-quarantine: set only for a quarantined hit — display marks ⚠q, ranking was damped. */
|
|
160
|
+
readonly quarantined?: boolean;
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
/** Outcome of {@link recallHybrid}. With no engine this is content-identical to `recallPatterns`. */
|
|
@@ -336,7 +339,7 @@ export function isVectorNoise(text: string): boolean {
|
|
|
336
339
|
* ACL: taught {@link PatternRecord} → {@link VectorEntry}. Returns `undefined` for noise (the
|
|
337
340
|
* ingest gate — I-6). Score is the record's REAL reward, never a fabricated 1.0.
|
|
338
341
|
*/
|
|
339
|
-
export function patternVectorEntry(p: PatternRecord, source = 'dz-teach'): VectorEntry | undefined {
|
|
342
|
+
export function patternVectorEntry(p: PatternRecord, source = 'dz-teach', opts: { quarantined?: boolean } = {}): VectorEntry | undefined {
|
|
340
343
|
if (isVectorNoise(p.pattern)) return undefined;
|
|
341
344
|
const dzId = patternRecordId(p);
|
|
342
345
|
return {
|
|
@@ -345,7 +348,9 @@ export function patternVectorEntry(p: PatternRecord, source = 'dz-teach'): Vecto
|
|
|
345
348
|
score: p.reward,
|
|
346
349
|
taskType: 'dz-teach',
|
|
347
350
|
tags: ['dz-teach', p.type],
|
|
348
|
-
|
|
351
|
+
// FR-8: quarantine rides into the mirror so the HOOK DAEMON (which reads only the mirror's
|
|
352
|
+
// sqlite metadata) can exclude unproven lessons from auto-injection.
|
|
353
|
+
metadata: { dzId, source, ts: p.ts, domain: p.domain, ...(opts.quarantined === true ? { qStatus: 'quarantined' } : {}) },
|
|
349
354
|
};
|
|
350
355
|
}
|
|
351
356
|
|
|
@@ -790,7 +795,20 @@ export async function recallHybrid(
|
|
|
790
795
|
const learning = resolveLearningBackend(projectRoot);
|
|
791
796
|
// Phase 3: opt-in SAFLA-delta re-rank. The map is built ONCE per recall (off ⇒ undefined ⇒ the
|
|
792
797
|
// reinforce-only path, byte-identical to today).
|
|
793
|
-
const
|
|
798
|
+
const memCfg = readMemoryLearningConfig(projectRoot);
|
|
799
|
+
const deltaMap = memCfg.deltaRerank ? lessonDeltaMap(projectRoot) : undefined;
|
|
800
|
+
// lesson-quarantine: damp read from the AUTHORITATIVE store records (idToRecord), never from
|
|
801
|
+
// mirror metadata — the mirror may lag a promotion; the store cannot.
|
|
802
|
+
const dampQuarantined = (hits: readonly HybridHit[]): HybridHit[] => {
|
|
803
|
+
if (!memCfg.quarantine) return [...hits];
|
|
804
|
+
return hits
|
|
805
|
+
.map((h) => {
|
|
806
|
+
const rec = idToRecord.get(idOf(h.pattern));
|
|
807
|
+
const q = rec !== undefined && readQuarantineState(rec).quarantined;
|
|
808
|
+
return q ? { ...h, score: h.score * memCfg.quarantineDamp, quarantined: true as const } : h;
|
|
809
|
+
})
|
|
810
|
+
.sort((a, b) => b.score - a.score);
|
|
811
|
+
};
|
|
794
812
|
const enhance = (hits: readonly HybridHit[]): HybridHit[] => {
|
|
795
813
|
const candidates = hits.map((h) => {
|
|
796
814
|
const dzId = idOf(h.pattern);
|
|
@@ -799,9 +817,9 @@ export async function recallHybrid(
|
|
|
799
817
|
});
|
|
800
818
|
if (deltaMap !== undefined) {
|
|
801
819
|
const deltaByIndex = candidates.map((c) => deltaMap.get(c.dzId) ?? 0);
|
|
802
|
-
return applyLearningSignalsWithDelta(hits, learning, candidates, REINFORCE_RRF_CAP, deltaByIndex, REINFORCE_RRF_CAP);
|
|
820
|
+
return dampQuarantined(applyLearningSignalsWithDelta(hits, learning, candidates, REINFORCE_RRF_CAP, deltaByIndex, REINFORCE_RRF_CAP));
|
|
803
821
|
}
|
|
804
|
-
return applyLearningSignals(hits, learning, candidates, REINFORCE_RRF_CAP);
|
|
822
|
+
return dampQuarantined(applyLearningSignals(hits, learning, candidates, REINFORCE_RRF_CAP));
|
|
805
823
|
};
|
|
806
824
|
const lexicalOnly = (extra: Partial<Pick<HybridRecall, 'vectorEngine' | 'vectorReason' | 'vectorError'>>): HybridRecall => ({
|
|
807
825
|
hits: enhance(lexical.map((h, rank) => ({ pattern: h.pattern, backend: h.backend, score: 1 / (RRF_K + rank + 1) }))),
|