@cat-factory/node-server 0.19.0 → 0.20.1

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.
@@ -1,7 +1,7 @@
1
1
  import { LLM_WARNING_FINISH_REASONS } from '@cat-factory/kernel';
2
- import { blockInsertValues, blockPatchToColumns, rowToBlock, rowToExecution, executionToDetail, rowToPipeline, rowToWorkspace, } from '@cat-factory/server';
3
- import { and, desc, eq, gte, inArray, isNull, lt, or, sql } from 'drizzle-orm';
4
- import { accountInvitations, accountSettings, accounts, agentRuns, blocks, consensusSessions, incidentEnrichmentConnections, observabilityConnections, emailConnections, llmCallMetrics, memberships, mergeThresholdPresets, releaseHealthConfigs, pipelineScheduleRuns, pipelineSchedules, pipelines, requirementReviews, clarityReviews, services, tokenUsage, trackerSettings, modelPresets, userIdentities, users, workspaceFragmentDefaults, workspaceServices, workspaceSettings, workspaces, } from '../db/schema.js';
2
+ import { blockInsertValues, blockPatchToColumns, rowToBlock, rowToExecution, executionToDetail, rowToPipeline, rowToSandboxExperiment, rowToSandboxFixture, rowToSandboxGrade, rowToSandboxPromptVersion, rowToSandboxRun, rowToWorkspace, } from '@cat-factory/server';
3
+ import { and, desc, eq, gte, inArray, isNull, lt, ne, or, sql } from 'drizzle-orm';
4
+ import { accountInvitations, accountSettings, accounts, agentRuns, blocks, consensusSessions, incidentEnrichmentConnections, observabilityConnections, emailConnections, llmCallMetrics, memberships, mergeThresholdPresets, releaseHealthConfigs, pipelineScheduleRuns, pipelineSchedules, pipelines, requirementReviews, clarityReviews, sandboxPromptVersions, sandboxFixtures, sandboxExperiments, sandboxRuns, sandboxGrades, services, tokenUsage, trackerSettings, modelPresets, userIdentities, users, workspaceFragmentDefaults, workspaceServices, workspaceSettings, workspaces, } from '../db/schema.js';
5
5
  // Drizzle/Postgres implementations of the core kernel repository ports. The
6
6
  // row<->domain mapping is the SAME shared mapping the Cloudflare D1 repos use
7
7
  // (@cat-factory/server), so behaviour matches across stores; this layer only owns
@@ -1839,6 +1839,371 @@ export class DrizzleMergePresetRepository {
1839
1839
  .where(and(eq(mergeThresholdPresets.workspace_id, workspaceId), eq(mergeThresholdPresets.id, id), eq(mergeThresholdPresets.is_default, 0)));
1840
1840
  }
1841
1841
  }
1842
+ // ---- Sandbox (parallel prompt/model testing surface; migration 0012) --------
1843
+ // The Drizzle mirror of the Worker's five `D1Sandbox*Repository` classes. JSON-shaped
1844
+ // fields are stored as text JSON, parsed defensively; behaviourally identical to the D1
1845
+ // repos so the cross-runtime conformance suite asserts the same Sandbox behaviour.
1846
+ export class DrizzleSandboxPromptVersionRepository {
1847
+ db;
1848
+ constructor(db) {
1849
+ this.db = db;
1850
+ }
1851
+ async get(workspaceId, id) {
1852
+ const rows = await this.db
1853
+ .select()
1854
+ .from(sandboxPromptVersions)
1855
+ .where(and(eq(sandboxPromptVersions.workspace_id, workspaceId), eq(sandboxPromptVersions.id, id)))
1856
+ .limit(1);
1857
+ return rows[0] ? rowToSandboxPromptVersion(rows[0]) : null;
1858
+ }
1859
+ async list(workspaceId) {
1860
+ const rows = await this.db
1861
+ .select()
1862
+ .from(sandboxPromptVersions)
1863
+ .where(and(eq(sandboxPromptVersions.workspace_id, workspaceId), isNull(sandboxPromptVersions.archived_at)))
1864
+ .orderBy(desc(sandboxPromptVersions.created_at));
1865
+ return rows.map((r) => rowToSandboxPromptVersion(r));
1866
+ }
1867
+ async listByKind(workspaceId, agentKind) {
1868
+ const rows = await this.db
1869
+ .select()
1870
+ .from(sandboxPromptVersions)
1871
+ .where(and(eq(sandboxPromptVersions.workspace_id, workspaceId), eq(sandboxPromptVersions.agent_kind, agentKind), isNull(sandboxPromptVersions.archived_at)))
1872
+ .orderBy(desc(sandboxPromptVersions.created_at));
1873
+ return rows.map((r) => rowToSandboxPromptVersion(r));
1874
+ }
1875
+ async upsert(workspaceId, version) {
1876
+ const values = {
1877
+ workspace_id: workspaceId,
1878
+ id: version.id,
1879
+ lineage_id: version.lineageId,
1880
+ agent_kind: version.agentKind,
1881
+ name: version.name,
1882
+ origin: version.origin,
1883
+ system_text: version.systemText,
1884
+ base_prompt_id: version.basePromptId,
1885
+ version: version.version,
1886
+ parent_id: version.parentId,
1887
+ labels: JSON.stringify(version.labels),
1888
+ created_at: version.createdAt,
1889
+ created_by: version.createdBy,
1890
+ archived_at: version.archivedAt,
1891
+ };
1892
+ await this.db
1893
+ .insert(sandboxPromptVersions)
1894
+ .values(values)
1895
+ .onConflictDoUpdate({
1896
+ target: [sandboxPromptVersions.workspace_id, sandboxPromptVersions.id],
1897
+ set: {
1898
+ lineage_id: values.lineage_id,
1899
+ agent_kind: values.agent_kind,
1900
+ name: values.name,
1901
+ origin: values.origin,
1902
+ system_text: values.system_text,
1903
+ base_prompt_id: values.base_prompt_id,
1904
+ version: values.version,
1905
+ parent_id: values.parent_id,
1906
+ labels: values.labels,
1907
+ created_by: values.created_by,
1908
+ archived_at: values.archived_at,
1909
+ },
1910
+ });
1911
+ }
1912
+ async archive(workspaceId, id, at) {
1913
+ await this.db
1914
+ .update(sandboxPromptVersions)
1915
+ .set({ archived_at: at })
1916
+ .where(and(eq(sandboxPromptVersions.workspace_id, workspaceId), eq(sandboxPromptVersions.id, id)));
1917
+ }
1918
+ }
1919
+ export class DrizzleSandboxFixtureRepository {
1920
+ db;
1921
+ constructor(db) {
1922
+ this.db = db;
1923
+ }
1924
+ async get(workspaceId, id) {
1925
+ const rows = await this.db
1926
+ .select()
1927
+ .from(sandboxFixtures)
1928
+ .where(and(eq(sandboxFixtures.workspace_id, workspaceId), eq(sandboxFixtures.id, id)))
1929
+ .limit(1);
1930
+ return rows[0] ? rowToSandboxFixture(rows[0]) : null;
1931
+ }
1932
+ async list(workspaceId) {
1933
+ const rows = await this.db
1934
+ .select()
1935
+ .from(sandboxFixtures)
1936
+ .where(eq(sandboxFixtures.workspace_id, workspaceId))
1937
+ .orderBy(sandboxFixtures.created_at);
1938
+ return rows.map((r) => rowToSandboxFixture(r));
1939
+ }
1940
+ async upsert(workspaceId, fixture) {
1941
+ const values = {
1942
+ workspace_id: workspaceId,
1943
+ id: fixture.id,
1944
+ kind: fixture.kind,
1945
+ name: fixture.name,
1946
+ payload: fixture.payload ? JSON.stringify(fixture.payload) : null,
1947
+ repo_ref: fixture.repoRef ? JSON.stringify(fixture.repoRef) : null,
1948
+ objective: fixture.objective ? JSON.stringify(fixture.objective) : null,
1949
+ origin: fixture.origin,
1950
+ created_at: fixture.createdAt,
1951
+ };
1952
+ await this.db
1953
+ .insert(sandboxFixtures)
1954
+ .values(values)
1955
+ .onConflictDoUpdate({
1956
+ target: [sandboxFixtures.workspace_id, sandboxFixtures.id],
1957
+ set: {
1958
+ kind: values.kind,
1959
+ name: values.name,
1960
+ payload: values.payload,
1961
+ repo_ref: values.repo_ref,
1962
+ objective: values.objective,
1963
+ origin: values.origin,
1964
+ },
1965
+ });
1966
+ }
1967
+ async remove(workspaceId, id) {
1968
+ await this.db
1969
+ .delete(sandboxFixtures)
1970
+ .where(and(eq(sandboxFixtures.workspace_id, workspaceId), eq(sandboxFixtures.id, id)));
1971
+ }
1972
+ }
1973
+ export class DrizzleSandboxExperimentRepository {
1974
+ db;
1975
+ constructor(db) {
1976
+ this.db = db;
1977
+ }
1978
+ async get(workspaceId, id) {
1979
+ const rows = await this.db
1980
+ .select()
1981
+ .from(sandboxExperiments)
1982
+ .where(and(eq(sandboxExperiments.workspace_id, workspaceId), eq(sandboxExperiments.id, id)))
1983
+ .limit(1);
1984
+ return rows[0] ? rowToSandboxExperiment(rows[0]) : null;
1985
+ }
1986
+ async list(workspaceId) {
1987
+ const rows = await this.db
1988
+ .select()
1989
+ .from(sandboxExperiments)
1990
+ .where(eq(sandboxExperiments.workspace_id, workspaceId))
1991
+ .orderBy(desc(sandboxExperiments.created_at));
1992
+ return rows.map((r) => rowToSandboxExperiment(r));
1993
+ }
1994
+ async upsert(workspaceId, experiment) {
1995
+ const values = {
1996
+ workspace_id: workspaceId,
1997
+ id: experiment.id,
1998
+ name: experiment.name,
1999
+ agent_kind: experiment.agentKind,
2000
+ judge_model: experiment.judgeModel,
2001
+ repeats: experiment.repeats,
2002
+ status: experiment.status,
2003
+ matrix: JSON.stringify(experiment.matrix),
2004
+ budget_tokens: experiment.budgetTokens,
2005
+ created_at: experiment.createdAt,
2006
+ created_by: experiment.createdBy,
2007
+ };
2008
+ await this.db
2009
+ .insert(sandboxExperiments)
2010
+ .values(values)
2011
+ .onConflictDoUpdate({
2012
+ target: [sandboxExperiments.workspace_id, sandboxExperiments.id],
2013
+ set: {
2014
+ name: values.name,
2015
+ agent_kind: values.agent_kind,
2016
+ judge_model: values.judge_model,
2017
+ repeats: values.repeats,
2018
+ status: values.status,
2019
+ matrix: values.matrix,
2020
+ budget_tokens: values.budget_tokens,
2021
+ created_by: values.created_by,
2022
+ },
2023
+ });
2024
+ }
2025
+ async setStatus(workspaceId, id, status) {
2026
+ await this.db
2027
+ .update(sandboxExperiments)
2028
+ .set({ status })
2029
+ .where(and(eq(sandboxExperiments.workspace_id, workspaceId), eq(sandboxExperiments.id, id)));
2030
+ }
2031
+ async claimForRun(workspaceId, id) {
2032
+ // Conditional update: only flips a non-running experiment to `running`. `.returning()`
2033
+ // reports whether this caller won the claim (empty ⇒ already running). Atomic, so
2034
+ // concurrent launches can't both clear + re-expand the grid (see the port doc).
2035
+ const rows = await this.db
2036
+ .update(sandboxExperiments)
2037
+ .set({ status: 'running' })
2038
+ .where(and(eq(sandboxExperiments.workspace_id, workspaceId), eq(sandboxExperiments.id, id), ne(sandboxExperiments.status, 'running')))
2039
+ .returning({ id: sandboxExperiments.id });
2040
+ return rows.length > 0;
2041
+ }
2042
+ }
2043
+ export class DrizzleSandboxRunRepository {
2044
+ db;
2045
+ constructor(db) {
2046
+ this.db = db;
2047
+ }
2048
+ async get(workspaceId, id) {
2049
+ const rows = await this.db
2050
+ .select()
2051
+ .from(sandboxRuns)
2052
+ .where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.id, id)))
2053
+ .limit(1);
2054
+ return rows[0] ? rowToSandboxRun(rows[0]) : null;
2055
+ }
2056
+ async listByExperiment(workspaceId, experimentId) {
2057
+ const rows = await this.db
2058
+ .select()
2059
+ .from(sandboxRuns)
2060
+ .where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.experiment_id, experimentId)))
2061
+ .orderBy(sandboxRuns.prompt_version_id, sandboxRuns.model, sandboxRuns.fixture_id, sandboxRuns.repeat_index);
2062
+ return rows.map((r) => rowToSandboxRun(r));
2063
+ }
2064
+ async listQueued(workspaceId, experimentId) {
2065
+ const rows = await this.db
2066
+ .select()
2067
+ .from(sandboxRuns)
2068
+ .where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.experiment_id, experimentId), eq(sandboxRuns.status, 'queued')))
2069
+ .orderBy(sandboxRuns.started_at, sandboxRuns.id);
2070
+ return rows.map((r) => rowToSandboxRun(r));
2071
+ }
2072
+ async upsert(workspaceId, run) {
2073
+ const values = {
2074
+ workspace_id: workspaceId,
2075
+ id: run.id,
2076
+ experiment_id: run.experimentId,
2077
+ prompt_version_id: run.promptVersionId,
2078
+ model: run.model,
2079
+ fixture_id: run.fixtureId,
2080
+ repeat_index: run.repeatIndex,
2081
+ status: run.status,
2082
+ output_text: run.outputText,
2083
+ usage: run.usage ? JSON.stringify(run.usage) : null,
2084
+ latency_ms: run.latencyMs,
2085
+ branch: run.branch,
2086
+ pr_url: run.prUrl,
2087
+ diff: run.diff,
2088
+ error: run.error,
2089
+ seed_sha: run.seedSha,
2090
+ prompt_label: run.promptLabel,
2091
+ started_at: run.startedAt,
2092
+ finished_at: run.finishedAt,
2093
+ };
2094
+ await this.db
2095
+ .insert(sandboxRuns)
2096
+ .values(values)
2097
+ .onConflictDoUpdate({
2098
+ target: [sandboxRuns.workspace_id, sandboxRuns.id],
2099
+ set: {
2100
+ experiment_id: values.experiment_id,
2101
+ prompt_version_id: values.prompt_version_id,
2102
+ model: values.model,
2103
+ fixture_id: values.fixture_id,
2104
+ repeat_index: values.repeat_index,
2105
+ status: values.status,
2106
+ output_text: values.output_text,
2107
+ usage: values.usage,
2108
+ latency_ms: values.latency_ms,
2109
+ branch: values.branch,
2110
+ pr_url: values.pr_url,
2111
+ diff: values.diff,
2112
+ error: values.error,
2113
+ seed_sha: values.seed_sha,
2114
+ prompt_label: values.prompt_label,
2115
+ started_at: values.started_at,
2116
+ finished_at: values.finished_at,
2117
+ },
2118
+ });
2119
+ }
2120
+ async setStatus(workspaceId, id, status) {
2121
+ await this.db
2122
+ .update(sandboxRuns)
2123
+ .set({ status })
2124
+ .where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.id, id)));
2125
+ }
2126
+ async removeByExperiment(workspaceId, experimentId) {
2127
+ await this.db
2128
+ .delete(sandboxRuns)
2129
+ .where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.experiment_id, experimentId)));
2130
+ }
2131
+ }
2132
+ export class DrizzleSandboxGradeRepository {
2133
+ db;
2134
+ constructor(db) {
2135
+ this.db = db;
2136
+ }
2137
+ async getByRun(workspaceId, runId) {
2138
+ const rows = await this.db
2139
+ .select()
2140
+ .from(sandboxGrades)
2141
+ .where(and(eq(sandboxGrades.workspace_id, workspaceId), eq(sandboxGrades.run_id, runId)))
2142
+ .orderBy(desc(sandboxGrades.created_at))
2143
+ .limit(1);
2144
+ return rows[0] ? rowToSandboxGrade(rows[0]) : null;
2145
+ }
2146
+ async listByExperiment(workspaceId, experimentId) {
2147
+ const rows = await this.db
2148
+ .select({ grade: sandboxGrades })
2149
+ .from(sandboxGrades)
2150
+ .innerJoin(sandboxRuns, and(eq(sandboxRuns.workspace_id, sandboxGrades.workspace_id), eq(sandboxRuns.id, sandboxGrades.run_id)))
2151
+ .where(and(eq(sandboxGrades.workspace_id, workspaceId), eq(sandboxRuns.experiment_id, experimentId)))
2152
+ .orderBy(sandboxGrades.created_at);
2153
+ return rows.map((r) => rowToSandboxGrade(r.grade));
2154
+ }
2155
+ async upsert(workspaceId, grade) {
2156
+ const values = {
2157
+ workspace_id: workspaceId,
2158
+ id: grade.id,
2159
+ run_id: grade.runId,
2160
+ judge_model: grade.judgeModel,
2161
+ scores: JSON.stringify(grade.scores),
2162
+ weighted_total: grade.weightedTotal,
2163
+ objective: grade.objective ? JSON.stringify(grade.objective) : null,
2164
+ created_at: grade.createdAt,
2165
+ };
2166
+ await this.db
2167
+ .insert(sandboxGrades)
2168
+ .values(values)
2169
+ .onConflictDoUpdate({
2170
+ target: [sandboxGrades.workspace_id, sandboxGrades.id],
2171
+ set: {
2172
+ run_id: values.run_id,
2173
+ judge_model: values.judge_model,
2174
+ scores: values.scores,
2175
+ weighted_total: values.weighted_total,
2176
+ objective: values.objective,
2177
+ },
2178
+ });
2179
+ }
2180
+ async removeByExperiment(workspaceId, experimentId) {
2181
+ // Grades carry no experiment_id; scope them through their run's experiment.
2182
+ const runIds = this.db
2183
+ .select({ id: sandboxRuns.id })
2184
+ .from(sandboxRuns)
2185
+ .where(and(eq(sandboxRuns.workspace_id, workspaceId), eq(sandboxRuns.experiment_id, experimentId)));
2186
+ await this.db
2187
+ .delete(sandboxGrades)
2188
+ .where(and(eq(sandboxGrades.workspace_id, workspaceId), inArray(sandboxGrades.run_id, runIds)));
2189
+ }
2190
+ }
2191
+ /**
2192
+ * The Sandbox's persistence as one spreadable mixin (the Drizzle analogue of the
2193
+ * Worker's `selectSandboxDeps`). The Node container spreads `...createDrizzleSandboxDeps(db)`
2194
+ * into its dependencies so the container body never enumerates the Sandbox repos — the
2195
+ * knowledge of which repos exist lives here, next to their implementations. Typed by the
2196
+ * kernel ports (not `CoreDependencies`) so this module stays free of the orchestration import.
2197
+ */
2198
+ export function createDrizzleSandboxDeps(db) {
2199
+ return {
2200
+ sandboxPromptVersionRepository: new DrizzleSandboxPromptVersionRepository(db),
2201
+ sandboxFixtureRepository: new DrizzleSandboxFixtureRepository(db),
2202
+ sandboxExperimentRepository: new DrizzleSandboxExperimentRepository(db),
2203
+ sandboxRunRepository: new DrizzleSandboxRunRepository(db),
2204
+ sandboxGradeRepository: new DrizzleSandboxGradeRepository(db),
2205
+ };
2206
+ }
1842
2207
  /**
1843
2208
  * Per-workspace runtime settings over Postgres (the Drizzle mirror of the Worker's
1844
2209
  * `D1WorkspaceSettingsRepository`, migration 0004). One row per workspace; the service