mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-31 06:03:29 +08:00
* feat: add SQLite state store and ECC status CLI * fix: replace better-sqlite3 with sql.js to eliminate native module CI failures better-sqlite3 requires native C++ compilation (node-gyp, prebuild-install) which fails in CI across npm/pnpm on all platforms: - npm ci: lock file out of sync with native transitive deps - pnpm: native bindings not found at runtime - Windows: native compilation fails entirely sql.js is a pure JavaScript/WASM SQLite implementation with zero native dependencies. The adapter in index.js wraps the sql.js API to match the better-sqlite3 interface used by migrations.js and queries.js. Key implementation detail: sql.js db.export() implicitly ends active transactions, so the adapter defers disk writes (saveToDisk) until after transaction commit via an inTransaction guard flag. createStateStore is now async (sql.js requires async WASM init). Updated status.js, sessions-cli.js, and tests accordingly.
698 lines
19 KiB
JavaScript
698 lines
19 KiB
JavaScript
'use strict';
|
|
|
|
const { assertValidEntity } = require('./schema');
|
|
|
|
const ACTIVE_SESSION_STATES = ['active', 'running', 'idle'];
|
|
const SUCCESS_OUTCOMES = new Set(['success', 'succeeded', 'passed']);
|
|
const FAILURE_OUTCOMES = new Set(['failure', 'failed', 'error']);
|
|
|
|
function normalizeLimit(value, fallback) {
|
|
if (value === undefined || value === null) {
|
|
return fallback;
|
|
}
|
|
|
|
const parsed = Number.parseInt(value, 10);
|
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
throw new Error(`Invalid limit: ${value}`);
|
|
}
|
|
|
|
return parsed;
|
|
}
|
|
|
|
function parseJsonColumn(value, fallback) {
|
|
if (value === null || value === undefined || value === '') {
|
|
return fallback;
|
|
}
|
|
|
|
return JSON.parse(value);
|
|
}
|
|
|
|
function stringifyJson(value, label) {
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch (error) {
|
|
throw new Error(`Failed to serialize ${label}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
function mapSessionRow(row) {
|
|
const snapshot = parseJsonColumn(row.snapshot, {});
|
|
return {
|
|
id: row.id,
|
|
adapterId: row.adapter_id,
|
|
harness: row.harness,
|
|
state: row.state,
|
|
repoRoot: row.repo_root,
|
|
startedAt: row.started_at,
|
|
endedAt: row.ended_at,
|
|
snapshot,
|
|
workerCount: Array.isArray(snapshot && snapshot.workers) ? snapshot.workers.length : 0,
|
|
};
|
|
}
|
|
|
|
function mapSkillRunRow(row) {
|
|
return {
|
|
id: row.id,
|
|
skillId: row.skill_id,
|
|
skillVersion: row.skill_version,
|
|
sessionId: row.session_id,
|
|
taskDescription: row.task_description,
|
|
outcome: row.outcome,
|
|
failureReason: row.failure_reason,
|
|
tokensUsed: row.tokens_used,
|
|
durationMs: row.duration_ms,
|
|
userFeedback: row.user_feedback,
|
|
createdAt: row.created_at,
|
|
};
|
|
}
|
|
|
|
function mapSkillVersionRow(row) {
|
|
return {
|
|
skillId: row.skill_id,
|
|
version: row.version,
|
|
contentHash: row.content_hash,
|
|
amendmentReason: row.amendment_reason,
|
|
promotedAt: row.promoted_at,
|
|
rolledBackAt: row.rolled_back_at,
|
|
};
|
|
}
|
|
|
|
function mapDecisionRow(row) {
|
|
return {
|
|
id: row.id,
|
|
sessionId: row.session_id,
|
|
title: row.title,
|
|
rationale: row.rationale,
|
|
alternatives: parseJsonColumn(row.alternatives, []),
|
|
supersedes: row.supersedes,
|
|
status: row.status,
|
|
createdAt: row.created_at,
|
|
};
|
|
}
|
|
|
|
function mapInstallStateRow(row) {
|
|
const modules = parseJsonColumn(row.modules, []);
|
|
const operations = parseJsonColumn(row.operations, []);
|
|
const status = row.source_version && row.installed_at ? 'healthy' : 'warning';
|
|
|
|
return {
|
|
targetId: row.target_id,
|
|
targetRoot: row.target_root,
|
|
profile: row.profile,
|
|
modules,
|
|
operations,
|
|
installedAt: row.installed_at,
|
|
sourceVersion: row.source_version,
|
|
moduleCount: Array.isArray(modules) ? modules.length : 0,
|
|
operationCount: Array.isArray(operations) ? operations.length : 0,
|
|
status,
|
|
};
|
|
}
|
|
|
|
function mapGovernanceEventRow(row) {
|
|
return {
|
|
id: row.id,
|
|
sessionId: row.session_id,
|
|
eventType: row.event_type,
|
|
payload: parseJsonColumn(row.payload, null),
|
|
resolvedAt: row.resolved_at,
|
|
resolution: row.resolution,
|
|
createdAt: row.created_at,
|
|
};
|
|
}
|
|
|
|
function classifyOutcome(outcome) {
|
|
const normalized = String(outcome || '').toLowerCase();
|
|
if (SUCCESS_OUTCOMES.has(normalized)) {
|
|
return 'success';
|
|
}
|
|
|
|
if (FAILURE_OUTCOMES.has(normalized)) {
|
|
return 'failure';
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
function toPercent(numerator, denominator) {
|
|
if (denominator === 0) {
|
|
return null;
|
|
}
|
|
|
|
return Number(((numerator / denominator) * 100).toFixed(1));
|
|
}
|
|
|
|
function summarizeSkillRuns(skillRuns) {
|
|
const summary = {
|
|
totalCount: skillRuns.length,
|
|
knownCount: 0,
|
|
successCount: 0,
|
|
failureCount: 0,
|
|
unknownCount: 0,
|
|
successRate: null,
|
|
failureRate: null,
|
|
};
|
|
|
|
for (const skillRun of skillRuns) {
|
|
const classification = classifyOutcome(skillRun.outcome);
|
|
if (classification === 'success') {
|
|
summary.successCount += 1;
|
|
summary.knownCount += 1;
|
|
} else if (classification === 'failure') {
|
|
summary.failureCount += 1;
|
|
summary.knownCount += 1;
|
|
} else {
|
|
summary.unknownCount += 1;
|
|
}
|
|
}
|
|
|
|
summary.successRate = toPercent(summary.successCount, summary.knownCount);
|
|
summary.failureRate = toPercent(summary.failureCount, summary.knownCount);
|
|
return summary;
|
|
}
|
|
|
|
function summarizeInstallHealth(installations) {
|
|
if (installations.length === 0) {
|
|
return {
|
|
status: 'missing',
|
|
totalCount: 0,
|
|
healthyCount: 0,
|
|
warningCount: 0,
|
|
installations: [],
|
|
};
|
|
}
|
|
|
|
const summary = installations.reduce((result, installation) => {
|
|
if (installation.status === 'healthy') {
|
|
result.healthyCount += 1;
|
|
} else {
|
|
result.warningCount += 1;
|
|
}
|
|
return result;
|
|
}, {
|
|
totalCount: installations.length,
|
|
healthyCount: 0,
|
|
warningCount: 0,
|
|
});
|
|
|
|
return {
|
|
status: summary.warningCount > 0 ? 'warning' : 'healthy',
|
|
...summary,
|
|
installations,
|
|
};
|
|
}
|
|
|
|
function normalizeSessionInput(session) {
|
|
return {
|
|
id: session.id,
|
|
adapterId: session.adapterId,
|
|
harness: session.harness,
|
|
state: session.state,
|
|
repoRoot: session.repoRoot ?? null,
|
|
startedAt: session.startedAt ?? null,
|
|
endedAt: session.endedAt ?? null,
|
|
snapshot: session.snapshot ?? {},
|
|
};
|
|
}
|
|
|
|
function normalizeSkillRunInput(skillRun) {
|
|
return {
|
|
id: skillRun.id,
|
|
skillId: skillRun.skillId,
|
|
skillVersion: skillRun.skillVersion,
|
|
sessionId: skillRun.sessionId,
|
|
taskDescription: skillRun.taskDescription,
|
|
outcome: skillRun.outcome,
|
|
failureReason: skillRun.failureReason ?? null,
|
|
tokensUsed: skillRun.tokensUsed ?? null,
|
|
durationMs: skillRun.durationMs ?? null,
|
|
userFeedback: skillRun.userFeedback ?? null,
|
|
createdAt: skillRun.createdAt || new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
function normalizeSkillVersionInput(skillVersion) {
|
|
return {
|
|
skillId: skillVersion.skillId,
|
|
version: skillVersion.version,
|
|
contentHash: skillVersion.contentHash,
|
|
amendmentReason: skillVersion.amendmentReason ?? null,
|
|
promotedAt: skillVersion.promotedAt ?? null,
|
|
rolledBackAt: skillVersion.rolledBackAt ?? null,
|
|
};
|
|
}
|
|
|
|
function normalizeDecisionInput(decision) {
|
|
return {
|
|
id: decision.id,
|
|
sessionId: decision.sessionId,
|
|
title: decision.title,
|
|
rationale: decision.rationale,
|
|
alternatives: decision.alternatives === undefined || decision.alternatives === null
|
|
? []
|
|
: decision.alternatives,
|
|
supersedes: decision.supersedes ?? null,
|
|
status: decision.status,
|
|
createdAt: decision.createdAt || new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
function normalizeInstallStateInput(installState) {
|
|
return {
|
|
targetId: installState.targetId,
|
|
targetRoot: installState.targetRoot,
|
|
profile: installState.profile ?? null,
|
|
modules: installState.modules === undefined || installState.modules === null
|
|
? []
|
|
: installState.modules,
|
|
operations: installState.operations === undefined || installState.operations === null
|
|
? []
|
|
: installState.operations,
|
|
installedAt: installState.installedAt || new Date().toISOString(),
|
|
sourceVersion: installState.sourceVersion ?? null,
|
|
};
|
|
}
|
|
|
|
function normalizeGovernanceEventInput(governanceEvent) {
|
|
return {
|
|
id: governanceEvent.id,
|
|
sessionId: governanceEvent.sessionId ?? null,
|
|
eventType: governanceEvent.eventType,
|
|
payload: governanceEvent.payload ?? null,
|
|
resolvedAt: governanceEvent.resolvedAt ?? null,
|
|
resolution: governanceEvent.resolution ?? null,
|
|
createdAt: governanceEvent.createdAt || new Date().toISOString(),
|
|
};
|
|
}
|
|
|
|
function createQueryApi(db) {
|
|
const listRecentSessionsStatement = db.prepare(`
|
|
SELECT *
|
|
FROM sessions
|
|
ORDER BY COALESCE(started_at, ended_at, '') DESC, id DESC
|
|
LIMIT ?
|
|
`);
|
|
const countSessionsStatement = db.prepare(`
|
|
SELECT COUNT(*) AS total_count
|
|
FROM sessions
|
|
`);
|
|
const getSessionStatement = db.prepare(`
|
|
SELECT *
|
|
FROM sessions
|
|
WHERE id = ?
|
|
`);
|
|
const getSessionSkillRunsStatement = db.prepare(`
|
|
SELECT *
|
|
FROM skill_runs
|
|
WHERE session_id = ?
|
|
ORDER BY created_at DESC, id DESC
|
|
`);
|
|
const getSessionDecisionsStatement = db.prepare(`
|
|
SELECT *
|
|
FROM decisions
|
|
WHERE session_id = ?
|
|
ORDER BY created_at DESC, id DESC
|
|
`);
|
|
const listActiveSessionsStatement = db.prepare(`
|
|
SELECT *
|
|
FROM sessions
|
|
WHERE ended_at IS NULL
|
|
AND state IN ('active', 'running', 'idle')
|
|
ORDER BY COALESCE(started_at, ended_at, '') DESC, id DESC
|
|
LIMIT ?
|
|
`);
|
|
const countActiveSessionsStatement = db.prepare(`
|
|
SELECT COUNT(*) AS total_count
|
|
FROM sessions
|
|
WHERE ended_at IS NULL
|
|
AND state IN ('active', 'running', 'idle')
|
|
`);
|
|
const listRecentSkillRunsStatement = db.prepare(`
|
|
SELECT *
|
|
FROM skill_runs
|
|
ORDER BY created_at DESC, id DESC
|
|
LIMIT ?
|
|
`);
|
|
const listInstallStateStatement = db.prepare(`
|
|
SELECT *
|
|
FROM install_state
|
|
ORDER BY installed_at DESC, target_id ASC
|
|
`);
|
|
const countPendingGovernanceStatement = db.prepare(`
|
|
SELECT COUNT(*) AS total_count
|
|
FROM governance_events
|
|
WHERE resolved_at IS NULL
|
|
`);
|
|
const listPendingGovernanceStatement = db.prepare(`
|
|
SELECT *
|
|
FROM governance_events
|
|
WHERE resolved_at IS NULL
|
|
ORDER BY created_at DESC, id DESC
|
|
LIMIT ?
|
|
`);
|
|
const getSkillVersionStatement = db.prepare(`
|
|
SELECT *
|
|
FROM skill_versions
|
|
WHERE skill_id = ? AND version = ?
|
|
`);
|
|
|
|
const upsertSessionStatement = db.prepare(`
|
|
INSERT INTO sessions (
|
|
id,
|
|
adapter_id,
|
|
harness,
|
|
state,
|
|
repo_root,
|
|
started_at,
|
|
ended_at,
|
|
snapshot
|
|
) VALUES (
|
|
@id,
|
|
@adapter_id,
|
|
@harness,
|
|
@state,
|
|
@repo_root,
|
|
@started_at,
|
|
@ended_at,
|
|
@snapshot
|
|
)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
adapter_id = excluded.adapter_id,
|
|
harness = excluded.harness,
|
|
state = excluded.state,
|
|
repo_root = excluded.repo_root,
|
|
started_at = excluded.started_at,
|
|
ended_at = excluded.ended_at,
|
|
snapshot = excluded.snapshot
|
|
`);
|
|
|
|
const insertSkillRunStatement = db.prepare(`
|
|
INSERT INTO skill_runs (
|
|
id,
|
|
skill_id,
|
|
skill_version,
|
|
session_id,
|
|
task_description,
|
|
outcome,
|
|
failure_reason,
|
|
tokens_used,
|
|
duration_ms,
|
|
user_feedback,
|
|
created_at
|
|
) VALUES (
|
|
@id,
|
|
@skill_id,
|
|
@skill_version,
|
|
@session_id,
|
|
@task_description,
|
|
@outcome,
|
|
@failure_reason,
|
|
@tokens_used,
|
|
@duration_ms,
|
|
@user_feedback,
|
|
@created_at
|
|
)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
skill_id = excluded.skill_id,
|
|
skill_version = excluded.skill_version,
|
|
session_id = excluded.session_id,
|
|
task_description = excluded.task_description,
|
|
outcome = excluded.outcome,
|
|
failure_reason = excluded.failure_reason,
|
|
tokens_used = excluded.tokens_used,
|
|
duration_ms = excluded.duration_ms,
|
|
user_feedback = excluded.user_feedback,
|
|
created_at = excluded.created_at
|
|
`);
|
|
|
|
const upsertSkillVersionStatement = db.prepare(`
|
|
INSERT INTO skill_versions (
|
|
skill_id,
|
|
version,
|
|
content_hash,
|
|
amendment_reason,
|
|
promoted_at,
|
|
rolled_back_at
|
|
) VALUES (
|
|
@skill_id,
|
|
@version,
|
|
@content_hash,
|
|
@amendment_reason,
|
|
@promoted_at,
|
|
@rolled_back_at
|
|
)
|
|
ON CONFLICT(skill_id, version) DO UPDATE SET
|
|
content_hash = excluded.content_hash,
|
|
amendment_reason = excluded.amendment_reason,
|
|
promoted_at = excluded.promoted_at,
|
|
rolled_back_at = excluded.rolled_back_at
|
|
`);
|
|
|
|
const insertDecisionStatement = db.prepare(`
|
|
INSERT INTO decisions (
|
|
id,
|
|
session_id,
|
|
title,
|
|
rationale,
|
|
alternatives,
|
|
supersedes,
|
|
status,
|
|
created_at
|
|
) VALUES (
|
|
@id,
|
|
@session_id,
|
|
@title,
|
|
@rationale,
|
|
@alternatives,
|
|
@supersedes,
|
|
@status,
|
|
@created_at
|
|
)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
session_id = excluded.session_id,
|
|
title = excluded.title,
|
|
rationale = excluded.rationale,
|
|
alternatives = excluded.alternatives,
|
|
supersedes = excluded.supersedes,
|
|
status = excluded.status,
|
|
created_at = excluded.created_at
|
|
`);
|
|
|
|
const upsertInstallStateStatement = db.prepare(`
|
|
INSERT INTO install_state (
|
|
target_id,
|
|
target_root,
|
|
profile,
|
|
modules,
|
|
operations,
|
|
installed_at,
|
|
source_version
|
|
) VALUES (
|
|
@target_id,
|
|
@target_root,
|
|
@profile,
|
|
@modules,
|
|
@operations,
|
|
@installed_at,
|
|
@source_version
|
|
)
|
|
ON CONFLICT(target_id, target_root) DO UPDATE SET
|
|
profile = excluded.profile,
|
|
modules = excluded.modules,
|
|
operations = excluded.operations,
|
|
installed_at = excluded.installed_at,
|
|
source_version = excluded.source_version
|
|
`);
|
|
|
|
const insertGovernanceEventStatement = db.prepare(`
|
|
INSERT INTO governance_events (
|
|
id,
|
|
session_id,
|
|
event_type,
|
|
payload,
|
|
resolved_at,
|
|
resolution,
|
|
created_at
|
|
) VALUES (
|
|
@id,
|
|
@session_id,
|
|
@event_type,
|
|
@payload,
|
|
@resolved_at,
|
|
@resolution,
|
|
@created_at
|
|
)
|
|
ON CONFLICT(id) DO UPDATE SET
|
|
session_id = excluded.session_id,
|
|
event_type = excluded.event_type,
|
|
payload = excluded.payload,
|
|
resolved_at = excluded.resolved_at,
|
|
resolution = excluded.resolution,
|
|
created_at = excluded.created_at
|
|
`);
|
|
|
|
function getSessionById(id) {
|
|
const row = getSessionStatement.get(id);
|
|
return row ? mapSessionRow(row) : null;
|
|
}
|
|
|
|
function listRecentSessions(options = {}) {
|
|
const limit = normalizeLimit(options.limit, 10);
|
|
return {
|
|
totalCount: countSessionsStatement.get().total_count,
|
|
sessions: listRecentSessionsStatement.all(limit).map(mapSessionRow),
|
|
};
|
|
}
|
|
|
|
function getSessionDetail(id) {
|
|
const session = getSessionById(id);
|
|
if (!session) {
|
|
return null;
|
|
}
|
|
|
|
const workers = Array.isArray(session.snapshot && session.snapshot.workers)
|
|
? session.snapshot.workers.map(worker => ({ ...worker }))
|
|
: [];
|
|
|
|
return {
|
|
session,
|
|
workers,
|
|
skillRuns: getSessionSkillRunsStatement.all(id).map(mapSkillRunRow),
|
|
decisions: getSessionDecisionsStatement.all(id).map(mapDecisionRow),
|
|
};
|
|
}
|
|
|
|
function getStatus(options = {}) {
|
|
const activeLimit = normalizeLimit(options.activeLimit, 5);
|
|
const recentSkillRunLimit = normalizeLimit(options.recentSkillRunLimit, 20);
|
|
const pendingLimit = normalizeLimit(options.pendingLimit, 5);
|
|
|
|
const activeSessions = listActiveSessionsStatement.all(activeLimit).map(mapSessionRow);
|
|
const recentSkillRuns = listRecentSkillRunsStatement.all(recentSkillRunLimit).map(mapSkillRunRow);
|
|
const installations = listInstallStateStatement.all().map(mapInstallStateRow);
|
|
const pendingGovernanceEvents = listPendingGovernanceStatement.all(pendingLimit).map(mapGovernanceEventRow);
|
|
|
|
return {
|
|
generatedAt: new Date().toISOString(),
|
|
activeSessions: {
|
|
activeCount: countActiveSessionsStatement.get().total_count,
|
|
sessions: activeSessions,
|
|
},
|
|
skillRuns: {
|
|
windowSize: recentSkillRunLimit,
|
|
summary: summarizeSkillRuns(recentSkillRuns),
|
|
recent: recentSkillRuns,
|
|
},
|
|
installHealth: summarizeInstallHealth(installations),
|
|
governance: {
|
|
pendingCount: countPendingGovernanceStatement.get().total_count,
|
|
events: pendingGovernanceEvents,
|
|
},
|
|
};
|
|
}
|
|
|
|
return {
|
|
getSessionById,
|
|
getSessionDetail,
|
|
getStatus,
|
|
insertDecision(decision) {
|
|
const normalized = normalizeDecisionInput(decision);
|
|
assertValidEntity('decision', normalized);
|
|
insertDecisionStatement.run({
|
|
id: normalized.id,
|
|
session_id: normalized.sessionId,
|
|
title: normalized.title,
|
|
rationale: normalized.rationale,
|
|
alternatives: stringifyJson(normalized.alternatives, 'decision.alternatives'),
|
|
supersedes: normalized.supersedes,
|
|
status: normalized.status,
|
|
created_at: normalized.createdAt,
|
|
});
|
|
return normalized;
|
|
},
|
|
insertGovernanceEvent(governanceEvent) {
|
|
const normalized = normalizeGovernanceEventInput(governanceEvent);
|
|
assertValidEntity('governanceEvent', normalized);
|
|
insertGovernanceEventStatement.run({
|
|
id: normalized.id,
|
|
session_id: normalized.sessionId,
|
|
event_type: normalized.eventType,
|
|
payload: stringifyJson(normalized.payload, 'governanceEvent.payload'),
|
|
resolved_at: normalized.resolvedAt,
|
|
resolution: normalized.resolution,
|
|
created_at: normalized.createdAt,
|
|
});
|
|
return normalized;
|
|
},
|
|
insertSkillRun(skillRun) {
|
|
const normalized = normalizeSkillRunInput(skillRun);
|
|
assertValidEntity('skillRun', normalized);
|
|
insertSkillRunStatement.run({
|
|
id: normalized.id,
|
|
skill_id: normalized.skillId,
|
|
skill_version: normalized.skillVersion,
|
|
session_id: normalized.sessionId,
|
|
task_description: normalized.taskDescription,
|
|
outcome: normalized.outcome,
|
|
failure_reason: normalized.failureReason,
|
|
tokens_used: normalized.tokensUsed,
|
|
duration_ms: normalized.durationMs,
|
|
user_feedback: normalized.userFeedback,
|
|
created_at: normalized.createdAt,
|
|
});
|
|
return normalized;
|
|
},
|
|
listRecentSessions,
|
|
upsertInstallState(installState) {
|
|
const normalized = normalizeInstallStateInput(installState);
|
|
assertValidEntity('installState', normalized);
|
|
upsertInstallStateStatement.run({
|
|
target_id: normalized.targetId,
|
|
target_root: normalized.targetRoot,
|
|
profile: normalized.profile,
|
|
modules: stringifyJson(normalized.modules, 'installState.modules'),
|
|
operations: stringifyJson(normalized.operations, 'installState.operations'),
|
|
installed_at: normalized.installedAt,
|
|
source_version: normalized.sourceVersion,
|
|
});
|
|
return normalized;
|
|
},
|
|
upsertSession(session) {
|
|
const normalized = normalizeSessionInput(session);
|
|
assertValidEntity('session', normalized);
|
|
upsertSessionStatement.run({
|
|
id: normalized.id,
|
|
adapter_id: normalized.adapterId,
|
|
harness: normalized.harness,
|
|
state: normalized.state,
|
|
repo_root: normalized.repoRoot,
|
|
started_at: normalized.startedAt,
|
|
ended_at: normalized.endedAt,
|
|
snapshot: stringifyJson(normalized.snapshot, 'session.snapshot'),
|
|
});
|
|
return getSessionById(normalized.id);
|
|
},
|
|
upsertSkillVersion(skillVersion) {
|
|
const normalized = normalizeSkillVersionInput(skillVersion);
|
|
assertValidEntity('skillVersion', normalized);
|
|
upsertSkillVersionStatement.run({
|
|
skill_id: normalized.skillId,
|
|
version: normalized.version,
|
|
content_hash: normalized.contentHash,
|
|
amendment_reason: normalized.amendmentReason,
|
|
promoted_at: normalized.promotedAt,
|
|
rolled_back_at: normalized.rolledBackAt,
|
|
});
|
|
const row = getSkillVersionStatement.get(normalized.skillId, normalized.version);
|
|
return row ? mapSkillVersionRow(row) : null;
|
|
},
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
ACTIVE_SESSION_STATES,
|
|
FAILURE_OUTCOMES,
|
|
SUCCESS_OUTCOMES,
|
|
createQueryApi,
|
|
};
|