NPC 결정에서 당시 정책 버전을 조회하고 기존 상세 화면 재사용

This commit is contained in:
2026-09-16 08:20:29 +00:00
parent 22cf566c6b
commit b470e32e81
6 changed files with 230 additions and 127 deletions
+28 -1
View File
@@ -61,7 +61,7 @@ const decision = {
coverage: 'PROCEDURES', coverage: 'PROCEDURES',
clockRevision: 1, clockRevision: 1,
codeVersion: null, codeVersion: null,
policyRefs: {}, policyRefs: { DEFENCE: 'a'.repeat(64) },
requestedAction: '휴식', requestedAction: '휴식',
selectedAction: 'che_징병', selectedAction: 'che_징병',
selectedReason: '징병 선택', selectedReason: '징병 선택',
@@ -1154,3 +1154,30 @@ test('NPC decision detail retry preserves history and other general information'
expect(requests.filter((r) => r.operation === 'playAudit.decisionHistory')).toHaveLength(count); expect(requests.filter((r) => r.operation === 'playAudit.decisionHistory')).toHaveLength(count);
await capture(page, 'desktop-npc-decision'); await capture(page, 'desktop-npc-decision');
}); });
test('NPC decision opens its immutable policy without querying policy history', async ({ page }) => {
const requests = await install(page);
await page.setViewportSize({ width: 390, height: 844 });
await page.goto(gamePath(`/play-audit?tab=generals&general=1&decision=${decision.id}`));
await expect(page.getByRole('list', { name: '판단 절차' })).toBeVisible();
expect(requests.some((r) => r.operation === 'playAudit.policyVersion')).toBe(false);
const before = requests.length;
await page.getByText('당시 정책 참조', { exact: true }).click();
await page.getByRole('button', { name: '국방 설정 당시 버전 조회', exact: true }).click();
await expect(page.getByText(/국가 #2 · 버전 1/)).toBeVisible();
expect(requests.slice(before).map((r) => r.operation)).toEqual(['playAudit.policyVersion']);
expect(requests.at(-1)?.input).toEqual({ id: 'a'.repeat(64) });
await expect(page.getByRole('button', { name: '이전 정책 버전', exact: true })).toHaveCount(0);
expect(await page.evaluate(() => Object.hasOwn(window, 'auditInjected'))).toBe(false);
await capture(page, 'mobile-decision-policy');
await page.reload();
await expect(page.getByText(/국가 #2 · 버전 1/)).toBeVisible();
expect(requests.some((r) => r.operation === 'playAudit.policyHistory')).toBe(false);
await page.getByRole('button', { name: '정책 상세 닫기', exact: true }).click();
await expect(page.getByRole('heading', { name: /선택 정책 버전/ })).toHaveCount(0);
const policyReads = requests.filter((r) => r.operation === 'playAudit.policyVersion').length;
await page.goto(gamePath(`/play-audit?tab=generals&general=1&decision=${decision.id}&policy=${'b'.repeat(64)}`));
await expect(page.getByRole('list', { name: '판단 절차' })).toBeVisible();
expect(requests.filter((r) => r.operation === 'playAudit.policyVersion')).toHaveLength(policyReads);
await expect(page.getByRole('heading', { name: /선택 정책 버전/ })).toHaveCount(0);
});
@@ -2,6 +2,7 @@
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { trpc } from '../../utils/trpc'; import { trpc } from '../../utils/trpc';
import AuditPolicyVersion from './AuditPolicyVersion.vue';
const props = defineProps<{ generalId: number; month?: { year: number; month: number } }>(); const props = defineProps<{ generalId: number; month?: { year: number; month: number } }>();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@@ -17,6 +18,19 @@ const detailLoading = ref(false);
let generation = 0; let generation = 0;
let detailGeneration = 0; let detailGeneration = 0;
const selected = computed(() => (typeof route.query.decision === 'string' ? route.query.decision : null)); const selected = computed(() => (typeof route.query.decision === 'string' ? route.query.decision : null));
const policyLabels = {
NPC_VALUES: 'NPC 설정 값',
NPC_NATION_PRIORITY: '수뇌 우선순위',
NPC_GENERAL_PRIORITY: '개인 우선순위',
DEFENCE: '국방 설정',
};
const selectedPolicy = computed(() => {
const id = route.query.policy;
return typeof id === 'string' && Object.values(detail.value?.decision.summary.policyRefs ?? {}).includes(id)
? id
: null;
});
const selectPolicy = (id: string | null) => router.push({ query: { ...route.query, policy: id ?? undefined } });
const message = (cause: unknown) => (cause instanceof Error ? cause.message : 'NPC 결정 기록을 조회하지 못했습니다.'); const message = (cause: unknown) => (cause instanceof Error ? cause.message : 'NPC 결정 기록을 조회하지 못했습니다.');
const load = async (more = false) => { const load = async (more = false) => {
if (loading.value) return; if (loading.value) return;
@@ -191,8 +205,19 @@ watch(
<details> <details>
<summary>당시 정책 참조</summary> <summary>당시 정책 참조</summary>
<p v-if="!Object.keys(detail.decision.summary.policyRefs).length">확보된 정책 참조가 없습니다.</p> <p v-if="!Object.keys(detail.decision.summary.policyRefs).length">확보된 정책 참조가 없습니다.</p>
<p v-for="(id, area) in detail.decision.summary.policyRefs" :key="area">{{ area }}: {{ id }}</p> <p>선택 당시 저장된 국가 설정입니다. NPC별 합성 유효 값과 다를 있습니다.</p>
<p v-for="(id, area) in detail.decision.summary.policyRefs" :key="area">
<button class="legacy-button" @click="selectPolicy(id ?? null)">
{{ policyLabels[area] }} 당시 버전 조회
</button>
</p>
</details> </details>
<AuditPolicyVersion
v-if="selectedPolicy"
:id="selectedPolicy"
:allow-previous="false"
@select="selectPolicy"
/>
<ol aria-label="판단 절차"> <ol aria-label="판단 절차">
<template v-for="chunk in detail.chunks" :key="chunk.ordinal" <template v-for="chunk in detail.chunks" :key="chunk.ordinal"
><li v-for="step in chunk.steps" :key="step.sequence" :value="step.sequence + 1"> ><li v-for="step in chunk.steps" :key="step.sequence" :value="step.sequence + 1">
@@ -221,6 +246,9 @@ watch(
min-width: 0; min-width: 0;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.audit-decisions > section {
min-width: 0;
}
.table-scroll { .table-scroll {
overflow-x: auto; overflow-x: auto;
} }
@@ -2,6 +2,7 @@
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { trpc } from '../../utils/trpc'; import { trpc } from '../../utils/trpc';
import AuditPolicyVersion from './AuditPolicyVersion.vue';
const props = defineProps<{ const props = defineProps<{
nationId: number; nationId: number;
@@ -10,47 +11,13 @@ const props = defineProps<{
to: { year: number; month: number }; to: { year: number; month: number };
}>(); }>();
type History = Awaited<ReturnType<typeof trpc.playAudit.policyHistory.query>>; type History = Awaited<ReturnType<typeof trpc.playAudit.policyHistory.query>>;
type Detail = Awaited<ReturnType<typeof trpc.playAudit.policyVersion.query>>;
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const data = ref<History | null>(null); const data = ref<History | null>(null);
const detail = ref<Detail | null>(null);
const error = ref(''); const error = ref('');
const detailError = ref('');
const loading = ref(false); const loading = ref(false);
const detailLoading = ref(false);
let generation = 0; let generation = 0;
let detailGeneration = 0;
const selected = computed(() => (typeof route.query.policy === 'string' ? route.query.policy : null)); const selected = computed(() => (typeof route.query.policy === 'string' ? route.query.policy : null));
const fieldLabels: Record<string, string> = {
reqNationGold: '국가 권장 금',
reqNationRice: '국가 권장 쌀',
reqHumanWarUrgentGold: '유저전투장 긴급포상 금',
reqHumanWarUrgentRice: '유저전투장 긴급포상 쌀',
reqHumanWarRecommandGold: '유저전투장 권장 금',
reqHumanWarRecommandRice: '유저전투장 권장 쌀',
reqHumanDevelGold: '유저내정장 권장 금',
reqHumanDevelRice: '유저내정장 권장 쌀',
reqNPCWarGold: 'NPC전투장 권장 금',
reqNPCWarRice: 'NPC전투장 권장 쌀',
reqNPCDevelGold: 'NPC내정장 권장 금',
reqNPCDevelRice: 'NPC내정장 권장 쌀',
minimumResourceActionAmount: '포상/몰수/헌납/삼/팜 최소 단위',
maximumResourceActionAmount: '포상/몰수/헌납/삼/팜 최대 단위',
minWarCrew: '최소 전투 가능 병력 수',
minNPCRecruitCityPopulation: 'NPC 최소 징병 가능 인구 수',
safeRecruitCityPopulationRatio: '제자리 징병 허용 인구율 (비율)',
minNPCWarLeadership: 'NPC 전투 참여 통솔 기준',
properWarTrainAtmos: '훈련/사기진작 목표치',
cureThreshold: '요양 기준',
CombatForce: '전투 부대 편성',
SupportForce: '지원 부대 편성',
DevelopForce: '내정 부대 편성',
priority: '행동 우선순위',
war: '전쟁 금지 설정',
scout: '임관 권유 설정',
secretlimit: '기밀 공개 기준 (년)',
};
const labels = { BASELINE: '최초 관측', CHANGE: '실제 변경', OBSERVED_GAP: '관측 누락 이후 기준' }; const labels = { BASELINE: '최초 관측', CHANGE: '실제 변경', OBSERVED_GAP: '관측 누락 이후 기준' };
const message = (cause: unknown) => (cause instanceof Error ? cause.message : '정책 이력을 조회하지 못했습니다.'); const message = (cause: unknown) => (cause instanceof Error ? cause.message : '정책 이력을 조회하지 못했습니다.');
const load = async (append = false) => { const load = async (append = false) => {
@@ -75,22 +42,6 @@ const load = async (append = false) => {
if (request === generation) loading.value = false; if (request === generation) loading.value = false;
} }
}; };
const loadDetail = async () => {
const request = ++detailGeneration;
detail.value = null;
detailError.value = '';
detailLoading.value = false;
if (!selected.value) return;
detailLoading.value = true;
try {
const response = await trpc.playAudit.policyVersion.query({ id: selected.value });
if (request === detailGeneration) detail.value = response;
} catch (cause) {
if (request === detailGeneration) detailError.value = message(cause);
} finally {
if (request === detailGeneration) detailLoading.value = false;
}
};
const select = (id: string | null) => router.push({ query: { ...route.query, policy: id ?? undefined } }); const select = (id: string | null) => router.push({ query: { ...route.query, policy: id ?? undefined } });
watch( watch(
[ [
@@ -106,13 +57,6 @@ watch(
}, },
{ immediate: true } { immediate: true }
); );
watch(
selected,
() => {
void loadDetail();
},
{ immediate: true }
);
</script> </script>
<template> <template>
@@ -153,72 +97,7 @@ watch(
다음 정책 50개 다음 정책 50개
</button> </button>
</template> </template>
<section v-if="selected" aria-label="선택 정책 버전"> <AuditPolicyVersion v-if="selected" :id="selected" @select="select" />
<h3>선택 정책 버전 <button class="legacy-button" @click="select(null)">정책 상세 닫기</button></h3>
<p v-if="detailLoading" role="status">정책 버전 조회 </p>
<p v-if="detailError" role="alert">
{{ detailError }} <button class="legacy-button" @click="loadDetail">버전 다시 조회</button>
</p>
<template v-if="detail">
<p>
국가 #{{ detail.version.nationId }} · 버전 {{ detail.version.revision }} ·
{{ labels[detail.version.source] }} · {{ detail.version.year }} {{ detail.version.month }}
</p>
<p>
기록 시각 {{ detail.version.createdAt }} · tick {{ detail.version.tick ?? '미상' }} · 순번
{{ detail.version.ordinal }}
</p>
<p v-if="detail.version.actor">
{{ detail.version.actor.name }} (#{{ detail.version.actor.generalId }}) · 당시 국가 #{{
detail.version.actor.nationId
}}
· 직책 {{ detail.version.actor.officerLevel }}
</p>
<p v-if="detail.version.source !== 'CHANGE'">
버전은 관측 기준입니다. 이전 값과 변경 주체를 추정하지 않습니다.
</p>
<p>
null은 개별 설정이 없음을 뜻합니다. 설정값을 기록하며 당시 NPC별 유효 값은 여기서 재계산하지
않습니다.
</p>
<div class="table-scroll" tabindex="0" aria-label="정책 전후 ">
<table>
<thead>
<tr>
<th>설정</th>
<th>변경 </th>
<th>변경 </th>
</tr>
</thead>
<tbody>
<tr v-for="field in detail.version.fields" :key="field.key">
<th scope="row">
{{ fieldLabels[field.key] ?? field.key }} <span v-if="field.changed">(변경)</span>
</th>
<td>
<pre>{{ field.beforeJson ?? '관측하지 않음' }}</pre>
</td>
<td>
<pre>{{ field.afterJson }}</pre>
</td>
</tr>
</tbody>
</table>
</div>
<button
v-if="detail.version.previousId"
class="legacy-button"
@click="select(detail.version.previousId)"
>
이전 정책 버전
</button>
<details>
<summary>요청 연결</summary>
<p>요청 {{ detail.version.requestId ?? '해당 없음' }}</p>
<p>입력 순번 {{ detail.version.inputSequence ?? '해당 없음' }}</p>
</details>
</template>
</section>
</section> </section>
</template> </template>
@@ -0,0 +1,165 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { trpc } from '../../utils/trpc';
const props = withDefaults(defineProps<{ id: string; allowPrevious?: boolean }>(), { allowPrevious: true });
const emit = defineEmits<{ select: [id: string | null] }>();
type Detail = Awaited<ReturnType<typeof trpc.playAudit.policyVersion.query>>;
const detail = ref<Detail | null>(null);
const detailError = ref('');
const detailLoading = ref(false);
let detailGeneration = 0;
const select = (id: string | null) => emit('select', id);
const message = (cause: unknown) => (cause instanceof Error ? cause.message : '정책 버전을 조회하지 못했습니다.');
const fieldLabels: Record<string, string> = {
reqNationGold: '국가 권장 금',
reqNationRice: '국가 권장 쌀',
reqHumanWarUrgentGold: '유저전투장 긴급포상 금',
reqHumanWarUrgentRice: '유저전투장 긴급포상 쌀',
reqHumanWarRecommandGold: '유저전투장 권장 금',
reqHumanWarRecommandRice: '유저전투장 권장 쌀',
reqHumanDevelGold: '유저내정장 권장 금',
reqHumanDevelRice: '유저내정장 권장 쌀',
reqNPCWarGold: 'NPC전투장 권장 금',
reqNPCWarRice: 'NPC전투장 권장 쌀',
reqNPCDevelGold: 'NPC내정장 권장 금',
reqNPCDevelRice: 'NPC내정장 권장 쌀',
minimumResourceActionAmount: '포상/몰수/헌납/삼/팜 최소 단위',
maximumResourceActionAmount: '포상/몰수/헌납/삼/팜 최대 단위',
minWarCrew: '최소 전투 가능 병력 수',
minNPCRecruitCityPopulation: 'NPC 최소 징병 가능 인구 수',
safeRecruitCityPopulationRatio: '제자리 징병 허용 인구율 (비율)',
minNPCWarLeadership: 'NPC 전투 참여 통솔 기준',
properWarTrainAtmos: '훈련/사기진작 목표치',
cureThreshold: '요양 기준',
CombatForce: '전투 부대 편성',
SupportForce: '지원 부대 편성',
DevelopForce: '내정 부대 편성',
priority: '행동 우선순위',
war: '전쟁 금지 설정',
scout: '임관 권유 설정',
secretlimit: '기밀 공개 기준 (년)',
};
const labels = { BASELINE: '최초 관측', CHANGE: '실제 변경', OBSERVED_GAP: '관측 누락 이후 기준' };
const loadDetail = async () => {
const request = ++detailGeneration;
detail.value = null;
detailError.value = '';
detailLoading.value = false;
if (!props.id) return;
detailLoading.value = true;
try {
const response = await trpc.playAudit.policyVersion.query({ id: props.id });
if (request === detailGeneration) detail.value = response;
} catch (cause) {
if (request === detailGeneration) detailError.value = message(cause);
} finally {
if (request === detailGeneration) detailLoading.value = false;
}
};
watch(
() => props.id,
() => {
void loadDetail();
},
{ immediate: true }
);
</script>
<template>
<section aria-label="선택 정책 버전">
<h3>선택 정책 버전 <button class="legacy-button" @click="select(null)">정책 상세 닫기</button></h3>
<p v-if="detailLoading" role="status">정책 버전 조회 </p>
<p v-if="detailError" role="alert">
{{ detailError }} <button class="legacy-button" @click="loadDetail">버전 다시 조회</button>
</p>
<template v-if="detail">
<p>
국가 #{{ detail.version.nationId }} · 버전 {{ detail.version.revision }} ·
{{ labels[detail.version.source] }} · {{ detail.version.year }} {{ detail.version.month }}
</p>
<p>
기록 시각 {{ detail.version.createdAt }} · tick {{ detail.version.tick ?? '미상' }} · 순번
{{ detail.version.ordinal }}
</p>
<p v-if="detail.version.actor">
{{ detail.version.actor.name }} (#{{ detail.version.actor.generalId }}) · 당시 국가 #{{
detail.version.actor.nationId
}}
· 직책 {{ detail.version.actor.officerLevel }}
</p>
<p v-if="detail.version.source !== 'CHANGE'">
버전은 관측 기준입니다. 이전 값과 변경 주체를 추정하지 않습니다.
</p>
<p>
null은 개별 설정이 없음을 뜻합니다. 설정값을 기록하며 당시 NPC별 유효 값은 여기서 재계산하지 않습니다.
</p>
<div class="table-scroll" tabindex="0" aria-label="정책 전후 ">
<table>
<thead>
<tr>
<th>설정</th>
<th>변경 </th>
<th>변경 </th>
</tr>
</thead>
<tbody>
<tr v-for="field in detail.version.fields" :key="field.key">
<th scope="row">
{{ fieldLabels[field.key] ?? field.key }} <span v-if="field.changed">(변경)</span>
</th>
<td>
<pre>{{ field.beforeJson ?? '관측하지 않음' }}</pre>
</td>
<td>
<pre>{{ field.afterJson }}</pre>
</td>
</tr>
</tbody>
</table>
</div>
<button
v-if="allowPrevious && detail.version.previousId"
class="legacy-button"
@click="select(detail.version.previousId)"
>
이전 정책 버전
</button>
<details>
<summary>요청 연결</summary>
<p>요청 {{ detail.version.requestId ?? '해당 없음' }}</p>
<p>입력 순번 {{ detail.version.inputSequence ?? '해당 없음' }}</p>
</details>
</template>
</section>
</template>
<style scoped>
.table-scroll {
overflow-x: auto;
}
table {
width: 100%;
min-width: 640px;
border-collapse: collapse;
}
th,
td {
border: 1px solid gray;
padding: 6px;
text-align: left;
}
pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
max-width: 400px;
font: inherit;
margin: 0;
}
h3 {
font-size: var(--sammo-font-size-normal);
}
[role='alert'] {
color: #ffb9b9;
}
details {
overflow-wrap: anywhere;
}
</style>
+4
View File
@@ -23,6 +23,10 @@ migration58은 기존 장수 인덱스를 `(server, general, year, month, tick,
버튼·표 스타일을 재사용한다. 열기/상세 선택/더 보기는 명시적으로 수행하고 polling하지 버튼·표 스타일을 재사용한다. 열기/상세 선택/더 보기는 명시적으로 수행하고 polling하지
않는다. 결정 URL 복원과 상세 재시도는 상위 장수 목록을 다시 읽지 않는다. 않는다. 결정 URL 복원과 상세 재시도는 상위 장수 목록을 다시 읽지 않는다.
절차 coverage와 미수집 코드 버전을 표시하며 전체 후보 조건·유효 정책 연결은 남는다. 절차 coverage와 미수집 코드 버전을 표시하며 전체 후보 조건·유효 정책 연결은 남는다.
당시 정책 참조는 공용 `AuditPolicyVersion`으로 연결했다. 기존 정책 이력의 버전 표시·
필드 한국어 이름·실패 재시도를 재사용하며 클릭 시 버전1건만 읽는다. 결정의 참조 ID에
포함되지 않은 URL policy는 해당 결정의 정책으로 읽거나 표시하지 않는다.
국가의 저장 설정과 NPC별 합성 유효 값은 구분하며 현재 설정으로 보충하지 않는다.
### NPC 결정 저장·복구 기반 ### NPC 결정 저장·복구 기반
+2 -2
View File
@@ -16,7 +16,7 @@
| 도시 | 현재/월말 소유·내정 상태와 국가별 주둔 장수 상세 연결 | 월말 주둔은 그달 모든 방문자가 아님. 지도 기반 탐색은 미완성 | | 도시 | 현재/월말 소유·내정 상태와 국가별 주둔 장수 상세 연결 | 월말 주둔은 그달 모든 방문자가 아님. 지도 기반 탐색은 미완성 |
| 외교 | 국가쌍·기간별 문서 제안/승인/철회/파기, 즉시 합의와 월간·명령 관계 전이, 생성/소멸 관계 | 문서 내용과 실제 관계는 별개. 최초 관측 이전 사건은 복원하지 않음 | | 외교 | 국가쌍·기간별 문서 제안/승인/철회/파기, 즉시 합의와 월간·명령 관계 전이, 생성/소멸 관계 | 문서 내용과 실제 관계는 별개. 최초 관측 이전 사건은 복원하지 않음 |
| NPC 결정 | 장수별 개인·수뇌 판단, 절차 시도/차단, 관측한 RNG 결과와 선택·실제 실행 | 새 실행부터 수집하며 후보 내부 조건 전체는 미완성 | | NPC 결정 | 장수별 개인·수뇌 판단, 절차 시도/차단, 관측한 RNG 결과와 선택·실제 실행 | 새 실행부터 수집하며 후보 내부 조건 전체는 미완성 |
| 정책 | NPC 국가 값·국가/장수 우선순위·국방 설정의 기준 버전과 변경 전후, 당시 주체 | 수뇌용 공개 화면과 NPC 결정의 정책 참조 연결은 아직 없음 | | 정책 | NPC 국가 값·국가/장수 우선순위·국방 설정의 기준 버전과 변경 전후, 당시 주체 | 수뇌용 공개 화면은 아직 없음. NPC 결정에서 저장된 당시 버전을 직접 조회 가능 |
목록/그래프와 선택 상세를 분리해 읽는다. 표는 기본50건이며 더 보기를 명시적으로 목록/그래프와 선택 상세를 분리해 읽는다. 표는 기본50건이며 더 보기를 명시적으로
누른다. 현재 상태는 수동으로 조회하며 백그라운드 polling은 하지 않는다. 필터·월·선택 누른다. 현재 상태는 수동으로 조회하며 백그라운드 polling은 하지 않는다. 필터·월·선택
@@ -91,7 +91,7 @@ Gateway의 새 진입/권한 catalog도 사용하려면 같은 commit의 Gateway
- NPC/유저 자동턴의 개인·수뇌 절차, 정책 차단, RNG utility 결과와 최종 실행 결과는 - NPC/유저 자동턴의 개인·수뇌 절차, 정책 차단, RNG utility 결과와 최종 실행 결과는
migration 이후 새 실행부터 저장한다. 후보 내부 조건 전체는 아직 없으며 `PROCEDURES` migration 이후 새 실행부터 저장한다. 후보 내부 조건 전체는 아직 없으며 `PROCEDURES`
coverage로 구분한다. 장수 상세의 **NPC 결정 기록 조회**에서 선택 월의 목록과 순서별 상세를 읽는다. 과거 결정은 역산하지 않는다. coverage로 구분한다. 장수 상세의 **NPC 결정 기록 조회**에서 선택 월의 목록과 순서별 상세를 읽는다. 과거 결정은 역산하지 않는다.
- 결정은 당시 확보된 정책 참조를 보존한다. 합성된 유효 정책 상세와 코드 버전 연결은 - 결정은 당시 확보된 정책 참조를 보존하며 **당시 정책 참조**에서 해당 불변 버전을 바로 조회한다. 합성된 유효 정책 상세와 코드 버전 연결은
아직 미완성이다. 코드 버전이 주입되지 않은 실행은 null로 남기며 현재 버전으로 메우지 않는다. 아직 미완성이다. 코드 버전이 주입되지 않은 실행은 null로 남기며 현재 버전으로 메우지 않는다.
- 계정/IP HMAC 조사, 상세 자원 이동, 예약 변경/실행 연결, 실패·rollback 조사와 알려진 - 계정/IP HMAC 조사, 상세 자원 이동, 예약 변경/실행 연결, 실패·rollback 조사와 알려진
버그 사례 조회는 미완성이다. 자동 탐지·자동 제재 기능도 제공하지 않는다. 버그 사례 조회는 미완성이다. 자동 탐지·자동 제재 기능도 제공하지 않는다.