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,41 @@
import { computed } from 'vue';
import DataGridShell from '../../../shared/ui/DataGridShell.vue';
// Template fixture only. Production data must come from DAT-03 and pass Zod validation.
const rows = [];
const columns = computed(() => [
{ field: 'source', header: 'Source' },
{ field: 'session', header: 'Session' },
{ field: 'status', header: 'DQ' },
{ field: 'rowCount', header: 'Rows' },
{ field: 'failedRows', header: 'Failed' },
{ field: 'sourceWatermark', header: 'Watermark' },
{ field: 'datasetId', header: 'Dataset' },
{ field: 'completedAt', header: 'Completed' }
]);
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)({});
const __VLS_0 = DataGridShell;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
rows: (__VLS_ctx.rows),
columns: (__VLS_ctx.columns),
emptyMessage: "DAT-03 계약이 구현되면 서버 검증 결과가 표시됩니다.",
}));
const __VLS_2 = __VLS_1({
rows: (__VLS_ctx.rows),
columns: (__VLS_ctx.columns),
emptyMessage: "DAT-03 계약이 구현되면 서버 검증 결과가 표시됩니다.",
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
// @ts-ignore
[rows, columns,];
const __VLS_export = (await import('vue')).defineComponent({});
export default {};
@@ -0,0 +1,23 @@
import { z } from 'zod';
export const dataQualityStatusSchema = z.enum(['PASS', 'WARN', 'QUARANTINED']);
export const dataQualityRunSchema = z.object({
runId: z.string().uuid(),
source: z.string().min(1),
session: z.string().min(1),
status: dataQualityStatusSchema,
rowCount: z.number().int().nonnegative(),
failedRows: z.number().int().nonnegative(),
sourceWatermark: z.string().min(1),
datasetId: z.string().min(1),
contentHash: z.string().min(1),
completedAt: z.string().datetime({ offset: true })
}).superRefine((value, ctx) => {
if (value.failedRows > value.rowCount) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: '실패 행 수는 전체 행 수를 초과할 수 없습니다.',
path: ['failedRows']
});
}
});
export const dataQualityRunsSchema = z.array(dataQualityRunSchema);
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { dataQualityRunSchema } from '../schema';
const valid = {
runId: '00000000-0000-0000-0000-000000000001',
source: 'KRX',
session: '2026-08-01',
status: 'PASS',
rowCount: 100,
failedRows: 0,
sourceWatermark: 'KRX:2026-08-01',
datasetId: 'dataset-1',
contentHash: 'hash-1',
completedAt: '2026-08-01T09:00:00Z'
};
describe('data quality contract', () => {
it('accepts a valid run', () => {
expect(dataQualityRunSchema.safeParse(valid).success).toBe(true);
});
it('rejects failed rows greater than total rows', () => {
expect(dataQualityRunSchema.safeParse({ ...valid, failedRows: 101 }).success).toBe(false);
});
});
@@ -0,0 +1,6 @@
import { api } from '../../shared/api/client';
import { modelOperationsPlanSchema } from './schema';
export async function getModelOperationsPlan() {
const response = await api.get('/internal/v1/model-operations/plan');
return modelOperationsPlanSchema.parse(response.data);
}
@@ -0,0 +1,35 @@
const __VLS_props = defineProps();
const __VLS_ctx = {
...{},
...{},
...{},
};
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
__VLS_asFunctionalElement1(__VLS_intrinsics.section, __VLS_intrinsics.section)({
'aria-labelledby': "automation-boundary-title",
});
__VLS_asFunctionalElement1(__VLS_intrinsics.h2, __VLS_intrinsics.h2)({
id: "automation-boundary-title",
});
__VLS_asFunctionalElement1(__VLS_intrinsics.dl, __VLS_intrinsics.dl)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
(__VLS_ctx.algorithmStatus);
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
(__VLS_ctx.orderCapability);
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dt, __VLS_intrinsics.dt)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.dd, __VLS_intrinsics.dd)({});
(__VLS_ctx.modelMutationBoundary);
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
// @ts-ignore
[algorithmStatus, orderCapability, modelMutationBoundary,];
const __VLS_export = (await import('vue')).defineComponent({
__typeProps: {},
});
export default {};
@@ -0,0 +1,61 @@
const __VLS_props = defineProps();
const __VLS_ctx = {
...{},
...{},
...{},
};
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
__VLS_asFunctionalElement1(__VLS_intrinsics.section, __VLS_intrinsics.section)({
'aria-labelledby': "operation-plan-title",
});
__VLS_asFunctionalElement1(__VLS_intrinsics.h2, __VLS_intrinsics.h2)({
id: "operation-plan-title",
});
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "table-wrap" },
});
/** @type {__VLS_StyleScopedClasses['table-wrap']} */ ;
__VLS_asFunctionalElement1(__VLS_intrinsics.table, __VLS_intrinsics.table)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.thead, __VLS_intrinsics.thead)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.tr, __VLS_intrinsics.tr)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.th, __VLS_intrinsics.th)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.th, __VLS_intrinsics.th)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.th, __VLS_intrinsics.th)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.th, __VLS_intrinsics.th)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.th, __VLS_intrinsics.th)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.th, __VLS_intrinsics.th)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.th, __VLS_intrinsics.th)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.th, __VLS_intrinsics.th)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.tbody, __VLS_intrinsics.tbody)({});
for (const [operation] of __VLS_vFor((__VLS_ctx.operations))) {
__VLS_asFunctionalElement1(__VLS_intrinsics.tr, __VLS_intrinsics.tr)({
key: (operation.operationCode),
});
__VLS_asFunctionalElement1(__VLS_intrinsics.td, __VLS_intrinsics.td)({});
(operation.operationCode);
__VLS_asFunctionalElement1(__VLS_intrinsics.td, __VLS_intrinsics.td)({});
(operation.name);
__VLS_asFunctionalElement1(__VLS_intrinsics.td, __VLS_intrinsics.td)({});
(operation.cadence);
__VLS_asFunctionalElement1(__VLS_intrinsics.td, __VLS_intrinsics.td)({});
(operation.automationMode);
__VLS_asFunctionalElement1(__VLS_intrinsics.td, __VLS_intrinsics.td)({});
(operation.queue);
__VLS_asFunctionalElement1(__VLS_intrinsics.td, __VLS_intrinsics.td)({});
(operation.gate);
__VLS_asFunctionalElement1(__VLS_intrinsics.td, __VLS_intrinsics.td)({});
(operation.primaryOwner);
(operation.secondaryOwner);
__VLS_asFunctionalElement1(__VLS_intrinsics.td, __VLS_intrinsics.td)({});
(operation.output);
// @ts-ignore
[operations,];
}
// @ts-ignore
[];
const __VLS_export = (await import('vue')).defineComponent({
__typeProps: {},
});
export default {};
@@ -0,0 +1,58 @@
import QueryStateBoundary from '../../../shared/ui/QueryStateBoundary.vue';
import AutomationBoundaryPanel from '../components/AutomationBoundaryPanel.vue';
import ModelOperationTable from '../components/ModelOperationTable.vue';
import { useModelOperationsPlanQuery } from '../queries';
const planQuery = useModelOperationsPlanQuery();
const __VLS_ctx = {
...{},
...{},
};
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
__VLS_asFunctionalElement1(__VLS_intrinsics.article, __VLS_intrinsics.article)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.header, __VLS_intrinsics.header)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.h1, __VLS_intrinsics.h1)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
const __VLS_0 = QueryStateBoundary || QueryStateBoundary;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
loading: (__VLS_ctx.planQuery.isLoading.value),
error: __VLS_ctx.planQuery.error.value,
empty: (!__VLS_ctx.planQuery.data.value),
}));
const __VLS_2 = __VLS_1({
loading: (__VLS_ctx.planQuery.isLoading.value),
error: __VLS_ctx.planQuery.error.value,
empty: (!__VLS_ctx.planQuery.data.value),
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
const { default: __VLS_5 } = __VLS_3.slots;
if (__VLS_ctx.planQuery.data.value) {
const __VLS_6 = AutomationBoundaryPanel;
// @ts-ignore
const __VLS_7 = __VLS_asFunctionalComponent1(__VLS_6, new __VLS_6({
algorithmStatus: (__VLS_ctx.planQuery.data.value.algorithmStatus),
orderCapability: (__VLS_ctx.planQuery.data.value.orderCapability),
modelMutationBoundary: (__VLS_ctx.planQuery.data.value.modelMutationBoundary),
}));
const __VLS_8 = __VLS_7({
algorithmStatus: (__VLS_ctx.planQuery.data.value.algorithmStatus),
orderCapability: (__VLS_ctx.planQuery.data.value.orderCapability),
modelMutationBoundary: (__VLS_ctx.planQuery.data.value.modelMutationBoundary),
}, ...__VLS_functionalComponentArgsRest(__VLS_7));
const __VLS_11 = ModelOperationTable;
// @ts-ignore
const __VLS_12 = __VLS_asFunctionalComponent1(__VLS_11, new __VLS_11({
operations: (__VLS_ctx.planQuery.data.value.operations),
}));
const __VLS_13 = __VLS_12({
operations: (__VLS_ctx.planQuery.data.value.operations),
}, ...__VLS_functionalComponentArgsRest(__VLS_12));
}
// @ts-ignore
[planQuery, planQuery, planQuery, planQuery, planQuery, planQuery, planQuery, planQuery,];
var __VLS_3;
// @ts-ignore
[];
const __VLS_export = (await import('vue')).defineComponent({});
export default {};
@@ -0,0 +1,14 @@
import { useQuery } from '@tanstack/vue-query';
import { getModelOperationsPlan } from './api';
export const modelOperationsKeys = {
all: ['model-operations'],
plan: () => [...modelOperationsKeys.all, 'plan']
};
export function useModelOperationsPlanQuery() {
return useQuery({
queryKey: modelOperationsKeys.plan(),
queryFn: getModelOperationsPlan,
staleTime: 5 * 60 * 1000,
retry: 1
});
}
@@ -0,0 +1,19 @@
import { z } from 'zod';
export const operationItemSchema = z.object({
operationCode: z.string().min(1),
name: z.string().min(1),
cadence: z.enum(['DAILY', 'WEEKLY', 'MONTHLY', 'QUARTERLY', 'EVENT_DRIVEN']),
automationMode: z.enum(['EVALUATION_ONLY', 'PROPOSAL_ONLY', 'DRILL_ONLY']),
queue: z.string().min(1),
primaryOwner: z.string().min(1),
secondaryOwner: z.string().min(1),
requiredEvidence: z.string().min(1),
output: z.string().min(1),
gate: z.string().min(1)
});
export const modelOperationsPlanSchema = z.object({
algorithmStatus: z.literal('RESEARCH_CANDIDATE_NOT_PRODUCTION'),
orderCapability: z.literal('AUTOMATIC_ORDER_AND_KIS_SUBMISSION_OFF'),
modelMutationBoundary: z.literal('EVALUATION_AND_PROPOSAL_ONLY_HUMAN_APPROVAL_REQUIRED'),
operations: z.array(operationItemSchema)
});
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import { modelOperationsPlanSchema } from '../schema';
describe('model operations plan schema', () => {
it('rejects an automatic promotion mode', () => {
const result = modelOperationsPlanSchema.safeParse({
algorithmStatus: 'RESEARCH_CANDIDATE_NOT_PRODUCTION',
orderCapability: 'AUTOMATIC_ORDER_AND_KIS_SUBMISSION_OFF',
modelMutationBoundary: 'EVALUATION_AND_PROPOSAL_ONLY_HUMAN_APPROVAL_REQUIRED',
operations: [{
operationCode: 'J22',
name: 'PromotionEvidenceReviewBuild',
cadence: 'MONTHLY',
automationMode: 'AUTO_PROMOTE',
queue: 'q-control',
primaryOwner: 'Risk',
secondaryOwner: 'Compliance',
requiredEvidence: 'all evidence',
output: 'review packet',
gate: 'G4'
}]
});
expect(result.success).toBe(false);
});
});
@@ -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);
});
});
@@ -0,0 +1,185 @@
import { computed, ref } from 'vue';
import { SearchListCrudPage } from '@/shared/ui/screen-types';
import { screenTemplateCatalogue } from '@/shared/ui/screen-types/catalogue';
import { KsButton, KsDataGrid, KsSelect, KsStatusTag, KsTextField } from '@/shared/ui/components';
import { useUiAdapter } from '@/shared/ui/adapter/useUiAdapter';
const adapter = useUiAdapter();
const query = ref('');
const status = ref('ALL');
const options = [{ label: '전체', value: 'ALL' }, { label: '검토 필요', value: 'REVIEW' }, { label: '보류', value: 'HOLD' }];
const rows = computed(() => screenTemplateCatalogue.filter(x => !query.value || `${x.id} ${x.name} ${x.component}`.toLowerCase().includes(query.value.toLowerCase())).map(x => ({ id: x.id, name: x.name, component: x.component, evidence: x.mandatoryEvidence.length, state: 'READY' })));
const columns = [{ field: 'id', header: '화면 ID', width: 100 }, { field: 'name', header: '화면 타입' }, { field: 'component', header: '표준 컴포넌트', minWidth: 220 }, { field: 'evidence', header: '필수 증거', width: 110 }, { field: 'state', header: '상태', width: 110 }];
const __VLS_ctx = {
...{},
...{},
};
let __VLS_components;
let __VLS_intrinsics;
let __VLS_directives;
/** @type {__VLS_StyleScopedClasses['summary']} */ ;
/** @type {__VLS_StyleScopedClasses['summary']} */ ;
/** @type {__VLS_StyleScopedClasses['detail']} */ ;
/** @type {__VLS_StyleScopedClasses['filters']} */ ;
let __VLS_0;
/** @ts-ignore @type { | typeof __VLS_components.SearchListCrudPage | typeof __VLS_components.SearchListCrudPage} */
SearchListCrudPage;
// @ts-ignore
const __VLS_1 = __VLS_asFunctionalComponent1(__VLS_0, new __VLS_0({
title: "표준 UI 패턴",
subtitle: "Feature는 공급자 라이브러리를 직접 사용하지 않고, v2 어댑터·레이아웃·화면 계약을 사용한다.",
state: "READY",
evidence: ({ asOf: '2026-08-02', version: 'UI-CONTRACT-2.0' }),
}));
const __VLS_2 = __VLS_1({
title: "표준 UI 패턴",
subtitle: "Feature는 공급자 라이브러리를 직접 사용하지 않고, v2 어댑터·레이아웃·화면 계약을 사용한다.",
state: "READY",
evidence: ({ asOf: '2026-08-02', version: 'UI-CONTRACT-2.0' }),
}, ...__VLS_functionalComponentArgsRest(__VLS_1));
var __VLS_5;
const { default: __VLS_6 } = __VLS_3.slots;
{
const { actions: __VLS_7 } = __VLS_3.slots;
let __VLS_8;
/** @ts-ignore @type { | typeof __VLS_components.KsButton} */
KsButton;
// @ts-ignore
const __VLS_9 = __VLS_asFunctionalComponent1(__VLS_8, new __VLS_8({
label: "새 화면 패킷",
severity: "secondary",
}));
const __VLS_10 = __VLS_9({
label: "새 화면 패킷",
severity: "secondary",
}, ...__VLS_functionalComponentArgsRest(__VLS_9));
}
{
const { summary: __VLS_13 } = __VLS_3.slots;
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "ks-card summary" },
});
/** @type {__VLS_StyleScopedClasses['ks-card']} */ ;
/** @type {__VLS_StyleScopedClasses['summary']} */ ;
__VLS_asFunctionalElement1(__VLS_intrinsics.strong, __VLS_intrinsics.strong)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "ks-card summary" },
});
/** @type {__VLS_StyleScopedClasses['ks-card']} */ ;
/** @type {__VLS_StyleScopedClasses['summary']} */ ;
__VLS_asFunctionalElement1(__VLS_intrinsics.strong, __VLS_intrinsics.strong)({});
(__VLS_ctx.adapter.descriptor.capabilities.size);
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "ks-card summary" },
});
/** @type {__VLS_StyleScopedClasses['ks-card']} */ ;
/** @type {__VLS_StyleScopedClasses['summary']} */ ;
let __VLS_14;
/** @ts-ignore @type { | typeof __VLS_components.KsStatusTag} */
KsStatusTag;
// @ts-ignore
const __VLS_15 = __VLS_asFunctionalComponent1(__VLS_14, new __VLS_14({
value: (__VLS_ctx.adapter.descriptor.id),
severity: "info",
}));
const __VLS_16 = __VLS_15({
value: (__VLS_ctx.adapter.descriptor.id),
severity: "info",
}, ...__VLS_functionalComponentArgsRest(__VLS_15));
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
(__VLS_ctx.adapter.descriptor.vendor);
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "ks-card summary" },
});
/** @type {__VLS_StyleScopedClasses['ks-card']} */ ;
/** @type {__VLS_StyleScopedClasses['summary']} */ ;
let __VLS_19;
/** @ts-ignore @type { | typeof __VLS_components.KsStatusTag} */
KsStatusTag;
// @ts-ignore
const __VLS_20 = __VLS_asFunctionalComponent1(__VLS_19, new __VLS_19({
value: "자동주문 OFF",
severity: "warning",
}));
const __VLS_21 = __VLS_20({
value: "자동주문 OFF",
severity: "warning",
}, ...__VLS_functionalComponentArgsRest(__VLS_20));
__VLS_asFunctionalElement1(__VLS_intrinsics.span, __VLS_intrinsics.span)({});
// @ts-ignore
[adapter, adapter, adapter,];
}
{
const { filters: __VLS_24 } = __VLS_3.slots;
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "filters" },
});
/** @type {__VLS_StyleScopedClasses['filters']} */ ;
let __VLS_25;
/** @ts-ignore @type { | typeof __VLS_components.KsTextField} */
KsTextField;
// @ts-ignore
const __VLS_26 = __VLS_asFunctionalComponent1(__VLS_25, new __VLS_25({
modelValue: (__VLS_ctx.query),
label: "검색",
placeholder: "화면 ID, 타입 또는 컴포넌트",
}));
const __VLS_27 = __VLS_26({
modelValue: (__VLS_ctx.query),
label: "검색",
placeholder: "화면 ID, 타입 또는 컴포넌트",
}, ...__VLS_functionalComponentArgsRest(__VLS_26));
let __VLS_30;
/** @ts-ignore @type { | typeof __VLS_components.KsSelect} */
KsSelect;
// @ts-ignore
const __VLS_31 = __VLS_asFunctionalComponent1(__VLS_30, new __VLS_30({
modelValue: (__VLS_ctx.status),
label: "상태",
options: (__VLS_ctx.options),
}));
const __VLS_32 = __VLS_31({
modelValue: (__VLS_ctx.status),
label: "상태",
options: (__VLS_ctx.options),
}, ...__VLS_functionalComponentArgsRest(__VLS_31));
// @ts-ignore
[query, status, options,];
}
let __VLS_35;
/** @ts-ignore @type { | typeof __VLS_components.KsDataGrid} */
KsDataGrid;
// @ts-ignore
const __VLS_36 = __VLS_asFunctionalComponent1(__VLS_35, new __VLS_35({
rows: (__VLS_ctx.rows),
columns: (__VLS_ctx.columns),
height: "25rem",
}));
const __VLS_37 = __VLS_36({
rows: (__VLS_ctx.rows),
columns: (__VLS_ctx.columns),
height: "25rem",
}, ...__VLS_functionalComponentArgsRest(__VLS_36));
{
const { detail: __VLS_40 } = __VLS_3.slots;
__VLS_asFunctionalElement1(__VLS_intrinsics.div, __VLS_intrinsics.div)({
...{ class: "ks-card detail" },
});
/** @type {__VLS_StyleScopedClasses['ks-card']} */ ;
/** @type {__VLS_StyleScopedClasses['detail']} */ ;
__VLS_asFunctionalElement1(__VLS_intrinsics.h2, __VLS_intrinsics.h2)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.code, __VLS_intrinsics.code)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
__VLS_asFunctionalElement1(__VLS_intrinsics.p, __VLS_intrinsics.p)({});
// @ts-ignore
[rows, columns,];
}
// @ts-ignore
[];
var __VLS_3;
// @ts-ignore
[];
const __VLS_export = (await import('vue')).defineComponent({});
export default {};