PR 6: Database migration validation - fresh/upgrade test complete

 Database Setup:
- Created PostgreSQL kartselldb with kartsell user
- SSH port forward established (localhost:5432 → 178.104.200.7:5432)

 DbMigrator Fixes:
- Fixed migration path discovery (AppContext.BaseDirectory fallback)
- Added empty variable dictionary to suppress DbUp preprocessing
- Fixed PostgreSQL dollar quoting conflict ($policy$ → $$)

 Migration Results:
- All 21 migrations executed successfully
- Schema versions journal created and tracked
- 21 scripts processed in order, no rollback needed

Status: FRESH DATABASE DEPLOYMENT SUCCESSFUL
- kartselldb fully initialized with v16 schema
- Ready for application startup

Next: Deploy application and run integration tests

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 06:45:20 +09:00
parent fc39c8d4bf
commit 3b76070394
135 changed files with 6673 additions and 2 deletions
@@ -0,0 +1,7 @@
import { api } from '../../shared/api/client';
import { researchSellPolicyRequestSchema, researchSellPolicyResponseSchema } from './schema';
export async function evaluateResearchSellPolicy(command) {
const request = researchSellPolicyRequestSchema.parse(command.request);
const { data } = await api.post('/internal/v1/research/sell-policy/evaluate', request, { headers: { 'Idempotency-Key': command.idempotencyKey } });
return researchSellPolicyResponseSchema.parse(data);
}
@@ -0,0 +1,48 @@
import { computed } from 'vue';
const props = defineProps();
const dispositionLabel = { 0: 'NOT_APPLICABLE', 1: 'BLOCKED', 2: 'APPLIED' };
const ordered = computed(() => [...props.entries].sort((a, b) => b.priority - a.priority));
const __VLS_ctx = {
...{},
...{},
...{},
...{},
};
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
__VLS_asFunctionalElement1(__VLS_intrinsics.section, __VLS_intrinsics.section)({
'aria-labelledby': "policy-trace-title",
});
__VLS_asFunctionalElement1(__VLS_intrinsics.h2, __VLS_intrinsics.h2)({
id: "policy-trace-title",
});
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
(props.schemaVersion);
__VLS_asFunctionalElement1(__VLS_intrinsics.ol, __VLS_intrinsics.ol)({});
for (const [entry] of __VLS_vFor((__VLS_ctx.ordered))) {
__VLS_asFunctionalElement1(__VLS_intrinsics.li, __VLS_intrinsics.li)({
key: (`${entry.priority}-${entry.policyId}`),
});
__VLS_asFunctionalElement1(__VLS_intrinsics.strong, __VLS_intrinsics.strong)({});
(entry.policyId);
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
(__VLS_ctx.dispositionLabel[entry.disposition]);
(entry.reasonCode);
if (entry.requestedSellRatioOfLot > 0) {
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
(entry.requestedSellRatioOfLot);
(entry.appliedSellRatioOfLot);
}
if (entry.strategicCoreClampApplied) {
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
}
// @ts-ignore
[ordered, dispositionLabel,];
}
// @ts-ignore
[];
const __VLS_export = (await import('vue')).defineComponent({
__typeProps: {},
});
export default {};
@@ -0,0 +1,162 @@
import { computed, ref } from 'vue';
import QueryStateBoundary from '../../../shared/ui/QueryStateBoundary.vue';
import PolicyTracePanel from '../components/PolicyTracePanel.vue';
import { useEvaluateResearchSellPolicy } from '../queries';
const mutation = useEvaluateResearchSellPolicy();
const hardImpairmentApproved = ref(false);
const capitalFloorBreached = ref(false);
const gapBelowFloorAtr = ref(1.6);
const consecutiveCloseBreaches = ref(0);
const lastCommand = ref(null);
const isBusy = computed(() => mutation.isPending.value);
function createCommand() {
const asOf = new Date().toISOString();
return {
idempotencyKey: crypto.randomUUID(),
request: {
positionLotId: '00000000-0000-0000-0000-000000000001',
cycleId: '00000000-0000-0000-0000-000000000002',
evidenceId: 'sample-evidence',
datasetId: 'sample-dataset',
modelVersion: 'research-v12.2',
configVersion: 'proposal-v12.2',
codeSha: 'sample-code-sha',
asOf,
publishedAtCutoff: asOf,
currentSecurityPortfolioWeight: 0.6,
currentLotPortfolioWeight: 0.2,
strategicCoreFloorWeight: 0.3,
hardImpairmentApproved: hardImpairmentApproved.value,
capitalFloorBreached: capitalFloorBreached.value,
survivalSellRatioOfLot: 0.5,
gapBelowFloorAtr: gapBelowFloorAtr.value,
consecutiveCloseBreaches: consecutiveCloseBreaches.value,
cooldownSatisfied: true,
concentrationSellRatioOfLot: 0,
opportunityEdgeLowerBound: 0,
opportunitySellRatioOfLot: 0
}
};
}
function run() {
const command = createCommand();
lastCommand.value = command;
mutation.mutate(command);
}
function retry() {
if (lastCommand.value)
mutation.mutate(lastCommand.value);
}
const __VLS_ctx = {
...{},
...{},
};
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
__VLS_asFunctionalElement1(__VLS_intrinsics.main, __VLS_intrinsics.main)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.header, __VLS_intrinsics.header)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.h1, __VLS_intrinsics.h1)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.strong, __VLS_intrinsics.strong)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.form, __VLS_intrinsics.form)({
...{ onSubmit: (__VLS_ctx.run) },
});
__VLS_asFunctionalElement1(__VLS_intrinsics.fieldset, __VLS_intrinsics.fieldset)({
disabled: (__VLS_ctx.isBusy),
});
__VLS_asFunctionalElement1(__VLS_intrinsics.legend, __VLS_intrinsics.legend)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.input)({
type: "checkbox",
});
(__VLS_ctx.hardImpairmentApproved);
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.input)({
type: "checkbox",
});
(__VLS_ctx.capitalFloorBreached);
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.input)({
type: "number",
min: "0",
step: "0.1",
});
(__VLS_ctx.gapBelowFloorAtr);
__VLS_asFunctionalElement1(__VLS_intrinsics.label, __VLS_intrinsics.label)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.input)({
type: "number",
min: "0",
step: "1",
});
(__VLS_ctx.consecutiveCloseBreaches);
__VLS_asFunctionalElement1(__VLS_intrinsics.button, __VLS_intrinsics.button)({
type: "submit",
});
const __VLS_0 = QueryStateBoundary || QueryStateBoundary;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
...{ 'onRetry': {} },
loading: (__VLS_ctx.isBusy),
error: __VLS_ctx.mutation.error.value,
empty: (!__VLS_ctx.mutation.data.value),
}));
const __VLS_2 = __VLS_1({
...{ 'onRetry': {} },
loading: (__VLS_ctx.isBusy),
error: __VLS_ctx.mutation.error.value,
empty: (!__VLS_ctx.mutation.data.value),
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
let __VLS_5;
const __VLS_6 = {
/** @type {typeof __VLS_5.retry} */
onRetry: (__VLS_ctx.retry),
};
const { default: __VLS_7 } = __VLS_3.slots;
if (__VLS_ctx.mutation.data.value) {
__VLS_asFunctionalElement1(__VLS_intrinsics.dl, __VLS_intrinsics.dl)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
(__VLS_ctx.mutation.data.value.action);
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
(__VLS_ctx.mutation.data.value.policyId);
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
(__VLS_ctx.mutation.data.value.reasonCode);
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
(__VLS_ctx.mutation.data.value.sellRatioOfLot);
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
(__VLS_ctx.mutation.data.value.targetSecurityPortfolioWeightAfter);
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
(__VLS_ctx.mutation.data.value.reentryEligible);
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
(__VLS_ctx.mutation.data.value.decisionContractVersion);
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
(__VLS_ctx.mutation.data.value.policyTrace.length);
}
if (__VLS_ctx.mutation.data.value) {
const __VLS_8 = PolicyTracePanel;
// @ts-ignore
const __VLS_9 = __VLS_asFunctionalComponent1(__VLS_8, new __VLS_8({
entries: (__VLS_ctx.mutation.data.value.policyTrace),
schemaVersion: (__VLS_ctx.mutation.data.value.policyTraceSchemaVersion),
}));
const __VLS_10 = __VLS_9({
entries: (__VLS_ctx.mutation.data.value.policyTrace),
schemaVersion: (__VLS_ctx.mutation.data.value.policyTraceSchemaVersion),
}, ...__VLS_functionalComponentArgsRest(__VLS_9));
}
// @ts-ignore
[run, isBusy, isBusy, hardImpairmentApproved, capitalFloorBreached, gapBelowFloorAtr, consecutiveCloseBreaches, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, mutation, retry,];
var __VLS_3;
var __VLS_4;
// @ts-ignore
[];
const __VLS_export = (await import('vue')).defineComponent({});
export default {};
@@ -0,0 +1,5 @@
import { useMutation } from '@tanstack/vue-query';
import { evaluateResearchSellPolicy } from './api';
export function useEvaluateResearchSellPolicy() {
return useMutation({ mutationFn: evaluateResearchSellPolicy });
}
@@ -0,0 +1,61 @@
import { z } from 'zod';
const ratio = z.number().min(0).max(1);
const policyTraceEntrySchema = z.object({
policyId: z.string().min(1),
priority: z.number().int(),
disposition: z.union([z.literal(0), z.literal(1), z.literal(2)]),
reasonCode: z.string().min(1),
requestedSellRatioOfLot: ratio,
appliedSellRatioOfLot: ratio,
strategicCoreClampApplied: z.boolean()
});
export const researchSellPolicyRequestSchema = z.object({
positionLotId: z.string().uuid(),
cycleId: z.string().uuid(),
evidenceId: z.string().min(1).max(128),
datasetId: z.string().min(1).max(128),
modelVersion: z.string().min(1).max(128),
configVersion: z.string().min(1).max(128),
codeSha: z.string().min(1).max(128),
asOf: z.string().datetime({ offset: true }),
publishedAtCutoff: z.string().datetime({ offset: true }),
currentSecurityPortfolioWeight: ratio,
currentLotPortfolioWeight: ratio,
strategicCoreFloorWeight: ratio,
hardImpairmentApproved: z.boolean(),
capitalFloorBreached: z.boolean(),
survivalSellRatioOfLot: ratio,
gapBelowFloorAtr: z.number().min(0),
consecutiveCloseBreaches: z.number().int().min(0),
cooldownSatisfied: z.boolean(),
concentrationSellRatioOfLot: ratio,
opportunityEdgeLowerBound: z.number(),
opportunitySellRatioOfLot: ratio
}).superRefine((value, ctx) => {
if (value.currentLotPortfolioWeight > value.currentSecurityPortfolioWeight) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Lot 비중은 종목 전체 비중을 초과할 수 없습니다.',
path: ['currentLotPortfolioWeight']
});
}
if (new Date(value.publishedAtCutoff) > new Date(value.asOf)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '공개 가능 시각은 평가 시각 이후일 수 없습니다.',
path: ['publishedAtCutoff']
});
}
});
export const researchSellPolicyResponseSchema = z.object({
action: z.enum(['Hold', 'PartialSell', 'FullSell']),
sellRatioOfLot: ratio,
targetSecurityPortfolioWeightAfter: ratio,
policyId: z.string().min(1),
reasonCode: z.string().min(1),
decisionContractVersion: z.literal('sell-decision.v2'),
policyTraceSchemaVersion: z.literal(2),
reentryEligible: z.boolean(),
policyTrace: z.array(policyTraceEntrySchema),
evidenceStatus: z.literal('RESEARCH_CANDIDATE_NOT_PRODUCTION')
});
@@ -0,0 +1,57 @@
import { describe, expect, it } from 'vitest';
import { researchSellPolicyRequestSchema, researchSellPolicyResponseSchema } from '../schema';
const baseRequest = {
positionLotId: '00000000-0000-0000-0000-000000000001',
cycleId: '00000000-0000-0000-0000-000000000002',
evidenceId: 'evidence-1',
datasetId: 'dataset-1',
modelVersion: 'model-1',
configVersion: 'config-1',
codeSha: 'sha-1',
asOf: '2026-08-01T07:00:00Z',
publishedAtCutoff: '2026-08-01T06:00:00Z',
currentSecurityPortfolioWeight: 0.6,
currentLotPortfolioWeight: 0.2,
strategicCoreFloorWeight: 0.3,
hardImpairmentApproved: false,
capitalFloorBreached: false,
survivalSellRatioOfLot: 0,
gapBelowFloorAtr: 0,
consecutiveCloseBreaches: 0,
cooldownSatisfied: true,
concentrationSellRatioOfLot: 0,
opportunityEdgeLowerBound: 0,
opportunitySellRatioOfLot: 0
};
describe('research sell policy contracts', () => {
it('accepts a valid point-in-time request', () => {
expect(researchSellPolicyRequestSchema.safeParse(baseRequest).success).toBe(true);
});
it('rejects a lot weight above security weight', () => {
const result = researchSellPolicyRequestSchema.safeParse({
...baseRequest,
currentLotPortfolioWeight: 0.7
});
expect(result.success).toBe(false);
});
it('rejects a future published-at cutoff', () => {
const result = researchSellPolicyRequestSchema.safeParse({
...baseRequest,
publishedAtCutoff: '2026-08-01T08:00:00Z'
});
expect(result.success).toBe(false);
});
it('rejects an unknown production status', () => {
const result = researchSellPolicyResponseSchema.safeParse({
action: 'Hold',
sellRatioOfLot: 0,
targetSecurityPortfolioWeightAfter: 0.6,
policyId: 'ALG-HOLD-001',
reasonCode: 'NO_SELL_CONDITION',
reentryEligible: false,
policyTrace: [],
evidenceStatus: 'PRODUCTION'
});
expect(result.success).toBe(false);
});
});