@dzhechkov/harness-core 0.8.30 → 0.8.31
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 +233 -93
- package/README.md +269 -0
- package/dist/agentdb-index.d.ts +45 -0
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +345 -85
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-reindex-marker.d.ts +170 -0
- package/dist/agentdb-reindex-marker.d.ts.map +1 -0
- package/dist/agentdb-reindex-marker.js +293 -0
- package/dist/agentdb-reindex-marker.js.map +1 -0
- package/dist/agentdb-snapshot-rotation.d.ts +169 -0
- package/dist/agentdb-snapshot-rotation.d.ts.map +1 -0
- package/dist/agentdb-snapshot-rotation.js +322 -0
- package/dist/agentdb-snapshot-rotation.js.map +1 -0
- package/dist/agentdb-snapshot.d.ts +126 -0
- package/dist/agentdb-snapshot.d.ts.map +1 -0
- package/dist/agentdb-snapshot.js +195 -0
- package/dist/agentdb-snapshot.js.map +1 -0
- package/dist/apply-leg.d.ts +202 -0
- package/dist/apply-leg.d.ts.map +1 -0
- package/dist/apply-leg.js +975 -0
- package/dist/apply-leg.js.map +1 -0
- package/dist/book-kb.d.ts.map +1 -1
- package/dist/book-kb.js +57 -1
- package/dist/book-kb.js.map +1 -1
- package/dist/brain.d.ts +3 -0
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +1 -1
- package/dist/brain.js.map +1 -1
- package/dist/cadence.d.ts +30 -1
- package/dist/cadence.d.ts.map +1 -1
- package/dist/cadence.js +66 -2
- package/dist/cadence.js.map +1 -1
- package/dist/guard.d.ts +14 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +36 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +23 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -5
- package/dist/index.js.map +1 -1
- package/dist/integration-probe-worker.d.ts.map +1 -1
- package/dist/integration-probe-worker.js +4 -6
- package/dist/integration-probe-worker.js.map +1 -1
- package/dist/integrations-verify.d.ts.map +1 -1
- package/dist/integrations-verify.js +3 -1
- package/dist/integrations-verify.js.map +1 -1
- package/dist/managed-hooks.d.ts +11 -2
- package/dist/managed-hooks.d.ts.map +1 -1
- package/dist/managed-hooks.js +33 -7
- package/dist/managed-hooks.js.map +1 -1
- package/dist/mutation-gate.d.ts +35 -1
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +49 -9
- package/dist/mutation-gate.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +94 -0
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +23 -4
- package/dist/patterns.js.map +1 -1
- package/dist/repository-origin.d.ts +1 -1
- package/dist/repository-origin.d.ts.map +1 -1
- package/dist/repository-origin.js +7 -1
- package/dist/repository-origin.js.map +1 -1
- package/dist/round-exec.d.ts +41 -0
- package/dist/round-exec.d.ts.map +1 -0
- package/dist/round-exec.js +45 -0
- package/dist/round-exec.js.map +1 -0
- package/dist/round.d.ts +109 -0
- package/dist/round.d.ts.map +1 -0
- package/dist/round.js +148 -0
- package/dist/round.js.map +1 -0
- package/dist/run-records.d.ts +8 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +69 -3
- package/dist/run-records.js.map +1 -1
- package/dist/setup.d.ts +8 -0
- package/dist/setup.d.ts.map +1 -1
- package/dist/setup.js +166 -3
- package/dist/setup.js.map +1 -1
- package/dist/sqlite-read-helpers.d.ts +60 -0
- package/dist/sqlite-read-helpers.d.ts.map +1 -0
- package/dist/sqlite-read-helpers.js +118 -0
- package/dist/sqlite-read-helpers.js.map +1 -0
- package/dist/statusline.d.ts +20 -3
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +40 -7
- package/dist/statusline.js.map +1 -1
- package/dist/store-counts.d.ts +30 -0
- package/dist/store-counts.d.ts.map +1 -1
- package/dist/store-counts.js +189 -0
- package/dist/store-counts.js.map +1 -1
- package/dist/usage.d.ts +55 -0
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +184 -33
- package/dist/usage.js.map +1 -1
- package/dist/vector-tier.d.ts +18 -1
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +26 -2
- package/dist/vector-tier.js.map +1 -1
- package/package.json +6 -6
- package/sbom.json +442 -92
- package/src/agentdb-index.ts +315 -32
- package/src/agentdb-reindex-marker.ts +330 -0
- package/src/agentdb-snapshot-rotation.ts +393 -0
- package/src/agentdb-snapshot.ts +222 -0
- package/src/apply-leg.ts +1046 -0
- package/src/book-kb.ts +64 -1
- package/src/brain.ts +11 -2
- package/src/cadence.ts +87 -2
- package/src/guard.ts +46 -0
- package/src/index.ts +58 -5
- package/src/integration-probe-worker.ts +4 -6
- package/src/integrations-verify.ts +3 -1
- package/src/managed-hooks.ts +32 -7
- package/src/mutation-gate.ts +87 -8
- package/src/operations.ts +89 -0
- package/src/patterns.ts +23 -4
- package/src/repository-origin.ts +7 -1
- package/src/round-exec.ts +85 -0
- package/src/round.ts +228 -0
- package/src/run-records.ts +77 -2
- package/src/setup.ts +198 -3
- package/src/sqlite-read-helpers.ts +128 -0
- package/src/statusline.ts +57 -9
- package/src/store-counts.ts +245 -1
- package/src/usage.ts +243 -29
- package/src/vector-tier.ts +45 -4
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small, shared helpers for the READ-ONLY sqlite paths (`book-kb.ts`, `agentdb-index.ts`,
|
|
3
|
+
* `patterns.ts`) — feature `readonly-residuals`.
|
|
4
|
+
*
|
|
5
|
+
* Two residuals fixed here (`features/readonly-residuals/01_requirements.md`):
|
|
6
|
+
*
|
|
7
|
+
* FR-1. `applyReadonlyPragmas` closes a handle that was already opened by
|
|
8
|
+
* `openSqliteReadOnly` (`@dzhechkov/memory`) if the follow-up `busy_timeout` pragma throws.
|
|
9
|
+
* Before this helper existed, `book-kb.ts`/`agentdb-index.ts` called `db.pragma(...)`
|
|
10
|
+
* directly on the handle's `db`, outside any `try` — a pragma failure left BOTH the native
|
|
11
|
+
* connection and a tmp-copy directory (ADR-001 ladder rung 2) leaked, because `handle.cleanup()`
|
|
12
|
+
* is only wired into the `close()` this code never reaches.
|
|
13
|
+
*
|
|
14
|
+
* FR-2. `classifySqliteReadFailure` tells apart "the native module isn't installed here"
|
|
15
|
+
* (silent JSON fallback, unchanged behaviour) from "the store file itself is unreadable"
|
|
16
|
+
* (corrupt file, permission failure, …) — the second case used to be swallowed by the same
|
|
17
|
+
* bare `catch {}` in `patterns.ts`, so a corrupted `patterns.sqlite` silently looked like
|
|
18
|
+
* "fewer lessons" instead of "the store broke". `warnOnce` prints that second case exactly
|
|
19
|
+
* once per process, on stderr, without changing the JSON-fallback data path.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Minimal shape `applyReadonlyPragmas` needs from an `openSqliteReadOnly` handle. */
|
|
23
|
+
export interface PragmaTarget {
|
|
24
|
+
readonly db: { pragma: (sql: string) => void; close: () => void };
|
|
25
|
+
readonly cleanup: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function closeQuietly(db: { close: () => void }): void {
|
|
29
|
+
try {
|
|
30
|
+
db.close();
|
|
31
|
+
} catch {
|
|
32
|
+
// already failing on the caller's side — a close failure here must not mask the real cause
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run `fn()` and swallow any exception it throws — used for `handle.cleanup()` in the
|
|
38
|
+
* pragma-failure path (fix round 1, MEDIUM #2): a throwing `cleanup()` must not replace the
|
|
39
|
+
* original pragma error the caller is already in the middle of rethrowing.
|
|
40
|
+
*/
|
|
41
|
+
function cleanupQuietly(fn: () => void): void {
|
|
42
|
+
try {
|
|
43
|
+
fn();
|
|
44
|
+
} catch {
|
|
45
|
+
// the pragma failure is the error of record — a cleanup failure here must not mask it
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Run `db.pragma('busy_timeout = 5000')` on an already-opened read-only handle. If the pragma
|
|
51
|
+
* throws, the connection is closed and `handle.cleanup()` is called (removing a tmp-copy
|
|
52
|
+
* directory, ADR-001 rung 2) BEFORE rethrowing — so a pragma failure never leaks either the
|
|
53
|
+
* native connection or the temporary copy. Both `close()` and `cleanup()` are individually
|
|
54
|
+
* guarded (`closeQuietly` / `cleanupQuietly`, fix round 1, MEDIUM #2) so that EITHER one throwing
|
|
55
|
+
* still lets the original pragma cause reach the caller with the db path attached, and both are
|
|
56
|
+
* still attempted exactly once each regardless of which one (if any) throws.
|
|
57
|
+
*/
|
|
58
|
+
export function applyReadonlyPragmas(handle: PragmaTarget, path: string): void {
|
|
59
|
+
try {
|
|
60
|
+
handle.db.pragma('busy_timeout = 5000'); // harmless on a readonly connection — kept for parity with the writer opener
|
|
61
|
+
} catch (err) {
|
|
62
|
+
closeQuietly(handle.db);
|
|
63
|
+
cleanupQuietly(() => handle.cleanup());
|
|
64
|
+
const cause = err instanceof Error ? err.message : String(err);
|
|
65
|
+
throw new Error(`failed to prepare ${path} for reading: ${cause}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Which of the two known catch-all reasons a store-opener failure names. */
|
|
70
|
+
export type SqliteReadFailureKind = 'native-unavailable' | 'store-unreadable';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Loader-specific signatures of "the `better-sqlite3` native module itself could not be loaded"
|
|
74
|
+
* (fix round 1, MEDIUM #1 — narrows the classifier after Codex review found the previous bare
|
|
75
|
+
* `/better-sqlite3/.test(msg)` swallowed `better-sqlite3: file is not a database`, a genuine
|
|
76
|
+
* corruption, as if the module were merely absent). Each pattern names a load-time failure mode,
|
|
77
|
+
* never a runtime/data failure that merely happens to mention the package name.
|
|
78
|
+
*/
|
|
79
|
+
const NATIVE_LOADER_PATTERNS: readonly RegExp[] = [
|
|
80
|
+
/Cannot find module ['"]?better-sqlite3/,
|
|
81
|
+
/better-sqlite3 is unavailable/,
|
|
82
|
+
/Could not locate the bindings file/,
|
|
83
|
+
/was compiled against a different Node\.js version/,
|
|
84
|
+
/invalid ELF header/,
|
|
85
|
+
/NODE_MODULE_VERSION/,
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Classify an error caught around `SqliteBackend.openReadOnly` (or an equivalent opener):
|
|
90
|
+
* `'native-unavailable'` only when the failure is genuinely about `better-sqlite3` failing to
|
|
91
|
+
* LOAD — either `code === 'MODULE_NOT_FOUND'` with `better-sqlite3` named in the message or
|
|
92
|
+
* `requireStack` (a bare `MODULE_NOT_FOUND` for an unrelated module is NOT this — it is a
|
|
93
|
+
* different bug and must not be hidden as "native unavailable"), or the message matches one of
|
|
94
|
+
* the {@link NATIVE_LOADER_PATTERNS} native-module-loader signatures (missing module, ABI/version
|
|
95
|
+
* mismatch, corrupt/foreign binary). The existing silent JSON fallback is correct here, nothing
|
|
96
|
+
* changes. Anything else — a corrupt file (`file is not a database`), a permission failure, a
|
|
97
|
+
* genuinely unreadable store — is `'store-unreadable'`: still falls back to JSON (FR-2 does not
|
|
98
|
+
* change returned data), but the caller is expected to warn once via {@link warnOnce}.
|
|
99
|
+
*/
|
|
100
|
+
export function classifySqliteReadFailure(err: unknown): SqliteReadFailureKind {
|
|
101
|
+
const code = (err as { code?: string } | undefined)?.code;
|
|
102
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
103
|
+
const requireStackRaw = (err as { requireStack?: unknown } | undefined)?.requireStack;
|
|
104
|
+
const requireStack = Array.isArray(requireStackRaw) ? requireStackRaw.join('\n') : '';
|
|
105
|
+
|
|
106
|
+
if (code === 'MODULE_NOT_FOUND' && (/better-sqlite3/.test(msg) || /better-sqlite3/.test(requireStack))) {
|
|
107
|
+
return 'native-unavailable';
|
|
108
|
+
}
|
|
109
|
+
if (NATIVE_LOADER_PATTERNS.some((re) => re.test(msg))) return 'native-unavailable';
|
|
110
|
+
return 'store-unreadable';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const warnedKeys = new Set<string>();
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Write `text` to stderr the first time this exact `key` is seen in this process, and never
|
|
117
|
+
* again. Used to surface a `'store-unreadable'` classification exactly once per store path per
|
|
118
|
+
* process, instead of once per read call.
|
|
119
|
+
*/
|
|
120
|
+
export function warnOnce(key: string, text: string): void {
|
|
121
|
+
if (warnedKeys.has(key)) return;
|
|
122
|
+
warnedKeys.add(key);
|
|
123
|
+
try {
|
|
124
|
+
process.stderr.write(`${text}\n`);
|
|
125
|
+
} catch {
|
|
126
|
+
// a closed/broken stderr must not break the caller's fallback
|
|
127
|
+
}
|
|
128
|
+
}
|
package/src/statusline.ts
CHANGED
|
@@ -70,15 +70,30 @@ export interface StatuslineData {
|
|
|
70
70
|
readonly patterns: number;
|
|
71
71
|
/** Absent on parity; missing/unreadable mirror is explicitly unavailable. */
|
|
72
72
|
readonly patternMirror?:
|
|
73
|
-
| {
|
|
73
|
+
| {
|
|
74
|
+
readonly state: 'in-sync' | 'different';
|
|
75
|
+
readonly lexicalMirrorable: number;
|
|
76
|
+
readonly vector: number;
|
|
77
|
+
readonly excluded: { readonly class: number; readonly noise: number };
|
|
78
|
+
}
|
|
74
79
|
| { readonly state: 'unavailable' };
|
|
80
|
+
/** Vector-mirror inventory; absence is explicit instead of being encoded as a missing field. */
|
|
81
|
+
readonly mirror: {
|
|
82
|
+
readonly available: boolean;
|
|
83
|
+
readonly rows: number;
|
|
84
|
+
readonly lessons: number;
|
|
85
|
+
readonly pending: number;
|
|
86
|
+
readonly source: 'agentdb';
|
|
87
|
+
};
|
|
75
88
|
/** Exact lexical-tier availability split; omitted when the enhanced readonly count cannot be established. */
|
|
76
89
|
readonly patternBreakdown?: {
|
|
77
|
-
readonly source: 'lexical';
|
|
90
|
+
readonly source: 'lexical' | 'lexical+mirror';
|
|
78
91
|
readonly active: number;
|
|
79
92
|
readonly quarantined: number;
|
|
80
93
|
/** True once quarantine contains at least one third of the lexical pool. */
|
|
81
94
|
readonly attention: boolean;
|
|
95
|
+
/** Exact label drift by dzId; present only when both tiers expose readable identities. */
|
|
96
|
+
readonly tierParity?: { readonly lexicalOnly: number; readonly mirrorOnly: number };
|
|
82
97
|
/** Absolute lexical/vector quarantine-label delta, present only above the tolerated drift threshold. */
|
|
83
98
|
readonly tierDelta?: number;
|
|
84
99
|
};
|
|
@@ -663,14 +678,38 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
|
|
|
663
678
|
// зеркалом не пользуется, он горел бы всегда, а вечно горящий показатель не несёт сведений.
|
|
664
679
|
// Зеркало ЕСТЬ, но прочитать или разложить его не удалось: это отказ инструмента, и он горит.
|
|
665
680
|
const mirrorLessons = storeRows?.vectorLessonRows;
|
|
681
|
+
const mirrorRows = storeRows?.vectorRows;
|
|
666
682
|
const mirrorAbsent = storeRows !== undefined && storeRows.vectorSourcePath === undefined;
|
|
683
|
+
const lexicalMirrorable = storeRows?.lexicalMirrorableRows;
|
|
684
|
+
const excluded = {
|
|
685
|
+
class: storeRows?.lexicalExcludedClassRows ?? 0,
|
|
686
|
+
noise: storeRows?.lexicalExcludedNoiseRows ?? 0,
|
|
687
|
+
};
|
|
688
|
+
const mirrorAvailable = storeRows?.vectorSourcePath !== undefined
|
|
689
|
+
&& typeof mirrorRows === 'number'
|
|
690
|
+
&& mirrorLessons !== undefined;
|
|
667
691
|
const patternMirror: StatuslineData['patternMirror'] = mirrorAbsent
|
|
668
692
|
? undefined
|
|
669
|
-
: storeRows === undefined || storeRows.vectorRows === 'unreadable' ||
|
|
693
|
+
: storeRows === undefined || storeRows.vectorRows === 'unreadable' || storeRows.vectorRows === 'busy'
|
|
694
|
+
|| mirrorLessons === undefined || lexicalMirrorable === undefined
|
|
670
695
|
? { state: 'unavailable' }
|
|
671
|
-
:
|
|
672
|
-
|
|
673
|
-
|
|
696
|
+
: {
|
|
697
|
+
state: mirrorLessons !== lexicalMirrorable ? 'different' : 'in-sync',
|
|
698
|
+
lexicalMirrorable,
|
|
699
|
+
vector: mirrorLessons,
|
|
700
|
+
excluded,
|
|
701
|
+
};
|
|
702
|
+
const mirror: StatuslineData['mirror'] = {
|
|
703
|
+
available: mirrorAvailable,
|
|
704
|
+
rows: typeof mirrorRows === 'number' ? mirrorRows : 0,
|
|
705
|
+
lessons: mirrorAvailable ? mirrorLessons : 0,
|
|
706
|
+
pending: mirrorAbsent && lexicalMirrorable !== undefined
|
|
707
|
+
? lexicalMirrorable
|
|
708
|
+
: mirrorAvailable && lexicalMirrorable !== undefined
|
|
709
|
+
? Math.max(0, lexicalMirrorable - mirrorLessons)
|
|
710
|
+
: 0,
|
|
711
|
+
source: 'agentdb',
|
|
712
|
+
};
|
|
674
713
|
|
|
675
714
|
let patternBreakdown: StatuslineData['patternBreakdown'];
|
|
676
715
|
try {
|
|
@@ -678,14 +717,22 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
|
|
|
678
717
|
&& typeof storeRows.lexicalRows === 'number'
|
|
679
718
|
&& typeof storeRows.lexicalQuarantinedRows === 'number') {
|
|
680
719
|
const quarantined = storeRows.lexicalQuarantinedRows;
|
|
681
|
-
const tierDelta =
|
|
682
|
-
|
|
720
|
+
const tierDelta = mirrorAvailable
|
|
721
|
+
&& typeof storeRows.lexicalMirrorableQuarantinedRows === 'number'
|
|
722
|
+
&& typeof storeRows.vectorQuarantinedRows === 'number'
|
|
723
|
+
? Math.abs(storeRows.lexicalMirrorableQuarantinedRows - storeRows.vectorQuarantinedRows)
|
|
683
724
|
: undefined;
|
|
684
725
|
patternBreakdown = {
|
|
685
|
-
source: 'lexical',
|
|
726
|
+
source: mirrorAvailable ? 'lexical+mirror' : 'lexical',
|
|
686
727
|
active: storeRows.lexicalRows - quarantined,
|
|
687
728
|
quarantined,
|
|
688
729
|
attention: quarantined > 0 && quarantined * 3 >= storeRows.lexicalRows,
|
|
730
|
+
...(storeRows.quarantineTierParity === undefined ? {} : {
|
|
731
|
+
tierParity: {
|
|
732
|
+
lexicalOnly: storeRows.quarantineTierParity.lexicalOnly,
|
|
733
|
+
mirrorOnly: storeRows.quarantineTierParity.mirrorOnly,
|
|
734
|
+
},
|
|
735
|
+
}),
|
|
689
736
|
...(tierDelta !== undefined && tierDelta > QUARANTINE_TIER_DRIFT_TOLERANCE ? { tierDelta } : {}),
|
|
690
737
|
};
|
|
691
738
|
}
|
|
@@ -723,6 +770,7 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
|
|
|
723
770
|
|
|
724
771
|
return {
|
|
725
772
|
patterns,
|
|
773
|
+
mirror,
|
|
726
774
|
...(patternMirror !== undefined ? { patternMirror } : {}),
|
|
727
775
|
...(patternBreakdown !== undefined ? { patternBreakdown } : {}),
|
|
728
776
|
...(usedPatterns !== undefined ? { usedPatterns } : {}),
|
package/src/store-counts.ts
CHANGED
|
@@ -2,10 +2,16 @@
|
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
|
+
import type { MemoryRecord } from '@dzhechkov/memory';
|
|
6
|
+
import { patternRecordId, recordToPattern } from './patterns.js';
|
|
7
|
+
import { isMirrorableRecord } from './vector-tier.js';
|
|
5
8
|
|
|
6
9
|
interface ReadonlyCountDb {
|
|
7
10
|
pragma: (s: string) => void;
|
|
8
|
-
prepare: (q: string) => {
|
|
11
|
+
prepare: (q: string) => {
|
|
12
|
+
get: (...a: unknown[]) => unknown;
|
|
13
|
+
all: (...a: unknown[]) => unknown[];
|
|
14
|
+
};
|
|
9
15
|
close: () => void;
|
|
10
16
|
}
|
|
11
17
|
|
|
@@ -16,6 +22,91 @@ export interface StoreCountOptions {
|
|
|
16
22
|
readonly attempts?: number;
|
|
17
23
|
}
|
|
18
24
|
|
|
25
|
+
export interface QuarantineTierRow {
|
|
26
|
+
readonly id?: string;
|
|
27
|
+
readonly dzId?: string;
|
|
28
|
+
/** Content-addressed alias used by the teach-time mirror before a lexical row is re-keyed. */
|
|
29
|
+
readonly patternRecordId?: string;
|
|
30
|
+
readonly quarantined?: boolean;
|
|
31
|
+
readonly qStatus?: unknown;
|
|
32
|
+
readonly metadata?: Record<string, unknown> | string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface QuarantineTierParity {
|
|
36
|
+
readonly both: number;
|
|
37
|
+
readonly lexicalOnly: number;
|
|
38
|
+
readonly mirrorOnly: number;
|
|
39
|
+
readonly ids: {
|
|
40
|
+
readonly both: readonly string[];
|
|
41
|
+
readonly lexicalOnly: readonly string[];
|
|
42
|
+
readonly mirrorOnly: readonly string[];
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function rowMetadata(row: QuarantineTierRow): Record<string, unknown> {
|
|
47
|
+
if (typeof row.metadata === 'object' && row.metadata !== null) return row.metadata;
|
|
48
|
+
if (typeof row.metadata !== 'string') return {};
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(row.metadata) as unknown;
|
|
51
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
52
|
+
? parsed as Record<string, unknown>
|
|
53
|
+
: {};
|
|
54
|
+
} catch {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface QuarantineIdentity {
|
|
60
|
+
readonly primary: string;
|
|
61
|
+
readonly aliases: ReadonlySet<string>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function quarantineIdentities(rows: readonly QuarantineTierRow[]): QuarantineIdentity[] {
|
|
65
|
+
const identities = new Map<string, QuarantineIdentity>();
|
|
66
|
+
for (const row of rows) {
|
|
67
|
+
const metadata = rowMetadata(row);
|
|
68
|
+
const quarantined = row.quarantined === true
|
|
69
|
+
|| row.qStatus === 'quarantined'
|
|
70
|
+
|| metadata['qStatus'] === 'quarantined';
|
|
71
|
+
if (!quarantined) continue;
|
|
72
|
+
const candidates = [row.dzId, metadata['dzId'], row.id, row.patternRecordId]
|
|
73
|
+
.filter((id): id is string => typeof id === 'string' && id !== '');
|
|
74
|
+
const primary = candidates[0];
|
|
75
|
+
if (primary === undefined) continue;
|
|
76
|
+
identities.set(primary, { primary, aliases: new Set(candidates) });
|
|
77
|
+
}
|
|
78
|
+
return [...identities.values()];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Pure dual-key comparison for active quarantine labels in the lexical and mirror tiers. */
|
|
82
|
+
export function quarantineTierParity(
|
|
83
|
+
lexicalRows: readonly QuarantineTierRow[],
|
|
84
|
+
mirrorRows: readonly QuarantineTierRow[],
|
|
85
|
+
): QuarantineTierParity {
|
|
86
|
+
const lexical = quarantineIdentities(lexicalRows);
|
|
87
|
+
const mirror = quarantineIdentities(mirrorRows);
|
|
88
|
+
const lexicalKeys = new Set(lexical.flatMap((identity) => [...identity.aliases]));
|
|
89
|
+
const mirrorKeys = new Set(mirror.flatMap((identity) => [...identity.aliases]));
|
|
90
|
+
const both = [...new Set(lexical.flatMap((identity) => {
|
|
91
|
+
const matched = [...identity.aliases].find((id) => mirrorKeys.has(id));
|
|
92
|
+
return matched === undefined ? [] : [matched];
|
|
93
|
+
}))].sort();
|
|
94
|
+
const lexicalOnly = lexical
|
|
95
|
+
.filter((identity) => ![...identity.aliases].some((id) => mirrorKeys.has(id)))
|
|
96
|
+
.map((identity) => identity.primary)
|
|
97
|
+
.sort();
|
|
98
|
+
const mirrorOnly = mirror
|
|
99
|
+
.filter((identity) => ![...identity.aliases].some((id) => lexicalKeys.has(id)))
|
|
100
|
+
.map((identity) => identity.primary)
|
|
101
|
+
.sort();
|
|
102
|
+
return {
|
|
103
|
+
both: both.length,
|
|
104
|
+
lexicalOnly: lexicalOnly.length,
|
|
105
|
+
mirrorOnly: mirrorOnly.length,
|
|
106
|
+
ids: { both, lexicalOnly, mirrorOnly },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
19
110
|
type DatabaseRequire = (id: string) => unknown;
|
|
20
111
|
|
|
21
112
|
function isBusy(error: unknown): boolean {
|
|
@@ -45,6 +136,13 @@ function normalizedOptions(options: StoreCountOptions): { busyTimeoutMs: number;
|
|
|
45
136
|
|
|
46
137
|
export interface LearningStoreRowCounts {
|
|
47
138
|
readonly lexicalRows: StoreRowCount;
|
|
139
|
+
/** Lexical rows accepted by the same class/noise gate as the vector mirror writer. */
|
|
140
|
+
readonly lexicalMirrorableRows?: number;
|
|
141
|
+
/** Mirrorable lexical rows whose current qStatus is quarantined. */
|
|
142
|
+
readonly lexicalMirrorableQuarantinedRows?: number;
|
|
143
|
+
/** Mutually exclusive exclusion buckets; class takes precedence over noise. */
|
|
144
|
+
readonly lexicalExcludedClassRows?: number;
|
|
145
|
+
readonly lexicalExcludedNoiseRows?: number;
|
|
48
146
|
/** Exact active quarantine labels in the selected lexical SQLite tier; absent for fallback/error paths. */
|
|
49
147
|
readonly lexicalQuarantinedRows?: number;
|
|
50
148
|
/** Physical lexical population counted; jsonl and SQLite maxima are not comparable. */
|
|
@@ -71,6 +169,8 @@ export interface LearningStoreRowCounts {
|
|
|
71
169
|
readonly vectorQuarantinedRows?: number;
|
|
72
170
|
/** Present when the vector store file exists, including when its count is unreadable. */
|
|
73
171
|
readonly vectorSourcePath?: string;
|
|
172
|
+
/** Exact qStatus parity by dzId; absent when either tier cannot establish quarantine ids. */
|
|
173
|
+
readonly quarantineTierParity?: QuarantineTierParity;
|
|
74
174
|
}
|
|
75
175
|
|
|
76
176
|
function countJsonlRowsReadonly(path: string): number | 'unreadable' {
|
|
@@ -133,8 +233,132 @@ export function countSqliteRowsReadonly(
|
|
|
133
233
|
interface SqliteRowsWithQuarantine {
|
|
134
234
|
readonly rows: number;
|
|
135
235
|
readonly quarantinedRows?: number;
|
|
236
|
+
readonly mirrorableRows?: number;
|
|
237
|
+
readonly mirrorableQuarantinedRows?: number;
|
|
238
|
+
readonly excludedClassRows?: number;
|
|
239
|
+
readonly excludedNoiseRows?: number;
|
|
136
240
|
/** Только для зеркала: подсчёт уроков внутри общего объёма. */
|
|
137
241
|
readonly lessonRows?: number;
|
|
242
|
+
readonly quarantinedIdentities?: readonly QuarantineTierRow[];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function lexicalMirrorPopulation(db: ReadonlyCountDb): Pick<
|
|
246
|
+
SqliteRowsWithQuarantine,
|
|
247
|
+
'mirrorableRows' | 'mirrorableQuarantinedRows' | 'excludedClassRows' | 'excludedNoiseRows' | 'quarantinedIdentities'
|
|
248
|
+
> | undefined {
|
|
249
|
+
let rows: unknown[];
|
|
250
|
+
let idsEstablished = true;
|
|
251
|
+
try {
|
|
252
|
+
rows = db.prepare('SELECT id, skill_id, text, score, outcome, timestamp, metadata FROM memory_records').all();
|
|
253
|
+
} catch {
|
|
254
|
+
try {
|
|
255
|
+
rows = db.prepare('SELECT id, text, metadata FROM memory_records').all();
|
|
256
|
+
} catch {
|
|
257
|
+
idsEstablished = false;
|
|
258
|
+
try {
|
|
259
|
+
rows = db.prepare('SELECT text, metadata FROM memory_records').all();
|
|
260
|
+
} catch {
|
|
261
|
+
try {
|
|
262
|
+
// Legacy/minimal schemas can still establish the class exclusion. With no text column there
|
|
263
|
+
// is no observable noise payload, so every non-class row stays in the comparable population.
|
|
264
|
+
rows = db.prepare("SELECT '' AS text, metadata FROM memory_records").all();
|
|
265
|
+
} catch {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let mirrorableRows = 0;
|
|
273
|
+
let mirrorableQuarantinedRows = 0;
|
|
274
|
+
let excludedClassRows = 0;
|
|
275
|
+
let excludedNoiseRows = 0;
|
|
276
|
+
const quarantinedRows: QuarantineTierRow[] = [];
|
|
277
|
+
for (const value of rows) {
|
|
278
|
+
const row = value as {
|
|
279
|
+
id?: unknown;
|
|
280
|
+
skill_id?: unknown;
|
|
281
|
+
text?: unknown;
|
|
282
|
+
score?: unknown;
|
|
283
|
+
outcome?: unknown;
|
|
284
|
+
timestamp?: unknown;
|
|
285
|
+
metadata?: unknown;
|
|
286
|
+
};
|
|
287
|
+
let metadata: Record<string, unknown> = {};
|
|
288
|
+
if (typeof row.metadata === 'string') {
|
|
289
|
+
try {
|
|
290
|
+
const parsed = JSON.parse(row.metadata) as unknown;
|
|
291
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
292
|
+
metadata = parsed as Record<string, unknown>;
|
|
293
|
+
}
|
|
294
|
+
} catch { /* malformed metadata carries no class/quarantine claim */ }
|
|
295
|
+
}
|
|
296
|
+
const text = typeof row.text === 'string' ? row.text : '';
|
|
297
|
+
const lessonForm = metadata['lessonForm'] === 'class' ? 'class' as const : undefined;
|
|
298
|
+
const record = { pattern: text, ...(lessonForm === undefined ? {} : { lessonForm }) };
|
|
299
|
+
if (!isMirrorableRecord(record)) {
|
|
300
|
+
if (lessonForm === 'class') excludedClassRows += 1;
|
|
301
|
+
else excludedNoiseRows += 1;
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
mirrorableRows += 1;
|
|
305
|
+
if (metadata['qStatus'] === 'quarantined') {
|
|
306
|
+
mirrorableQuarantinedRows += 1;
|
|
307
|
+
if (typeof row.id !== 'string' || row.id === '') {
|
|
308
|
+
idsEstablished = false;
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
let derivedId: string | undefined;
|
|
312
|
+
if (typeof row.text === 'string'
|
|
313
|
+
&& typeof row.score === 'number'
|
|
314
|
+
&& typeof row.outcome === 'string'
|
|
315
|
+
&& typeof row.timestamp === 'string') {
|
|
316
|
+
const stringMetadata = Object.fromEntries(
|
|
317
|
+
Object.entries(metadata).filter((entry): entry is [string, string] => typeof entry[1] === 'string'),
|
|
318
|
+
);
|
|
319
|
+
const record: MemoryRecord = {
|
|
320
|
+
id: row.id,
|
|
321
|
+
skillId: typeof row.skill_id === 'string' ? row.skill_id : '',
|
|
322
|
+
text: row.text,
|
|
323
|
+
score: row.score,
|
|
324
|
+
outcome: row.outcome,
|
|
325
|
+
timestamp: row.timestamp,
|
|
326
|
+
metadata: stringMetadata,
|
|
327
|
+
};
|
|
328
|
+
derivedId = patternRecordId(recordToPattern(record));
|
|
329
|
+
}
|
|
330
|
+
quarantinedRows.push({
|
|
331
|
+
id: row.id,
|
|
332
|
+
...(derivedId === undefined ? {} : { patternRecordId: derivedId }),
|
|
333
|
+
qStatus: 'quarantined',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
mirrorableRows,
|
|
339
|
+
mirrorableQuarantinedRows,
|
|
340
|
+
excludedClassRows,
|
|
341
|
+
excludedNoiseRows,
|
|
342
|
+
...(idsEstablished ? { quarantinedIdentities: quarantinedRows } : {}),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function vectorQuarantinedIds(db: ReadonlyCountDb): readonly QuarantineTierRow[] | undefined {
|
|
347
|
+
try {
|
|
348
|
+
const rows = db.prepare(`SELECT metadata FROM reasoning_patterns
|
|
349
|
+
WHERE task_type IN ('dz-teach', 'dz-learning')
|
|
350
|
+
AND json_valid(metadata)
|
|
351
|
+
AND json_extract(metadata, '$.qStatus') = 'quarantined'`).all() as Array<{ metadata?: unknown }>;
|
|
352
|
+
const rowsWithIds: QuarantineTierRow[] = [];
|
|
353
|
+
for (const row of rows) {
|
|
354
|
+
const metadata = rowMetadata({ metadata: typeof row.metadata === 'string' ? row.metadata : null });
|
|
355
|
+
if (typeof metadata['dzId'] !== 'string' || metadata['dzId'] === '') return undefined;
|
|
356
|
+
rowsWithIds.push({ dzId: metadata['dzId'], qStatus: 'quarantined' });
|
|
357
|
+
}
|
|
358
|
+
return rowsWithIds;
|
|
359
|
+
} catch {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
138
362
|
}
|
|
139
363
|
|
|
140
364
|
/** One aggregate query on the healthy path; an unsupported metadata shape falls back to total-only. */
|
|
@@ -168,9 +392,13 @@ function countSqliteRowsWithQuarantineReadonly(
|
|
|
168
392
|
FROM reasoning_patterns`;
|
|
169
393
|
const row = db.prepare(sql).get() as { cnt?: unknown; quarantined?: unknown; lessons?: unknown };
|
|
170
394
|
if (typeof row?.cnt !== 'number' || typeof row.quarantined !== 'number') return 'unreadable';
|
|
395
|
+
const lexical = table === 'memory_records' ? lexicalMirrorPopulation(db) : undefined;
|
|
396
|
+
const quarantinedRows = table === 'reasoning_patterns' ? vectorQuarantinedIds(db) : undefined;
|
|
171
397
|
return {
|
|
172
398
|
rows: row.cnt,
|
|
173
399
|
quarantinedRows: row.quarantined,
|
|
400
|
+
...(lexical ?? {}),
|
|
401
|
+
...(quarantinedRows === undefined ? {} : { quarantinedIdentities: quarantinedRows }),
|
|
174
402
|
...(typeof row.lessons === 'number' ? { lessonRows: row.lessons } : {}),
|
|
175
403
|
};
|
|
176
404
|
} catch (error) {
|
|
@@ -247,8 +475,23 @@ export function countLearningStoreRowsReadonly(
|
|
|
247
475
|
const ignoredJsonl = lexicalSource === 'sqlite' && existsSync(jsonlPath)
|
|
248
476
|
? countJsonlRowsReadonly(jsonlPath)
|
|
249
477
|
: undefined;
|
|
478
|
+
const quarantineParity = typeof lexicalSqlite === 'object'
|
|
479
|
+
&& lexicalSqlite.quarantinedIdentities !== undefined
|
|
480
|
+
&& typeof vectorSqlite === 'object'
|
|
481
|
+
&& vectorSqlite.quarantinedIdentities !== undefined
|
|
482
|
+
? quarantineTierParity(
|
|
483
|
+
lexicalSqlite.quarantinedIdentities,
|
|
484
|
+
vectorSqlite.quarantinedIdentities,
|
|
485
|
+
)
|
|
486
|
+
: undefined;
|
|
250
487
|
return {
|
|
251
488
|
lexicalRows,
|
|
489
|
+
...(typeof lexicalSqlite !== 'object' || lexicalSqlite.mirrorableRows === undefined ? {} : {
|
|
490
|
+
lexicalMirrorableRows: lexicalSqlite.mirrorableRows,
|
|
491
|
+
lexicalMirrorableQuarantinedRows: lexicalSqlite.mirrorableQuarantinedRows ?? 0,
|
|
492
|
+
lexicalExcludedClassRows: lexicalSqlite.excludedClassRows ?? 0,
|
|
493
|
+
lexicalExcludedNoiseRows: lexicalSqlite.excludedNoiseRows ?? 0,
|
|
494
|
+
}),
|
|
252
495
|
...(typeof lexicalSqlite !== 'object' || lexicalSqlite.quarantinedRows === undefined ? {} : {
|
|
253
496
|
lexicalQuarantinedRows: lexicalSqlite.quarantinedRows,
|
|
254
497
|
}),
|
|
@@ -267,5 +510,6 @@ export function countLearningStoreRowsReadonly(
|
|
|
267
510
|
vectorQuarantinedRows: vectorSqlite.quarantinedRows,
|
|
268
511
|
}),
|
|
269
512
|
...(vectorExists ? { vectorSourcePath: vectorPath } : {}),
|
|
513
|
+
...(quarantineParity === undefined ? {} : { quarantineTierParity: quarantineParity }),
|
|
270
514
|
};
|
|
271
515
|
}
|