refs #732: stabilize carrier report result schema
This commit is contained in:
@@ -14,6 +14,8 @@ Oracle VPD로 제한한다. 팀원은 본인 행, 팀장은 본인과 직속 팀
|
||||
- `HMM_CARRIER_ASSIGNMENTS_V.EMPLOYEE_ID`에 `SELF`, `MANAGED_TEAM`, `ALL` 규칙을 적용한다.
|
||||
- 기존 MCP 이름 `search_carrier_performance`, 자연어 Select AI, 선사 `CARRIER_CODE` 조인과 HTML
|
||||
renderer는 유지한다. 고정 SQL이나 대체 조회 패키지를 만들지 않는다.
|
||||
- 연속 호출에서도 renderer 입력이 달라지지 않도록 Select AI 질의 계약이 출력 컬럼의 ASCII
|
||||
uppercase underscore 별칭을 고정한다.
|
||||
|
||||
## 전체 구성
|
||||
|
||||
@@ -43,6 +45,7 @@ Oracle VPD로 제한한다. 팀원은 본인 행, 팀장은 본인과 직속 팀
|
||||
- MCP E1001: `vpdEnforced=true`, 8건
|
||||
- MCP E1002: `vpdEnforced=true`, 2건, 직원 범위 E1002만 포함
|
||||
- E1002 renderer: 입력 2건, HTML에 C001·C002 포함, E1003 미포함
|
||||
- 동일 E1002 조회와 renderer를 두 번 연속 호출해 두 호출 모두 표준 컬럼 9개와 HTML 2건을 확인
|
||||
|
||||
## 문서 지도
|
||||
|
||||
|
||||
@@ -20,6 +20,21 @@
|
||||
선사 조회의 자연어 처리와 `CARRIER_CODE` 조인은 기존 Select AI 프로필을 유지한다. 권한 조건을
|
||||
질문이나 고정 SQL에 넣지 않고 Oracle VPD가 세션 사용자 ID로 자동 적용한다.
|
||||
|
||||
## 출력 스키마 계약
|
||||
|
||||
Select AI는 같은 의미의 컬럼도 호출마다 `CARRIER_CODE`, `carrier Code`, `선사코드`처럼 다른
|
||||
alias로 생성할 수 있다. `vpd-backoffice/config/hmm_carrier_query_contract.json`은 SQL을 고정하지
|
||||
않고 다음 결과 alias만 고정한다.
|
||||
|
||||
```text
|
||||
EMPLOYEE_CODE, EMPLOYEE_NAME, MANAGER_EMPLOYEE_CODE,
|
||||
CARRIER_CODE, CARRIER_NAME, LATEST_REVENUE_USD,
|
||||
LATEST_GROSS_MARGIN_USD, LATEST_SCHEDULE_RELIABILITY_PCT,
|
||||
LATEST_RISK_LEVEL
|
||||
```
|
||||
|
||||
따라서 권한에 따라 행 수는 달라져도 renderer에 전달되는 행 구조는 매 호출 동일하다.
|
||||
|
||||
## 신뢰 경계
|
||||
|
||||
- 화면에 표시된 사용자 코드는 권한 근거가 아니다. 선택 preset의 전용 Bearer token이 근거다.
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
3. 포털 preset마다 `HMM_MCP_BEARER_TOKEN_<EMPLOYEE_CODE>` 전용 토큰을 발급한다.
|
||||
4. 선사 조회 MCP는 기존 `HMM_RDS_FEDERATION_PROFILE`로 `SHOWSQL`을 생성하고 `CB_ORDS`에서 실행한다.
|
||||
운영 도구 정의는 이름과 입력 계약을 유지하고 `executionType`만 `SELECT_AI`로 설정한다.
|
||||
`BACKOFFICE_SELECT_AI_QUERY_CONTRACT_FILE`에는 배포한
|
||||
`config/hmm_carrier_query_contract.json`의 절대 경로를 설정한다.
|
||||
5. 서비스를 재시작하고 동일 질문을 E1001과 E1002로 각각 호출한다.
|
||||
|
||||
성공 기준:
|
||||
@@ -20,6 +22,7 @@
|
||||
비밀번호는 명령·로그·문서에 출력하지 않는다.
|
||||
|
||||
MCP 응답에서 `vpdEnforced=true`, `scopeEmployeeCode`가 선택 사용자와 같은지도 확인한다.
|
||||
같은 E1002 질문을 두 번 실행해 두 응답 모두 표준 alias 9개와 C001·C002 두 행을 반환하는지 확인한다.
|
||||
|
||||
롤백할 때는 VPD 정책을 삭제하지 말고 disable하여 조사 가능 상태로 보존한다. 사용자별 token
|
||||
환경변수도 공용 token으로 되돌리지 않고 사용자 선택 기능을 일시 비활성화한다.
|
||||
|
||||
@@ -104,3 +104,16 @@ underscore만 camelCase로 변환했다.
|
||||
조건문에 하드코딩하지 않는다.
|
||||
|
||||
**재발 방지**: 서로 다른 컬럼 label 표기의 두 응답을 연속 정규화하는 회귀 테스트를 유지한다.
|
||||
|
||||
## 두 번째 리포트의 컬럼이 한글 alias로 바뀌어 깨짐
|
||||
|
||||
**원인**: Select AI가 첫 호출에는 `CARRIER_CODE`, 두 번째 호출에는 `선사코드`처럼 의미는 같지만
|
||||
번역된 alias를 생성했다. 구분자 정규화만으로는 서로 다른 언어의 key를 같은 필드로 판단할 수 없다.
|
||||
|
||||
**확인**: 선행 조회 2건은 정상인데 두 번째 `reportJson.rows`가 `선사명`, `선사코드`,
|
||||
`최신매출Usd` 등을 가지는지 확인한다.
|
||||
|
||||
**해결**: 선사 Select AI query contract에 renderer가 요구하는 ASCII uppercase underscore 출력 alias를
|
||||
선언한다. 실제 SQL, 직원 코드, 결과 값은 고정하지 않는다.
|
||||
|
||||
**재발 방지**: 같은 팀원 질의를 두 번 호출해 두 응답의 key 집합과 renderer HTML을 비교한다.
|
||||
|
||||
28
vpd-backoffice/config/hmm_carrier_query_contract.json
Normal file
28
vpd-backoffice/config/hmm_carrier_query_contract.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"version": 1,
|
||||
"purpose": "Stable structured output contract for HMM carrier performance reports",
|
||||
"applies_to": "HMM_RDS_FEDERATION_PROFILE carrier assignment and performance questions",
|
||||
"required_objects": [
|
||||
"HMM_CARRIER_ASSIGNMENTS_V",
|
||||
"HMM_RDS_CARRIER_LATEST_V"
|
||||
],
|
||||
"join_contract": "Join HMM_CARRIER_ASSIGNMENTS_V.CARRIER_CODE to HMM_RDS_CARRIER_LATEST_V.CARRIER_CODE.",
|
||||
"required_output_aliases": [
|
||||
"EMPLOYEE_CODE",
|
||||
"EMPLOYEE_NAME",
|
||||
"MANAGER_EMPLOYEE_CODE",
|
||||
"CARRIER_CODE",
|
||||
"CARRIER_NAME",
|
||||
"LATEST_REVENUE_USD",
|
||||
"LATEST_GROSS_MARGIN_USD",
|
||||
"LATEST_SCHEDULE_RELIABILITY_PCT",
|
||||
"LATEST_RISK_LEVEL"
|
||||
],
|
||||
"rules": [
|
||||
"Return one ordinary structured row per employee and assigned carrier.",
|
||||
"Always include every required output alias, even when the natural-language question does not explicitly request the employee columns.",
|
||||
"Use the required ASCII uppercase underscore aliases exactly as written. Never translate aliases, insert spaces in aliases, or return HTML and Markdown.",
|
||||
"For latest or current metrics use HMM_RDS_CARRIER_LATEST_V.",
|
||||
"Do not add, weaken, or emulate authorization predicates. Oracle VPD on HMM_CARRIER_ASSIGNMENTS_V enforces the authenticated employee scope."
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class SelectAiServiceTest {
|
||||
@@ -32,4 +34,24 @@ class SelectAiServiceTest {
|
||||
service.validateReadOnlySql("SELECT * FROM x -- bypass"))
|
||||
.isInstanceOf(AppException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void carrierQueryContractRequiresStableAsciiAliases() throws Exception {
|
||||
var contract = new ObjectMapper().readTree(Files.readString(
|
||||
Path.of("config/hmm_carrier_query_contract.json")));
|
||||
|
||||
assertThat(contract.path("required_output_aliases").toString())
|
||||
.contains(
|
||||
"EMPLOYEE_CODE",
|
||||
"CARRIER_CODE",
|
||||
"CARRIER_NAME",
|
||||
"LATEST_REVENUE_USD",
|
||||
"LATEST_GROSS_MARGIN_USD",
|
||||
"LATEST_SCHEDULE_RELIABILITY_PCT",
|
||||
"LATEST_RISK_LEVEL"
|
||||
);
|
||||
assertThat(contract.path("rules").toString())
|
||||
.contains("Never translate aliases")
|
||||
.contains("Oracle VPD");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user