@devflow-tools/database 0.16.21 → 0.16.22
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 +11 -0
- package/__tests__/database.learning-candidates.test.ts +93 -0
- package/dist/database.d.ts +17 -0
- package/dist/database.js +308 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -1
- package/dist/learning-candidates.d.ts +87 -0
- package/dist/learning-candidates.js +81 -0
- package/dist/work-queue.d.ts +1 -1
- package/package.json +1 -1
- package/src/database.ts +427 -0
- package/src/index.ts +18 -0
- package/src/learning-candidates.ts +181 -0
- package/src/work-queue.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.16.22](https://github.com/shilongfeicool/dev-flow/compare/v0.16.21...v0.16.22) (2026-07-28)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **learning:** add governed overlays and evaluation ([795747d](https://github.com/shilongfeicool/dev-flow/commit/795747db10b3f13dfe5b442e8332673084774d5e))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
6
17
|
## [0.16.21](https://github.com/shilongfeicool/dev-flow/compare/v0.16.20...v0.16.21) (2026-07-28)
|
|
7
18
|
|
|
8
19
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { DevFlowDatabase } from '../src/index.js';
|
|
6
|
+
|
|
7
|
+
describe('governed learning candidates', () => {
|
|
8
|
+
const roots: string[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('requires independent evidence, grader receipt, and rolls active candidates back on contradiction', () => {
|
|
15
|
+
const root = mkdtempSync(join(tmpdir(), 'devflow-learning-db-'));
|
|
16
|
+
roots.push(root);
|
|
17
|
+
const database = new DevFlowDatabase(root);
|
|
18
|
+
const candidate = database.upsertLearningCandidate({
|
|
19
|
+
id: 'candidate:install-flag',
|
|
20
|
+
projectRoot: '/project',
|
|
21
|
+
scope: 'project',
|
|
22
|
+
kind: 'tool_preference',
|
|
23
|
+
trigger: { skills: ['devflow:react'], entities: ['--legacy-peer-deps'] },
|
|
24
|
+
instruction: 'Use --legacy-peer-deps when installing dependencies.',
|
|
25
|
+
confidence: 0.95,
|
|
26
|
+
});
|
|
27
|
+
expect(candidate.state).toBe('observed');
|
|
28
|
+
|
|
29
|
+
for (let index = 1; index <= 3; index += 1) {
|
|
30
|
+
database.addLearningCandidateEvidence({
|
|
31
|
+
id: `evidence:${index}`,
|
|
32
|
+
candidateId: candidate.id,
|
|
33
|
+
projectRoot: '/project',
|
|
34
|
+
sessionId: `session:${index}`,
|
|
35
|
+
sourceType: 'tool_preference',
|
|
36
|
+
polarity: 'supporting',
|
|
37
|
+
outcome: index <= 2 ? 'positive' : 'unknown',
|
|
38
|
+
evidenceHash: `hash:${index}`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
expect(database.getLearningCandidate(candidate.id)).toMatchObject({
|
|
42
|
+
state: 'candidate', supportingSessions: 3, successfulOutcomes: 2,
|
|
43
|
+
});
|
|
44
|
+
database.transitionLearningCandidate(candidate.id, { target: 'shadow', reason: 'threshold' });
|
|
45
|
+
database.transitionLearningCandidate(candidate.id, {
|
|
46
|
+
target: 'evaluated', reason: 'pass^3', graderReceipt: 'grader:receipt:12345678',
|
|
47
|
+
});
|
|
48
|
+
expect(database.transitionLearningCandidate(candidate.id, {
|
|
49
|
+
target: 'active', reason: 'automatic project activation',
|
|
50
|
+
}).state).toBe('active');
|
|
51
|
+
|
|
52
|
+
expect(database.addLearningCandidateEvidence({
|
|
53
|
+
id: 'evidence:contradiction',
|
|
54
|
+
candidateId: candidate.id,
|
|
55
|
+
projectRoot: '/project',
|
|
56
|
+
sessionId: 'session:4',
|
|
57
|
+
sourceType: 'correction',
|
|
58
|
+
polarity: 'contradicting',
|
|
59
|
+
outcome: 'positive',
|
|
60
|
+
evidenceHash: 'hash:contradiction',
|
|
61
|
+
})).toMatchObject({ state: 'shadow', contradictions: 1 });
|
|
62
|
+
database.close();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('versions changed overlays and requires manual approval for global candidates', () => {
|
|
66
|
+
const root = mkdtempSync(join(tmpdir(), 'devflow-learning-version-'));
|
|
67
|
+
roots.push(root);
|
|
68
|
+
const database = new DevFlowDatabase(root);
|
|
69
|
+
const id = 'candidate:global';
|
|
70
|
+
database.upsertLearningCandidate({
|
|
71
|
+
id, projectRoot: '/project', scope: 'global', kind: 'convention',
|
|
72
|
+
trigger: {}, instruction: 'Use the first convention.', confidence: 0.8,
|
|
73
|
+
});
|
|
74
|
+
database.upsertLearningCandidate({
|
|
75
|
+
id, projectRoot: '/project', scope: 'global', kind: 'convention',
|
|
76
|
+
trigger: {}, instruction: 'Use the corrected convention.', confidence: 0.9,
|
|
77
|
+
});
|
|
78
|
+
expect(database.listLearningCandidateVersions(id)).toHaveLength(2);
|
|
79
|
+
for (let index = 1; index <= 3; index += 1) {
|
|
80
|
+
database.addLearningCandidateEvidence({
|
|
81
|
+
id: `global:${index}`, candidateId: id, projectRoot: '/project', sessionId: `s:${index}`,
|
|
82
|
+
sourceType: 'convention', polarity: 'supporting', outcome: 'positive', evidenceHash: `g:${index}`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
database.transitionLearningCandidate(id, { target: 'shadow', reason: 'threshold' });
|
|
86
|
+
database.transitionLearningCandidate(id, { target: 'evaluated', reason: 'graded', graderReceipt: 'grader:global:12345678' });
|
|
87
|
+
expect(() => database.transitionLearningCandidate(id, { target: 'active', reason: 'auto' }))
|
|
88
|
+
.toThrow('manual approval');
|
|
89
|
+
expect(database.transitionLearningCandidate(id, { target: 'active', reason: 'manual', manualApproval: true }).state)
|
|
90
|
+
.toBe('active');
|
|
91
|
+
database.close();
|
|
92
|
+
});
|
|
93
|
+
});
|
package/dist/database.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { EnqueueWorkInput, LeaseWorkInput, RequestSessionClosureInput, Sess
|
|
|
2
2
|
import { type SessionObligationRecord, type SessionObligationState } from './obligation-ledger';
|
|
3
3
|
import { type FailHostActionInput, type HostActionRecord, type ReportHostActionInput, type RequestHostActionInput, type StartHostActionInput, type VerifyHostActionInput } from './host-actions';
|
|
4
4
|
import { type AppendRetrievalCycleInput, type CreateRetrievalSessionInput, type RetrievalCycleRecord, type RetrievalSessionRecord, type RetrievalSessionState } from './retrieval-sessions';
|
|
5
|
+
import { type AddLearningCandidateEvidenceInput, type LearningCandidateEvidenceRecord, type LearningCandidateRecord, type LearningCandidateState, type LearningCandidateVersionRecord, type TransitionLearningCandidateInput, type UpsertLearningCandidateInput } from './learning-candidates';
|
|
5
6
|
export interface BenchmarkReportRecord {
|
|
6
7
|
runId: string;
|
|
7
8
|
suiteId: string;
|
|
@@ -325,6 +326,22 @@ export declare class DevFlowDatabase {
|
|
|
325
326
|
items: GovernanceAuditRecord[];
|
|
326
327
|
total: number;
|
|
327
328
|
};
|
|
329
|
+
upsertLearningCandidate(input: UpsertLearningCandidateInput): LearningCandidateRecord;
|
|
330
|
+
getLearningCandidate(id: string): LearningCandidateRecord | null;
|
|
331
|
+
listLearningCandidates(options?: {
|
|
332
|
+
projectRoot?: string;
|
|
333
|
+
states?: LearningCandidateState[];
|
|
334
|
+
limit?: number;
|
|
335
|
+
}): LearningCandidateRecord[];
|
|
336
|
+
addLearningCandidateEvidence(input: AddLearningCandidateEvidenceInput): LearningCandidateRecord;
|
|
337
|
+
listLearningCandidateEvidence(candidateId: string): LearningCandidateEvidenceRecord[];
|
|
338
|
+
resolveLearningContradiction(candidateId: string, evidenceId: string, resolvedAt?: number): LearningCandidateRecord;
|
|
339
|
+
transitionLearningCandidate(candidateId: string, input: TransitionLearningCandidateInput): LearningCandidateRecord;
|
|
340
|
+
listLearningCandidateVersions(candidateId: string): LearningCandidateVersionRecord[];
|
|
341
|
+
private assertLearningTransition;
|
|
342
|
+
private getLearningEvidenceAggregates;
|
|
343
|
+
private insertLearningCandidateVersion;
|
|
344
|
+
private insertLearningActivation;
|
|
328
345
|
enqueueWork(input: EnqueueWorkInput): WorkItemRecord;
|
|
329
346
|
getWorkByIdempotencyKey(idempotencyKey: string): WorkItemRecord | null;
|
|
330
347
|
requestSessionClosure(input: RequestSessionClosureInput): SessionClosureRecord;
|
package/dist/database.js
CHANGED
|
@@ -11,6 +11,7 @@ const crypto_1 = require("crypto");
|
|
|
11
11
|
const obligation_ledger_1 = require("./obligation-ledger");
|
|
12
12
|
const host_actions_1 = require("./host-actions");
|
|
13
13
|
const retrieval_sessions_1 = require("./retrieval-sessions");
|
|
14
|
+
const learning_candidates_1 = require("./learning-candidates");
|
|
14
15
|
const CONTEXT_REQUIRED_SKILLS = new Set([
|
|
15
16
|
'react', 'vue', 'nest', 'nextjs', 'graphql', 'typescript',
|
|
16
17
|
]);
|
|
@@ -477,6 +478,74 @@ class DevFlowDatabase {
|
|
|
477
478
|
CREATE INDEX IF NOT EXISTS idx_session_closures_state
|
|
478
479
|
ON devflow_session_closures(project_root, state, updated_at DESC);
|
|
479
480
|
|
|
481
|
+
CREATE TABLE IF NOT EXISTS devflow_learning_candidates (
|
|
482
|
+
id TEXT PRIMARY KEY,
|
|
483
|
+
project_root TEXT NOT NULL,
|
|
484
|
+
scope TEXT NOT NULL CHECK(scope IN ('project', 'global')),
|
|
485
|
+
state TEXT NOT NULL DEFAULT 'observed'
|
|
486
|
+
CHECK(state IN ('observed', 'candidate', 'shadow', 'evaluated', 'active', 'rejected', 'retired')),
|
|
487
|
+
kind TEXT NOT NULL CHECK(kind IN ('convention', 'workflow', 'tool_preference', 'correction')),
|
|
488
|
+
trigger_json TEXT NOT NULL,
|
|
489
|
+
instruction TEXT NOT NULL,
|
|
490
|
+
confidence REAL NOT NULL CHECK(confidence BETWEEN 0 AND 1),
|
|
491
|
+
overlay_version INTEGER NOT NULL DEFAULT 1 CHECK(overlay_version > 0),
|
|
492
|
+
supporting_sessions INTEGER NOT NULL DEFAULT 0 CHECK(supporting_sessions >= 0),
|
|
493
|
+
successful_outcomes INTEGER NOT NULL DEFAULT 0 CHECK(successful_outcomes >= 0),
|
|
494
|
+
contradictions INTEGER NOT NULL DEFAULT 0 CHECK(contradictions >= 0),
|
|
495
|
+
manual_only INTEGER NOT NULL DEFAULT 0 CHECK(manual_only IN (0, 1)),
|
|
496
|
+
risk TEXT NOT NULL DEFAULT 'low' CHECK(risk IN ('low', 'medium', 'high')),
|
|
497
|
+
grader_receipt TEXT,
|
|
498
|
+
expires_at INTEGER,
|
|
499
|
+
created_at INTEGER NOT NULL,
|
|
500
|
+
updated_at INTEGER NOT NULL
|
|
501
|
+
);
|
|
502
|
+
CREATE INDEX IF NOT EXISTS idx_learning_candidates_project
|
|
503
|
+
ON devflow_learning_candidates(project_root, state, kind, updated_at DESC);
|
|
504
|
+
|
|
505
|
+
CREATE TABLE IF NOT EXISTS devflow_learning_candidate_versions (
|
|
506
|
+
candidate_id TEXT NOT NULL,
|
|
507
|
+
version INTEGER NOT NULL CHECK(version > 0),
|
|
508
|
+
trigger_json TEXT NOT NULL,
|
|
509
|
+
instruction TEXT NOT NULL,
|
|
510
|
+
created_at INTEGER NOT NULL,
|
|
511
|
+
PRIMARY KEY(candidate_id, version)
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
CREATE TABLE IF NOT EXISTS devflow_learning_candidate_evidence (
|
|
515
|
+
id TEXT PRIMARY KEY,
|
|
516
|
+
candidate_id TEXT NOT NULL,
|
|
517
|
+
project_root TEXT NOT NULL,
|
|
518
|
+
session_id TEXT NOT NULL,
|
|
519
|
+
execution_id TEXT,
|
|
520
|
+
memory_observation_id TEXT,
|
|
521
|
+
outcome_id TEXT,
|
|
522
|
+
source_type TEXT NOT NULL
|
|
523
|
+
CHECK(source_type IN ('memory', 'correction', 'workflow_outcome', 'tool_preference', 'convention', 'grader', 'manual')),
|
|
524
|
+
polarity TEXT NOT NULL CHECK(polarity IN ('supporting', 'contradicting')),
|
|
525
|
+
outcome TEXT NOT NULL DEFAULT 'unknown' CHECK(outcome IN ('positive', 'negative', 'unknown')),
|
|
526
|
+
evidence_hash TEXT NOT NULL,
|
|
527
|
+
receipt TEXT,
|
|
528
|
+
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
529
|
+
created_at INTEGER NOT NULL,
|
|
530
|
+
resolved_at INTEGER,
|
|
531
|
+
UNIQUE(candidate_id, evidence_hash)
|
|
532
|
+
);
|
|
533
|
+
CREATE INDEX IF NOT EXISTS idx_learning_evidence_candidate
|
|
534
|
+
ON devflow_learning_candidate_evidence(candidate_id, polarity, resolved_at, created_at);
|
|
535
|
+
|
|
536
|
+
CREATE TABLE IF NOT EXISTS devflow_learning_activation_history (
|
|
537
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
538
|
+
candidate_id TEXT NOT NULL,
|
|
539
|
+
from_state TEXT NOT NULL,
|
|
540
|
+
to_state TEXT NOT NULL,
|
|
541
|
+
reason TEXT NOT NULL,
|
|
542
|
+
grader_receipt TEXT,
|
|
543
|
+
manual_approval INTEGER NOT NULL DEFAULT 0 CHECK(manual_approval IN (0, 1)),
|
|
544
|
+
changed_at INTEGER NOT NULL
|
|
545
|
+
);
|
|
546
|
+
CREATE INDEX IF NOT EXISTS idx_learning_activation_candidate
|
|
547
|
+
ON devflow_learning_activation_history(candidate_id, changed_at DESC);
|
|
548
|
+
|
|
480
549
|
CREATE TABLE IF NOT EXISTS devflow_host_actions (
|
|
481
550
|
action_id TEXT PRIMARY KEY,
|
|
482
551
|
run_id TEXT NOT NULL,
|
|
@@ -1815,6 +1884,245 @@ class DevFlowDatabase {
|
|
|
1815
1884
|
total: Number(count?.total ?? 0),
|
|
1816
1885
|
};
|
|
1817
1886
|
}
|
|
1887
|
+
// ---- Governed Learning ----
|
|
1888
|
+
upsertLearningCandidate(input) {
|
|
1889
|
+
if (!input.id.trim())
|
|
1890
|
+
throw new Error('Learning candidate requires an ID');
|
|
1891
|
+
if (!input.projectRoot.trim())
|
|
1892
|
+
throw new Error('Learning candidate requires a project root');
|
|
1893
|
+
if (!input.instruction.trim())
|
|
1894
|
+
throw new Error('Learning candidate requires an instruction');
|
|
1895
|
+
if (!Number.isFinite(input.confidence) || input.confidence < 0 || input.confidence > 1) {
|
|
1896
|
+
throw new Error('Learning candidate confidence must be between 0 and 1');
|
|
1897
|
+
}
|
|
1898
|
+
return this.db.transaction(() => {
|
|
1899
|
+
const now = Date.now();
|
|
1900
|
+
const existing = this.getLearningCandidate(input.id);
|
|
1901
|
+
const triggerJson = (0, learning_candidates_1.stableLearningJson)(input.trigger);
|
|
1902
|
+
const instruction = input.instruction.trim().slice(0, 2000);
|
|
1903
|
+
if (!existing) {
|
|
1904
|
+
this.db.prepare(`
|
|
1905
|
+
INSERT INTO devflow_learning_candidates (
|
|
1906
|
+
id, project_root, scope, state, kind, trigger_json, instruction,
|
|
1907
|
+
confidence, overlay_version, supporting_sessions, successful_outcomes,
|
|
1908
|
+
contradictions, manual_only, risk, expires_at, created_at, updated_at
|
|
1909
|
+
) VALUES (?, ?, ?, 'observed', ?, ?, ?, ?, 1, 0, 0, 0, ?, ?, ?, ?, ?)
|
|
1910
|
+
`).run(input.id, input.projectRoot, input.scope, input.kind, triggerJson, instruction, input.confidence, input.manualOnly ? 1 : 0, input.risk ?? 'low', input.expiresAt ?? null, now, now);
|
|
1911
|
+
this.insertLearningCandidateVersion(input.id, 1, triggerJson, instruction, now);
|
|
1912
|
+
return this.getLearningCandidate(input.id);
|
|
1913
|
+
}
|
|
1914
|
+
if (existing.projectRoot !== input.projectRoot || existing.scope !== input.scope || existing.kind !== input.kind) {
|
|
1915
|
+
throw new Error(`Learning candidate identity mismatch for ${input.id}`);
|
|
1916
|
+
}
|
|
1917
|
+
const contentChanged = (0, learning_candidates_1.stableLearningJson)(existing.trigger) !== triggerJson
|
|
1918
|
+
|| existing.instruction !== instruction;
|
|
1919
|
+
const nextVersion = contentChanged ? existing.overlayVersion + 1 : existing.overlayVersion;
|
|
1920
|
+
const nextState = contentChanged && (existing.state === 'active' || existing.state === 'evaluated')
|
|
1921
|
+
? 'shadow'
|
|
1922
|
+
: existing.state;
|
|
1923
|
+
this.db.prepare(`
|
|
1924
|
+
UPDATE devflow_learning_candidates
|
|
1925
|
+
SET trigger_json = ?, instruction = ?, confidence = ?, overlay_version = ?,
|
|
1926
|
+
state = ?, manual_only = MAX(manual_only, ?),
|
|
1927
|
+
risk = CASE
|
|
1928
|
+
WHEN risk = 'high' OR ? = 'high' THEN 'high'
|
|
1929
|
+
WHEN risk = 'medium' OR ? = 'medium' THEN 'medium'
|
|
1930
|
+
ELSE 'low'
|
|
1931
|
+
END,
|
|
1932
|
+
expires_at = ?, updated_at = ?
|
|
1933
|
+
WHERE id = ?
|
|
1934
|
+
`).run(triggerJson, instruction, Math.max(existing.confidence, input.confidence), nextVersion, nextState, input.manualOnly ? 1 : 0, input.risk ?? 'low', input.risk ?? 'low', input.expiresAt ?? existing.expiresAt ?? null, now, input.id);
|
|
1935
|
+
if (contentChanged) {
|
|
1936
|
+
this.insertLearningCandidateVersion(input.id, nextVersion, triggerJson, instruction, now);
|
|
1937
|
+
if (nextState !== existing.state) {
|
|
1938
|
+
this.insertLearningActivation(input.id, existing.state, nextState, 'candidate_content_changed', undefined, false, now);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
return this.getLearningCandidate(input.id);
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
getLearningCandidate(id) {
|
|
1945
|
+
const row = this.db.prepare('SELECT * FROM devflow_learning_candidates WHERE id = ?')
|
|
1946
|
+
.get(id);
|
|
1947
|
+
return row ? (0, learning_candidates_1.mapLearningCandidateRow)(row) : null;
|
|
1948
|
+
}
|
|
1949
|
+
listLearningCandidates(options = {}) {
|
|
1950
|
+
const predicates = [];
|
|
1951
|
+
const params = [];
|
|
1952
|
+
if (options.projectRoot) {
|
|
1953
|
+
predicates.push('(project_root = ? OR scope = \'global\')');
|
|
1954
|
+
params.push(options.projectRoot);
|
|
1955
|
+
}
|
|
1956
|
+
const states = [...new Set(options.states ?? [])];
|
|
1957
|
+
if (states.length > 0) {
|
|
1958
|
+
predicates.push(`state IN (${states.map(() => '?').join(',')})`);
|
|
1959
|
+
params.push(...states);
|
|
1960
|
+
}
|
|
1961
|
+
const rows = this.db.prepare(`
|
|
1962
|
+
SELECT * FROM devflow_learning_candidates
|
|
1963
|
+
${predicates.length > 0 ? `WHERE ${predicates.join(' AND ')}` : ''}
|
|
1964
|
+
ORDER BY updated_at DESC, id ASC
|
|
1965
|
+
LIMIT ?
|
|
1966
|
+
`).all(...params, Math.max(1, Math.min(options.limit ?? 200, 2000)));
|
|
1967
|
+
return rows.map(learning_candidates_1.mapLearningCandidateRow);
|
|
1968
|
+
}
|
|
1969
|
+
addLearningCandidateEvidence(input) {
|
|
1970
|
+
if (!input.evidenceHash.trim())
|
|
1971
|
+
throw new Error('Learning evidence requires a hash');
|
|
1972
|
+
if (!input.sessionId.trim())
|
|
1973
|
+
throw new Error('Learning evidence requires a session ID');
|
|
1974
|
+
return this.db.transaction(() => {
|
|
1975
|
+
const candidate = this.getLearningCandidate(input.candidateId);
|
|
1976
|
+
if (!candidate)
|
|
1977
|
+
throw new Error(`Learning candidate ${input.candidateId} does not exist`);
|
|
1978
|
+
if (candidate.projectRoot !== input.projectRoot && candidate.scope !== 'global') {
|
|
1979
|
+
throw new Error(`Learning evidence project mismatch for ${input.candidateId}`);
|
|
1980
|
+
}
|
|
1981
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
1982
|
+
const inserted = this.db.prepare(`
|
|
1983
|
+
INSERT OR IGNORE INTO devflow_learning_candidate_evidence (
|
|
1984
|
+
id, candidate_id, project_root, session_id, execution_id,
|
|
1985
|
+
memory_observation_id, outcome_id, source_type, polarity, outcome,
|
|
1986
|
+
evidence_hash, receipt, payload_json, created_at
|
|
1987
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1988
|
+
`).run(input.id, input.candidateId, input.projectRoot, input.sessionId, input.executionId ?? null, input.memoryObservationId ?? null, input.outcomeId ?? null, input.sourceType, input.polarity, input.outcome ?? 'unknown', input.evidenceHash, input.receipt ?? null, (0, learning_candidates_1.stableLearningJson)(input.payload ?? {}), createdAt).changes === 1;
|
|
1989
|
+
if (!inserted)
|
|
1990
|
+
return candidate;
|
|
1991
|
+
const previousState = candidate.state;
|
|
1992
|
+
const aggregates = this.getLearningEvidenceAggregates(input.candidateId);
|
|
1993
|
+
let state = previousState;
|
|
1994
|
+
if (state === 'observed' && aggregates.supportingSessions > 0)
|
|
1995
|
+
state = 'candidate';
|
|
1996
|
+
if (aggregates.contradictions > 0 && (state === 'active' || state === 'evaluated'))
|
|
1997
|
+
state = 'shadow';
|
|
1998
|
+
this.db.prepare(`
|
|
1999
|
+
UPDATE devflow_learning_candidates
|
|
2000
|
+
SET state = ?, supporting_sessions = ?, successful_outcomes = ?,
|
|
2001
|
+
contradictions = ?, updated_at = ?
|
|
2002
|
+
WHERE id = ?
|
|
2003
|
+
`).run(state, aggregates.supportingSessions, aggregates.successfulOutcomes, aggregates.contradictions, createdAt, input.candidateId);
|
|
2004
|
+
if (state !== previousState) {
|
|
2005
|
+
this.insertLearningActivation(input.candidateId, previousState, state, aggregates.contradictions > 0 ? 'unresolved_contradiction' : 'first_supporting_evidence', undefined, false, createdAt);
|
|
2006
|
+
}
|
|
2007
|
+
return this.getLearningCandidate(input.candidateId);
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
listLearningCandidateEvidence(candidateId) {
|
|
2011
|
+
const rows = this.db.prepare(`
|
|
2012
|
+
SELECT * FROM devflow_learning_candidate_evidence
|
|
2013
|
+
WHERE candidate_id = ? ORDER BY created_at ASC, id ASC
|
|
2014
|
+
`).all(candidateId);
|
|
2015
|
+
return rows.map(learning_candidates_1.mapLearningEvidenceRow);
|
|
2016
|
+
}
|
|
2017
|
+
resolveLearningContradiction(candidateId, evidenceId, resolvedAt = Date.now()) {
|
|
2018
|
+
return this.db.transaction(() => {
|
|
2019
|
+
this.db.prepare(`
|
|
2020
|
+
UPDATE devflow_learning_candidate_evidence
|
|
2021
|
+
SET resolved_at = ?
|
|
2022
|
+
WHERE id = ? AND candidate_id = ? AND polarity = 'contradicting' AND resolved_at IS NULL
|
|
2023
|
+
`).run(resolvedAt, evidenceId, candidateId);
|
|
2024
|
+
const aggregates = this.getLearningEvidenceAggregates(candidateId);
|
|
2025
|
+
this.db.prepare(`
|
|
2026
|
+
UPDATE devflow_learning_candidates
|
|
2027
|
+
SET supporting_sessions = ?, successful_outcomes = ?, contradictions = ?, updated_at = ?
|
|
2028
|
+
WHERE id = ?
|
|
2029
|
+
`).run(aggregates.supportingSessions, aggregates.successfulOutcomes, aggregates.contradictions, resolvedAt, candidateId);
|
|
2030
|
+
const candidate = this.getLearningCandidate(candidateId);
|
|
2031
|
+
if (!candidate)
|
|
2032
|
+
throw new Error(`Learning candidate ${candidateId} does not exist`);
|
|
2033
|
+
return candidate;
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
transitionLearningCandidate(candidateId, input) {
|
|
2037
|
+
return this.db.transaction(() => {
|
|
2038
|
+
const candidate = this.getLearningCandidate(candidateId);
|
|
2039
|
+
if (!candidate)
|
|
2040
|
+
throw new Error(`Learning candidate ${candidateId} does not exist`);
|
|
2041
|
+
if (candidate.state === input.target)
|
|
2042
|
+
return candidate;
|
|
2043
|
+
this.assertLearningTransition(candidate, input);
|
|
2044
|
+
const changedAt = input.changedAt ?? Date.now();
|
|
2045
|
+
const graderReceipt = input.graderReceipt ?? candidate.graderReceipt;
|
|
2046
|
+
this.db.prepare(`
|
|
2047
|
+
UPDATE devflow_learning_candidates
|
|
2048
|
+
SET state = ?, grader_receipt = COALESCE(?, grader_receipt), updated_at = ?
|
|
2049
|
+
WHERE id = ?
|
|
2050
|
+
`).run(input.target, graderReceipt ?? null, changedAt, candidateId);
|
|
2051
|
+
this.insertLearningActivation(candidateId, candidate.state, input.target, input.reason, graderReceipt, input.manualApproval === true, changedAt);
|
|
2052
|
+
return this.getLearningCandidate(candidateId);
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
listLearningCandidateVersions(candidateId) {
|
|
2056
|
+
const rows = this.db.prepare(`
|
|
2057
|
+
SELECT * FROM devflow_learning_candidate_versions
|
|
2058
|
+
WHERE candidate_id = ? ORDER BY version ASC
|
|
2059
|
+
`).all(candidateId);
|
|
2060
|
+
return rows.map(learning_candidates_1.mapLearningVersionRow);
|
|
2061
|
+
}
|
|
2062
|
+
assertLearningTransition(candidate, input) {
|
|
2063
|
+
const terminal = input.target === 'rejected' || input.target === 'retired';
|
|
2064
|
+
const rollback = input.target === 'shadow'
|
|
2065
|
+
&& (candidate.state === 'active' || candidate.state === 'evaluated');
|
|
2066
|
+
const legal = {
|
|
2067
|
+
observed: ['candidate', 'rejected', 'retired'],
|
|
2068
|
+
candidate: ['shadow', 'rejected', 'retired'],
|
|
2069
|
+
shadow: ['evaluated', 'rejected', 'retired'],
|
|
2070
|
+
evaluated: ['active', 'shadow', 'rejected', 'retired'],
|
|
2071
|
+
active: ['shadow', 'retired'],
|
|
2072
|
+
rejected: ['retired'],
|
|
2073
|
+
retired: [],
|
|
2074
|
+
};
|
|
2075
|
+
if (!terminal && !rollback && !legal[candidate.state].includes(input.target)) {
|
|
2076
|
+
throw new Error(`Illegal learning transition ${candidate.state} -> ${input.target}`);
|
|
2077
|
+
}
|
|
2078
|
+
if (input.target === 'shadow') {
|
|
2079
|
+
if (candidate.supportingSessions < 3 || candidate.successfulOutcomes < 2) {
|
|
2080
|
+
throw new Error('Learning candidate requires three sessions and two verified outcomes before shadow');
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
if (input.target === 'evaluated' && candidate.contradictions > 0) {
|
|
2084
|
+
throw new Error('Learning candidate has unresolved contradictions');
|
|
2085
|
+
}
|
|
2086
|
+
if (input.target === 'active') {
|
|
2087
|
+
const receipt = input.graderReceipt ?? candidate.graderReceipt;
|
|
2088
|
+
if (!receipt)
|
|
2089
|
+
throw new Error('Learning candidate activation requires a grader receipt');
|
|
2090
|
+
if (candidate.contradictions > 0)
|
|
2091
|
+
throw new Error('Learning candidate has unresolved contradictions');
|
|
2092
|
+
const manualRequired = candidate.scope === 'global' || candidate.manualOnly || candidate.risk === 'high';
|
|
2093
|
+
if (manualRequired && input.manualApproval !== true) {
|
|
2094
|
+
throw new Error('Learning candidate requires manual approval');
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
getLearningEvidenceAggregates(candidateId) {
|
|
2099
|
+
const row = this.db.prepare(`
|
|
2100
|
+
SELECT
|
|
2101
|
+
COUNT(DISTINCT CASE WHEN polarity = 'supporting' THEN session_id END) AS supporting_sessions,
|
|
2102
|
+
COUNT(DISTINCT CASE WHEN polarity = 'supporting' AND outcome = 'positive' THEN session_id END) AS successful_outcomes,
|
|
2103
|
+
SUM(CASE WHEN polarity = 'contradicting' AND resolved_at IS NULL THEN 1 ELSE 0 END) AS contradictions
|
|
2104
|
+
FROM devflow_learning_candidate_evidence WHERE candidate_id = ?
|
|
2105
|
+
`).get(candidateId);
|
|
2106
|
+
return {
|
|
2107
|
+
supportingSessions: Number(row?.supporting_sessions ?? 0),
|
|
2108
|
+
successfulOutcomes: Number(row?.successful_outcomes ?? 0),
|
|
2109
|
+
contradictions: Number(row?.contradictions ?? 0),
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
insertLearningCandidateVersion(candidateId, version, triggerJson, instruction, createdAt) {
|
|
2113
|
+
this.db.prepare(`
|
|
2114
|
+
INSERT OR IGNORE INTO devflow_learning_candidate_versions
|
|
2115
|
+
(candidate_id, version, trigger_json, instruction, created_at)
|
|
2116
|
+
VALUES (?, ?, ?, ?, ?)
|
|
2117
|
+
`).run(candidateId, version, triggerJson, instruction, createdAt);
|
|
2118
|
+
}
|
|
2119
|
+
insertLearningActivation(candidateId, fromState, toState, reason, graderReceipt, manualApproval, changedAt) {
|
|
2120
|
+
this.db.prepare(`
|
|
2121
|
+
INSERT INTO devflow_learning_activation_history
|
|
2122
|
+
(candidate_id, from_state, to_state, reason, grader_receipt, manual_approval, changed_at)
|
|
2123
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
2124
|
+
`).run(candidateId, fromState, toState, reason.trim().slice(0, 500), graderReceipt ?? null, manualApproval ? 1 : 0, changedAt);
|
|
2125
|
+
}
|
|
1818
2126
|
// ---- Durable Work Queue ----
|
|
1819
2127
|
enqueueWork(input) {
|
|
1820
2128
|
if (!input.idempotencyKey.trim())
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,6 @@ export { RETRIEVAL_MAX_CYCLES, isRetrievalSessionState } from './retrieval-sessi
|
|
|
6
6
|
export type { AppendRetrievalCycleInput, CreateRetrievalSessionInput, RetrievalCycleRecord, RetrievalGapKind, RetrievalGapRecord, RetrievalSessionRecord, RetrievalSessionState, } from './retrieval-sessions';
|
|
7
7
|
export type { FailHostActionInput, HostActionRecord, HostActionState, ReportHostActionInput, RequestHostActionInput, StartHostActionInput, VerifyHostActionInput, } from './host-actions';
|
|
8
8
|
export { mapSessionObligationRow, normalizeTurnId, } from './obligation-ledger';
|
|
9
|
+
export { mapLearningCandidateRow, mapLearningEvidenceRow, mapLearningVersionRow, stableLearningJson, } from './learning-candidates';
|
|
10
|
+
export type { AddLearningCandidateEvidenceInput, LearningCandidateEvidenceRecord, LearningCandidateKind, LearningCandidateRecord, LearningCandidateState, LearningCandidateVersionRecord, LearningEvidencePolarity, LearningOutcomeState, TransitionLearningCandidateInput, UpsertLearningCandidateInput, } from './learning-candidates';
|
|
9
11
|
export type { SessionObligationKind, SessionObligationRecord, SessionObligationState, } from './obligation-ledger';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.normalizeTurnId = exports.mapSessionObligationRow = exports.isRetrievalSessionState = exports.RETRIEVAL_MAX_CYCLES = exports.serializeHostActionReport = exports.mapHostActionRow = exports.isHostActionState = exports.hostActionReportsEqual = exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
|
|
3
|
+
exports.stableLearningJson = exports.mapLearningVersionRow = exports.mapLearningEvidenceRow = exports.mapLearningCandidateRow = exports.normalizeTurnId = exports.mapSessionObligationRow = exports.isRetrievalSessionState = exports.RETRIEVAL_MAX_CYCLES = exports.serializeHostActionReport = exports.mapHostActionRow = exports.isHostActionState = exports.hostActionReportsEqual = exports.openGlobalDevFlowDatabase = exports.getGlobalDevFlowDbPath = exports.DevFlowDatabase = void 0;
|
|
4
4
|
var database_1 = require("./database");
|
|
5
5
|
Object.defineProperty(exports, "DevFlowDatabase", { enumerable: true, get: function () { return database_1.DevFlowDatabase; } });
|
|
6
6
|
Object.defineProperty(exports, "getGlobalDevFlowDbPath", { enumerable: true, get: function () { return database_1.getGlobalDevFlowDbPath; } });
|
|
@@ -16,3 +16,8 @@ Object.defineProperty(exports, "isRetrievalSessionState", { enumerable: true, ge
|
|
|
16
16
|
var obligation_ledger_1 = require("./obligation-ledger");
|
|
17
17
|
Object.defineProperty(exports, "mapSessionObligationRow", { enumerable: true, get: function () { return obligation_ledger_1.mapSessionObligationRow; } });
|
|
18
18
|
Object.defineProperty(exports, "normalizeTurnId", { enumerable: true, get: function () { return obligation_ledger_1.normalizeTurnId; } });
|
|
19
|
+
var learning_candidates_1 = require("./learning-candidates");
|
|
20
|
+
Object.defineProperty(exports, "mapLearningCandidateRow", { enumerable: true, get: function () { return learning_candidates_1.mapLearningCandidateRow; } });
|
|
21
|
+
Object.defineProperty(exports, "mapLearningEvidenceRow", { enumerable: true, get: function () { return learning_candidates_1.mapLearningEvidenceRow; } });
|
|
22
|
+
Object.defineProperty(exports, "mapLearningVersionRow", { enumerable: true, get: function () { return learning_candidates_1.mapLearningVersionRow; } });
|
|
23
|
+
Object.defineProperty(exports, "stableLearningJson", { enumerable: true, get: function () { return learning_candidates_1.stableLearningJson; } });
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export type LearningCandidateState = 'observed' | 'candidate' | 'shadow' | 'evaluated' | 'active' | 'rejected' | 'retired';
|
|
2
|
+
export type LearningCandidateKind = 'convention' | 'workflow' | 'tool_preference' | 'correction';
|
|
3
|
+
export type LearningEvidencePolarity = 'supporting' | 'contradicting';
|
|
4
|
+
export type LearningOutcomeState = 'positive' | 'negative' | 'unknown';
|
|
5
|
+
export interface LearningCandidateRecord {
|
|
6
|
+
id: string;
|
|
7
|
+
projectRoot: string;
|
|
8
|
+
scope: 'project' | 'global';
|
|
9
|
+
state: LearningCandidateState;
|
|
10
|
+
kind: LearningCandidateKind;
|
|
11
|
+
trigger: Record<string, unknown>;
|
|
12
|
+
instruction: string;
|
|
13
|
+
confidence: number;
|
|
14
|
+
overlayVersion: number;
|
|
15
|
+
supportingSessions: number;
|
|
16
|
+
successfulOutcomes: number;
|
|
17
|
+
contradictions: number;
|
|
18
|
+
manualOnly: boolean;
|
|
19
|
+
risk: 'low' | 'medium' | 'high';
|
|
20
|
+
graderReceipt?: string;
|
|
21
|
+
expiresAt?: number;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
updatedAt: number;
|
|
24
|
+
}
|
|
25
|
+
export interface LearningCandidateVersionRecord {
|
|
26
|
+
candidateId: string;
|
|
27
|
+
version: number;
|
|
28
|
+
trigger: Record<string, unknown>;
|
|
29
|
+
instruction: string;
|
|
30
|
+
createdAt: number;
|
|
31
|
+
}
|
|
32
|
+
export interface LearningCandidateEvidenceRecord {
|
|
33
|
+
id: string;
|
|
34
|
+
candidateId: string;
|
|
35
|
+
projectRoot: string;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
executionId?: string;
|
|
38
|
+
memoryObservationId?: string;
|
|
39
|
+
outcomeId?: string;
|
|
40
|
+
sourceType: 'memory' | 'correction' | 'workflow_outcome' | 'tool_preference' | 'convention' | 'grader' | 'manual';
|
|
41
|
+
polarity: LearningEvidencePolarity;
|
|
42
|
+
outcome: LearningOutcomeState;
|
|
43
|
+
evidenceHash: string;
|
|
44
|
+
receipt?: string;
|
|
45
|
+
payload: Record<string, unknown>;
|
|
46
|
+
createdAt: number;
|
|
47
|
+
resolvedAt?: number;
|
|
48
|
+
}
|
|
49
|
+
export interface UpsertLearningCandidateInput {
|
|
50
|
+
id: string;
|
|
51
|
+
projectRoot: string;
|
|
52
|
+
scope: 'project' | 'global';
|
|
53
|
+
kind: LearningCandidateKind;
|
|
54
|
+
trigger: Record<string, unknown>;
|
|
55
|
+
instruction: string;
|
|
56
|
+
confidence: number;
|
|
57
|
+
manualOnly?: boolean;
|
|
58
|
+
risk?: 'low' | 'medium' | 'high';
|
|
59
|
+
expiresAt?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface AddLearningCandidateEvidenceInput {
|
|
62
|
+
id: string;
|
|
63
|
+
candidateId: string;
|
|
64
|
+
projectRoot: string;
|
|
65
|
+
sessionId: string;
|
|
66
|
+
executionId?: string;
|
|
67
|
+
memoryObservationId?: string;
|
|
68
|
+
outcomeId?: string;
|
|
69
|
+
sourceType: LearningCandidateEvidenceRecord['sourceType'];
|
|
70
|
+
polarity: LearningEvidencePolarity;
|
|
71
|
+
outcome?: LearningOutcomeState;
|
|
72
|
+
evidenceHash: string;
|
|
73
|
+
receipt?: string;
|
|
74
|
+
payload?: Record<string, unknown>;
|
|
75
|
+
createdAt?: number;
|
|
76
|
+
}
|
|
77
|
+
export interface TransitionLearningCandidateInput {
|
|
78
|
+
target: LearningCandidateState;
|
|
79
|
+
reason: string;
|
|
80
|
+
graderReceipt?: string;
|
|
81
|
+
manualApproval?: boolean;
|
|
82
|
+
changedAt?: number;
|
|
83
|
+
}
|
|
84
|
+
export declare function mapLearningCandidateRow(row: Record<string, unknown>): LearningCandidateRecord;
|
|
85
|
+
export declare function mapLearningEvidenceRow(row: Record<string, unknown>): LearningCandidateEvidenceRecord;
|
|
86
|
+
export declare function mapLearningVersionRow(row: Record<string, unknown>): LearningCandidateVersionRecord;
|
|
87
|
+
export declare function stableLearningJson(value: unknown): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapLearningCandidateRow = mapLearningCandidateRow;
|
|
4
|
+
exports.mapLearningEvidenceRow = mapLearningEvidenceRow;
|
|
5
|
+
exports.mapLearningVersionRow = mapLearningVersionRow;
|
|
6
|
+
exports.stableLearningJson = stableLearningJson;
|
|
7
|
+
function mapLearningCandidateRow(row) {
|
|
8
|
+
return {
|
|
9
|
+
id: String(row.id),
|
|
10
|
+
projectRoot: String(row.project_root),
|
|
11
|
+
scope: row.scope === 'global' ? 'global' : 'project',
|
|
12
|
+
state: row.state,
|
|
13
|
+
kind: row.kind,
|
|
14
|
+
trigger: parseRecord(row.trigger_json),
|
|
15
|
+
instruction: String(row.instruction),
|
|
16
|
+
confidence: Number(row.confidence),
|
|
17
|
+
overlayVersion: Number(row.overlay_version),
|
|
18
|
+
supportingSessions: Number(row.supporting_sessions),
|
|
19
|
+
successfulOutcomes: Number(row.successful_outcomes),
|
|
20
|
+
contradictions: Number(row.contradictions),
|
|
21
|
+
manualOnly: Number(row.manual_only) === 1,
|
|
22
|
+
risk: row.risk === 'high' ? 'high' : row.risk === 'medium' ? 'medium' : 'low',
|
|
23
|
+
graderReceipt: optionalString(row.grader_receipt),
|
|
24
|
+
expiresAt: optionalNumber(row.expires_at),
|
|
25
|
+
createdAt: Number(row.created_at),
|
|
26
|
+
updatedAt: Number(row.updated_at),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function mapLearningEvidenceRow(row) {
|
|
30
|
+
return {
|
|
31
|
+
id: String(row.id),
|
|
32
|
+
candidateId: String(row.candidate_id),
|
|
33
|
+
projectRoot: String(row.project_root),
|
|
34
|
+
sessionId: String(row.session_id),
|
|
35
|
+
executionId: optionalString(row.execution_id),
|
|
36
|
+
memoryObservationId: optionalString(row.memory_observation_id),
|
|
37
|
+
outcomeId: optionalString(row.outcome_id),
|
|
38
|
+
sourceType: row.source_type,
|
|
39
|
+
polarity: row.polarity,
|
|
40
|
+
outcome: row.outcome,
|
|
41
|
+
evidenceHash: String(row.evidence_hash),
|
|
42
|
+
receipt: optionalString(row.receipt),
|
|
43
|
+
payload: parseRecord(row.payload_json),
|
|
44
|
+
createdAt: Number(row.created_at),
|
|
45
|
+
resolvedAt: optionalNumber(row.resolved_at),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function mapLearningVersionRow(row) {
|
|
49
|
+
return {
|
|
50
|
+
candidateId: String(row.candidate_id),
|
|
51
|
+
version: Number(row.version),
|
|
52
|
+
trigger: parseRecord(row.trigger_json),
|
|
53
|
+
instruction: String(row.instruction),
|
|
54
|
+
createdAt: Number(row.created_at),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function stableLearningJson(value) {
|
|
58
|
+
if (Array.isArray(value))
|
|
59
|
+
return `[${value.map(stableLearningJson).join(',')}]`;
|
|
60
|
+
if (!value || typeof value !== 'object')
|
|
61
|
+
return JSON.stringify(value) ?? 'null';
|
|
62
|
+
const record = value;
|
|
63
|
+
return `{${Object.keys(record).sort().map(key => `${JSON.stringify(key)}:${stableLearningJson(record[key])}`).join(',')}}`;
|
|
64
|
+
}
|
|
65
|
+
function parseRecord(value) {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
68
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
69
|
+
? parsed
|
|
70
|
+
: {};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function optionalString(value) {
|
|
77
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
function optionalNumber(value) {
|
|
80
|
+
return value === null || value === undefined ? undefined : Number(value);
|
|
81
|
+
}
|