Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions apps/server/src/decision/Errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Schema } from "effect";

export class DecisionWorkspaceError extends Schema.TaggedErrorClass<DecisionWorkspaceError>()(
"DecisionWorkspaceError",
{
operation: Schema.String,
detail: Schema.String,
cause: Schema.optional(Schema.Defect),
},
) {
override get message(): string {
return `Decision workspace failed in ${this.operation}: ${this.detail}`;
}
}

export type DecisionWorkspaceServiceError = DecisionWorkspaceError;
30 changes: 30 additions & 0 deletions apps/server/src/decision/Services/DecisionConsultationService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type {
DecisionCase,
DecisionConsultation,
DecisionConsultationQuestion,
DecisionContextPack,
} from "@okcode/contracts";
import { ServiceMap } from "effect";
import type { Effect } from "effect";
import type { DecisionPolicyDefinition } from "./DecisionPolicy.ts";
import type { DecisionWorkspaceServiceError } from "../Errors.ts";

export interface DecisionConsultationServiceShape {
readonly request: (input: {
readonly decisionCase: DecisionCase;
readonly target: "operator" | "orchestrator";
readonly reason: string;
readonly questions: ReadonlyArray<DecisionConsultationQuestion>;
readonly contextPack: DecisionContextPack;
readonly policy: DecisionPolicyDefinition;
}) => Effect.Effect<DecisionConsultation, DecisionWorkspaceServiceError>;
readonly respond: (input: {
readonly consultationId: string;
readonly resolution: string;
}) => Effect.Effect<DecisionConsultation, DecisionWorkspaceServiceError>;
}

export class DecisionConsultationService extends ServiceMap.Service<
DecisionConsultationService,
DecisionConsultationServiceShape
>()("okcode/decision/Services/DecisionConsultationService") {}
32 changes: 32 additions & 0 deletions apps/server/src/decision/Services/DecisionContextPackBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type {
DecisionCase,
DecisionContextPack,
DecisionPrincipleResult,
DecisionRecommendation,
} from "@okcode/contracts";
import { ServiceMap } from "effect";
import type { Effect } from "effect";
import type { DecisionPolicyDefinition } from "./DecisionPolicy.ts";
import type { DecisionWorkspaceServiceError } from "../Errors.ts";

export interface DecisionContextPackBuilderShape {
readonly listAutoCases: (input: {
readonly cwd: string;
}) => Effect.Effect<ReadonlyArray<DecisionCase>, DecisionWorkspaceServiceError>;
readonly buildCaseArtifacts: (input: {
readonly decisionCase: DecisionCase;
readonly policy: DecisionPolicyDefinition;
}) => Effect.Effect<
{
readonly contextPack: DecisionContextPack;
readonly principles: ReadonlyArray<DecisionPrincipleResult>;
readonly recommendations: ReadonlyArray<DecisionRecommendation>;
},
DecisionWorkspaceServiceError
>;
}

export class DecisionContextPackBuilder extends ServiceMap.Service<
DecisionContextPackBuilder,
DecisionContextPackBuilderShape
>()("okcode/decision/Services/DecisionContextPackBuilder") {}
33 changes: 33 additions & 0 deletions apps/server/src/decision/Services/DecisionPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { DecisionConflictKind } from "@okcode/contracts";
import { ServiceMap } from "effect";
import type { Effect } from "effect";
import type { DecisionWorkspaceServiceError } from "../Errors.ts";

export interface DecisionPolicyDefinition {
readonly version: string;
readonly principles: ReadonlyArray<{
readonly id: string;
readonly label: string;
readonly blocking: boolean;
}>;
readonly executionThresholds: {
readonly autoExecuteScore: number;
readonly minimumContextCompleteness: number;
readonly minimumPolicyAlignment: number;
};
readonly requiredContextByKind: Readonly<Record<DecisionConflictKind, ReadonlyArray<string>>>;
readonly consultationDefaults: {
readonly orchestratorMinScore: number;
readonly operatorMaxScore: number;
};
}

export interface DecisionPolicyShape {
readonly getPolicy: (input: {
readonly cwd: string;
}) => Effect.Effect<DecisionPolicyDefinition, DecisionWorkspaceServiceError>;
}

export class DecisionPolicy extends ServiceMap.Service<DecisionPolicy, DecisionPolicyShape>()(
"okcode/decision/Services/DecisionPolicy",
) {}
56 changes: 56 additions & 0 deletions apps/server/src/decision/Services/DecisionProjection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type {
DecisionCase,
DecisionConfidenceAnalysis,
DecisionConsultation,
DecisionConsultationQuestion,
DecisionConsultationStatus,
DecisionConsultationTarget,
} from "@okcode/contracts";
import { ServiceMap } from "effect";
import type { Effect } from "effect";
import type { DecisionWorkspaceServiceError } from "../Errors.ts";

export interface DecisionProjectionShape {
readonly upsertCase: (input: DecisionCase) => Effect.Effect<void, DecisionWorkspaceServiceError>;
readonly getCase: (input: {
readonly caseId: string;
}) => Effect.Effect<DecisionCase | null, DecisionWorkspaceServiceError>;
readonly listCasesByCwd: (input: {
readonly cwd: string;
}) => Effect.Effect<ReadonlyArray<DecisionCase>, DecisionWorkspaceServiceError>;
readonly upsertConsultation: (input: {
readonly consultation: DecisionConsultation;
readonly questions: ReadonlyArray<DecisionConsultationQuestion>;
}) => Effect.Effect<void, DecisionWorkspaceServiceError>;
readonly getConsultation: (input: {
readonly consultationId: string;
}) => Effect.Effect<
{ consultation: DecisionConsultation; questions: ReadonlyArray<DecisionConsultationQuestion> } | null,
DecisionWorkspaceServiceError
>;
readonly listConsultationsByCaseId: (input: {
readonly caseId: string;
}) => Effect.Effect<ReadonlyArray<DecisionConsultation>, DecisionWorkspaceServiceError>;
readonly appendScoreSnapshot: (input: {
readonly caseId: string;
readonly analysis: DecisionConfidenceAnalysis;
}) => Effect.Effect<void, DecisionWorkspaceServiceError>;
readonly listScoreSnapshots: (input: {
readonly caseId: string;
}) => Effect.Effect<ReadonlyArray<DecisionConfidenceAnalysis>, DecisionWorkspaceServiceError>;
readonly createConsultation: (input: {
readonly caseId: string;
readonly target: DecisionConsultationTarget;
readonly status: DecisionConsultationStatus;
readonly reason: string;
readonly questions: ReadonlyArray<DecisionConsultationQuestion>;
readonly linkedThreadId: string | null;
readonly responseSummary?: string | null;
readonly resolvedAt?: string | null;
}) => Effect.Effect<DecisionConsultation, DecisionWorkspaceServiceError>;
}

export class DecisionProjection extends ServiceMap.Service<
DecisionProjection,
DecisionProjectionShape
>()("okcode/decision/Services/DecisionProjection") {}
39 changes: 39 additions & 0 deletions apps/server/src/decision/Services/DecisionWorkspace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type {
DecisionCaseSummary,
DecisionExecuteRecommendationInput,
DecisionExecutionResult,
DecisionGetWorkspaceInput,
DecisionListCasesInput,
DecisionRequestConsultationInput,
DecisionRespondConsultationInput,
DecisionWorkspace as DecisionWorkspaceResult,
} from "@okcode/contracts";
import { ServiceMap } from "effect";
import type { Effect } from "effect";
import type { DecisionWorkspaceServiceError } from "../Errors.ts";

export interface DecisionWorkspaceShape {
readonly listCases: (
input: DecisionListCasesInput,
) => Effect.Effect<ReadonlyArray<DecisionCaseSummary>, DecisionWorkspaceServiceError>;
readonly getWorkspace: (
input: DecisionGetWorkspaceInput,
) => Effect.Effect<DecisionWorkspaceResult, DecisionWorkspaceServiceError>;
readonly reanalyze: (
input: DecisionGetWorkspaceInput,
) => Effect.Effect<DecisionWorkspaceResult, DecisionWorkspaceServiceError>;
readonly requestConsultation: (
input: DecisionRequestConsultationInput,
) => Effect.Effect<DecisionWorkspaceResult, DecisionWorkspaceServiceError>;
readonly respondConsultation: (
input: DecisionRespondConsultationInput,
) => Effect.Effect<DecisionWorkspaceResult, DecisionWorkspaceServiceError>;
readonly executeRecommendation: (
input: DecisionExecuteRecommendationInput,
) => Effect.Effect<DecisionExecutionResult, DecisionWorkspaceServiceError>;
}

export class DecisionWorkspace extends ServiceMap.Service<
DecisionWorkspace,
DecisionWorkspaceShape
>()("okcode/decision/Services/DecisionWorkspace") {}
134 changes: 134 additions & 0 deletions apps/server/src/persistence/Layers/DecisionCases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as SqlClient from "effect/unstable/sql/SqlClient";
import * as SqlSchema from "effect/unstable/sql/SqlSchema";
import { Effect, Layer, Option, Schema } from "effect";
import { toPersistenceDecodeError, toPersistenceSqlError } from "../Errors.ts";
import {
DecisionCaseRepository,
DecisionCaseRow,
GetDecisionCaseInput,
ListDecisionCasesInput,
type DecisionCaseRepositoryShape,
} from "../Services/DecisionCases.ts";

function toPersistenceSqlOrDecodeError(sqlOperation: string, decodeOperation: string) {
return (cause: unknown) =>
Schema.isSchemaError(cause)
? toPersistenceDecodeError(decodeOperation)(cause)
: toPersistenceSqlError(sqlOperation)(cause);
}

const makeDecisionCaseRepository = Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;

const upsertRow = SqlSchema.void({
Request: DecisionCaseRow,
execute: (row) =>
sql`
INSERT INTO decision_cases (
case_id, project_id, cwd, source_kind, source_id, title, conflict_kind,
linked_thread_id, created_at, updated_at
) VALUES (
${row.caseId}, ${row.projectId}, ${row.cwd}, ${row.sourceKind}, ${row.sourceId}, ${row.title},
${row.conflictKind}, ${row.linkedThreadId}, ${row.createdAt}, ${row.updatedAt}
)
ON CONFLICT (case_id)
DO UPDATE SET
project_id = excluded.project_id,
cwd = excluded.cwd,
source_kind = excluded.source_kind,
source_id = excluded.source_id,
title = excluded.title,
conflict_kind = excluded.conflict_kind,
linked_thread_id = excluded.linked_thread_id,
updated_at = excluded.updated_at
`,
});

const getRow = SqlSchema.findOneOption({
Request: GetDecisionCaseInput,
Result: DecisionCaseRow,
execute: ({ caseId }) =>
sql`
SELECT
case_id AS "caseId",
project_id AS "projectId",
cwd,
source_kind AS "sourceKind",
source_id AS "sourceId",
title,
conflict_kind AS "conflictKind",
linked_thread_id AS "linkedThreadId",
created_at AS "createdAt",
updated_at AS "updatedAt"
FROM decision_cases
WHERE case_id = ${caseId}
`,
});

const listRows = SqlSchema.findAll({
Request: ListDecisionCasesInput,
Result: DecisionCaseRow,
execute: ({ cwd }) =>
sql`
SELECT
case_id AS "caseId",
project_id AS "projectId",
cwd,
source_kind AS "sourceKind",
source_id AS "sourceId",
title,
conflict_kind AS "conflictKind",
linked_thread_id AS "linkedThreadId",
created_at AS "createdAt",
updated_at AS "updatedAt"
FROM decision_cases
WHERE cwd = ${cwd}
ORDER BY updated_at DESC
`,
});

const upsert: DecisionCaseRepositoryShape["upsert"] = (row) =>
upsertRow(row).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"DecisionCaseRepository.upsert:query",
"DecisionCaseRepository.upsert:encodeRequest",
),
),
);

const getById: DecisionCaseRepositoryShape["getById"] = (input) =>
getRow(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"DecisionCaseRepository.getById:query",
"DecisionCaseRepository.getById:decodeRow",
),
),
Effect.flatMap((rowOption) =>
Option.match(rowOption, {
onNone: () => Effect.succeed(Option.none()),
onSome: (row) =>
Effect.succeed(Option.some(row as Schema.Schema.Type<typeof DecisionCaseRow>)),
}),
),
);

const listByCwd: DecisionCaseRepositoryShape["listByCwd"] = (input) =>
listRows(input).pipe(
Effect.mapError(
toPersistenceSqlOrDecodeError(
"DecisionCaseRepository.listByCwd:query",
"DecisionCaseRepository.listByCwd:decodeRows",
),
),
Effect.map((rows) => rows as ReadonlyArray<Schema.Schema.Type<typeof DecisionCaseRow>>),
);

return { upsert, getById, listByCwd } satisfies DecisionCaseRepositoryShape;
});

export const DecisionCaseRepositoryLive = Layer.effect(
DecisionCaseRepository,
makeDecisionCaseRepository,
);
Loading
Loading