@cat-factory/node-server 0.87.10 → 0.89.0

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.
@@ -2,7 +2,7 @@ import { LLM_WARNING_FINISH_REASONS } from '@cat-factory/kernel';
2
2
  import { agentRunKindSchema, decodeInitiativeRow, isWebSearchProvider, } from '@cat-factory/contracts';
3
3
  import { decodeEnum, tryDecodeRows, blockInsertValues, blockPatchToColumns, parseIssueIntakeColumn, serializeIssueIntakeColumn, rowToBlock, rowToExecution, executionToDetail, rowToPipeline, rowToSandboxExperiment, rowToSandboxFixture, rowToSandboxGrade, rowToSandboxPromptVersion, rowToSandboxRun, rowToWorkspace, } from '@cat-factory/server';
4
4
  import { and, asc, count, desc, eq, gte, inArray, isNull, lt, ne, notInArray, or, sql, } from 'drizzle-orm';
5
- import { accountInvitations, passwordResetTokens, accountSettings, localSettings, accounts, agentContextSnapshots, agentSearchQueries, agentRuns, blocks, consensusSessions, incidentEnrichmentConnections, observabilityConnections, packageRegistryConnections, emailConnections, llmCallMetrics, provisioningLog, sharedStacks, memberships, mergeThresholdPresets, releaseHealthConfigs, pipelineScheduleRuns, pipelineSchedules, pipelines, requirementReviews, docInterviewSessions, kaizenGradings, kaizenVerifiedCombos, clarityReviews, binaryArtifacts, brainstormSessions, initiatives, sandboxPromptVersions, sandboxFixtures, sandboxExperiments, sandboxRuns, sandboxGrades, services, tokenUsage, trackerSettings, modelPresets, userIdentities, users, userSettings, workspaceFragmentDefaults, workspaceServices, workspaceSettings, workspaces, } from '../db/schema.js';
5
+ import { accountInvitations, passwordResetTokens, accountSettings, localSettings, accounts, agentContextSnapshots, agentSearchQueries, agentRuns, blocks, consensusSessions, incidentEnrichmentConnections, observabilityConnections, packageRegistryConnections, emailConnections, llmCallMetrics, provisioningLog, sharedStacks, memberships, riskPolicies, releaseHealthConfigs, testSecrets, pipelineScheduleRuns, pipelineSchedules, pipelines, requirementReviews, docInterviewSessions, kaizenGradings, kaizenVerifiedCombos, clarityReviews, binaryArtifacts, brainstormSessions, initiatives, sandboxPromptVersions, sandboxFixtures, sandboxExperiments, sandboxRuns, sandboxGrades, services, tokenUsage, trackerSettings, modelPresets, userIdentities, users, userSettings, workspaceFragmentDefaults, workspaceServices, workspaceSettings, workspaces, } from '../db/schema.js';
6
6
  // Drizzle/Postgres implementations of the core kernel repository ports. The
7
7
  // row<->domain mapping is the SAME shared mapping the Cloudflare D1 repos use
8
8
  // (@cat-factory/server), so behaviour matches across stores; this layer only owns
@@ -222,6 +222,7 @@ class DrizzlePipelineRepository {
222
222
  gating: pipeline.gating ? JSON.stringify(pipeline.gating) : null,
223
223
  follow_ups: pipeline.followUps ? JSON.stringify(pipeline.followUps) : null,
224
224
  tester_quality: pipeline.testerQuality ? JSON.stringify(pipeline.testerQuality) : null,
225
+ step_options: pipeline.stepOptions ? JSON.stringify(pipeline.stepOptions) : null,
225
226
  labels: pipeline.labels ? JSON.stringify(pipeline.labels) : null,
226
227
  archived: pipeline.archived ? 1 : null,
227
228
  builtin: pipeline.builtin ? 1 : null,
@@ -246,6 +247,7 @@ class DrizzlePipelineRepository {
246
247
  gating: pipeline.gating ? JSON.stringify(pipeline.gating) : null,
247
248
  follow_ups: pipeline.followUps ? JSON.stringify(pipeline.followUps) : null,
248
249
  tester_quality: pipeline.testerQuality ? JSON.stringify(pipeline.testerQuality) : null,
250
+ step_options: pipeline.stepOptions ? JSON.stringify(pipeline.stepOptions) : null,
249
251
  labels: pipeline.labels ? JSON.stringify(pipeline.labels) : null,
250
252
  archived: pipeline.archived ? 1 : null,
251
253
  version: pipeline.version ?? null,
@@ -969,9 +971,41 @@ class DrizzleTokenUsageRepository {
969
971
  input_tokens: usage.inputTokens,
970
972
  output_tokens: usage.outputTokens,
971
973
  cost_estimate: usage.costEstimate,
974
+ billing: usage.billing,
975
+ vendor: usage.vendor,
972
976
  created_at: usage.createdAt,
973
977
  });
974
978
  }
979
+ async usageBreakdownForWorkspace(workspaceId, epochMs) {
980
+ // One GROUP BY over the workspace's current period — both billing kinds (the report
981
+ // shows total usage). Never a per-model loop. sum() of int columns is bigint; cast +
982
+ // coerce like the totals rollups. Ordered heaviest-first in SQL, mirroring the D1 repo.
983
+ const rows = await this.db
984
+ .select({
985
+ billing: tokenUsage.billing,
986
+ vendor: tokenUsage.vendor,
987
+ provider: tokenUsage.provider,
988
+ model: tokenUsage.model,
989
+ input: sql `coalesce(sum(${tokenUsage.input_tokens}), 0)::bigint`,
990
+ output: sql `coalesce(sum(${tokenUsage.output_tokens}), 0)::bigint`,
991
+ cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
992
+ calls: sql `count(*)::bigint`,
993
+ })
994
+ .from(tokenUsage)
995
+ .where(and(eq(tokenUsage.workspace_id, workspaceId), gte(tokenUsage.created_at, epochMs)))
996
+ .groupBy(tokenUsage.billing, tokenUsage.vendor, tokenUsage.provider, tokenUsage.model)
997
+ .orderBy(sql `(coalesce(sum(${tokenUsage.input_tokens}), 0) + coalesce(sum(${tokenUsage.output_tokens}), 0)) desc`);
998
+ return rows.map((r) => ({
999
+ billing: (r.billing === 'subscription' ? 'subscription' : 'metered'),
1000
+ vendor: r.vendor,
1001
+ provider: r.provider,
1002
+ model: r.model,
1003
+ inputTokens: Number(r.input ?? 0),
1004
+ outputTokens: Number(r.output ?? 0),
1005
+ costEstimate: r.cost ?? 0,
1006
+ calls: Number(r.calls ?? 0),
1007
+ }));
1008
+ }
975
1009
  async totalsSince(epochMs) {
976
1010
  // sum() of int columns is bigint in Postgres — cast to bigint (NOT int4, which
977
1011
  // overflows past ~2.1B tokens) and coerce: node-postgres returns bigint as a
@@ -984,7 +1018,7 @@ class DrizzleTokenUsageRepository {
984
1018
  cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
985
1019
  })
986
1020
  .from(tokenUsage)
987
- .where(gte(tokenUsage.created_at, epochMs));
1021
+ .where(and(gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
988
1022
  return {
989
1023
  inputTokens: Number(row?.input ?? 0),
990
1024
  outputTokens: Number(row?.output ?? 0),
@@ -999,7 +1033,7 @@ class DrizzleTokenUsageRepository {
999
1033
  cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
1000
1034
  })
1001
1035
  .from(tokenUsage)
1002
- .where(and(eq(tokenUsage.workspace_id, workspaceId), gte(tokenUsage.created_at, epochMs)));
1036
+ .where(and(eq(tokenUsage.workspace_id, workspaceId), gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
1003
1037
  return {
1004
1038
  inputTokens: Number(row?.input ?? 0),
1005
1039
  outputTokens: Number(row?.output ?? 0),
@@ -1014,7 +1048,7 @@ class DrizzleTokenUsageRepository {
1014
1048
  cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
1015
1049
  })
1016
1050
  .from(tokenUsage)
1017
- .where(and(eq(tokenUsage.account_id, accountId), gte(tokenUsage.created_at, epochMs)));
1051
+ .where(and(eq(tokenUsage.account_id, accountId), gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
1018
1052
  return {
1019
1053
  inputTokens: Number(row?.input ?? 0),
1020
1054
  outputTokens: Number(row?.output ?? 0),
@@ -1029,7 +1063,7 @@ class DrizzleTokenUsageRepository {
1029
1063
  cost: sql `coalesce(sum(${tokenUsage.cost_estimate}), 0)::float8`,
1030
1064
  })
1031
1065
  .from(tokenUsage)
1032
- .where(and(eq(tokenUsage.user_id, userId), gte(tokenUsage.created_at, epochMs)));
1066
+ .where(and(eq(tokenUsage.user_id, userId), gte(tokenUsage.created_at, epochMs), eq(tokenUsage.billing, 'metered')));
1033
1067
  return {
1034
1068
  inputTokens: Number(row?.input ?? 0),
1035
1069
  outputTokens: Number(row?.output ?? 0),
@@ -2801,7 +2835,7 @@ class DrizzleInitiativeRepository {
2801
2835
  .where(and(eq(initiatives.workspace_id, workspaceId), eq(initiatives.id, id)));
2802
2836
  }
2803
2837
  }
2804
- function rowToMergePreset(row) {
2838
+ function rowToRiskPolicy(row) {
2805
2839
  return {
2806
2840
  id: row.id,
2807
2841
  name: row.name,
@@ -2823,13 +2857,13 @@ function rowToMergePreset(row) {
2823
2857
  }
2824
2858
  /**
2825
2859
  * Per-workspace merge threshold presets over Postgres (the Drizzle mirror of the
2826
- * Worker's `D1MergePresetRepository`, migration 0024). Enforces the single-default
2860
+ * Worker's `D1RiskPolicyRepository`, migration 0024). Enforces the single-default
2827
2861
  * invariant: promoting a preset to default demotes every other in the workspace
2828
2862
  * before the upsert. The default preset cannot be removed (the service keeps that
2829
2863
  * rule too; the DELETE also guards `is_default = 0`). Behaviourally identical to the
2830
2864
  * D1 repo so the cross-runtime conformance suite asserts the same preset resolution.
2831
2865
  */
2832
- class DrizzleMergePresetRepository {
2866
+ class DrizzleRiskPolicyRepository {
2833
2867
  db;
2834
2868
  constructor(db) {
2835
2869
  this.db = db;
@@ -2837,27 +2871,27 @@ class DrizzleMergePresetRepository {
2837
2871
  async get(workspaceId, id) {
2838
2872
  const rows = await this.db
2839
2873
  .select()
2840
- .from(mergeThresholdPresets)
2841
- .where(and(eq(mergeThresholdPresets.workspace_id, workspaceId), eq(mergeThresholdPresets.id, id)))
2874
+ .from(riskPolicies)
2875
+ .where(and(eq(riskPolicies.workspace_id, workspaceId), eq(riskPolicies.id, id)))
2842
2876
  .limit(1);
2843
- return rows[0] ? rowToMergePreset(rows[0]) : null;
2877
+ return rows[0] ? rowToRiskPolicy(rows[0]) : null;
2844
2878
  }
2845
2879
  async list(workspaceId) {
2846
2880
  const rows = await this.db
2847
2881
  .select()
2848
- .from(mergeThresholdPresets)
2849
- .where(eq(mergeThresholdPresets.workspace_id, workspaceId))
2850
- .orderBy(mergeThresholdPresets.created_at);
2851
- return rows.map(rowToMergePreset);
2882
+ .from(riskPolicies)
2883
+ .where(eq(riskPolicies.workspace_id, workspaceId))
2884
+ .orderBy(riskPolicies.created_at);
2885
+ return rows.map(rowToRiskPolicy);
2852
2886
  }
2853
2887
  async getDefault(workspaceId) {
2854
2888
  const rows = await this.db
2855
2889
  .select()
2856
- .from(mergeThresholdPresets)
2857
- .where(and(eq(mergeThresholdPresets.workspace_id, workspaceId), eq(mergeThresholdPresets.is_default, 1)))
2858
- .orderBy(mergeThresholdPresets.created_at)
2890
+ .from(riskPolicies)
2891
+ .where(and(eq(riskPolicies.workspace_id, workspaceId), eq(riskPolicies.is_default, 1)))
2892
+ .orderBy(riskPolicies.created_at)
2859
2893
  .limit(1);
2860
- return rows[0] ? rowToMergePreset(rows[0]) : null;
2894
+ return rows[0] ? rowToRiskPolicy(rows[0]) : null;
2861
2895
  }
2862
2896
  async upsert(workspaceId, preset) {
2863
2897
  const values = {
@@ -2885,15 +2919,15 @@ class DrizzleMergePresetRepository {
2885
2919
  // Promoting this preset to default demotes any other default first.
2886
2920
  if (preset.isDefault) {
2887
2921
  await tx
2888
- .update(mergeThresholdPresets)
2922
+ .update(riskPolicies)
2889
2923
  .set({ is_default: 0 })
2890
- .where(and(eq(mergeThresholdPresets.workspace_id, workspaceId), sql `${mergeThresholdPresets.id} <> ${preset.id}`));
2924
+ .where(and(eq(riskPolicies.workspace_id, workspaceId), sql `${riskPolicies.id} <> ${preset.id}`));
2891
2925
  }
2892
2926
  await tx
2893
- .insert(mergeThresholdPresets)
2927
+ .insert(riskPolicies)
2894
2928
  .values(values)
2895
2929
  .onConflictDoUpdate({
2896
- target: [mergeThresholdPresets.workspace_id, mergeThresholdPresets.id],
2930
+ target: [riskPolicies.workspace_id, riskPolicies.id],
2897
2931
  set: {
2898
2932
  name: values.name,
2899
2933
  max_complexity: values.max_complexity,
@@ -2915,8 +2949,8 @@ class DrizzleMergePresetRepository {
2915
2949
  }
2916
2950
  async remove(workspaceId, id) {
2917
2951
  await this.db
2918
- .delete(mergeThresholdPresets)
2919
- .where(and(eq(mergeThresholdPresets.workspace_id, workspaceId), eq(mergeThresholdPresets.id, id), eq(mergeThresholdPresets.is_default, 0)));
2952
+ .delete(riskPolicies)
2953
+ .where(and(eq(riskPolicies.workspace_id, workspaceId), eq(riskPolicies.id, id), eq(riskPolicies.is_default, 0)));
2920
2954
  }
2921
2955
  }
2922
2956
  // Shape-guarded parsers matching the D1 mirror (`D1SharedStackRepository`) EXACTLY, so a
@@ -3825,6 +3859,77 @@ class DrizzleReleaseHealthConfigRepository {
3825
3859
  .where(and(eq(releaseHealthConfigs.workspace_id, workspaceId), eq(releaseHealthConfigs.block_id, blockId)));
3826
3860
  }
3827
3861
  }
3862
+ /**
3863
+ * A service frame's sensitive test credentials over Postgres (the Drizzle mirror of the
3864
+ * Worker's `D1TestSecretsRepository`, migration 0044). At most one row per (workspace, block);
3865
+ * `credentials` is a sealed envelope of the `TestSecretEntry[]` JSON, `summary` a non-secret
3866
+ * `TestSecretRef[]` display blob.
3867
+ */
3868
+ export class DrizzleTestSecretsRepository {
3869
+ db;
3870
+ constructor(db) {
3871
+ this.db = db;
3872
+ }
3873
+ async getByBlock(workspaceId, blockId) {
3874
+ const rows = await this.db
3875
+ .select()
3876
+ .from(testSecrets)
3877
+ .where(and(eq(testSecrets.workspace_id, workspaceId), eq(testSecrets.block_id, blockId)))
3878
+ .limit(1);
3879
+ const row = rows[0];
3880
+ if (!row)
3881
+ return null;
3882
+ return {
3883
+ workspaceId: row.workspace_id,
3884
+ blockId: row.block_id,
3885
+ credentials: row.credentials,
3886
+ summary: row.summary,
3887
+ createdAt: row.created_at,
3888
+ updatedAt: row.updated_at,
3889
+ };
3890
+ }
3891
+ async listByWorkspace(workspaceId) {
3892
+ const rows = await this.db
3893
+ .select()
3894
+ .from(testSecrets)
3895
+ .where(eq(testSecrets.workspace_id, workspaceId))
3896
+ .orderBy(testSecrets.block_id);
3897
+ return rows.map((row) => ({
3898
+ workspaceId: row.workspace_id,
3899
+ blockId: row.block_id,
3900
+ credentials: row.credentials,
3901
+ summary: row.summary,
3902
+ createdAt: row.created_at,
3903
+ updatedAt: row.updated_at,
3904
+ }));
3905
+ }
3906
+ async upsert(record) {
3907
+ const values = {
3908
+ workspace_id: record.workspaceId,
3909
+ block_id: record.blockId,
3910
+ credentials: record.credentials,
3911
+ summary: record.summary,
3912
+ created_at: record.createdAt,
3913
+ updated_at: record.updatedAt,
3914
+ };
3915
+ await this.db
3916
+ .insert(testSecrets)
3917
+ .values(values)
3918
+ .onConflictDoUpdate({
3919
+ target: [testSecrets.workspace_id, testSecrets.block_id],
3920
+ set: {
3921
+ credentials: values.credentials,
3922
+ summary: values.summary,
3923
+ updated_at: values.updated_at,
3924
+ },
3925
+ });
3926
+ }
3927
+ async deleteByBlock(workspaceId, blockId) {
3928
+ await this.db
3929
+ .delete(testSecrets)
3930
+ .where(and(eq(testSecrets.workspace_id, workspaceId), eq(testSecrets.block_id, blockId)));
3931
+ }
3932
+ }
3828
3933
  /** Build the Drizzle/Postgres-backed core repositories. */
3829
3934
  export function createDrizzleRepositories(db, clock) {
3830
3935
  return {
@@ -3858,7 +3963,7 @@ export function createDrizzleRepositories(db, clock) {
3858
3963
  clarityReviewRepository: new DrizzleClarityReviewRepository(db),
3859
3964
  brainstormSessionRepository: new DrizzleBrainstormSessionRepository(db),
3860
3965
  initiativeRepository: new DrizzleInitiativeRepository(db),
3861
- mergePresetRepository: new DrizzleMergePresetRepository(db),
3966
+ riskPolicyRepository: new DrizzleRiskPolicyRepository(db),
3862
3967
  sharedStackRepository: new DrizzleSharedStackRepository(db),
3863
3968
  workspaceSettingsRepository: new DrizzleWorkspaceSettingsRepository(db),
3864
3969
  userSettingsRepository: new DrizzleUserSettingsRepository(db),
@@ -3867,6 +3972,7 @@ export function createDrizzleRepositories(db, clock) {
3867
3972
  incidentEnrichmentConnectionRepository: new DrizzleIncidentEnrichmentConnectionRepository(db),
3868
3973
  accountSettingsRepository: new DrizzleAccountSettingsRepository(db),
3869
3974
  releaseHealthConfigRepository: new DrizzleReleaseHealthConfigRepository(db),
3975
+ testSecretsRepository: new DrizzleTestSecretsRepository(db),
3870
3976
  provisioningLogRepository: new DrizzleProvisioningLogRepository(db),
3871
3977
  };
3872
3978
  }