@devflow-tools/database 0.17.0 → 0.17.2
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/CHANGELOG.md +30 -0
- package/dist/database.d.ts +51 -0
- package/dist/database.js +429 -29
- package/dist/index.d.ts +3 -1
- package/dist/index.js +10 -1
- package/dist/node-sqlite.d.ts +1 -1
- package/dist/node-sqlite.js +8 -4
- package/dist/task-semantic-control.d.ts +79 -0
- package/dist/task-semantic-control.js +196 -0
- package/dist/work-queue.d.ts +1 -0
- package/package.json +1 -1
- package/src/database.ts +548 -35
- package/src/index.ts +19 -0
- package/src/node-sqlite.ts +8 -4
- package/src/task-semantic-control.ts +270 -0
- package/src/work-queue.ts +1 -0
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export {
|
|
|
2
2
|
DevFlowDatabase,
|
|
3
3
|
getGlobalDevFlowDbPath,
|
|
4
4
|
openGlobalDevFlowDatabase,
|
|
5
|
+
openGlobalDevFlowReadOnlyDatabase,
|
|
5
6
|
} from './database';
|
|
6
7
|
export type {
|
|
7
8
|
BenchmarkReportMetaRecord,
|
|
@@ -65,6 +66,24 @@ export {
|
|
|
65
66
|
mapSessionObligationRow,
|
|
66
67
|
normalizeTurnId,
|
|
67
68
|
} from './obligation-ledger';
|
|
69
|
+
export {
|
|
70
|
+
assertTaskIdentity,
|
|
71
|
+
mapChannelQueryPlanRow,
|
|
72
|
+
mapTaskIntentArtifactRow,
|
|
73
|
+
mapTerminalTransitionRow,
|
|
74
|
+
mapToolNameResolutionRow,
|
|
75
|
+
mapTranscriptCheckpointRow,
|
|
76
|
+
stableSemanticJson,
|
|
77
|
+
} from './task-semantic-control';
|
|
78
|
+
export type {
|
|
79
|
+
ChannelQueryPlanRecord,
|
|
80
|
+
TaskIdentityRecord,
|
|
81
|
+
TaskIntentArtifactRecord,
|
|
82
|
+
TerminalTransitionRecord,
|
|
83
|
+
ToolNameResolutionRecord,
|
|
84
|
+
ToolNameResolutionStatus,
|
|
85
|
+
TranscriptCheckpointRecord,
|
|
86
|
+
} from './task-semantic-control';
|
|
68
87
|
export {
|
|
69
88
|
LEGAL_WORKFLOW_WORKER_TRANSITIONS,
|
|
70
89
|
TERMINAL_WORKFLOW_WORKER_STATES,
|
package/src/node-sqlite.ts
CHANGED
|
@@ -4,11 +4,15 @@ import type { Database, Statement, RunResult } from './types';
|
|
|
4
4
|
export class NodeSqliteDatabase implements Database {
|
|
5
5
|
private db: DatabaseSync;
|
|
6
6
|
|
|
7
|
-
constructor(path: string, busyTimeoutMs = 10000) {
|
|
8
|
-
this.db =
|
|
7
|
+
constructor(path: string, busyTimeoutMs = 10000, readOnly = false) {
|
|
8
|
+
this.db = readOnly
|
|
9
|
+
? new DatabaseSync(path, { readOnly: true })
|
|
10
|
+
: new DatabaseSync(path);
|
|
9
11
|
this.db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(busyTimeoutMs))}`);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
if (!readOnly) {
|
|
13
|
+
this.db.exec('PRAGMA journal_mode = WAL');
|
|
14
|
+
this.db.exec('PRAGMA synchronous = NORMAL');
|
|
15
|
+
}
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
exec(sql: string): void {
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
export interface TaskIdentityRecord {
|
|
2
|
+
projectRoot: string;
|
|
3
|
+
projectId: string;
|
|
4
|
+
hostId: string;
|
|
5
|
+
sessionId: string;
|
|
6
|
+
turnId: string;
|
|
7
|
+
requestId: string;
|
|
8
|
+
executionId?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TaskIntentArtifactRecord extends TaskIdentityRecord {
|
|
12
|
+
version: number;
|
|
13
|
+
supersedesHash?: string;
|
|
14
|
+
rawPrompt: string;
|
|
15
|
+
normalizedPrompt: string;
|
|
16
|
+
command?: string;
|
|
17
|
+
slashArgs: string[];
|
|
18
|
+
activeSkill?: string;
|
|
19
|
+
intent: string;
|
|
20
|
+
action: string;
|
|
21
|
+
entities: string[];
|
|
22
|
+
targetAnchors: string[];
|
|
23
|
+
policyConstraints: string[];
|
|
24
|
+
classificationEvidence: Array<Record<string, unknown>>;
|
|
25
|
+
sourceEventIds: string[];
|
|
26
|
+
sourceHash: string;
|
|
27
|
+
artifactHash: string;
|
|
28
|
+
createdAt: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ChannelQueryPlanRecord extends TaskIdentityRecord {
|
|
32
|
+
sourceIntentHash: string;
|
|
33
|
+
planHash: string;
|
|
34
|
+
code: Array<Record<string, unknown>>;
|
|
35
|
+
memory: Array<Record<string, unknown>>;
|
|
36
|
+
knowledge: Array<Record<string, unknown>>;
|
|
37
|
+
generatedBy: 'deterministic' | 'host_semantic' | 'hybrid';
|
|
38
|
+
degradation?: Record<string, unknown>;
|
|
39
|
+
createdAt: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ToolNameResolutionStatus = 'canonical' | 'alias' | 'unsupported';
|
|
43
|
+
|
|
44
|
+
export interface ToolNameResolutionRecord extends TaskIdentityRecord {
|
|
45
|
+
id: string;
|
|
46
|
+
sourceHash: string;
|
|
47
|
+
requestedName: string;
|
|
48
|
+
canonicalName?: string;
|
|
49
|
+
projectedName?: string;
|
|
50
|
+
status: ToolNameResolutionStatus;
|
|
51
|
+
attempt: number;
|
|
52
|
+
capabilityMechanism?: string;
|
|
53
|
+
reason?: string;
|
|
54
|
+
createdAt: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface TerminalTransitionRecord extends TaskIdentityRecord {
|
|
58
|
+
sequence: number;
|
|
59
|
+
fromState?: string;
|
|
60
|
+
toState: string;
|
|
61
|
+
receiptId: string;
|
|
62
|
+
sourceReceiptId?: string;
|
|
63
|
+
reason: string;
|
|
64
|
+
payload: Record<string, unknown>;
|
|
65
|
+
createdAt: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface TranscriptCheckpointRecord {
|
|
69
|
+
id: string;
|
|
70
|
+
projectRoot: string;
|
|
71
|
+
hostId: string;
|
|
72
|
+
sessionId: string;
|
|
73
|
+
sourcePath: string;
|
|
74
|
+
sourceHash: string;
|
|
75
|
+
byteOffset: number;
|
|
76
|
+
eventCount: number;
|
|
77
|
+
createdAt: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function stableSemanticJson(value: unknown): string {
|
|
81
|
+
return JSON.stringify(canonicalize(value));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function mapTaskIntentArtifactRow(row: Record<string, unknown>): TaskIntentArtifactRecord {
|
|
85
|
+
return {
|
|
86
|
+
...mapTaskIdentityRow(row),
|
|
87
|
+
version: requiredPositiveInteger(row.version, 'version'),
|
|
88
|
+
supersedesHash: optionalString(row.supersedes_hash),
|
|
89
|
+
rawPrompt: requiredString(row.raw_prompt, 'raw_prompt'),
|
|
90
|
+
normalizedPrompt: requiredString(row.normalized_prompt, 'normalized_prompt'),
|
|
91
|
+
command: optionalString(row.command),
|
|
92
|
+
slashArgs: parseStringArray(row.slash_args_json, 'slash_args_json'),
|
|
93
|
+
activeSkill: optionalString(row.active_skill),
|
|
94
|
+
intent: requiredString(row.intent, 'intent'),
|
|
95
|
+
action: requiredString(row.action, 'action'),
|
|
96
|
+
entities: parseStringArray(row.entities_json, 'entities_json'),
|
|
97
|
+
targetAnchors: parseStringArray(row.target_anchors_json, 'target_anchors_json'),
|
|
98
|
+
policyConstraints: parseStringArray(row.policy_constraints_json, 'policy_constraints_json'),
|
|
99
|
+
classificationEvidence: parseObjectArray(row.classification_evidence_json, 'classification_evidence_json'),
|
|
100
|
+
sourceEventIds: parseStringArray(row.source_event_ids_json, 'source_event_ids_json'),
|
|
101
|
+
sourceHash: requiredString(row.source_hash, 'source_hash'),
|
|
102
|
+
artifactHash: requiredString(row.artifact_hash, 'artifact_hash'),
|
|
103
|
+
createdAt: requiredNonNegativeInteger(row.created_at, 'created_at'),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function mapChannelQueryPlanRow(row: Record<string, unknown>): ChannelQueryPlanRecord {
|
|
108
|
+
const generatedBy = requiredString(row.generated_by, 'generated_by');
|
|
109
|
+
if (generatedBy !== 'deterministic' && generatedBy !== 'host_semantic' && generatedBy !== 'hybrid') {
|
|
110
|
+
throw new Error(`Invalid persisted generated_by: ${generatedBy}`);
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
...mapTaskIdentityRow(row),
|
|
114
|
+
sourceIntentHash: requiredString(row.source_intent_hash, 'source_intent_hash'),
|
|
115
|
+
planHash: requiredString(row.plan_hash, 'plan_hash'),
|
|
116
|
+
code: parseObjectArray(row.code_queries_json, 'code_queries_json'),
|
|
117
|
+
memory: parseObjectArray(row.memory_queries_json, 'memory_queries_json'),
|
|
118
|
+
knowledge: parseObjectArray(row.knowledge_queries_json, 'knowledge_queries_json'),
|
|
119
|
+
generatedBy,
|
|
120
|
+
degradation: parseOptionalObject(row.degradation_json, 'degradation_json'),
|
|
121
|
+
createdAt: requiredNonNegativeInteger(row.created_at, 'created_at'),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function mapToolNameResolutionRow(row: Record<string, unknown>): ToolNameResolutionRecord {
|
|
126
|
+
const status = requiredString(row.status, 'status');
|
|
127
|
+
if (status !== 'canonical' && status !== 'alias' && status !== 'unsupported') {
|
|
128
|
+
throw new Error(`Invalid persisted tool resolution status: ${status}`);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
...mapTaskIdentityRow(row),
|
|
132
|
+
id: requiredString(row.id, 'id'),
|
|
133
|
+
sourceHash: requiredString(row.source_hash, 'source_hash'),
|
|
134
|
+
requestedName: requiredString(row.requested_name, 'requested_name'),
|
|
135
|
+
canonicalName: optionalString(row.canonical_name),
|
|
136
|
+
projectedName: optionalString(row.projected_name),
|
|
137
|
+
status,
|
|
138
|
+
attempt: requiredNonNegativeInteger(row.attempt, 'attempt'),
|
|
139
|
+
capabilityMechanism: optionalString(row.capability_mechanism),
|
|
140
|
+
reason: optionalString(row.reason),
|
|
141
|
+
createdAt: requiredNonNegativeInteger(row.created_at, 'created_at'),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function mapTerminalTransitionRow(row: Record<string, unknown>): TerminalTransitionRecord {
|
|
146
|
+
return {
|
|
147
|
+
...mapTaskIdentityRow(row),
|
|
148
|
+
sequence: requiredPositiveInteger(row.sequence, 'sequence'),
|
|
149
|
+
fromState: optionalString(row.from_state),
|
|
150
|
+
toState: requiredString(row.to_state, 'to_state'),
|
|
151
|
+
receiptId: requiredString(row.receipt_id, 'receipt_id'),
|
|
152
|
+
sourceReceiptId: optionalString(row.source_receipt_id),
|
|
153
|
+
reason: requiredString(row.reason, 'reason'),
|
|
154
|
+
payload: parseObject(row.payload_json, 'payload_json'),
|
|
155
|
+
createdAt: requiredNonNegativeInteger(row.created_at, 'created_at'),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function mapTranscriptCheckpointRow(row: Record<string, unknown>): TranscriptCheckpointRecord {
|
|
160
|
+
return {
|
|
161
|
+
id: requiredString(row.id, 'id'),
|
|
162
|
+
projectRoot: requiredString(row.project_root, 'project_root'),
|
|
163
|
+
hostId: requiredString(row.host_id, 'host_id'),
|
|
164
|
+
sessionId: requiredString(row.session_id, 'session_id'),
|
|
165
|
+
sourcePath: requiredString(row.source_path, 'source_path'),
|
|
166
|
+
sourceHash: requiredString(row.source_hash, 'source_hash'),
|
|
167
|
+
byteOffset: requiredNonNegativeInteger(row.byte_offset, 'byte_offset'),
|
|
168
|
+
eventCount: requiredNonNegativeInteger(row.event_count, 'event_count'),
|
|
169
|
+
createdAt: requiredNonNegativeInteger(row.created_at, 'created_at'),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function assertTaskIdentity(record: TaskIdentityRecord): void {
|
|
174
|
+
for (const [field, value] of Object.entries({
|
|
175
|
+
projectRoot: record.projectRoot,
|
|
176
|
+
projectId: record.projectId,
|
|
177
|
+
hostId: record.hostId,
|
|
178
|
+
sessionId: record.sessionId,
|
|
179
|
+
turnId: record.turnId,
|
|
180
|
+
requestId: record.requestId,
|
|
181
|
+
})) {
|
|
182
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
183
|
+
throw new Error(`TASK_IDENTITY_INVALID:${field}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (record.executionId !== undefined && record.executionId.trim().length === 0) {
|
|
187
|
+
throw new Error('TASK_IDENTITY_INVALID:executionId');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function mapTaskIdentityRow(row: Record<string, unknown>): TaskIdentityRecord {
|
|
192
|
+
return {
|
|
193
|
+
projectRoot: requiredString(row.project_root, 'project_root'),
|
|
194
|
+
projectId: requiredString(row.project_id, 'project_id'),
|
|
195
|
+
hostId: requiredString(row.host_id, 'host_id'),
|
|
196
|
+
sessionId: requiredString(row.session_id, 'session_id'),
|
|
197
|
+
turnId: requiredString(row.turn_id, 'turn_id'),
|
|
198
|
+
requestId: requiredString(row.request_id, 'request_id'),
|
|
199
|
+
executionId: optionalString(row.execution_id),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function canonicalize(value: unknown): unknown {
|
|
204
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
205
|
+
if (typeof value === 'number') {
|
|
206
|
+
if (!Number.isFinite(value) || Object.is(value, -0)) throw new Error('Semantic control evidence must be lossless JSON');
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
210
|
+
if (!value || typeof value !== 'object' || Object.getPrototypeOf(value) !== Object.prototype) {
|
|
211
|
+
throw new Error('Semantic control evidence must be plain JSON');
|
|
212
|
+
}
|
|
213
|
+
const record = value as Record<string, unknown>;
|
|
214
|
+
return Object.fromEntries(Object.keys(record).sort().map(key => [key, canonicalize(record[key])]));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseJson(value: unknown, field: string): unknown {
|
|
218
|
+
if (typeof value !== 'string') throw new Error(`Invalid persisted ${field}: expected JSON text`);
|
|
219
|
+
try { return JSON.parse(value) as unknown; } catch { throw new Error(`Invalid persisted ${field}: malformed JSON`); }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function parseStringArray(value: unknown, field: string): string[] {
|
|
223
|
+
const parsed = parseJson(value, field);
|
|
224
|
+
if (!Array.isArray(parsed) || parsed.some(item => typeof item !== 'string')) {
|
|
225
|
+
throw new Error(`Invalid persisted ${field}: expected string array`);
|
|
226
|
+
}
|
|
227
|
+
return parsed as string[];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function parseObjectArray(value: unknown, field: string): Array<Record<string, unknown>> {
|
|
231
|
+
const parsed = parseJson(value, field);
|
|
232
|
+
if (!Array.isArray(parsed) || parsed.some(item => !item || typeof item !== 'object' || Array.isArray(item))) {
|
|
233
|
+
throw new Error(`Invalid persisted ${field}: expected object array`);
|
|
234
|
+
}
|
|
235
|
+
return parsed as Array<Record<string, unknown>>;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function parseObject(value: unknown, field: string): Record<string, unknown> {
|
|
239
|
+
const parsed = parseJson(value, field);
|
|
240
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
241
|
+
throw new Error(`Invalid persisted ${field}: expected object`);
|
|
242
|
+
}
|
|
243
|
+
return parsed as Record<string, unknown>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function parseOptionalObject(value: unknown, field: string): Record<string, unknown> | undefined {
|
|
247
|
+
if (value === null || value === undefined) return undefined;
|
|
248
|
+
return parseObject(value, field);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function requiredString(value: unknown, field: string): string {
|
|
252
|
+
if (typeof value !== 'string' || value.length === 0) throw new Error(`Invalid persisted ${field}`);
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function optionalString(value: unknown): string | undefined {
|
|
257
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function requiredNonNegativeInteger(value: unknown, field: string): number {
|
|
261
|
+
const result = Number(value);
|
|
262
|
+
if (!Number.isSafeInteger(result) || result < 0) throw new Error(`Invalid persisted ${field}`);
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function requiredPositiveInteger(value: unknown, field: string): number {
|
|
267
|
+
const result = requiredNonNegativeInteger(value, field);
|
|
268
|
+
if (result === 0) throw new Error(`Invalid persisted ${field}`);
|
|
269
|
+
return result;
|
|
270
|
+
}
|