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
2 changes: 0 additions & 2 deletions src/services/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { schedulesModule } from "@/services/api/modules/schedules/index.ts";
import { climateModule } from "@/services/api/modules/climate/index.ts";
import { monitoringModule } from "@/services/api/modules/monitoring/index.ts";
import { handlersModule } from "@/services/api/modules/handlers/index.ts";
import { modelsModule } from "@/services/api/modules/models/index.ts";
import { evaluationsModule } from "@/services/api/modules/evaluations/index.ts";
import { systemModule } from "@/services/api/modules/system/index.ts";

Expand Down Expand Up @@ -52,7 +51,6 @@ const app = new Elysia()
.use(climateModule)
.use(monitoringModule)
.use(handlersModule)
.use(modelsModule)
.use(evaluationsModule)
.use(systemModule);

Expand Down
21 changes: 0 additions & 21 deletions src/services/api/modules/models/index.ts

This file was deleted.

100 changes: 0 additions & 100 deletions src/services/api/modules/models/service.ts

This file was deleted.

2 changes: 2 additions & 0 deletions src/services/worker/constants/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export type HandlerRegistryEntry = {
config?: ZodType;
context?: ZodType;
};
contextReplaces?: string[];
};

export enum Handlers {
Expand Down Expand Up @@ -92,6 +93,7 @@ export const HANDLERS: Map<Handlers, HandlerRegistryEntry> = new Map<
config: predictionTriggerConfigSchema,
context: predictionTriggerContextSchema,
},
contextReplaces: ["period"],
},
],
[
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,24 @@
import { z } from "zod";

const trainingPeriodSchema = z
.object({
endPeriod: z.string().min(1).optional(),
periodOffset: z.number().int().min(0).optional(),
numberOfPeriodsToGenerate: z.number().int().positive().default(3),
})
.refine(({ endPeriod, periodOffset }) => endPeriod !== undefined || periodOffset !== undefined, {
message: "Either endPeriod or periodOffset must be set for the training period",
});

export const predictionTriggerConfigSchema = z.object({
backtestId: z.number().int().positive(),
predictionSetupId: z.number().int().positive(),
name: z.string().min(1),
period: z
.object({
endPeriod: z.string().min(1).optional(),
periodOffset: z.number().int().min(0).optional(),
numberOfPeriodsToGenerate: z.number().int().positive().default(3),
})
.refine(
({ endPeriod, periodOffset }) => endPeriod !== undefined || periodOffset !== undefined,
{
message: "Either endPeriod or periodOffset must be set for the training period",
}
),
period: trainingPeriodSchema,
});

export type PredictionTriggerConfig = z.infer<typeof predictionTriggerConfigSchema>;

/** Per-run overrides: a schedule ends the training window relative to each run. */
export const predictionTriggerContextSchema = z.object({
period: z.object({
periodOffset: z.number().default(0),
numberOfPeriodsToGenerate: z.number().default(3),
}),
period: trainingPeriodSchema,
});
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,6 @@ function parsePeriodId(periodId: string, type: PeriodTypeEnum): DateTime {
return parsed.startOf("week");
}

/**
* Builds the training window: every period from the setup's `startPeriod` up to
* `periodOffset` periods back from today. Offset 0 ends on the current (incomplete)
* period, 1 ends on the previous one.
*/
export function getTrainingPeriods({
startPeriod,
periodType,
Expand All @@ -172,9 +167,9 @@ export function getTrainingPeriods({
}

const end =
periodOffset !== undefined
? now.minus({ [durationLabel]: periodOffset }).startOf(unit)
: parsePeriodId(endPeriod as string, periodType);
endPeriod !== undefined
? parsePeriodId(endPeriod, periodType)
: now.minus({ [durationLabel]: periodOffset as number }).startOf(unit);

if (end < start) {
throw new Error(
Expand Down
15 changes: 12 additions & 3 deletions src/services/worker/utils/resolveEffectiveHandlerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { merge } from "lodash-es";
import {
formatHandlerConfigIssues,
formatHandlerContextIssues,
getHandlerContextReplaceKeys,
validateHandlerConfig,
validateHandlerContext,
} from "@/shared/handlers/catalog.ts";
Expand Down Expand Up @@ -33,7 +34,8 @@ export class EffectiveHandlerConfigError extends Error {

/**
* Merges pipeline execution context step overrides onto static handlerConfig.
* Context wins on conflicts (deep merge). Validates override and merged config.
* Context wins on conflicts (deep merge), except for the handler's `contextReplaces` keys,
* which the context replaces whole. Validates override and merged config.
*/
export function resolveEffectiveHandlerConfig(args: {
handlerKey: string;
Expand All @@ -52,8 +54,15 @@ export function resolveEffectiveHandlerConfig(args: {
}
}

const merged =
override !== undefined && Object.keys(override).length > 0 ? merge({}, base, override) : base;
let merged = base;
if (override !== undefined && Object.keys(override).length > 0) {
merged = merge({}, base, override);
for (const key of getHandlerContextReplaceKeys(args.handlerKey)) {
if (key in override) {
merged[key] = override[key];
}
}
}

const configResult = validateHandlerConfig(args.handlerKey, merged);
if (!configResult.success) {
Expand Down
6 changes: 0 additions & 6 deletions src/shared/clients/climate.ts

This file was deleted.

5 changes: 5 additions & 0 deletions src/shared/handlers/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ function getRegistryContextSchema(key: string): ZodType | undefined {
return entry?.schemas.context;
}

/** Context keys that replace the handler config value whole rather than merging into it. */
export function getHandlerContextReplaceKeys(handlerKey: string): string[] {
return HANDLERS.get(handlerKey as Handlers)?.contextReplaces ?? [];
}

export function hasHandlerContextSchema(handlerKey: string): boolean {
return getRegistryContextSchema(handlerKey) !== undefined;
}
Expand Down
18 changes: 16 additions & 2 deletions src/tests/unit/handler-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,29 @@ describe("validateHandlerContext", () => {
});

it("accepts valid prediction-trigger context slice", () => {
const result = validateHandlerContext("prediction-trigger", {
period: { endPeriod: "202608", numberOfPeriodsToGenerate: 3 },
});
expect(result.success).toBe(true);
});

it("accepts a schedule's period offset on prediction-trigger", () => {
const result = validateHandlerContext("prediction-trigger", {
period: { periodOffset: 1, numberOfPeriodsToGenerate: 3 },
});
expect(result.success).toBe(true);
});

it("rejects a non-numeric period offset on prediction-trigger", () => {
it("rejects a negative period offset on prediction-trigger", () => {
const result = validateHandlerContext("prediction-trigger", {
period: { periodOffset: -1, numberOfPeriodsToGenerate: 3 },
});
expect(result.success).toBe(false);
});

it("rejects a prediction-trigger context with neither an end period nor an offset", () => {
const result = validateHandlerContext("prediction-trigger", {
period: { periodOffset: "last", numberOfPeriodsToGenerate: 3 },
period: { numberOfPeriodsToGenerate: 3 },
});
expect(result.success).toBe(false);
});
Expand Down
4 changes: 2 additions & 2 deletions src/tests/unit/predictionTrigger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ describe("getTrainingPeriods", () => {
expect(periods.at(-1)).toBe("202604");
});

it("lets a schedule's offset win over the period picked on the step", () => {
it("lets an end period picked for a run win over an offset left on the step", () => {
const periods = getTrainingPeriods({
startPeriod: "202207",
periodType: PeriodTypeEnum.MONTHLY,
Expand All @@ -101,7 +101,7 @@ describe("getTrainingPeriods", () => {
now: september2026,
});

expect(periods.at(-1)).toBe("202608");
expect(periods.at(-1)).toBe("202604");
});

it("refuses a training period with neither an offset nor an end period", () => {
Expand Down
42 changes: 37 additions & 5 deletions src/tests/unit/resolveEffectiveHandlerConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,27 +36,59 @@ describe("resolveEffectiveHandlerConfig", () => {
expect(merged).toEqual(baseConfig);
});

it("deep-merges context override onto handlerConfig", () => {
it("replaces the step's period with the context's", () => {
const merged = resolveEffectiveHandlerConfig({
handlerKey: "prediction-trigger",
handlerConfig: baseConfig,
pipelineContext: {
steps: {
"step-1": {
period: { periodOffset: 1, numberOfPeriodsToGenerate: 6 },
period: { endPeriod: "202610", numberOfPeriodsToGenerate: 6 },
},
},
},
stepId: "step-1",
});
expect(merged.period).toEqual({
endPeriod: "202608",
periodOffset: 1,
endPeriod: "202610",
numberOfPeriodsToGenerate: 6,
});
expect(merged.predictionSetupId).toBe(3);
});

it("lets a schedule's offset replace the step's fixed end period", () => {
const merged = resolveEffectiveHandlerConfig({
handlerKey: "prediction-trigger",
handlerConfig: baseConfig,
pipelineContext: {
steps: {
"step-1": {
period: { periodOffset: 1, numberOfPeriodsToGenerate: 3 },
},
},
},
stepId: "step-1",
});
expect(merged.period).toEqual({ periodOffset: 1, numberOfPeriodsToGenerate: 3 });
});

it("deep-merges keys the handler does not replace", () => {
const merged = resolveEffectiveHandlerConfig({
handlerKey: "prediction-trigger",
handlerConfig: baseConfig,
pipelineContext: {
steps: {
"step-1": {
period: { endPeriod: "202610", numberOfPeriodsToGenerate: 6 },
},
},
},
stepId: "step-1",
});
expect(merged.name).toBe("run-1");
expect(merged.backtestId).toBe(2);
});

it("throws when context override is invalid", () => {
expect(() =>
resolveEffectiveHandlerConfig({
Expand All @@ -65,7 +97,7 @@ describe("resolveEffectiveHandlerConfig", () => {
pipelineContext: {
steps: {
"step-1": {
period: { periodOffset: "last" },
period: { endPeriod: "" },
},
},
},
Expand Down
Loading
Loading