@dzhechkov/harness-core 0.8.28 → 0.8.30
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 +106 -46
- package/README.md +17 -2
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +10 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/cmd-usage.d.ts.map +1 -1
- package/dist/cmd-usage.js +22 -10
- package/dist/cmd-usage.js.map +1 -1
- package/dist/core-boundary.d.ts +1 -1
- package/dist/core-boundary.d.ts.map +1 -1
- package/dist/core-boundary.js +8 -1
- package/dist/core-boundary.js.map +1 -1
- package/dist/guard.d.ts +19 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +110 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/publish.d.ts +30 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +82 -0
- package/dist/publish.js.map +1 -1
- package/dist/release-line.d.ts +10 -0
- package/dist/release-line.d.ts.map +1 -0
- package/dist/release-line.js +21 -0
- package/dist/release-line.js.map +1 -0
- package/dist/repo-boundary.d.ts +13 -0
- package/dist/repo-boundary.d.ts.map +1 -0
- package/dist/repo-boundary.js +16 -0
- package/dist/repo-boundary.js.map +1 -0
- package/dist/repository-origin.d.ts +2 -0
- package/dist/repository-origin.d.ts.map +1 -0
- package/dist/repository-origin.js +2 -0
- package/dist/repository-origin.js.map +1 -0
- package/dist/statusline.d.ts +2 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +3 -0
- package/dist/statusline.js.map +1 -1
- package/dist/store-counts.d.ts +11 -3
- package/dist/store-counts.d.ts.map +1 -1
- package/dist/store-counts.js +120 -63
- 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/package.json +8 -7
- package/sbom.json +195 -45
- package/src/amendment-trace.ts +10 -1
- package/src/cmd-usage.ts +13 -6
- package/src/core-boundary.ts +7 -1
- package/src/guard.ts +137 -1
- package/src/index.ts +6 -1
- package/src/publish.ts +107 -0
- package/src/release-line.ts +31 -0
- package/src/repo-boundary.ts +25 -0
- package/src/repository-origin.ts +1 -0
- package/src/statusline.ts +4 -0
- package/src/store-counts.ts +146 -60
- package/src/store-guard.ts +8 -2
package/src/store-counts.ts
CHANGED
|
@@ -9,8 +9,42 @@ interface ReadonlyCountDb {
|
|
|
9
9
|
close: () => void;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export type StoreRowCount = number | 'unreadable' | 'busy';
|
|
13
|
+
|
|
14
|
+
export interface StoreCountOptions {
|
|
15
|
+
readonly busyTimeoutMs?: number;
|
|
16
|
+
readonly attempts?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type DatabaseRequire = (id: string) => unknown;
|
|
20
|
+
|
|
21
|
+
function isBusy(error: unknown): boolean {
|
|
22
|
+
return typeof error === 'object' && error !== null
|
|
23
|
+
&& (error as { code?: unknown }).code === 'SQLITE_BUSY';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isInitializing(error: unknown): boolean {
|
|
27
|
+
return typeof error === 'object' && error !== null
|
|
28
|
+
&& (error as { code?: unknown }).code === 'SQLITE_ERROR'
|
|
29
|
+
&& typeof (error as { message?: unknown }).message === 'string'
|
|
30
|
+
&& (error as { message: string }).message.includes('no such table');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const INITIALIZATION_RETRY_WAIT = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
|
|
34
|
+
|
|
35
|
+
function waitForInitializationRetry(busyTimeoutMs: number): void {
|
|
36
|
+
Atomics.wait(INITIALIZATION_RETRY_WAIT, 0, 0, busyTimeoutMs);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function normalizedOptions(options: StoreCountOptions): { busyTimeoutMs: number; attempts: number } {
|
|
40
|
+
return {
|
|
41
|
+
busyTimeoutMs: options.busyTimeoutMs ?? 100,
|
|
42
|
+
attempts: options.attempts ?? 1,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
12
46
|
export interface LearningStoreRowCounts {
|
|
13
|
-
readonly lexicalRows:
|
|
47
|
+
readonly lexicalRows: StoreRowCount;
|
|
14
48
|
/** Exact active quarantine labels in the selected lexical SQLite tier; absent for fallback/error paths. */
|
|
15
49
|
readonly lexicalQuarantinedRows?: number;
|
|
16
50
|
/** Physical lexical population counted; jsonl and SQLite maxima are not comparable. */
|
|
@@ -26,7 +60,7 @@ export interface LearningStoreRowCounts {
|
|
|
26
60
|
* до уроков уронило бы наблюдение с 1774 до 631 — страж прочитал бы это как обвал стора.
|
|
27
61
|
* Для показателя зеркала в панели есть отдельное поле `vectorLessonRows`.
|
|
28
62
|
*/
|
|
29
|
-
readonly vectorRows:
|
|
63
|
+
readonly vectorRows: StoreRowCount;
|
|
30
64
|
/**
|
|
31
65
|
* Только зеркальные УРОКИ (task_type dz-teach/dz-learning) — величина, сравнимая с `lexicalRows`.
|
|
32
66
|
* Отсутствует, когда зеркала нет или разложить его по родам не удалось: тогда показатель обязан
|
|
@@ -52,21 +86,48 @@ function countJsonlRowsReadonly(path: string): number | 'unreadable' {
|
|
|
52
86
|
export function countSqliteRowsReadonly(
|
|
53
87
|
sqlitePath: string,
|
|
54
88
|
table: 'memory_records' | 'reasoning_patterns',
|
|
55
|
-
): number | undefined
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
89
|
+
): number | undefined;
|
|
90
|
+
export function countSqliteRowsReadonly(
|
|
91
|
+
sqlitePath: string,
|
|
92
|
+
table: 'memory_records' | 'reasoning_patterns',
|
|
93
|
+
options: StoreCountOptions,
|
|
94
|
+
requireModule?: DatabaseRequire,
|
|
95
|
+
): StoreRowCount;
|
|
96
|
+
export function countSqliteRowsReadonly(
|
|
97
|
+
sqlitePath: string,
|
|
98
|
+
table: 'memory_records' | 'reasoning_patterns',
|
|
99
|
+
options?: StoreCountOptions,
|
|
100
|
+
requireModule: DatabaseRequire = createRequire(import.meta.url),
|
|
101
|
+
): number | undefined | StoreRowCount {
|
|
102
|
+
const legacyUndefined = options === undefined;
|
|
103
|
+
const { busyTimeoutMs, attempts } = normalizedOptions(options ?? {});
|
|
104
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
60
105
|
try {
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
106
|
+
const Database = requireModule('better-sqlite3') as new (p: string, o?: object) => ReadonlyCountDb;
|
|
107
|
+
const db = new Database(sqlitePath, { readonly: true });
|
|
108
|
+
try {
|
|
109
|
+
db.pragma(`busy_timeout = ${busyTimeoutMs}`);
|
|
110
|
+
const row = db.prepare(`SELECT COUNT(*) as cnt FROM ${table}`).get() as { cnt?: unknown };
|
|
111
|
+
return typeof row?.cnt === 'number' ? row.cnt : (legacyUndefined ? undefined : 'unreadable');
|
|
112
|
+
} finally {
|
|
113
|
+
db.close();
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (isBusy(error)) {
|
|
117
|
+
if (attempt + 1 < attempts) continue;
|
|
118
|
+
return legacyUndefined ? undefined : 'busy';
|
|
119
|
+
}
|
|
120
|
+
if (isInitializing(error)) {
|
|
121
|
+
if (attempt + 1 < attempts) {
|
|
122
|
+
waitForInitializationRetry(busyTimeoutMs);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
return legacyUndefined ? undefined : 'unreadable';
|
|
126
|
+
}
|
|
127
|
+
return legacyUndefined ? undefined : 'unreadable';
|
|
66
128
|
}
|
|
67
|
-
} catch {
|
|
68
|
-
return undefined;
|
|
69
129
|
}
|
|
130
|
+
return legacyUndefined ? undefined : 'busy';
|
|
70
131
|
}
|
|
71
132
|
|
|
72
133
|
interface SqliteRowsWithQuarantine {
|
|
@@ -80,61 +141,79 @@ interface SqliteRowsWithQuarantine {
|
|
|
80
141
|
function countSqliteRowsWithQuarantineReadonly(
|
|
81
142
|
sqlitePath: string,
|
|
82
143
|
table: 'memory_records' | 'reasoning_patterns',
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
144
|
+
options: StoreCountOptions,
|
|
145
|
+
requireModule: DatabaseRequire,
|
|
146
|
+
): SqliteRowsWithQuarantine | 'unreadable' | 'busy' {
|
|
147
|
+
const { busyTimeoutMs, attempts } = normalizedOptions(options);
|
|
148
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
88
149
|
try {
|
|
89
|
-
|
|
150
|
+
const Database = requireModule('better-sqlite3') as new (p: string, o?: object) => ReadonlyCountDb;
|
|
151
|
+
const db = new Database(sqlitePath, { readonly: true });
|
|
90
152
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
153
|
+
db.pragma(`busy_timeout = ${busyTimeoutMs}`);
|
|
154
|
+
try {
|
|
155
|
+
const sql = table === 'memory_records'
|
|
156
|
+
? `SELECT COALESCE(SUM(n), 0) AS cnt,
|
|
93
157
|
COALESCE(SUM(CASE WHEN q_status = 'quarantined' THEN n ELSE 0 END), 0) AS quarantined
|
|
94
158
|
FROM (
|
|
95
159
|
SELECT CASE WHEN json_valid(metadata) THEN json_extract(metadata, '$.qStatus') END AS q_status,
|
|
96
160
|
COUNT(*) AS n
|
|
97
161
|
FROM memory_records GROUP BY 1
|
|
98
162
|
)`
|
|
99
|
-
|
|
163
|
+
: `SELECT COUNT(*) AS cnt,
|
|
100
164
|
COALESCE(SUM(CASE WHEN task_type IN ('dz-teach', 'dz-learning') THEN 1 ELSE 0 END), 0) AS lessons,
|
|
101
165
|
COALESCE(SUM(CASE WHEN task_type IN ('dz-teach', 'dz-learning')
|
|
102
166
|
AND json_valid(metadata)
|
|
103
167
|
AND json_extract(metadata, '$.qStatus') = 'quarantined' THEN 1 ELSE 0 END), 0) AS quarantined
|
|
104
168
|
FROM reasoning_patterns`;
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
169
|
+
const row = db.prepare(sql).get() as { cnt?: unknown; quarantined?: unknown; lessons?: unknown };
|
|
170
|
+
if (typeof row?.cnt !== 'number' || typeof row.quarantined !== 'number') return 'unreadable';
|
|
171
|
+
return {
|
|
172
|
+
rows: row.cnt,
|
|
173
|
+
quarantinedRows: row.quarantined,
|
|
174
|
+
...(typeof row.lessons === 'number' ? { lessonRows: row.lessons } : {}),
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (isBusy(error) || isInitializing(error)) throw error;
|
|
178
|
+
// Запасной путь: общий объём берём всегда, а разложение по родам пробуем ОТДЕЛЬНО —
|
|
179
|
+
// схема без `metadata`, но с `task_type` уроки различает, и терять это не за что.
|
|
180
|
+
// Если не различает и её — lessonRows остаётся неизвестным, и показатель обязан сказать
|
|
181
|
+
// «не читается» вместо мнимого совпадения.
|
|
182
|
+
const row = db.prepare(`SELECT COUNT(*) AS cnt FROM ${table}`).get() as { cnt?: unknown };
|
|
183
|
+
if (typeof row?.cnt !== 'number') return 'unreadable';
|
|
184
|
+
if (table !== 'reasoning_patterns') return { rows: row.cnt };
|
|
185
|
+
try {
|
|
186
|
+
const lesson = db.prepare(
|
|
187
|
+
`SELECT COUNT(*) AS cnt FROM reasoning_patterns
|
|
123
188
|
WHERE task_type IN ('dz-teach', 'dz-learning')`,
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
189
|
+
).get() as { cnt?: unknown };
|
|
190
|
+
return typeof lesson?.cnt === 'number'
|
|
191
|
+
? { rows: row.cnt, lessonRows: lesson.cnt }
|
|
192
|
+
: { rows: row.cnt };
|
|
193
|
+
} catch (lessonError) {
|
|
194
|
+
if (isBusy(lessonError) || isInitializing(lessonError)) throw lessonError;
|
|
195
|
+
return { rows: row.cnt };
|
|
196
|
+
}
|
|
130
197
|
}
|
|
198
|
+
} finally {
|
|
199
|
+
db.close();
|
|
131
200
|
}
|
|
132
|
-
}
|
|
133
|
-
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (isBusy(error)) {
|
|
203
|
+
if (attempt + 1 < attempts) continue;
|
|
204
|
+
return 'busy';
|
|
205
|
+
}
|
|
206
|
+
if (isInitializing(error)) {
|
|
207
|
+
if (attempt + 1 < attempts) {
|
|
208
|
+
waitForInitializationRetry(busyTimeoutMs);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
return 'unreadable';
|
|
212
|
+
}
|
|
213
|
+
return 'unreadable';
|
|
134
214
|
}
|
|
135
|
-
} catch {
|
|
136
|
-
return undefined;
|
|
137
215
|
}
|
|
216
|
+
return 'busy';
|
|
138
217
|
}
|
|
139
218
|
|
|
140
219
|
/**
|
|
@@ -142,7 +221,11 @@ function countSqliteRowsWithQuarantineReadonly(
|
|
|
142
221
|
* Missing tiers count as zero; an existing tier that cannot be counted is
|
|
143
222
|
* explicitly `unreadable`.
|
|
144
223
|
*/
|
|
145
|
-
export function countLearningStoreRowsReadonly(
|
|
224
|
+
export function countLearningStoreRowsReadonly(
|
|
225
|
+
projectRoot: string,
|
|
226
|
+
options: StoreCountOptions = {},
|
|
227
|
+
requireModule: DatabaseRequire = createRequire(import.meta.url),
|
|
228
|
+
): LearningStoreRowCounts {
|
|
146
229
|
const root = resolve(projectRoot);
|
|
147
230
|
const lexicalPath = join(root, '.dz', 'memory', 'patterns.sqlite');
|
|
148
231
|
const jsonlPath = join(root, '.dz', 'patterns.jsonl');
|
|
@@ -150,23 +233,23 @@ export function countLearningStoreRowsReadonly(projectRoot: string): LearningSto
|
|
|
150
233
|
const vectorExists = existsSync(vectorPath);
|
|
151
234
|
const lexicalSource = existsSync(lexicalPath) ? 'sqlite' : 'jsonl';
|
|
152
235
|
const lexicalSqlite = lexicalSource === 'sqlite'
|
|
153
|
-
? countSqliteRowsWithQuarantineReadonly(lexicalPath, 'memory_records')
|
|
236
|
+
? countSqliteRowsWithQuarantineReadonly(lexicalPath, 'memory_records', options, requireModule)
|
|
154
237
|
: undefined;
|
|
155
|
-
let lexicalRows:
|
|
238
|
+
let lexicalRows: StoreRowCount;
|
|
156
239
|
if (lexicalSource === 'sqlite') {
|
|
157
|
-
lexicalRows = lexicalSqlite
|
|
240
|
+
lexicalRows = typeof lexicalSqlite === 'object' ? lexicalSqlite.rows : lexicalSqlite ?? 'unreadable';
|
|
158
241
|
} else {
|
|
159
242
|
lexicalRows = countJsonlRowsReadonly(jsonlPath);
|
|
160
243
|
}
|
|
161
244
|
const vectorSqlite = vectorExists
|
|
162
|
-
? countSqliteRowsWithQuarantineReadonly(vectorPath, 'reasoning_patterns')
|
|
245
|
+
? countSqliteRowsWithQuarantineReadonly(vectorPath, 'reasoning_patterns', options, requireModule)
|
|
163
246
|
: undefined;
|
|
164
247
|
const ignoredJsonl = lexicalSource === 'sqlite' && existsSync(jsonlPath)
|
|
165
248
|
? countJsonlRowsReadonly(jsonlPath)
|
|
166
249
|
: undefined;
|
|
167
250
|
return {
|
|
168
251
|
lexicalRows,
|
|
169
|
-
...(lexicalSqlite
|
|
252
|
+
...(typeof lexicalSqlite !== 'object' || lexicalSqlite.quarantinedRows === undefined ? {} : {
|
|
170
253
|
lexicalQuarantinedRows: lexicalSqlite.quarantinedRows,
|
|
171
254
|
}),
|
|
172
255
|
lexicalSource,
|
|
@@ -175,9 +258,12 @@ export function countLearningStoreRowsReadonly(projectRoot: string): LearningSto
|
|
|
175
258
|
lexicalIgnoredRows: ignoredJsonl,
|
|
176
259
|
lexicalIgnoredSourcePath: jsonlPath,
|
|
177
260
|
}),
|
|
178
|
-
vectorRows: vectorExists
|
|
179
|
-
|
|
180
|
-
|
|
261
|
+
vectorRows: vectorExists
|
|
262
|
+
? (typeof vectorSqlite === 'object' ? vectorSqlite.rows : vectorSqlite ?? 'unreadable')
|
|
263
|
+
: 0,
|
|
264
|
+
...(typeof vectorSqlite !== 'object' || vectorSqlite.lessonRows === undefined
|
|
265
|
+
? {} : { vectorLessonRows: vectorSqlite.lessonRows }),
|
|
266
|
+
...(typeof vectorSqlite !== 'object' || vectorSqlite.quarantinedRows === undefined ? {} : {
|
|
181
267
|
vectorQuarantinedRows: vectorSqlite.quarantinedRows,
|
|
182
268
|
}),
|
|
183
269
|
...(vectorExists ? { vectorSourcePath: vectorPath } : {}),
|
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;
|