@dzhechkov/harness-core 0.8.29 → 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 +242 -102
- package/README.md +271 -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/publish.d.ts +21 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.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 +22 -3
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +43 -7
- package/dist/statusline.js.map +1 -1
- package/dist/store-counts.d.ts +41 -3
- package/dist/store-counts.d.ts.map +1 -1
- package/dist/store-counts.js +305 -59
- package/dist/store-counts.js.map +1 -1
- package/dist/store-guard.d.ts +2 -2
- package/dist/store-guard.d.ts.map +1 -1
- package/dist/store-guard.js +6 -0
- package/dist/store-guard.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 +7 -6
- package/sbom.json +451 -101
- 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/publish.ts +23 -0
- 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 +61 -9
- package/src/store-counts.ts +391 -61
- package/src/store-guard.ts +8 -2
- package/src/usage.ts +243 -29
- package/src/vector-tier.ts +45 -4
package/src/store-counts.ts
CHANGED
|
@@ -2,15 +2,147 @@
|
|
|
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
|
|
|
18
|
+
export type StoreRowCount = number | 'unreadable' | 'busy';
|
|
19
|
+
|
|
20
|
+
export interface StoreCountOptions {
|
|
21
|
+
readonly busyTimeoutMs?: number;
|
|
22
|
+
readonly attempts?: number;
|
|
23
|
+
}
|
|
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
|
+
|
|
110
|
+
type DatabaseRequire = (id: string) => unknown;
|
|
111
|
+
|
|
112
|
+
function isBusy(error: unknown): boolean {
|
|
113
|
+
return typeof error === 'object' && error !== null
|
|
114
|
+
&& (error as { code?: unknown }).code === 'SQLITE_BUSY';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isInitializing(error: unknown): boolean {
|
|
118
|
+
return typeof error === 'object' && error !== null
|
|
119
|
+
&& (error as { code?: unknown }).code === 'SQLITE_ERROR'
|
|
120
|
+
&& typeof (error as { message?: unknown }).message === 'string'
|
|
121
|
+
&& (error as { message: string }).message.includes('no such table');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const INITIALIZATION_RETRY_WAIT = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
125
|
+
|
|
126
|
+
function waitForInitializationRetry(busyTimeoutMs: number): void {
|
|
127
|
+
Atomics.wait(INITIALIZATION_RETRY_WAIT, 0, 0, busyTimeoutMs);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function normalizedOptions(options: StoreCountOptions): { busyTimeoutMs: number; attempts: number } {
|
|
131
|
+
return {
|
|
132
|
+
busyTimeoutMs: options.busyTimeoutMs ?? 100,
|
|
133
|
+
attempts: options.attempts ?? 1,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
12
137
|
export interface LearningStoreRowCounts {
|
|
13
|
-
readonly lexicalRows:
|
|
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;
|
|
14
146
|
/** Exact active quarantine labels in the selected lexical SQLite tier; absent for fallback/error paths. */
|
|
15
147
|
readonly lexicalQuarantinedRows?: number;
|
|
16
148
|
/** Physical lexical population counted; jsonl and SQLite maxima are not comparable. */
|
|
@@ -26,7 +158,7 @@ export interface LearningStoreRowCounts {
|
|
|
26
158
|
* до уроков уронило бы наблюдение с 1774 до 631 — страж прочитал бы это как обвал стора.
|
|
27
159
|
* Для показателя зеркала в панели есть отдельное поле `vectorLessonRows`.
|
|
28
160
|
*/
|
|
29
|
-
readonly vectorRows:
|
|
161
|
+
readonly vectorRows: StoreRowCount;
|
|
30
162
|
/**
|
|
31
163
|
* Только зеркальные УРОКИ (task_type dz-teach/dz-learning) — величина, сравнимая с `lexicalRows`.
|
|
32
164
|
* Отсутствует, когда зеркала нет или разложить его по родам не удалось: тогда показатель обязан
|
|
@@ -37,6 +169,8 @@ export interface LearningStoreRowCounts {
|
|
|
37
169
|
readonly vectorQuarantinedRows?: number;
|
|
38
170
|
/** Present when the vector store file exists, including when its count is unreadable. */
|
|
39
171
|
readonly vectorSourcePath?: string;
|
|
172
|
+
/** Exact qStatus parity by dzId; absent when either tier cannot establish quarantine ids. */
|
|
173
|
+
readonly quarantineTierParity?: QuarantineTierParity;
|
|
40
174
|
}
|
|
41
175
|
|
|
42
176
|
function countJsonlRowsReadonly(path: string): number | 'unreadable' {
|
|
@@ -52,89 +186,262 @@ function countJsonlRowsReadonly(path: string): number | 'unreadable' {
|
|
|
52
186
|
export function countSqliteRowsReadonly(
|
|
53
187
|
sqlitePath: string,
|
|
54
188
|
table: 'memory_records' | 'reasoning_patterns',
|
|
55
|
-
): number | undefined
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
189
|
+
): number | undefined;
|
|
190
|
+
export function countSqliteRowsReadonly(
|
|
191
|
+
sqlitePath: string,
|
|
192
|
+
table: 'memory_records' | 'reasoning_patterns',
|
|
193
|
+
options: StoreCountOptions,
|
|
194
|
+
requireModule?: DatabaseRequire,
|
|
195
|
+
): StoreRowCount;
|
|
196
|
+
export function countSqliteRowsReadonly(
|
|
197
|
+
sqlitePath: string,
|
|
198
|
+
table: 'memory_records' | 'reasoning_patterns',
|
|
199
|
+
options?: StoreCountOptions,
|
|
200
|
+
requireModule: DatabaseRequire = createRequire(import.meta.url),
|
|
201
|
+
): number | undefined | StoreRowCount {
|
|
202
|
+
const legacyUndefined = options === undefined;
|
|
203
|
+
const { busyTimeoutMs, attempts } = normalizedOptions(options ?? {});
|
|
204
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
60
205
|
try {
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
206
|
+
const Database = requireModule('better-sqlite3') as new (p: string, o?: object) => ReadonlyCountDb;
|
|
207
|
+
const db = new Database(sqlitePath, { readonly: true });
|
|
208
|
+
try {
|
|
209
|
+
db.pragma(`busy_timeout = ${busyTimeoutMs}`);
|
|
210
|
+
const row = db.prepare(`SELECT COUNT(*) as cnt FROM ${table}`).get() as { cnt?: unknown };
|
|
211
|
+
return typeof row?.cnt === 'number' ? row.cnt : (legacyUndefined ? undefined : 'unreadable');
|
|
212
|
+
} finally {
|
|
213
|
+
db.close();
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (isBusy(error)) {
|
|
217
|
+
if (attempt + 1 < attempts) continue;
|
|
218
|
+
return legacyUndefined ? undefined : 'busy';
|
|
219
|
+
}
|
|
220
|
+
if (isInitializing(error)) {
|
|
221
|
+
if (attempt + 1 < attempts) {
|
|
222
|
+
waitForInitializationRetry(busyTimeoutMs);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
return legacyUndefined ? undefined : 'unreadable';
|
|
226
|
+
}
|
|
227
|
+
return legacyUndefined ? undefined : 'unreadable';
|
|
66
228
|
}
|
|
67
|
-
} catch {
|
|
68
|
-
return undefined;
|
|
69
229
|
}
|
|
230
|
+
return legacyUndefined ? undefined : 'busy';
|
|
70
231
|
}
|
|
71
232
|
|
|
72
233
|
interface SqliteRowsWithQuarantine {
|
|
73
234
|
readonly rows: number;
|
|
74
235
|
readonly quarantinedRows?: number;
|
|
236
|
+
readonly mirrorableRows?: number;
|
|
237
|
+
readonly mirrorableQuarantinedRows?: number;
|
|
238
|
+
readonly excludedClassRows?: number;
|
|
239
|
+
readonly excludedNoiseRows?: number;
|
|
75
240
|
/** Только для зеркала: подсчёт уроков внутри общего объёма. */
|
|
76
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
|
+
}
|
|
77
362
|
}
|
|
78
363
|
|
|
79
364
|
/** One aggregate query on the healthy path; an unsupported metadata shape falls back to total-only. */
|
|
80
365
|
function countSqliteRowsWithQuarantineReadonly(
|
|
81
366
|
sqlitePath: string,
|
|
82
367
|
table: 'memory_records' | 'reasoning_patterns',
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
368
|
+
options: StoreCountOptions,
|
|
369
|
+
requireModule: DatabaseRequire,
|
|
370
|
+
): SqliteRowsWithQuarantine | 'unreadable' | 'busy' {
|
|
371
|
+
const { busyTimeoutMs, attempts } = normalizedOptions(options);
|
|
372
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
88
373
|
try {
|
|
89
|
-
|
|
374
|
+
const Database = requireModule('better-sqlite3') as new (p: string, o?: object) => ReadonlyCountDb;
|
|
375
|
+
const db = new Database(sqlitePath, { readonly: true });
|
|
90
376
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
377
|
+
db.pragma(`busy_timeout = ${busyTimeoutMs}`);
|
|
378
|
+
try {
|
|
379
|
+
const sql = table === 'memory_records'
|
|
380
|
+
? `SELECT COALESCE(SUM(n), 0) AS cnt,
|
|
93
381
|
COALESCE(SUM(CASE WHEN q_status = 'quarantined' THEN n ELSE 0 END), 0) AS quarantined
|
|
94
382
|
FROM (
|
|
95
383
|
SELECT CASE WHEN json_valid(metadata) THEN json_extract(metadata, '$.qStatus') END AS q_status,
|
|
96
384
|
COUNT(*) AS n
|
|
97
385
|
FROM memory_records GROUP BY 1
|
|
98
386
|
)`
|
|
99
|
-
|
|
387
|
+
: `SELECT COUNT(*) AS cnt,
|
|
100
388
|
COALESCE(SUM(CASE WHEN task_type IN ('dz-teach', 'dz-learning') THEN 1 ELSE 0 END), 0) AS lessons,
|
|
101
389
|
COALESCE(SUM(CASE WHEN task_type IN ('dz-teach', 'dz-learning')
|
|
102
390
|
AND json_valid(metadata)
|
|
103
391
|
AND json_extract(metadata, '$.qStatus') = 'quarantined' THEN 1 ELSE 0 END), 0) AS quarantined
|
|
104
392
|
FROM reasoning_patterns`;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
393
|
+
const row = db.prepare(sql).get() as { cnt?: unknown; quarantined?: unknown; lessons?: unknown };
|
|
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;
|
|
397
|
+
return {
|
|
398
|
+
rows: row.cnt,
|
|
399
|
+
quarantinedRows: row.quarantined,
|
|
400
|
+
...(lexical ?? {}),
|
|
401
|
+
...(quarantinedRows === undefined ? {} : { quarantinedIdentities: quarantinedRows }),
|
|
402
|
+
...(typeof row.lessons === 'number' ? { lessonRows: row.lessons } : {}),
|
|
403
|
+
};
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (isBusy(error) || isInitializing(error)) throw error;
|
|
406
|
+
// Запасной путь: общий объём берём всегда, а разложение по родам пробуем ОТДЕЛЬНО —
|
|
407
|
+
// схема без `metadata`, но с `task_type` уроки различает, и терять это не за что.
|
|
408
|
+
// Если не различает и её — lessonRows остаётся неизвестным, и показатель обязан сказать
|
|
409
|
+
// «не читается» вместо мнимого совпадения.
|
|
410
|
+
const row = db.prepare(`SELECT COUNT(*) AS cnt FROM ${table}`).get() as { cnt?: unknown };
|
|
411
|
+
if (typeof row?.cnt !== 'number') return 'unreadable';
|
|
412
|
+
if (table !== 'reasoning_patterns') return { rows: row.cnt };
|
|
413
|
+
try {
|
|
414
|
+
const lesson = db.prepare(
|
|
415
|
+
`SELECT COUNT(*) AS cnt FROM reasoning_patterns
|
|
123
416
|
WHERE task_type IN ('dz-teach', 'dz-learning')`,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
417
|
+
).get() as { cnt?: unknown };
|
|
418
|
+
return typeof lesson?.cnt === 'number'
|
|
419
|
+
? { rows: row.cnt, lessonRows: lesson.cnt }
|
|
420
|
+
: { rows: row.cnt };
|
|
421
|
+
} catch (lessonError) {
|
|
422
|
+
if (isBusy(lessonError) || isInitializing(lessonError)) throw lessonError;
|
|
423
|
+
return { rows: row.cnt };
|
|
424
|
+
}
|
|
130
425
|
}
|
|
426
|
+
} finally {
|
|
427
|
+
db.close();
|
|
428
|
+
}
|
|
429
|
+
} catch (error) {
|
|
430
|
+
if (isBusy(error)) {
|
|
431
|
+
if (attempt + 1 < attempts) continue;
|
|
432
|
+
return 'busy';
|
|
131
433
|
}
|
|
132
|
-
|
|
133
|
-
|
|
434
|
+
if (isInitializing(error)) {
|
|
435
|
+
if (attempt + 1 < attempts) {
|
|
436
|
+
waitForInitializationRetry(busyTimeoutMs);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
return 'unreadable';
|
|
440
|
+
}
|
|
441
|
+
return 'unreadable';
|
|
134
442
|
}
|
|
135
|
-
} catch {
|
|
136
|
-
return undefined;
|
|
137
443
|
}
|
|
444
|
+
return 'busy';
|
|
138
445
|
}
|
|
139
446
|
|
|
140
447
|
/**
|
|
@@ -142,7 +449,11 @@ function countSqliteRowsWithQuarantineReadonly(
|
|
|
142
449
|
* Missing tiers count as zero; an existing tier that cannot be counted is
|
|
143
450
|
* explicitly `unreadable`.
|
|
144
451
|
*/
|
|
145
|
-
export function countLearningStoreRowsReadonly(
|
|
452
|
+
export function countLearningStoreRowsReadonly(
|
|
453
|
+
projectRoot: string,
|
|
454
|
+
options: StoreCountOptions = {},
|
|
455
|
+
requireModule: DatabaseRequire = createRequire(import.meta.url),
|
|
456
|
+
): LearningStoreRowCounts {
|
|
146
457
|
const root = resolve(projectRoot);
|
|
147
458
|
const lexicalPath = join(root, '.dz', 'memory', 'patterns.sqlite');
|
|
148
459
|
const jsonlPath = join(root, '.dz', 'patterns.jsonl');
|
|
@@ -150,23 +461,38 @@ export function countLearningStoreRowsReadonly(projectRoot: string): LearningSto
|
|
|
150
461
|
const vectorExists = existsSync(vectorPath);
|
|
151
462
|
const lexicalSource = existsSync(lexicalPath) ? 'sqlite' : 'jsonl';
|
|
152
463
|
const lexicalSqlite = lexicalSource === 'sqlite'
|
|
153
|
-
? countSqliteRowsWithQuarantineReadonly(lexicalPath, 'memory_records')
|
|
464
|
+
? countSqliteRowsWithQuarantineReadonly(lexicalPath, 'memory_records', options, requireModule)
|
|
154
465
|
: undefined;
|
|
155
|
-
let lexicalRows:
|
|
466
|
+
let lexicalRows: StoreRowCount;
|
|
156
467
|
if (lexicalSource === 'sqlite') {
|
|
157
|
-
lexicalRows = lexicalSqlite
|
|
468
|
+
lexicalRows = typeof lexicalSqlite === 'object' ? lexicalSqlite.rows : lexicalSqlite ?? 'unreadable';
|
|
158
469
|
} else {
|
|
159
470
|
lexicalRows = countJsonlRowsReadonly(jsonlPath);
|
|
160
471
|
}
|
|
161
472
|
const vectorSqlite = vectorExists
|
|
162
|
-
? countSqliteRowsWithQuarantineReadonly(vectorPath, 'reasoning_patterns')
|
|
473
|
+
? countSqliteRowsWithQuarantineReadonly(vectorPath, 'reasoning_patterns', options, requireModule)
|
|
163
474
|
: undefined;
|
|
164
475
|
const ignoredJsonl = lexicalSource === 'sqlite' && existsSync(jsonlPath)
|
|
165
476
|
? countJsonlRowsReadonly(jsonlPath)
|
|
166
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;
|
|
167
487
|
return {
|
|
168
488
|
lexicalRows,
|
|
169
|
-
...(lexicalSqlite
|
|
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
|
+
}),
|
|
495
|
+
...(typeof lexicalSqlite !== 'object' || lexicalSqlite.quarantinedRows === undefined ? {} : {
|
|
170
496
|
lexicalQuarantinedRows: lexicalSqlite.quarantinedRows,
|
|
171
497
|
}),
|
|
172
498
|
lexicalSource,
|
|
@@ -175,11 +501,15 @@ export function countLearningStoreRowsReadonly(projectRoot: string): LearningSto
|
|
|
175
501
|
lexicalIgnoredRows: ignoredJsonl,
|
|
176
502
|
lexicalIgnoredSourcePath: jsonlPath,
|
|
177
503
|
}),
|
|
178
|
-
vectorRows: vectorExists
|
|
179
|
-
|
|
180
|
-
|
|
504
|
+
vectorRows: vectorExists
|
|
505
|
+
? (typeof vectorSqlite === 'object' ? vectorSqlite.rows : vectorSqlite ?? 'unreadable')
|
|
506
|
+
: 0,
|
|
507
|
+
...(typeof vectorSqlite !== 'object' || vectorSqlite.lessonRows === undefined
|
|
508
|
+
? {} : { vectorLessonRows: vectorSqlite.lessonRows }),
|
|
509
|
+
...(typeof vectorSqlite !== 'object' || vectorSqlite.quarantinedRows === undefined ? {} : {
|
|
181
510
|
vectorQuarantinedRows: vectorSqlite.quarantinedRows,
|
|
182
511
|
}),
|
|
183
512
|
...(vectorExists ? { vectorSourcePath: vectorPath } : {}),
|
|
513
|
+
...(quarantineParity === undefined ? {} : { quarantineTierParity: quarantineParity }),
|
|
184
514
|
};
|
|
185
515
|
}
|
package/src/store-guard.ts
CHANGED
|
@@ -92,9 +92,9 @@ export interface StoreMarkWriteOptions {
|
|
|
92
92
|
readonly expectedPreviousLexicalSource?: StoreCountSource | 'unknown';
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
export type StoreRowCount = number | 'unreadable';
|
|
95
|
+
export type StoreRowCount = number | 'unreadable' | 'busy';
|
|
96
96
|
|
|
97
|
-
export type StoreHealthVerdict = 'ok' | 'collapsed' | 'cold-start-over-existing' | 'source-changed' | 'unreadable' | 'no-mark';
|
|
97
|
+
export type StoreHealthVerdict = 'ok' | 'collapsed' | 'cold-start-over-existing' | 'source-changed' | 'unreadable' | 'busy' | 'no-mark';
|
|
98
98
|
|
|
99
99
|
export interface StoreHealth {
|
|
100
100
|
readonly verdict: StoreHealthVerdict;
|
|
@@ -354,6 +354,12 @@ export function checkStoreHealth(input: StoreHealthInput): StoreHealth {
|
|
|
354
354
|
reason: `${unreadable.join(' and ')} store${unreadable.length > 1 ? 's are' : ' is'} unreadable`,
|
|
355
355
|
};
|
|
356
356
|
}
|
|
357
|
+
if (lexicalRows === 'busy' || vectorRows === 'busy') {
|
|
358
|
+
return {
|
|
359
|
+
verdict: 'busy',
|
|
360
|
+
reason: 'lexical store busy — another writer holds it; health not measured this run',
|
|
361
|
+
};
|
|
362
|
+
}
|
|
357
363
|
if (mark === undefined) return { verdict: 'no-mark', reason: 'no store mark exists for this project' };
|
|
358
364
|
|
|
359
365
|
const lexicalCount = lexicalRows as number;
|