Compare commits

67 Commits

Author SHA1 Message Date
devmrko
016d1af025 refs #708 #739: use Cohere command and vector game scope 2026-07-29 10:50:51 +09:00
devmrko
8b04fb4e89 refs #708: benchmark Llama game scope profile 2026-07-29 10:07:20 +09:00
devmrko
271f764f45 refs #739: formalize game query plan contract 2026-07-29 10:07:20 +09:00
devmrko
9d2c75459f refs #740: add Few-shot management tab 2026-07-28 15:46:53 +09:00
devmrko
041ab4f287 refs #739: separate catalog and fact availability 2026-07-28 13:39:33 +09:00
devmrko
98c8ccde85 refs #739: generalize game target profile guidance 2026-07-28 13:25:00 +09:00
devmrko
987ea44f32 refs #739: guide no-target few-shot SQL generation 2026-07-28 12:49:46 +09:00
devmrko
5409a29297 refs #739: seed verified Smilegate QA few-shot candidates 2026-07-28 12:34:40 +09:00
devmrko
154572c885 refs #739: govern Smilegate few-shot references 2026-07-28 12:17:21 +09:00
devmrko
6d98931ce1 refs #739: normalize MCP query plan envelopes 2026-07-28 10:31:20 +09:00
devmrko
d13d6d48ca refs #739: unify Smilegate game target planning 2026-07-28 10:09:12 +09:00
devmrko
57f6677afb refs #737: align STD-06 partial success baseline 2026-07-27 16:51:05 +09:00
devmrko
00a939f9a1 refs #737: move game query planning into ADB MCP tool 2026-07-27 16:39:58 +09:00
devmrko
8dffa2f515 refs #737: preserve unmatched game mentions in query plan 2026-07-27 16:25:33 +09:00
devmrko
3cc033e4e6 refs #737: align MCP tests with game planning tools 2026-07-27 16:19:03 +09:00
devmrko
f31292ecb3 refs #731: document game scope deployment contract 2026-07-27 16:13:50 +09:00
devmrko
040d4f3d4b remove unconfigured scope bypass 2026-07-27 14:54:52 +09:00
devmrko
fbb04a55a2 allow plan context when optional scope view unavailable 2026-07-27 14:54:10 +09:00
devmrko
956693bb23 return structured next action for game query plan 2026-07-27 14:49:30 +09:00
devmrko
ec5d731bf5 pass previous tool context into few-shot query 2026-07-27 14:47:37 +09:00
devmrko
3a7e2ced00 mark approved game plan candidates as supported 2026-07-27 14:34:51 +09:00
devmrko
55faaa0794 remove hardcoded game scope policy from prompt 2026-07-27 14:33:30 +09:00
devmrko
10581dc605 add game query plan MCP tool 2026-07-27 14:23:17 +09:00
devmrko
beaef2d171 chain ADB chat game extraction before vector resolution 2026-07-27 14:14:20 +09:00
devmrko
ed3139f9f7 add OCI GenAI game mention extraction function 2026-07-27 14:12:37 +09:00
devmrko
bd651eb66f inject game catalog context into few-shot prompt 2026-07-27 13:58:37 +09:00
devmrko
2e26e3261f implement game catalog vector MCP resolver 2026-07-27 13:54:49 +09:00
devmrko
97cd5c9f25 add game catalog resolver MCP tool 2026-07-27 13:51:56 +09:00
devmrko
7ddb8ef0fc add metadata-driven game catalog vector schema 2026-07-27 13:47:30 +09:00
devmrko
455e2aa222 refs #731: expose database game scope MCP 2026-07-27 13:43:59 +09:00
devmrko
a17f18cffe refs #731: add DB game scope metadata 2026-07-27 13:43:37 +09:00
devmrko
943c38f181 derive unresolved scope response from resolver status 2026-07-27 13:30:51 +09:00
devmrko
443d6677a3 add metadata-backed game scope resolver 2026-07-27 13:04:43 +09:00
devmrko
9c48d86696 generalize game scope guidance through metadata few-shot 2026-07-27 13:03:33 +09:00
devmrko
c58cdb5797 refs #736: route Smilegate portal to few-shot MCP 2026-07-27 10:01:44 +09:00
devmrko
e753027536 refs #735: add few-shot NL2SQL MCP tool 2026-07-24 16:21:01 +09:00
devmrko
da992ff524 refs #731: guard missing game identifiers in few-shot prompt 2026-07-24 16:14:18 +09:00
devmrko
00b62bfb41 refs #731: preserve Select AI configuration binding 2026-07-24 16:09:02 +09:00
devmrko
d552e5e25b refs #734: add validated annotation PL/SQL API 2026-07-24 16:03:50 +09:00
devmrko
3fcd699d92 refs #731: enrich Text2SQL prompts with QA examples 2026-07-24 15:59:41 +09:00
devmrko
c8fa6af679 refs #731: fix autonomous vector example storage 2026-07-24 15:53:47 +09:00
devmrko
8e4d2f656f refs #731: allow QA vector MCP tools in portal 2026-07-24 14:45:08 +09:00
devmrko
df19912378 refs #731: add QA vector MCP tools 2026-07-24 14:41:00 +09:00
devmrko
744dcad634 feat: add dedicated Cohere Embed 4 QA vector retrieval 2026-07-24 14:23:33 +09:00
devmrko
23bc4424e3 refs #726: add Select AI SHOWPROMPT diagnostic tool 2026-07-23 20:27:14 +09:00
devmrko
53342e7bc3 refs #722: externalize backoffice customer configuration 2026-07-23 19:14:37 +09:00
devmrko
7ba173240c refs #708: report configured Smilegate Select AI profile 2026-07-23 14:06:16 +09:00
devmrko
ebc8aeee41 refs #710: remove POC4 URL remember tokens 2026-07-23 14:03:20 +09:00
devmrko
75d0a9a998 refs #708: move Smilegate Select AI to OCI GPT 5.4 Mini 2026-07-23 13:45:55 +09:00
devmrko
32fccced20 refs #703: avoid blocking Smilegate backoffice rendering 2026-07-23 13:04:28 +09:00
devmrko
a46ebe6bfd refs #703: force Smilegate PoC HTTP 1.1 2026-07-23 12:57:59 +09:00
devmrko
1771dccd96 refs #703: use gzip for Smilegate backoffice 2026-07-23 12:50:45 +09:00
devmrko
cbee6c31c7 refs #703: fix Smilegate annotation metadata query 2026-07-23 10:46:11 +09:00
devmrko
f730c73ceb refs #703: log schema metadata lookup failures 2026-07-23 10:38:02 +09:00
devmrko
b106e40631 refs #703: route schema metadata through MyBatis 2026-07-23 10:29:22 +09:00
devmrko
99798aa6bb refs #706: add Smilegate QA history benchmark 2026-07-23 10:12:36 +09:00
devmrko
1fe8404e4b refs #703: cover Streamlit expander dark theme internals 2026-07-23 09:38:10 +09:00
devmrko
1fcc231eff refs #703: fix dark mode expander contrast 2026-07-22 20:53:02 +09:00
devmrko
63f294a857 refs #703: enforce chat answer contrast in dark mode 2026-07-22 20:49:28 +09:00
devmrko
20836e458d refs #703: fix dark mode secondary button contrast 2026-07-22 20:36:55 +09:00
devmrko
9eb1f4d1fd refs #703: preserve read-only transaction for Text2SQL 2026-07-22 20:19:47 +09:00
devmrko
914dc087ed refs #703: execute validated Smilegate Text2SQL 2026-07-22 20:15:10 +09:00
devmrko
27757ebff3 refs #703: configure OCI GenAI root compartment 2026-07-22 20:07:47 +09:00
devmrko
4211f11cae refs #703: include OCI SDK for console runtime 2026-07-22 16:29:37 +09:00
devmrko
a91ad3c364 refs #703: fix MCP detail contrast 2026-07-22 16:26:44 +09:00
devmrko
90d76f1033 refs #703: configure smilegate mcp token presets 2026-07-22 16:16:57 +09:00
devmrko
211dd2b2c8 refs #703 #704: finalize smilegate game data poc 2026-07-22 15:35:02 +09:00
563 changed files with 3708 additions and 12471 deletions

View File

@@ -43,8 +43,7 @@ export BACKOFFICE_SESSION_COOKIE_SECURE="false"
export BACKOFFICE_REMEMBER_ME_ENABLED="false"
export BACKOFFICE_REMEMBER_ME_KEY=""
export BACKOFFICE_REMEMBER_ME_DAYS="14"
# HMM HR 질의는 HMM MCP/DBMS_CLOUD_AI_AGENT를 사용합니다. 기존 ORDS 운영 기능이 필요할 때만 설정합니다.
export BACKOFFICE_ORDS_BASE_URL=""
export BACKOFFICE_ORDS_BASE_URL="https://yh0olybn5pqce4n-d8aukro81636mon0.adb.ap-seoul-1.oraclecloudapps.com/ords"
export BACKOFFICE_ORDS_TIMEOUT_SECONDS="10"
# ORDS metadata 생성/수정 전용 계정. 비워두면 BACKOFFICE_DB_* 연결을 사용하므로
# ADMIN으로 실행 중이면 Handler 생성은 막히고 소스 보기만 사용합니다.
@@ -113,7 +112,7 @@ export BACKOFFICE_AI_OCI_COMPARTMENT_ID=""
export VPDTEST1_API_KEY=""
export VPDTEST2_API_KEY=""
# --- (3) 데모용 ADB 엔드유저 비밀번호 (database/adb/07_end_users.sql 에서 사용) ---
# --- (3) 데모용 ADB 엔드유저 비밀번호 (sql/adb/07_end_users.sql 에서 사용) ---
# ADB 비번 정책: 12자 이상, 대/소/숫자/특수 조합.
# 4명의 데모 유저:
# vpduser_my → MySQL view 만 SELECT 가능
@@ -126,7 +125,7 @@ export VPDUSER_BOTH_PASSWORD="RowFilter#Both26"
export VPDUSER_NONE_PASSWORD="RowFilter#None26"
# --- (3b) OPTIONAL — DDS 변형 데모용 엔드유저 비밀번호 ---
# database/adb/13_dds_variant.sql (Oracle 26ai Deep Data Security 재구현) 에서만 사용.
# sql/adb/13_dds_variant.sql (Oracle 26ai Deep Data Security 재구현) 에서만 사용.
# 동일한 4-user 매트릭스를 declarative Data Grants 로 재현. VPD 데모와 공존.
export DDSUSER_MY_PASSWORD="DdsGrant#My2026"
export DDSUSER_PG_PASSWORD="DdsGrant#Pg2026"
@@ -147,7 +146,7 @@ export DDS_MCP_BASE_URL="http://127.0.0.1:8083"
export DDS_MCP_TEST_BEARER="" # 발급한 활성 CB_APP_USER Bearer만 로컬 테스트에 사용
# --- (4) 원격 Postgres (AWS RDS, Cloud SQL, ...) ---
# database/source/postgres_setup.sql 가 여기로 customers 테이블/seed 생성.
# sql/source/postgres_setup.sql 가 여기로 customers 테이블/seed 생성.
# ADB 의 RDS_POSTGRES_LINK 가 이 인스턴스를 가리킴.
export PG_HOST="" # 예: vpd-poc.xxxxx.ap-northeast-2.rds.amazonaws.com
export PG_PORT="5432"

View File

@@ -32,7 +32,7 @@ End-to-End 데모입니다.
행 자체는 보여주되 특정 컬럼만 가리는 통제입니다. 예: 영업팀은 고객 행은 다 보지만
`email` 컬럼은 항상 `NULL` 로 반환.
* VPD 경로: 별도의 `DBMS_REDACT` 정책 (`database/adb/06a_redaction.sql`).
* VPD 경로: 별도의 `DBMS_REDACT` 정책 (`sql/adb/06a_redaction.sql`).
* DDS 경로: 데이터 그랜트 안에 `(ALL COLUMNS EXCEPT email)` 한 줄로 흡수.
### 3. 두 가지 구현 — VPD 와 DDS
@@ -59,21 +59,19 @@ DDS 변형의 능력 (멀티테넌트, OAuth2 federated identity, MAC 모드,
## 소스 위치와 Git 저장 기준
현재 이 저장소에는 서로 독립적으로 빌드·실행되는 세 애플리케이션이 있다. 각 제품은
최상위 폴더를 하나씩 사용하며 저장소 루트에 프레임워크 `src`를 두지 않는다.
현재 이 저장소에는 **VPD 관리 백오피스**와 **PoC4 MCP AI Console 외피**가 함께 들어 있다.
두 소스는 별도 Git repository가 아니라 같은 repository의 서로 다른 경로로 관리한다.
| 구분 | 저장 위치 | 역할 |
|---|---|---|
| VPD/ASO/FGA 관리 백오피스 | `vpd-backoffice/` | Spring Boot 기반 데이터 접근 제어 관리 콘솔 |
| VPD 백오피스 화면 | `vpd-backoffice/src/main/resources/templates/` | Thymeleaf 서버 렌더링 화면 |
| VPD 백오피스 CSS/JS | `vpd-backoffice/src/main/resources/static/` | 화면 동작과 스타일 |
| VPD/ASO/FGA/ORDS/Select AI SQL | `database/adb/` | ADB 적용·검증 SQL 스크립트 |
| 원격 원장 seed/검증 SQL | `database/source/` | PostgreSQL·MySQL 가상 원장과 HMM 선사 실적 데이터 |
| VPD/ASO/FGA 관리 백오피스 | `src/main/java/com/cloudhandson/vpdbackoffice/` | Spring Boot 기반 데이터 접근 제어 관리 콘솔 |
| VPD 백오피스 화면 | `src/main/resources/templates/` | Thymeleaf 서버 렌더링 화면 |
| VPD 백오피스 CSS/JS | `src/main/resources/static/` | 화면 동작과 스타일 |
| VPD/ASO/FGA/ORDS/Select AI SQL | `sql/adb/` | ADB 적용·검증 SQL 스크립트 |
| DDS 백오피스 | `dds-backoffice/` | VPD와 분리된 DDS 전용 Spring Boot 데모 |
| AI Web Agent Console | `ai-web-agent-console/` | Streamlit 기반 MCP 업무 에이전트 콘솔 |
| AI Web Agent Console 진입점 | `ai-web-agent-console/app.py` | MCP discovery, 도구 호출, 답변 합성, 감사 증적 표시 |
| AI Web Agent Console 패키지 | `ai-web-agent-console/ai_web_agent_console/` | 인증, 화면, MCP, 모델과 질의 계약 모듈 |
| AI Web Agent Console 설정 | `ai-web-agent-console/config/` | 제품, MCP, 모델, 데모 사용자와 질문 JSON |
| Streamlit 외피 / PoC4 MCP AI Console | `poc4_active_source_20260714/` | ReAct-style로 MCP tool을 호출하는 PoC4 데모 소스 snapshot |
| Streamlit 외피 메인 화면 | `poc4_active_source_20260714/apps/poc4/mcp_discovery_ui.py` | MCP discovery, tool routing, ReAct-style agent loop, FGA evidence 표시 |
| Streamlit 외피 MCP 설정 | `poc4_active_source_20260714/config/mcp_servers.json` | `kb_mcp`, `kb_vector_mcp` endpoint와 allowlist |
Git 기준은 다음과 같다.
@@ -82,12 +80,14 @@ Git 기준은 다음과 같다.
| 로컬 작업 디렉터리 | `/home/opc/workspaces/vpd-permission-poc-20260628213409` |
| 운영 기준 remote | `gitea` |
| 운영 기준 repository | `https://gittea.cloud-handson.com/joungmin/vpd-permission-poc.git` |
| HMM 기준 branch | `hmm-backoffice` |
| 기준 branch | `main` |
| 보조 remote | `origin` = `https://github.com/devmrko/vpd-permission-poc.git` |
주의:
- HMM 운영·현행화 기준은 Gitea`hmm-backoffice` 브랜치다.
- 날짜 스냅샷과 PoC 번호는 현재 소스 경로로 사용하지 않는다.
- 현재 운영·현행화 기준은 `gitea/main`다.
- `origin/main`은 보조 remote이며, 이 작업공간의 `main``origin/main`보다 앞서 있을 수 있다.
- `poc4_active_source_20260714/`는 배포 VM의 PoC4 활성 소스를 가져온 snapshot이다. 원본은 배포 VM의 `/home/opc/poc_4/` 계열 경로에서 운영되었고, 저장소에는 비밀정보 없이 소스와 설정 sample만 보관한다.
- 실제 DB password, wallet password, Bearer token, AI key, 대화 SQLite DB는 Git에 넣지 않는다.
---
@@ -97,7 +97,6 @@ Git 기준은 다음과 같다.
| 계층 | 객체 | 역할 |
|---|---|---|
| 원격 | `public.customers` (PG), `ecommerce_poc.customers` (MySQL) | 원본 데이터 |
| 원격 HMM 데모 | `hmm_demo.carriers`, `hmm_demo.carrier_monthly_performance` (PG) | 가상 선사 기준정보와 최근 18개월 월간 KPI |
| ADB - 연결 | `RDS_POSTGRES_LINK`, `RDS_LINK` | `DBMS_CLOUD_ADMIN.CREATE_DATABASE_LINK` 로 만든 heterogeneous DB Link |
| ADB - 매핑 | `app_customer`, `app_user`, `app_group`, `user_group`, `db_source`, `permission` | 누가 어느 소스의 어느 region 을 볼 수 있는지 |
| ADB - 컨텍스트 | `vpd_ctx` (Secure Application Context) + `ctx_pkg` | 로그인 시 권한을 세션 컨텍스트로 로딩 |
@@ -110,7 +109,7 @@ Git 기준은 다음과 같다.
## 빠른 시작 (One-Click)
```bash
git clone --branch hmm-backoffice https://gittea.cloud-handson.com/joungmin/vpd-permission-poc.git
git clone https://github.com/<you>/vpd-permission-poc.git
cd vpd-permission-poc
# 1) 환경값 채우기
@@ -168,7 +167,7 @@ $EDITOR .env
## 데모 시나리오 — 2×2 source access matrix
`database/adb/03_seed.sql` 의 매핑 (4 유저, 4 케이스):
`sql/adb/03_seed.sql` 의 매핑 (4 유저, 4 케이스):
| DB 유저 | 그룹 | PG 뷰 | MySQL 뷰 | VPD 결과 |
|---|---|---|---|---|
@@ -186,7 +185,7 @@ $EDITOR .env
(`1=0` predicate) — **deny by default**.
* 누구든 원본 테이블 직접 접근 시도 (`@RDS_POSTGRES_LINK` 등) → 권한 없음.
`database/adb/08_tests_user_my.sql` 가 우회 시도 5개 (원격 직접 SELECT, 컨텍스트
`sql/adb/08_tests_user_my.sql` 가 우회 시도 5개 (원격 직접 SELECT, 컨텍스트
스푸핑, DBMS_RLS 변경, 매핑 테이블 SELECT) 를 시도하고 모두 ORA-xxxxx 로 실패하는 것을
보여줍니다. 09/10/11 은 각 유저의 expected 행 수를 가볍게 확인합니다.
@@ -198,7 +197,7 @@ $EDITOR .env
## DDS 설정 핵심
DDS 변형을 직접 셋업할 때 알아야 하는 최소 단위. `database/adb/13_dds_variant.sql` 가 이
DDS 변형을 직접 셋업할 때 알아야 하는 최소 단위. `sql/adb/13_dds_variant.sql` 가 이
순서로 돌아가며, 행/컬럼 통제가 모두 **선언형 DDL 한 줄** 로 끝납니다.
### 사전 조건
@@ -258,23 +257,10 @@ CREATE DATA GRANT admin.alice_apac_grant
.
├── run.sh # 원클릭 엔트리포인트
├── .env.example
├── vpd-backoffice/ # Spring Boot VPD 관리 백오피스
│ ├── pom.xml
│ └── src/
├── ai-web-agent-console/ # Streamlit MCP 업무 에이전트
│ ├── app.py
│ ├── ai_web_agent_console/
│ ├── config/
│ └── tests/
├── dds-backoffice/ # DDS 전용 Spring Boot 백오피스
├── deploy/ # 애플리케이션별 배포 설정
├── tests/ # 저장소 폴더 경계와 경로 규칙 검증
├── scripts/lib/common.sh # log/ok/warn/die + env 검증 헬퍼
├── database/
├── sql/
│ ├── source/
│ │ ├── postgres_setup.sql # 원격 PG: customers + 12 rows
│ │ ├── postgres_hmm_carrier_performance.sql # 원격 PG: HMM 가상 선사 8개 + 월간 KPI 144 rows
│ │ ├── postgres_hmm_carrier_performance_verify.sql # HMM 선사 실적 건수·패턴·제약·TLS·권한 검증
│ │ └── mysql_setup.sql # 원격 MySQL: customers + 12 rows
│ └── adb/
│ ├── 00_cleanup.sql # 멱등 teardown

View File

@@ -1,48 +0,0 @@
# AI Web Agent Console local/VM configuration example.
# Copy to .env and keep real credentials out of Git.
# MCP
HMM_MCP_BEARER_TOKEN=
AI_WEB_AGENT_CONSOLE_MCP_TIMEOUT_SECONDS=45
# OCI Generative AI SDK
OCI_AUTH_TYPE=config_file
OCI_CONFIG_FILE=~/.oci/config
OCI_PROFILE=DEFAULT
OCI_GENAI_COMPARTMENT_ID=
AI_WEB_AGENT_CONSOLE_MODEL_PROFILE=gpt54_mini_oci
AI_WEB_AGENT_CONSOLE_COMPLEX_REASONING_MODEL_PROFILE=gpt54_mini_oci
# Optional model route overrides
# AI_WEB_AGENT_CONSOLE_LLM_GPT54_MINI_OCI_MODEL_ID=openai.gpt-5.4-mini
# AI_WEB_AGENT_CONSOLE_LLM_GPT54_MINI_OCI_REGION=us-chicago-1
# AI_WEB_AGENT_CONSOLE_LLM_GPT54_MINI_OCI_ENDPOINT=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
# Login gateway. Generate PBKDF2 hash and random cookie secret outside Git.
AI_WEB_AGENT_CONSOLE_LOGIN_USER=demo-admin
AI_WEB_AGENT_CONSOLE_LOGIN_PASSWORD_PBKDF2=
AI_WEB_AGENT_CONSOLE_LOGIN_COOKIE_SECRET=
PORTAL_AUTH_BIND_ADDRESS=127.0.0.1
PORTAL_AUTH_PORT=8621
PORTAL_AUTH_SESSION_SECONDS=43200
PORTAL_AUTH_REMEMBER_SECONDS=604800
# Product display
AGENT_CONSOLE_NAME=HMM AI 업무 에이전트
AGENT_CONSOLE_SHORT_NAME=HMM
AGENT_CONSOLE_PAGE_TITLE=HMM AI 업무 에이전트
AGENT_CONSOLE_PRIMARY_COLOR=#004b87
# Local conversation store
AI_WEB_AGENT_CONSOLE_CHAT_DB_PATH=data/ai_web_agent_console_chat.sqlite3
# Optional audit DB. Password and Wallet remain outside Git.
AI_WEB_AGENT_CONSOLE_AUDIT_DB_ENV_FILE=.env
AI_WEB_AGENT_CONSOLE_AUDIT_DB_USER=ADMIN
AI_WEB_AGENT_CONSOLE_AUDIT_DB_PASSWORD=
AI_WEB_AGENT_CONSOLE_AUDIT_DSN=
AI_WEB_AGENT_CONSOLE_AUDIT_WALLET_DIR=
AI_WEB_AGENT_CONSOLE_AUDIT_WALLET_PASSWORD=
# Optional query contract override
AI_WEB_AGENT_CONSOLE_QUERY_CONTRACTS_PATH=config/hmm_hr_query_contracts.json

View File

@@ -1,57 +0,0 @@
# AI Web Agent Console
HMM 업무 데이터를 MCP 도구로 조회하고 OCI Generative AI로 답변을 구성하는 Streamlit
애플리케이션이다. 과거 PoC 번호나 소스 추출 날짜는 애플리케이션 이름과 패키지 경계로
사용하지 않는다.
## 구조
```text
ai-web-agent-console/
├── app.py Streamlit 실행 진입점
├── ai_web_agent_console/ 인증, 화면, MCP, 모델, 질의 계약 모듈
├── config/ 제품·모델·MCP·시나리오 JSON
├── tests/ Python 회귀 테스트
├── .env.example 환경변수 예시
└── requirements.txt
```
## 실행
```bash
cd ai-web-agent-console
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/streamlit run app.py --server.address 0.0.0.0 --server.port 8622
```
인증 게이트웨이는 같은 디렉토리에서 다음과 같이 실행한다.
```bash
.venv/bin/python -m ai_web_agent_console.auth_gateway
```
## 설정
- `config/app_profile.json`: 제품명, 설명, 아이콘, 색상
- `config/mcp_servers.json`: MCP endpoint와 허용 도구
- `config/model_profiles.json`: OCI Generative AI 모델 profile
- `config/vpd_token_presets.json`: 데모 사용자와 토큰 환경변수 매핑
- `config/hmm_demo_scenarios.json`: 화면의 예제 질문
- `config/hmm_hr_query_contracts.json`: 질의별 필수 근거와 계산·시간 규칙
실제 토큰, DB 비밀번호, Wallet, OCI private key는 Git에 넣지 않는다. 데모 사용자 JSON은
`HMM_MCP_BEARER_TOKEN` 같은 환경변수 이름만 참조한다.
## 검증
```bash
python3 -m compileall -q app.py ai_web_agent_console tests
python3 -m unittest discover -s tests -p 'test_*.py'
```
## 운영 호환성
운영 서버의 물리 경로 `/opt/hmm-poc4`와 기존 SQLite 테이블명은 대화 이력과 서비스 전환
호환을 위해 유지할 수 있다. Git 폴더, Python 패키지와 실행 명령은 이 문서의 새 이름만
사용한다.

View File

@@ -1,190 +0,0 @@
"""Reusable audit-tab renderer with data loaders supplied by the application."""
from __future__ import annotations
from typing import Any, Callable
AuditInventoryLoader = Callable[[], list[dict[str, Any]]]
AuditEventsLoader = Callable[[int, int, str, str], list[dict[str, Any]]]
def render_hmm_audit_tab(
st: Any,
inventory_loader: AuditInventoryLoader,
events_loader: AuditEventsLoader,
error_type: type[Exception],
) -> None:
"""Render HMM access audit data without owning DB connection details."""
st.markdown(
'<div class="kb-section-title input" role="heading" aria-level="3">'
'감사로그 ( <strong>HMM 접근 관리</strong> )'
'</div>',
unsafe_allow_html=True,
)
st.markdown(
'<div class="kb-audit-lead">'
'HMM 백오피스의 사용자·그룹·역할·토큰·접근 정책 변경 이력을 시간순으로 확인합니다. '
'이벤트 유형과 처리 상태로 필터링해 운영 변경의 성공·실패를 추적할 수 있습니다.'
'</div>',
unsafe_allow_html=True,
)
try:
inventory = inventory_loader()
except error_type as exc:
st.error(str(exc))
return
event_types = tuple(
str(item.get("event_type") or "").strip()
for item in inventory
if str(item.get("event_type") or "").strip()
)
st.markdown('<div class="kb-audit-heading">조회 조건</div>', unsafe_allow_html=True)
with st.container(key="poc4_hmm_audit_filters"):
event_column, status_column = st.columns(2)
with event_column:
selected_event_type = st.selectbox(
"이벤트 유형",
options=("", *event_types),
format_func=lambda value: "전체 이벤트" if not value else value,
key="poc4_hmm_audit_event_filter",
)
with status_column:
selected_status = st.selectbox(
"처리 상태",
options=("", "SUCCESS", "FAILURE", "DENIED"),
format_func=lambda value: "전체 상태" if not value else value,
key="poc4_hmm_audit_status_filter",
)
days_column, limit_column, refresh_column = st.columns([1.5, 1, 0.8])
with days_column:
days = st.slider(
"조회 기간",
min_value=1,
max_value=90,
value=7,
format="%d",
key="poc4_hmm_audit_days",
)
with limit_column:
row_limit = st.number_input(
"최대 건수",
min_value=10,
max_value=500,
value=100,
step=10,
key="poc4_hmm_audit_row_limit",
)
with refresh_column:
st.markdown('<div style="height: 28px"></div>', unsafe_allow_html=True)
if st.button(
"새로고침",
icon=":material/refresh:",
width="stretch",
key="poc4_hmm_audit_refresh",
):
inventory_loader.clear()
events_loader.clear()
st.rerun()
try:
events = events_loader(
int(days), int(row_limit), selected_event_type, selected_status
)
except error_type as exc:
st.error(str(exc))
return
success_count = sum(
1 for item in events if str(item.get("status") or "").upper() == "SUCCESS"
)
with st.container(key="poc4_hmm_audit_metrics"):
type_metric, event_metric, success_metric, failure_metric = st.columns(4)
type_metric.metric("이벤트 유형", len(inventory))
event_metric.metric("조회 이벤트", len(events))
success_metric.metric("성공", success_count)
failure_metric.metric("실패·거부", len(events) - success_count)
st.markdown(
'<div class="kb-audit-heading">이벤트 유형 현황</div>'
f'<div class="kb-audit-caption">현재 기록된 이벤트 유형 {len(inventory)}개 · '
'유형별 누적 건수와 최근 발생 시각을 확인합니다.</div>',
unsafe_allow_html=True,
)
with st.container(key="poc4_hmm_audit_inventory_panel"):
with st.expander("감사 이벤트 유형", expanded=True):
if inventory:
st.dataframe(
[
{
"이벤트 유형": str(item.get("event_type") or ""),
"누적 건수": int(item.get("event_count") or 0),
"최근 발생(KST)": str(item.get("latest_event_time") or ""),
}
for item in inventory
],
column_config={
"이벤트 유형": st.column_config.TextColumn(width="large"),
"누적 건수": st.column_config.NumberColumn(width="small"),
"최근 발생(KST)": st.column_config.TextColumn(width="medium"),
},
hide_index=True,
width="stretch",
height=min(360, 72 + 36 * len(inventory)),
)
else:
st.caption("아직 기록된 HMM 접근 관리 이벤트가 없습니다.")
st.markdown(
'<div class="kb-audit-heading">감사 이벤트</div>'
'<div class="kb-audit-caption">최신 이벤트부터 표시합니다. '
'처리 상태와 대상 식별자, 오류 메시지를 먼저 확인하세요.</div>',
unsafe_allow_html=True,
)
show_details = st.toggle(
"상세 메시지 표시",
value=True,
key="poc4_hmm_audit_show_details",
)
if not events:
st.info("선택한 조건에 해당하는 HMM 접근 관리 이벤트가 없습니다.")
return
display_rows: list[dict[str, Any]] = []
for event in events:
row: dict[str, Any] = {
"감사 ID": int(event.get("audit_id") or 0),
"발생시각(KST)": str(event.get("event_time") or ""),
"이벤트 유형": str(event.get("event_type") or ""),
"상태": str(event.get("status") or ""),
"토큰 Key ID": event.get("key_id"),
"대상 Object ID": event.get("object_id"),
"처리 행": event.get("row_count"),
"오류 코드": str(event.get("error_code") or ""),
}
if show_details:
row["메시지"] = str(event.get("message") or "")
display_rows.append(row)
column_config: dict[str, Any] = {
"감사 ID": st.column_config.NumberColumn(width="small"),
"발생시각(KST)": st.column_config.TextColumn(width="medium"),
"이벤트 유형": st.column_config.TextColumn(width="large"),
"상태": st.column_config.TextColumn(width="small"),
"토큰 Key ID": st.column_config.NumberColumn(width="small"),
"대상 Object ID": st.column_config.NumberColumn(width="small"),
"처리 행": st.column_config.NumberColumn(width="small"),
"오류 코드": st.column_config.TextColumn(width="medium"),
}
if show_details:
column_config["메시지"] = st.column_config.TextColumn(width="large")
with st.container(key="poc4_hmm_audit_event_panel"):
st.dataframe(
display_rows,
column_config=column_config,
hide_index=True,
width="stretch",
height=min(640, 104 + 38 * len(display_rows)),
)

View File

@@ -1,563 +0,0 @@
"""Small localhost authentication service for the HMM Streamlit portal.
Nginx owns the public security boundary. This module validates the existing
PBKDF2 login, issues a signed HttpOnly cookie, and answers Nginx auth_request
subrequests. Authentication values are never accepted from a URL.
"""
from __future__ import annotations
import base64
import binascii
from collections import defaultdict, deque
from dataclasses import dataclass
from datetime import datetime, timezone
from http import HTTPStatus
from http.cookies import SimpleCookie
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import hashlib
import hmac
import html
import json
import logging
import os
import secrets
import threading
import time
from typing import Deque
from urllib.parse import parse_qs, urlsplit
def _environment_value(*names: str, default: str = "") -> str:
for name in names:
value = os.environ.get(name)
if value is not None and value.strip():
return value.strip()
return default
LOG = logging.getLogger("hmm_portal_auth")
SESSION_COOKIE_NAME = "__Host-HMM_PORTAL_SESSION"
CSRF_COOKIE_NAME = "__Host-HMM_LOGIN_CSRF"
SESSION_TOKEN_VERSION = 2
MAX_REQUEST_BYTES = 8_192
MAX_FIELD_CHARS = 200
@dataclass(frozen=True)
class AuthConfig:
username: str
password_pbkdf2: str
cookie_secret: str
bind_address: str = "127.0.0.1"
port: int = 8621
session_seconds: int = 12 * 60 * 60
remember_seconds: int = 7 * 24 * 60 * 60
product_name: str = "HMM AI 업무 에이전트"
login_title: str = "HMM AI 업무 에이전트"
login_description: str = "사용자 인증 후 AI 업무 질의 기능을 이용할 수 있습니다."
login_footer: str = "승인된 사용자만 접속할 수 있습니다."
primary_color: str = "#004b87"
@classmethod
def from_environment(cls) -> "AuthConfig":
config = cls(
username=_environment_value(
"AI_WEB_AGENT_CONSOLE_LOGIN_USER", "POC4_LOGIN_USER"
),
password_pbkdf2=_environment_value(
"AI_WEB_AGENT_CONSOLE_LOGIN_PASSWORD_PBKDF2",
"POC4_LOGIN_PASSWORD_PBKDF2",
),
cookie_secret=_environment_value(
"AI_WEB_AGENT_CONSOLE_LOGIN_COOKIE_SECRET",
"POC4_LOGIN_COOKIE_SECRET",
),
bind_address=os.environ.get(
"PORTAL_AUTH_BIND_ADDRESS", "127.0.0.1"
).strip(),
port=int(os.environ.get("PORTAL_AUTH_PORT", "8621")),
session_seconds=int(
os.environ.get("PORTAL_AUTH_SESSION_SECONDS", str(12 * 60 * 60))
),
remember_seconds=int(
os.environ.get(
"PORTAL_AUTH_REMEMBER_SECONDS", str(7 * 24 * 60 * 60)
)
),
product_name=os.environ.get(
"AGENT_CONSOLE_NAME", "HMM AI 업무 에이전트"
).strip(),
login_title=os.environ.get(
"AGENT_CONSOLE_LOGIN_TITLE", "HMM AI 업무 에이전트"
).strip(),
login_description=os.environ.get(
"AGENT_CONSOLE_LOGIN_DESCRIPTION",
"사용자 인증 후 AI 업무 질의 기능을 이용할 수 있습니다.",
).strip(),
login_footer=os.environ.get(
"AGENT_CONSOLE_LOGIN_FOOTER",
"승인된 사용자만 접속할 수 있습니다.",
).strip(),
primary_color=os.environ.get(
"AGENT_CONSOLE_PRIMARY_COLOR", "#004b87"
).strip(),
)
config.validate()
return config
def validate(self) -> None:
if not self.username or not self.password_pbkdf2:
raise ValueError("AI web agent console login credentials are not configured")
if len(self.cookie_secret.encode("utf-8")) < 32:
raise ValueError(
"AI_WEB_AGENT_CONSOLE_LOGIN_COOKIE_SECRET must be at least 32 bytes"
)
if self.bind_address not in {"127.0.0.1", "::1"}:
raise ValueError("Portal authentication service must bind to loopback")
if not 1 <= self.port <= 65535:
raise ValueError("PORTAL_AUTH_PORT is invalid")
if not 300 <= self.session_seconds <= 24 * 60 * 60:
raise ValueError("PORTAL_AUTH_SESSION_SECONDS is outside the safe range")
if not self.session_seconds <= self.remember_seconds <= 30 * 24 * 60 * 60:
raise ValueError("PORTAL_AUTH_REMEMBER_SECONDS is outside the safe range")
@dataclass(frozen=True)
class AuthenticatedSession:
username: str
expires_at: int
class SessionTokenCodec:
def __init__(self, secret: str):
self._secret = secret.encode("utf-8")
def issue(self, username: str, lifetime_seconds: int, now: int | None = None) -> str:
issued_at = int(time.time()) if now is None else now
payload = {
"v": SESSION_TOKEN_VERSION,
"u": username,
"i": issued_at,
"e": issued_at + lifetime_seconds,
"n": secrets.token_urlsafe(18),
}
encoded = _base64url_encode(
json.dumps(payload, separators=(",", ":")).encode("utf-8")
)
signature = hmac.new(
self._secret, encoded.encode("ascii"), hashlib.sha256
).hexdigest()
return f"{encoded}.{signature}"
def verify(self, token: str, expected_username: str, now: int | None = None) -> AuthenticatedSession | None:
if not token or len(token) > 2048:
return None
current_time = int(time.time()) if now is None else now
try:
encoded, supplied_signature = token.split(".", 1)
expected_signature = hmac.new(
self._secret, encoded.encode("ascii"), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(supplied_signature, expected_signature):
return None
payload = json.loads(_base64url_decode(encoded).decode("utf-8"))
version = int(payload["v"])
username = str(payload["u"])
issued_at = int(payload["i"])
expires_at = int(payload["e"])
except (
binascii.Error,
KeyError,
TypeError,
ValueError,
UnicodeDecodeError,
json.JSONDecodeError,
):
return None
if version != SESSION_TOKEN_VERSION:
return None
if issued_at > current_time + 30 or expires_at <= current_time:
return None
if expires_at - issued_at > 30 * 24 * 60 * 60:
return None
if not hmac.compare_digest(username, expected_username):
return None
return AuthenticatedSession(username=username, expires_at=expires_at)
class LoginAttemptLimiter:
def __init__(self, maximum_failures: int = 5, window_seconds: int = 300):
self._maximum_failures = maximum_failures
self._window_seconds = window_seconds
self._failures: dict[str, Deque[float]] = defaultdict(deque)
self._lock = threading.Lock()
def blocked(self, key: str, now: float | None = None) -> bool:
current_time = time.monotonic() if now is None else now
with self._lock:
failures = self._failures[key]
self._prune(failures, current_time)
return len(failures) >= self._maximum_failures
def record_failure(self, key: str, now: float | None = None) -> None:
current_time = time.monotonic() if now is None else now
with self._lock:
failures = self._failures[key]
self._prune(failures, current_time)
failures.append(current_time)
def reset(self, key: str) -> None:
with self._lock:
self._failures.pop(key, None)
def _prune(self, failures: Deque[float], now: float) -> None:
cutoff = now - self._window_seconds
while failures and failures[0] < cutoff:
failures.popleft()
def password_matches(password: str, encoded_password: str) -> bool:
try:
scheme, iterations_text, salt_hex, expected_hex = encoded_password.split(
"$", 3
)
iterations = int(iterations_text)
salt = bytes.fromhex(salt_hex)
expected = bytes.fromhex(expected_hex)
except (TypeError, ValueError):
return False
if scheme != "pbkdf2_sha256" or not 100_000 <= iterations <= 2_000_000:
return False
candidate = hashlib.pbkdf2_hmac(
"sha256", password.encode("utf-8"), salt, iterations
)
return hmac.compare_digest(candidate, expected)
def session_cookie_header(token: str, max_age: int | None) -> str:
attributes = [
f"{SESSION_COOKIE_NAME}={token}",
"Path=/",
"Secure",
"HttpOnly",
"SameSite=Lax",
]
if max_age is not None:
attributes.append(f"Max-Age={max_age}")
return "; ".join(attributes)
def clear_session_cookie_header() -> str:
return (
f"{SESSION_COOKIE_NAME}=; Path=/; Max-Age=0; "
"Secure; HttpOnly; SameSite=Lax"
)
def csrf_cookie_header(value: str, max_age: int = 600) -> str:
return (
f"{CSRF_COOKIE_NAME}={value}; Path=/; Max-Age={max_age}; "
"Secure; HttpOnly; SameSite=Strict"
)
def clear_csrf_cookie_header() -> str:
return (
f"{CSRF_COOKIE_NAME}=; Path=/; Max-Age=0; "
"Secure; HttpOnly; SameSite=Strict"
)
def _base64url_encode(value: bytes) -> str:
return base64.urlsafe_b64encode(value).decode("ascii").rstrip("=")
def _base64url_decode(value: str) -> bytes:
padded = value + "=" * (-len(value) % 4)
return base64.urlsafe_b64decode(padded)
def _cookie_value(cookie_header: str, name: str) -> str:
try:
cookies = SimpleCookie()
cookies.load(cookie_header)
morsel = cookies.get(name)
return morsel.value if morsel is not None else ""
except (KeyError, TypeError):
return ""
def _login_page(config: AuthConfig, csrf_value: str, error: str = "") -> bytes:
error_html = (
f'<div class="error" role="alert">{html.escape(error)}</div>'
if error
else ""
)
return f"""<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{html.escape(config.product_name)}</title>
<style>
:root {{ --primary:{html.escape(config.primary_color)}; --text:#17232d;
--muted:#60717f; --border:#d9e0e5; }}
* {{ box-sizing:border-box; }}
body {{ margin:0; background:#fff; color:var(--text);
font-family:"Noto Sans KR","Malgun Gothic",sans-serif; }}
main {{ width:min(420px,calc(100% - 40px)); margin:12vh auto 0; }}
.wordmark {{ color:var(--primary); font-size:1.25rem; font-weight:800;
letter-spacing:.08em; }}
h1 {{ margin:16px 0 8px; font-size:1.75rem; }}
.description,.footer {{ color:var(--muted); line-height:1.55; }}
form {{ margin-top:28px; }}
label {{ display:block; margin:0 0 18px; font-weight:700; }}
input[type="text"],input[type="password"] {{ width:100%; margin-top:8px;
padding:12px 13px; border:1px solid var(--border); border-radius:5px;
font:inherit; color:var(--text); background:#fff; }}
.remember {{ display:flex; align-items:center; gap:8px; font-weight:500; }}
.remember input {{ width:17px; height:17px; }}
button {{ width:100%; padding:12px; border:1px solid var(--primary);
border-radius:5px; background:var(--primary); color:#fff; font:inherit;
font-weight:800; cursor:pointer; }}
.error {{ margin:18px 0 0; padding:11px 12px; border:1px solid #d99898;
border-radius:5px; color:#8a2222; background:#fff7f7; }}
.footer {{ margin-top:22px; font-size:.9rem; }}
</style>
</head>
<body>
<main>
<div class="wordmark">HMM</div>
<h1>{html.escape(config.login_title)}</h1>
<p class="description">{html.escape(config.login_description)}</p>
{error_html}
<form action="/auth/login" method="post" autocomplete="on">
<input type="hidden" name="csrf" value="{html.escape(csrf_value)}">
<label>사용자 ID
<input name="username" type="text" maxlength="80" autocomplete="username"
required autofocus>
</label>
<label>비밀번호
<input name="password" type="password" maxlength="200"
autocomplete="current-password" required>
</label>
<label class="remember">
<input name="remember" type="checkbox" value="yes"> 로그인 유지 (7일)
</label>
<button type="submit">로그인</button>
</form>
<p class="footer">{html.escape(config.login_footer)}</p>
</main>
</body>
</html>""".encode("utf-8")
def build_handler(config: AuthConfig) -> type[BaseHTTPRequestHandler]:
codec = SessionTokenCodec(config.cookie_secret)
limiter = LoginAttemptLimiter()
class PortalAuthHandler(BaseHTTPRequestHandler):
server_version = "HMMPortalAuth/1.0"
sys_version = ""
def do_HEAD(self) -> None:
self._route(send_body=False)
def do_GET(self) -> None:
self._route(send_body=True)
def do_POST(self) -> None:
path = urlsplit(self.path).path
if path == "/auth/login":
self._login()
elif path == "/auth/logout":
self._logout()
else:
self._send_text(HTTPStatus.NOT_FOUND, "Not found")
def _route(self, send_body: bool) -> None:
path = urlsplit(self.path).path
if path == "/auth/check":
self._check()
elif path == "/auth/login":
self._show_login(send_body=send_body)
elif path == "/auth/logout":
self._logout()
elif path == "/auth/healthz":
self._send_text(HTTPStatus.OK, "ok", send_body=send_body)
else:
self._send_text(HTTPStatus.NOT_FOUND, "Not found", send_body=send_body)
def _check(self) -> None:
session = self._session()
if session is None:
self._send_empty(HTTPStatus.UNAUTHORIZED)
return
self.send_response(HTTPStatus.NO_CONTENT)
self._security_headers()
self.send_header("X-Auth-User", session.username)
self.send_header("X-Auth-Expires", str(session.expires_at))
self.end_headers()
def _show_login(self, send_body: bool = True, error: str = "") -> None:
if self._session() is not None and not error:
self._redirect("/")
return
csrf_value = secrets.token_urlsafe(32)
body = _login_page(config, csrf_value, error)
self.send_response(HTTPStatus.OK)
self._security_headers()
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.send_header("Set-Cookie", csrf_cookie_header(csrf_value))
self.end_headers()
if send_body:
self.wfile.write(body)
def _login(self) -> None:
client_key = self._client_key()
if limiter.blocked(client_key):
self._show_login(error="로그인 시도가 잠시 제한되었습니다. 잠시 후 다시 시도해 주세요.")
return
try:
content_length = int(self.headers.get("Content-Length", "0"))
except ValueError:
content_length = 0
if not 1 <= content_length <= MAX_REQUEST_BYTES:
self._send_text(HTTPStatus.BAD_REQUEST, "Invalid request")
return
raw_body = self.rfile.read(content_length)
try:
form = parse_qs(
raw_body.decode("utf-8"),
keep_blank_values=True,
strict_parsing=False,
max_num_fields=8,
)
except (UnicodeDecodeError, ValueError):
self._send_text(HTTPStatus.BAD_REQUEST, "Invalid request")
return
username = _form_value(form, "username")
password = _form_value(form, "password")
csrf_form = _form_value(form, "csrf")
csrf_cookie = _cookie_value(
self.headers.get("Cookie", ""), CSRF_COOKIE_NAME
)
if (
not csrf_form
or not csrf_cookie
or not hmac.compare_digest(csrf_form, csrf_cookie)
):
self._send_text(HTTPStatus.BAD_REQUEST, "Invalid request")
return
valid_credentials = (
len(username) <= 80
and len(password) <= MAX_FIELD_CHARS
and hmac.compare_digest(username.strip(), config.username)
and password_matches(password, config.password_pbkdf2)
)
if not valid_credentials:
limiter.record_failure(client_key)
self._show_login(error="사용자 ID 또는 비밀번호를 확인해 주세요.")
return
limiter.reset(client_key)
remember = _form_value(form, "remember") == "yes"
lifetime = (
config.remember_seconds if remember else config.session_seconds
)
token = codec.issue(config.username, lifetime)
self.send_response(HTTPStatus.SEE_OTHER)
self._security_headers()
self.send_header("Location", "/")
self.send_header(
"Set-Cookie",
session_cookie_header(token, lifetime if remember else None),
)
self.send_header("Set-Cookie", clear_csrf_cookie_header())
self.end_headers()
def _logout(self) -> None:
self.send_response(HTTPStatus.SEE_OTHER)
self._security_headers()
self.send_header("Location", "/auth/login")
self.send_header("Set-Cookie", clear_session_cookie_header())
self.send_header("Set-Cookie", clear_csrf_cookie_header())
self.end_headers()
def _session(self) -> AuthenticatedSession | None:
token = _cookie_value(
self.headers.get("Cookie", ""), SESSION_COOKIE_NAME
)
return codec.verify(token, config.username)
def _client_key(self) -> str:
forwarded = self.headers.get("X-Real-IP", "").strip()
return forwarded or self.client_address[0]
def _redirect(self, location: str) -> None:
self.send_response(HTTPStatus.SEE_OTHER)
self._security_headers()
self.send_header("Location", location)
self.end_headers()
def _send_empty(self, status: HTTPStatus) -> None:
self.send_response(status)
self._security_headers()
self.send_header("Content-Length", "0")
self.end_headers()
def _send_text(
self,
status: HTTPStatus,
message: str,
send_body: bool = True,
) -> None:
body = message.encode("utf-8")
self.send_response(status)
self._security_headers()
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
if send_body:
self.wfile.write(body)
def _security_headers(self) -> None:
self.send_header("Cache-Control", "no-store")
self.send_header("Pragma", "no-cache")
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY")
self.send_header("Referrer-Policy", "no-referrer")
self.send_header(
"Content-Security-Policy",
"default-src 'none'; style-src 'unsafe-inline'; "
"form-action 'self'; base-uri 'none'; frame-ancestors 'none'",
)
def log_message(self, _format: str, *args: object) -> None:
# Do not log query strings, cookies, form bodies, or tokens.
LOG.info("%s %s", self.command, urlsplit(self.path).path)
return PortalAuthHandler
def _form_value(form: dict[str, list[str]], name: str) -> str:
values = form.get(name)
return values[0] if values else ""
def main() -> None:
logging.basicConfig(
level=os.environ.get("PORTAL_AUTH_LOG_LEVEL", "INFO"),
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
config = AuthConfig.from_environment()
server = ThreadingHTTPServer(
(config.bind_address, config.port), build_handler(config)
)
LOG.info("HMM portal authentication service listening on loopback port %s", config.port)
server.serve_forever()
if __name__ == "__main__":
main()

View File

@@ -1,80 +0,0 @@
"""Pure helpers for MCP result envelopes used by the Streamlit console."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
def response_payload(value: Any) -> Mapping[str, Any]:
"""Return the business payload from a direct or nested MCP response."""
if not isinstance(value, Mapping):
return {}
nested = value.get("response")
return nested if isinstance(nested, Mapping) else value
def text_result(value: Any) -> str:
"""Return a textual `result` field without stringifying other structures."""
result = response_payload(value).get("result")
return result.strip() if isinstance(result, str) else ""
def status_result_summary(value: Any, *, excerpt_chars: int = 900) -> dict[str, Any]:
"""Build a safe UI summary for status/result-style compatibility tools."""
payload = response_payload(value)
summary: dict[str, Any] = {}
for key in ("status", "success", "error", "errorCode", "errorMessage"):
item = payload.get(key)
if item not in (None, "", []):
summary[key] = item
result = text_result(value)
if result:
summary["result_chars"] = len(result)
summary["result_excerpt"] = result[:excerpt_chars] + (
"..." if len(result) > excerpt_chars else ""
)
return summary
def status_result_evidence(value: Any, *, max_chars: int = 7000) -> dict[str, Any]:
"""Preserve bounded textual policy/data evidence for final answer synthesis."""
payload = response_payload(value)
evidence: dict[str, Any] = {}
for key in ("status", "success", "error", "errorCode", "errorMessage"):
item = payload.get(key)
if item not in (None, "", []):
evidence[key] = item
result = text_result(value)
if result:
evidence["result"] = result[:max_chars] + (
"..." if len(result) > max_chars else ""
)
evidence["result_chars"] = len(result)
return evidence
def has_actionable_text_result(value: Any) -> bool:
"""Return whether a textual result contains evidence worth stopping on."""
result = text_result(value)
if not result:
return False
normalized = " ".join(result.casefold().split())
return not any(
marker in normalized
for marker in ("no data found", "no evidence found", "error:")
)
__all__ = [
"has_actionable_text_result",
"response_payload",
"status_result_evidence",
"status_result_summary",
"text_result",
]

View File

@@ -1,407 +0,0 @@
"""Configuration-driven query and answer evidence contracts."""
from __future__ import annotations
import ast
from decimal import Decimal, InvalidOperation
from functools import lru_cache
import json
import os
from pathlib import Path
import re
from typing import Any, Mapping
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_CONTRACT_FILE = ROOT / "config" / "hmm_hr_query_contracts.json"
def _contract_file() -> Path:
configured = str(
os.getenv("AI_WEB_AGENT_CONSOLE_QUERY_CONTRACTS_PATH")
or os.getenv("POC4_QUERY_CONTRACTS_PATH")
or ""
).strip()
if not configured:
return DEFAULT_CONTRACT_FILE
path = Path(configured).expanduser()
return path if path.is_absolute() else ROOT / path
@lru_cache(maxsize=4)
def _load_contract_file(path_text: str) -> tuple[Mapping[str, Any], ...]:
path = Path(path_text)
if not path.exists():
return ()
value = json.loads(path.read_text(encoding="utf-8"))
contracts = value.get("contracts") if isinstance(value, Mapping) else None
if not isinstance(contracts, list):
raise ValueError("query contracts must contain a contracts array")
return tuple(item for item in contracts if isinstance(item, Mapping))
def load_query_contracts() -> tuple[Mapping[str, Any], ...]:
return _load_contract_file(str(_contract_file()))
def matching_query_contracts(
question: str,
tool_name: str,
) -> tuple[Mapping[str, Any], ...]:
normalized_question = " ".join(str(question or "").casefold().split())
normalized_tool = str(tool_name or "").strip()
selected: list[Mapping[str, Any]] = []
for contract in load_query_contracts():
tools = contract.get("applies_to_tools")
if isinstance(tools, list) and normalized_tool not in {
str(item) for item in tools
}:
continue
intent = contract.get("intent_match")
if not isinstance(intent, Mapping):
continue
subject_terms = [
str(item).casefold()
for item in intent.get("subject_terms_any", [])
if str(item).strip()
]
action_terms = [
str(item).casefold()
for item in intent.get("action_terms_any", [])
if str(item).strip()
]
if subject_terms and not any(
term in normalized_question for term in subject_terms
):
continue
if action_terms and not any(
term in normalized_question for term in action_terms
):
continue
selected.append(contract)
return tuple(selected)
def query_contract_guidance(question: str, tool_name: str) -> str:
contracts = matching_query_contracts(question, tool_name)
if not contracts:
return ""
return json.dumps(
{"query_contracts": contracts},
ensure_ascii=False,
separators=(",", ":"),
)
def append_query_contract_guidance(
tool_query: str,
*,
original_question: str,
tool_name: str,
) -> str:
guidance = query_contract_guidance(original_question, tool_name)
normalized = str(tool_query or "").strip()
if not guidance:
return normalized
return (
f"{normalized}\n"
"다음 질의 계약의 필드·계산·시간 기준을 반드시 지켜 결과를 반환하세요. "
f"계약: {guidance}"
)
def _json_data_result(value: str) -> Any:
marker = "DATA_RESULT"
position = value.find(marker)
if position < 0:
return None
remainder = value[position + len(marker) :]
object_position = remainder.find("{")
array_position = remainder.find("[")
positions = [
candidate
for candidate in (object_position, array_position)
if candidate >= 0
]
if not positions:
return None
candidate = remainder[min(positions) :]
try:
parsed, _ = json.JSONDecoder().raw_decode(candidate)
except (TypeError, ValueError, json.JSONDecodeError):
return None
return parsed
def _evidence_rows(value: Any) -> list[Mapping[str, Any]]:
rows: list[Mapping[str, Any]] = []
if isinstance(value, Mapping):
rows.append(value)
for item in value.values():
rows.extend(_evidence_rows(item))
elif isinstance(value, list):
for item in value:
rows.extend(_evidence_rows(item))
elif isinstance(value, str):
parsed = _json_data_result(value)
if parsed is not None:
rows.extend(_evidence_rows(parsed))
return rows
def _decimal(value: Any) -> Decimal:
if isinstance(value, bool) or value is None:
raise InvalidOperation
return Decimal(str(value))
def _evaluate_contract_expression(
expression: str,
row: Mapping[str, Any],
) -> Decimal:
tree = ast.parse(expression, mode="eval")
def evaluate(node: ast.AST) -> Decimal:
if isinstance(node, ast.Expression):
return evaluate(node.body)
if isinstance(node, ast.Name):
key = node.id.casefold()
if key not in row:
raise InvalidOperation
return _decimal(row[key])
if isinstance(node, ast.Constant):
return _decimal(node.value)
if isinstance(node, ast.UnaryOp) and isinstance(
node.op, (ast.UAdd, ast.USub)
):
value = evaluate(node.operand)
return value if isinstance(node.op, ast.UAdd) else -value
if isinstance(node, ast.BinOp) and isinstance(
node.op, (ast.Add, ast.Sub, ast.Mult, ast.Div)
):
left = evaluate(node.left)
right = evaluate(node.right)
if isinstance(node.op, ast.Add):
return left + right
if isinstance(node.op, ast.Sub):
return left - right
if isinstance(node.op, ast.Mult):
return left * right
return left / right
raise ValueError("unsupported contract expression")
return evaluate(tree)
def evidence_contract_report(
contracts: tuple[Mapping[str, Any], ...],
evidence: Any,
) -> list[dict[str, Any]]:
rows = [
{str(key).strip().casefold(): value for key, value in row.items()}
for row in _evidence_rows(evidence)
]
reports: list[dict[str, Any]] = []
for contract in contracts:
data_contract = contract.get("data_contract")
if not isinstance(data_contract, Mapping):
continue
required = [
str(item).strip().casefold()
for item in data_contract.get("required_fields", [])
if str(item).strip()
]
matching_row = next(
(row for row in rows if required and all(key in row for key in required)),
None,
)
observed = sorted(
{
key
for row in rows
for key in row
if not required or key in required
}
)
missing = [
key.upper()
for key in required
if matching_row is None or key not in matching_row
]
computed_checks: list[dict[str, Any]] = []
computed_fields = data_contract.get("computed_fields")
if matching_row is not None and isinstance(computed_fields, Mapping):
for field, definition in computed_fields.items():
normalized_field = str(field).strip().casefold()
expression = (
str(definition.get("expression") or "").strip()
if isinstance(definition, Mapping)
else ""
)
if not normalized_field or not expression:
continue
try:
actual = _decimal(matching_row.get(normalized_field))
expected = _evaluate_contract_expression(
expression,
matching_row,
)
matches = actual == expected
computed_checks.append(
{
"field": normalized_field.upper(),
"expression": expression,
"actual": str(actual),
"expected": str(expected),
"satisfied": matches,
}
)
except (InvalidOperation, ValueError, ZeroDivisionError):
computed_checks.append(
{
"field": normalized_field.upper(),
"expression": expression,
"satisfied": False,
}
)
calculations_satisfied = all(
bool(check.get("satisfied")) for check in computed_checks
)
temporal_checks: list[dict[str, Any]] = []
temporal_contract = contract.get("temporal_contract")
if matching_row is not None and isinstance(temporal_contract, Mapping):
status_fields = [
str(item).strip().casefold()
for item in temporal_contract.get("period_status_fields", [])
if str(item).strip()
]
past_status = str(
temporal_contract.get("past_period_status") or ""
).strip()
observed_status = next(
(
str(matching_row.get(field) or "").strip()
for field in status_fields
if str(matching_row.get(field) or "").strip()
),
"",
)
if observed_status == past_status and past_status:
decision_field = str(
temporal_contract.get("past_period_decision_field") or ""
).strip().casefold()
decision_value = str(
temporal_contract.get("past_period_decision_value") or ""
).strip()
actual_decision = str(
matching_row.get(decision_field) or ""
).strip()
temporal_checks.append(
{
"check": "past_period_decision",
"field": decision_field.upper(),
"expected": decision_value,
"actual": actual_decision,
"satisfied": bool(
decision_field
and decision_value
and actual_decision == decision_value
),
}
)
forbidden_patterns = [
str(item).strip()
for item in temporal_contract.get(
"forbidden_past_period_decision_field_patterns", []
)
if str(item).strip()
]
forbidden_fields = sorted(
key.upper()
for key in matching_row
if any(
re.search(pattern, key, flags=re.IGNORECASE)
for pattern in forbidden_patterns
)
)
temporal_checks.append(
{
"check": "no_past_period_yes_no_decision",
"forbidden_fields": forbidden_fields,
"satisfied": not forbidden_fields,
}
)
required_counts = [
str(item).strip().casefold()
for item in temporal_contract.get(
"validate_stated_days_against", []
)
if str(item).strip()
]
temporal_checks.append(
{
"check": "date_range_counts",
"required_fields": [
field.upper() for field in required_counts
],
"missing_fields": [
field.upper()
for field in required_counts
if field not in matching_row
],
"satisfied": all(
field in matching_row for field in required_counts
),
}
)
temporal_satisfied = all(
bool(check.get("satisfied")) for check in temporal_checks
)
reports.append(
{
"contract_id": str(contract.get("id") or ""),
"satisfied": bool(
matching_row is not None
and not missing
and calculations_satisfied
and temporal_satisfied
),
"required_fields": [key.upper() for key in required],
"observed_required_fields": [key.upper() for key in observed],
"missing_fields": missing,
"computed_field_checks": computed_checks,
"temporal_contract_checks": temporal_checks,
"validated_record": (
{
key.upper(): matching_row.get(key)
for key in required
}
if matching_row is not None
else {}
),
}
)
return reports
def missing_evidence_message(
contracts: tuple[Mapping[str, Any], ...],
) -> str:
for contract in contracts:
answer_contract = contract.get("answer_contract")
if not isinstance(answer_contract, Mapping):
continue
message = str(answer_contract.get("missing_evidence_message") or "").strip()
if message:
return message
return "조회 결과가 답변 계약의 필수 근거를 충족하지 않아 결론을 제공할 수 없습니다."
__all__ = [
"append_query_contract_guidance",
"evidence_contract_report",
"load_query_contracts",
"matching_query_contracts",
"missing_evidence_message",
"query_contract_guidance",
]

View File

@@ -1,5 +0,0 @@
"""Smilegate demo modules.
The portal is assembled from small modules so each feature can be reviewed and
released independently.
"""

View File

@@ -1 +0,0 @@
"""Presentation modules for the Smilegate demo."""

View File

@@ -1,28 +0,0 @@
"""Blank presentation shell.
No authentication, data access, MCP call, persistence, or customer text belongs
in this module. It exists only to prove the minimal Streamlit runtime path.
"""
from __future__ import annotations
from typing import Any
def render_blank_shell(st: Any) -> None:
"""Render the intentionally empty first review screen."""
st.set_page_config(page_title="Smilegate Demo", layout="wide")
st.markdown(
"""
<style>
[data-testid="stHeader"],
[data-testid="stToolbar"],
#MainMenu,
footer { display: none; }
[data-testid="stAppViewContainer"],
.stApp { background: #ffffff; }
.block-container { padding: 0; max-width: none; }
</style>
""",
unsafe_allow_html=True,
)

View File

@@ -1,84 +0,0 @@
{
"version": 1,
"contracts": [
{
"id": "leave_balance_eligibility",
"description": "연차·휴가 잔여와 신청 가능 여부를 원장 연도 및 구성값으로 검증한다.",
"applies_to_tools": [
"search_hr_data"
],
"intent_match": {
"subject_terms_any": [
"연차",
"휴가"
],
"action_terms_any": [
"잔여",
"가능",
"승인",
"신청",
"사용할 수"
]
},
"data_contract": {
"source_object": "HMM_LEAVE_BALANCES",
"identity_fields": [
"EMPLOYEE_CODE"
],
"required_fields": [
"LEAVE_YEAR",
"ANNUAL_ENTITLEMENT",
"CARRYOVER_DAYS",
"USED_DAYS",
"PENDING_DAYS",
"AVAILABLE_REMAINING_DAYS"
],
"computed_fields": {
"AVAILABLE_REMAINING_DAYS": {
"expression": "ANNUAL_ENTITLEMENT + CARRYOVER_DAYS - USED_DAYS - PENDING_DAYS",
"authoritative_source": "DATABASE_VIRTUAL_COLUMN"
}
},
"missing_record_semantics": "NO_BALANCE_DATA_FOR_YEAR",
"forbidden_missing_record_fallbacks": [
"ZERO",
"NVL",
"COALESCE",
"OUTER_JOIN_DEFAULT",
"LITERAL_VALUE"
]
},
"temporal_contract": {
"current_balance_year": "LATEST_AVAILABLE_LEAVE_YEAR",
"requested_period_and_current_balance_are_separate": true,
"period_status_fields": [
"REQUEST_PERIOD_LABEL",
"PERIOD_STATUS"
],
"past_period_status": "HISTORICAL_REQUEST_PERIOD",
"past_period_decision": "DO_NOT_DECIDE_NEW_APPLICATION_FROM_CURRENT_BALANCE",
"past_period_decision_field": "APPLICATION_DECISION_STATUS",
"past_period_decision_value": "NOT_APPLICABLE_FOR_HISTORICAL_PERIOD",
"forbidden_past_period_decision_field_patterns": [
"^CAN_",
"^APPROVAL_(ALLOWED|DENIED)$"
],
"validate_stated_days_against": [
"INCLUSIVE_CALENDAR_DAYS",
"MONDAY_TO_FRIDAY_DAYS"
],
"day_count_mismatch_status": "DATE_COUNT_REQUIRES_VALIDATION"
},
"answer_contract": {
"separate_data_facts_and_policy_requirements": true,
"deny_conclusions_without_required_fields": [
"ZERO_REMAINING_DAYS",
"LEAVE_APPROVAL_ALLOWED",
"LEAVE_APPROVAL_DENIED"
],
"missing_evidence_response": "STATE_INSUFFICIENT_EVIDENCE_WITHOUT_INVENTING_VALUES",
"missing_evidence_message": "조회 결과가 필수 원장 근거나 시간 기준 계약을 충족하지 않아 휴가 가능 여부를 확정할 수 없습니다. 계약 필드가 충족된 원장 결과로 다시 조회해야 합니다."
}
}
]
}

View File

@@ -1,58 +0,0 @@
{
"default_server_id": "smilegate_game_data_mcp",
"servers": [
{
"id": "smilegate_game_data_mcp",
"enabled": true,
"provider": "smilegate_select_ai_mcp",
"transport": "http",
"endpoint_url": "https://smilegate-backoffice.cloud-handson.com/mcp",
"auth_token_env": "SMILEGATE_MCP_BEARER_TOKEN",
"timeout_seconds_env": "POC3_MCP_TIMEOUT_SECONDS",
"default_tool": "oracle.select_ai.smilegate_fewshot_nl2sql",
"router_model_profile": "gpt54_mini_oci",
"tool_allowlist": [
"oracle.select_ai.fewshot_preflight",
"oracle.select_ai.game_query_plan",
"oracle.select_ai.game_daily_au_lookup",
"oracle.select_ai.smilegate_fewshot_nl2sql",
"oracle.select_ai.smilegate_game_text2sql",
"oracle.select_ai.qa_vector_search",
"oracle.select_ai.qa_vector_store"
],
"tool_workflow": [
{
"tool": "oracle.select_ai.fewshot_preflight",
"prelude": true
},
{
"tool": "oracle.select_ai.game_query_plan",
"prelude": true
},
{
"tool": "oracle.select_ai.smilegate_fewshot_nl2sql",
"arguments_from": [
{
"argument": "fewShotPreflight",
"tool": "oracle.select_ai.fewshot_preflight"
},
{
"argument": "queryPlan",
"tool": "oracle.select_ai.game_query_plan"
}
]
},
{
"tool": "oracle.select_ai.game_daily_au_lookup",
"arguments_from": [
{
"argument": "queryPlan",
"tool": "oracle.select_ai.game_query_plan"
}
]
}
],
"description": "Smilegate game-data Text2SQL MCP server"
}
]
}

View File

@@ -1,128 +0,0 @@
"""Small same-origin authentication gateway for the Smilegate Streamlit portal.
The gateway issues a signed HttpOnly cookie after validating the configured
PBKDF2 password. The Streamlit application verifies the signature and expiry
from the incoming request, so browser refreshes and WebSocket reconnects do not
require a new login.
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import json
import os
import time
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs
COOKIE_NAME = "poc4_portal_auth"
MAX_BODY_BYTES = 8_192
COOKIE_TTL_SECONDS = int(os.environ.get("POC4_LOGIN_COOKIE_TTL_SECONDS", "43200"))
def _password_matches(password: str, encoded_password: str) -> bool:
try:
scheme, iterations_text, salt_hex, expected_hex = encoded_password.split("$", 3)
iterations = int(iterations_text)
salt = bytes.fromhex(salt_hex)
expected = bytes.fromhex(expected_hex)
except (TypeError, ValueError):
return False
if scheme != "pbkdf2_sha256" or not 100_000 <= iterations <= 2_000_000:
return False
candidate = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations)
return hmac.compare_digest(candidate, expected)
def _cookie_value(username: str) -> str:
secret = os.environ["POC4_LOGIN_REMEMBER_SECRET"]
claims = {"v": 1, "u": username, "e": int(time.time()) + COOKIE_TTL_SECONDS}
encoded = base64.urlsafe_b64encode(
json.dumps(claims, separators=(",", ":")).encode("utf-8")
).decode("ascii").rstrip("=")
signature = hmac.new(secret.encode("utf-8"), encoded.encode("ascii"), hashlib.sha256).hexdigest()
return f"{encoded}.{signature}"
def _set_cookie(handler: BaseHTTPRequestHandler, value: str, max_age: int) -> None:
attributes = [
f"{COOKIE_NAME}={value}",
"Path=/",
f"Max-Age={max_age}",
"HttpOnly",
"Secure",
"SameSite=Lax",
]
handler.send_header("Set-Cookie", "; ".join(attributes))
class PortalAuthHandler(BaseHTTPRequestHandler):
server_version = "SmilegatePortalAuth/1.0"
def log_message(self, _format: str, *_args: object) -> None:
# Do not log form data or authentication details.
return
def _redirect(self, location: str, cookie_value: str | None = None, max_age: int = 0) -> None:
self.send_response(HTTPStatus.SEE_OTHER)
if cookie_value is not None:
_set_cookie(self, cookie_value, max_age)
self.send_header("Location", location)
self.send_header("Cache-Control", "no-store")
self.end_headers()
def do_GET(self) -> None: # noqa: N802
if self.path == "/health":
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(b"ok\n")
return
if self.path == "/logout":
self._redirect("/", "", 0)
return
self.send_error(HTTPStatus.NOT_FOUND)
def do_POST(self) -> None: # noqa: N802
if self.path != "/login":
self.send_error(HTTPStatus.NOT_FOUND)
return
try:
content_length = int(self.headers.get("Content-Length", "0"))
except ValueError:
content_length = 0
if content_length <= 0 or content_length > MAX_BODY_BYTES:
self._redirect("/?login=failed")
return
form = parse_qs(self.rfile.read(content_length).decode("utf-8"), keep_blank_values=True)
username = form.get("username", [""])[0].strip()
password = form.get("password", [""])[0]
expected_username = os.environ.get("POC4_LOGIN_USER", "").strip()
encoded_password = os.environ.get("POC4_LOGIN_PASSWORD_PBKDF2", "").strip()
if (
expected_username
and hmac.compare_digest(username, expected_username)
and _password_matches(password, encoded_password)
):
self._redirect("/", _cookie_value(username), COOKIE_TTL_SECONDS)
return
self._redirect("/?login=failed")
def main() -> None:
address = os.environ.get("POC4_AUTH_BIND", "127.0.0.1")
port = int(os.environ.get("POC4_AUTH_PORT", "8623"))
required = ("POC4_LOGIN_USER", "POC4_LOGIN_PASSWORD_PBKDF2", "POC4_LOGIN_REMEMBER_SECRET")
missing = [name for name in required if not os.environ.get(name, "").strip()]
if missing:
raise RuntimeError("missing required portal auth configuration")
ThreadingHTTPServer((address, port), PortalAuthHandler).serve_forever()
if __name__ == "__main__":
main()

View File

@@ -1,20 +0,0 @@
"""Minimal Smilegate Streamlit demo entrypoint.
This entrypoint intentionally wires only the blank presentation shell. Feature
modules such as authentication, MCP querying, and history are added separately
after each review.
"""
from __future__ import annotations
import streamlit as st
from ai_web_agent_console.smilegate_demo.ui.shell import render_blank_shell
def main() -> None:
render_blank_shell(st)
if __name__ == "__main__":
main()

View File

@@ -1,217 +0,0 @@
from __future__ import annotations
import hashlib
import http.client
import os
from pathlib import Path
import sys
import threading
import unittest
from urllib.parse import urlencode
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from ai_web_agent_console.auth_gateway import ( # noqa: E402
AuthConfig,
LoginAttemptLimiter,
SESSION_COOKIE_NAME,
SessionTokenCodec,
build_handler,
clear_session_cookie_header,
password_matches,
session_cookie_header,
)
class AuthGatewayTest(unittest.TestCase):
def setUp(self) -> None:
self.secret = "s" * 48
self.codec = SessionTokenCodec(self.secret)
def test_session_token_round_trip_and_tamper_rejection(self) -> None:
token = self.codec.issue("demo-admin", 3600, now=1_000)
session = self.codec.verify(token, "demo-admin", now=1_001)
self.assertIsNotNone(session)
self.assertEqual("demo-admin", session.username)
self.assertEqual(4_600, session.expires_at)
self.assertIsNone(self.codec.verify(token + "x", "demo-admin", now=1_001))
self.assertIsNone(self.codec.verify(token, "other-user", now=1_001))
def test_expired_session_token_is_rejected(self) -> None:
token = self.codec.issue("demo-admin", 300, now=1_000)
self.assertIsNone(self.codec.verify(token, "demo-admin", now=1_300))
def test_remember_cookie_has_required_security_attributes(self) -> None:
header = session_cookie_header("signed-value", 604_800)
self.assertIn(f"{SESSION_COOKIE_NAME}=signed-value", header)
self.assertIn("Path=/", header)
self.assertIn("Secure", header)
self.assertIn("HttpOnly", header)
self.assertIn("SameSite=Lax", header)
self.assertIn("Max-Age=604800", header)
self.assertNotIn("Domain=", header)
def test_session_cookie_omits_persistent_max_age(self) -> None:
header = session_cookie_header("signed-value", None)
self.assertNotIn("Max-Age", header)
self.assertIn("HttpOnly", header)
def test_logout_cookie_expires_immediately(self) -> None:
header = clear_session_cookie_header()
self.assertIn("Max-Age=0", header)
self.assertIn("Secure", header)
self.assertIn("HttpOnly", header)
def test_pbkdf2_password_verification(self) -> None:
salt = bytes.fromhex("00112233445566778899aabbccddeeff")
expected = hashlib.pbkdf2_hmac(
"sha256", b"correct-password", salt, 200_000
).hex()
encoded = f"pbkdf2_sha256$200000${salt.hex()}${expected}"
self.assertTrue(password_matches("correct-password", encoded))
self.assertFalse(password_matches("wrong-password", encoded))
def test_rate_limiter_blocks_only_after_threshold(self) -> None:
limiter = LoginAttemptLimiter(maximum_failures=2, window_seconds=10)
limiter.record_failure("client", now=1)
self.assertFalse(limiter.blocked("client", now=2))
limiter.record_failure("client", now=3)
self.assertTrue(limiter.blocked("client", now=4))
self.assertFalse(limiter.blocked("client", now=20))
def test_environment_config_requires_new_cookie_secret(self) -> None:
previous = dict(os.environ)
try:
os.environ["POC4_LOGIN_USER"] = "demo-admin"
os.environ["POC4_LOGIN_PASSWORD_PBKDF2"] = "encoded"
os.environ.pop("POC4_LOGIN_COOKIE_SECRET", None)
with self.assertRaisesRegex(ValueError, "COOKIE_SECRET"):
AuthConfig.from_environment()
finally:
os.environ.clear()
os.environ.update(previous)
def test_http_login_check_and_logout_flow_never_uses_url_token(self) -> None:
salt = bytes.fromhex("00112233445566778899aabbccddeeff")
expected = hashlib.pbkdf2_hmac(
"sha256", b"correct-password", salt, 200_000
).hex()
config = AuthConfig(
username="demo-admin",
password_pbkdf2=(
f"pbkdf2_sha256$200000${salt.hex()}${expected}"
),
cookie_secret=self.secret,
port=8621,
)
from http.server import ThreadingHTTPServer
server = ThreadingHTTPServer(("127.0.0.1", 0), build_handler(config))
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
connection = http.client.HTTPConnection(
"127.0.0.1", server.server_address[1], timeout=3
)
try:
connection.request("GET", "/auth/login")
login_page = connection.getresponse()
body = login_page.read().decode("utf-8")
self.assertEqual(200, login_page.status)
csrf_header = next(
value
for name, value in login_page.getheaders()
if name.lower() == "set-cookie"
and value.startswith("__Host-HMM_LOGIN_CSRF=")
)
csrf_value = csrf_header.split("=", 1)[1].split(";", 1)[0]
self.assertIn(
f'name="csrf" value="{csrf_value}"',
body,
)
payload = urlencode(
{
"csrf": csrf_value,
"username": "demo-admin",
"password": "correct-password",
"remember": "yes",
}
)
connection.request(
"POST",
"/auth/login",
body=payload,
headers={
"Content-Type": "application/x-www-form-urlencoded",
"Cookie": f"__Host-HMM_LOGIN_CSRF={csrf_value}",
},
)
logged_in = connection.getresponse()
logged_in.read()
self.assertEqual(303, logged_in.status)
self.assertEqual("/", logged_in.getheader("Location"))
self.assertNotRegex(logged_in.getheader("Location"), r"token|remember")
session_header = next(
value
for name, value in logged_in.getheaders()
if name.lower() == "set-cookie"
and value.startswith(f"{SESSION_COOKIE_NAME}=")
)
session_value = session_header.split("=", 1)[1].split(";", 1)[0]
self.assertIn("Secure", session_header)
self.assertIn("HttpOnly", session_header)
self.assertIn("SameSite=Lax", session_header)
connection.request(
"GET",
"/auth/check",
headers={"Cookie": f"{SESSION_COOKIE_NAME}={session_value}"},
)
check = connection.getresponse()
check.read()
self.assertEqual(204, check.status)
self.assertEqual("demo-admin", check.getheader("X-Auth-User"))
connection.request(
"GET",
"/auth/check",
headers={"Cookie": f"{SESSION_COOKIE_NAME}={session_value}x"},
)
tampered = connection.getresponse()
tampered.read()
self.assertEqual(401, tampered.status)
connection.request(
"GET",
"/auth/logout",
headers={"Cookie": f"{SESSION_COOKIE_NAME}={session_value}"},
)
logout = connection.getresponse()
logout.read()
self.assertEqual(303, logout.status)
self.assertEqual("/auth/login", logout.getheader("Location"))
self.assertTrue(
any(
name.lower() == "set-cookie" and "Max-Age=0" in value
for name, value in logout.getheaders()
)
)
finally:
connection.close()
server.shutdown()
server.server_close()
thread.join(timeout=3)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,181 +0,0 @@
import unittest
from ai_web_agent_console.query_contracts import (
append_query_contract_guidance,
evidence_contract_report,
matching_query_contracts,
)
def test_leave_eligibility_question_selects_external_contract():
contracts = matching_query_contracts(
"직원 E9999의 연차 잔여와 신청 가능 여부를 알려줘",
"search_hr_data",
)
assert [contract["id"] for contract in contracts] == [
"leave_balance_eligibility"
]
def test_unrelated_tool_does_not_receive_leave_contract():
contracts = matching_query_contracts(
"직원 E9999의 연차 잔여를 알려줘",
"search_carrier_performance",
)
assert contracts == ()
def test_incomplete_result_cannot_be_interpreted_as_zero_balance():
contracts = matching_query_contracts(
"직원 E9999의 연차 사용 가능 여부를 알려줘",
"search_hr_data",
)
evidence = {
"result": (
'DATA_RESULT: {"items":[{"EMPLOYEE_CODE":"E9999",'
'"REMAINING_DAYS":0}]}'
)
}
report = evidence_contract_report(contracts, evidence)
assert report[0]["satisfied"] is False
assert "AVAILABLE_REMAINING_DAYS" in report[0]["missing_fields"]
assert "LEAVE_YEAR" in report[0]["missing_fields"]
def test_complete_database_row_satisfies_contract_without_fixture_values():
contracts = matching_query_contracts(
"직원 E9999의 연차 신청 가능 여부를 알려줘",
"search_hr_data",
)
evidence = {
"result": (
'DATA_RESULT\n{"items":[{"EMPLOYEE_CODE":"E9999",'
'"LEAVE_YEAR":2031,"ANNUAL_ENTITLEMENT":17,'
'"CARRYOVER_DAYS":2,"USED_DAYS":4,"PENDING_DAYS":1,'
'"AVAILABLE_REMAINING_DAYS":14}]}'
)
}
report = evidence_contract_report(contracts, evidence)
assert report[0]["satisfied"] is True
assert report[0]["validated_record"]["LEAVE_YEAR"] == 2031
assert report[0]["validated_record"]["AVAILABLE_REMAINING_DAYS"] == 14
def test_conflicting_computed_value_fails_contract():
contracts = matching_query_contracts(
"직원 E9999의 연차 신청 가능 여부를 알려줘",
"search_hr_data",
)
evidence = {
"result": (
'DATA_RESULT\n{"items":[{"EMPLOYEE_CODE":"E9999",'
'"LEAVE_YEAR":2031,"ANNUAL_ENTITLEMENT":17,'
'"CARRYOVER_DAYS":2,"USED_DAYS":4,"PENDING_DAYS":1,'
'"AVAILABLE_REMAINING_DAYS":0}]}'
)
}
report = evidence_contract_report(contracts, evidence)
assert report[0]["satisfied"] is False
assert report[0]["computed_field_checks"][0]["expected"] == "14"
assert report[0]["computed_field_checks"][0]["actual"] == "0"
def test_historical_period_rejects_yes_no_decision_and_missing_counts():
contracts = matching_query_contracts(
"직원 E9999의 과거 연차 신청 가능 여부를 알려줘",
"search_hr_data",
)
evidence = {
"result": (
'DATA_RESULT\n{"items":[{"EMPLOYEE_CODE":"E9999",'
'"LEAVE_YEAR":2031,"ANNUAL_ENTITLEMENT":17,'
'"CARRYOVER_DAYS":2,"USED_DAYS":4,"PENDING_DAYS":1,'
'"AVAILABLE_REMAINING_DAYS":14,'
'"REQUEST_PERIOD_LABEL":"HISTORICAL_REQUEST_PERIOD",'
'"CAN_APPLY":"YES","INCLUSIVE_CALENDAR_DAYS":11}]}'
)
}
report = evidence_contract_report(contracts, evidence)
assert report[0]["satisfied"] is False
failed = {
check["check"]
for check in report[0]["temporal_contract_checks"]
if not check["satisfied"]
}
assert failed == {
"past_period_decision",
"no_past_period_yes_no_decision",
"date_range_counts",
}
def test_historical_period_contract_accepts_non_decision_status():
contracts = matching_query_contracts(
"직원 E9999의 과거 연차 신청 가능 여부를 알려줘",
"search_hr_data",
)
evidence = {
"result": (
'DATA_RESULT\n{"items":[{"EMPLOYEE_CODE":"E9999",'
'"LEAVE_YEAR":2031,"ANNUAL_ENTITLEMENT":17,'
'"CARRYOVER_DAYS":2,"USED_DAYS":4,"PENDING_DAYS":1,'
'"AVAILABLE_REMAINING_DAYS":14,'
'"REQUEST_PERIOD_LABEL":"HISTORICAL_REQUEST_PERIOD",'
'"APPLICATION_DECISION_STATUS":'
'"NOT_APPLICABLE_FOR_HISTORICAL_PERIOD",'
'"INCLUSIVE_CALENDAR_DAYS":11,'
'"MONDAY_TO_FRIDAY_DAYS":7}]}'
)
}
report = evidence_contract_report(contracts, evidence)
assert report[0]["satisfied"] is True
def test_guidance_is_appended_from_json_contract():
guided = append_query_contract_guidance(
"직원 E9999의 휴가 잔여를 조회해줘",
original_question="직원 E9999의 휴가 잔여를 조회해줘",
tool_name="search_hr_data",
)
assert '"id":"leave_balance_eligibility"' in guided
assert "AVAILABLE_REMAINING_DAYS" in guided
class QueryContractTest(unittest.TestCase):
test_leave_eligibility_question_selects_external_contract = staticmethod(
test_leave_eligibility_question_selects_external_contract
)
test_unrelated_tool_does_not_receive_leave_contract = staticmethod(
test_unrelated_tool_does_not_receive_leave_contract
)
test_incomplete_result_cannot_be_interpreted_as_zero_balance = staticmethod(
test_incomplete_result_cannot_be_interpreted_as_zero_balance
)
test_complete_database_row_satisfies_contract_without_fixture_values = (
staticmethod(test_complete_database_row_satisfies_contract_without_fixture_values)
)
test_conflicting_computed_value_fails_contract = staticmethod(
test_conflicting_computed_value_fails_contract
)
test_historical_period_rejects_yes_no_decision_and_missing_counts = (
staticmethod(test_historical_period_rejects_yes_no_decision_and_missing_counts)
)
test_historical_period_contract_accepts_non_decision_status = staticmethod(
test_historical_period_contract_accepts_non_decision_status
)
test_guidance_is_appended_from_json_contract = staticmethod(
test_guidance_is_appended_from_json_contract
)

View File

@@ -1,22 +0,0 @@
-- Approve the reviewed customer QA example for a grouped business-AU query.
-- Empty result sets remain valid executed query results.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
inspection_status = 'VERIFIED',
answer_text = 'Expected focus: aggregate business AU by the user-master country attribute. '
|| 'Join CZN_CUSTOM_BIZ_USER_TXN to CZN_COMN_USER_MST by GUID and BASE_DT; filter BIZ_AU_FLAG=1 and EXPT_USER_YN=''N'', then group by LAST_CONN_COUNTRY_CD. '
|| 'A successfully executed query with no country rows is a valid result, not a SQL failure. '
|| 'Historical answer: no result rows.',
inspection_note = 'Customer QA verified: country business-AU is a grouped join; an empty result is a valid query outcome.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-05';
COMMIT;
SELECT example_id, reference_status, inspection_status, source_case_id, answer_text
FROM sg_qa_vector_example
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-05';

View File

@@ -1,16 +0,0 @@
-- Approve the reviewed customer QA example for a grouped standard-AU query.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
inspection_status = 'VERIFIED',
answer_text = 'Expected focus: aggregate standard AU by user-master country, joining COMN_COUNTRY_BAS only for the country display name. '
|| 'Use CZN_COMN_USER_MST with AU_FLAG=1 and EXPT_USER_YN=''N'', grouped by LAST_CONN_COUNTRY_CD and COUNTRY_KR_NM. '
|| 'The label standard AU does not imply STD_USER_YN. A successfully executed empty result is valid. '
|| 'Historical answer: no result rows.',
inspection_note = 'Customer QA verified: country standard-AU is grouped AU_FLAG aggregation; empty output is valid.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-06';
COMMIT;

View File

@@ -1,15 +0,0 @@
-- Approve the reviewed customer QA example for daily in-game currency holdings.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
inspection_status = 'VERIFIED',
answer_text = 'Expected focus: daily crystal holdings use CZN_CUSTOM_GOODS_HAVE_TXN joined to CZN_COMN_USER_MST and CZN_COMN_SVC_DIM_BAS. '
|| 'Filter the goods dimension to crystal, nonzero HAVE_CNT, eligible returning-user population, and the requested date range; group by BASE_DT. '
|| 'A successfully executed empty result is valid. Historical answer: no result rows.',
inspection_note = 'Customer QA verified: daily crystal holdings are a date-grouped goods/user/dimension join; empty output is valid.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-07';
COMMIT;

View File

@@ -1,15 +0,0 @@
-- Approve the exact customer QA for standard-AU crystal holdings per user.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
inspection_status = 'VERIFIED',
answer_text = 'Use the verified customer SQL template for crystal holdings among standard AU. '
|| 'The standard-AU population uses AU_FLAG=1 and EXPT_USER_YN=''N''; do not add STD_USER_YN unless explicitly requested. '
|| 'Use the template population denominator for the per-user average. Null aggregate values are valid when the qualifying set is empty.',
inspection_note = 'Customer QA verified: retain the approved standard-AU population and average denominator semantics.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-08';
COMMIT;

View File

@@ -1,21 +0,0 @@
-- Customer-provided CZN benchmark examples are the approved reference corpus
-- for exact-question Few-shot retrieval. Their SQL and expected-answer text
-- remain the source of metric semantics; no runtime game/table branching is added.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
inspection_status = 'VERIFIED',
inspection_note = 'Customer QA benchmark approved for exact-question Few-shot retrieval.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id LIKE 'CZN-%'
AND reference_status <> 'APPROVED';
COMMIT;
SELECT source_case_id, reference_status, inspection_status
FROM sg_qa_vector_example
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id LIKE 'CZN-%'
ORDER BY source_case_id;

View File

@@ -1,11 +0,0 @@
-- Preserve customer QA output semantics for empty numeric aggregates.
UPDATE sg_qa_vector_example
SET answer_text = NVL(answer_text, '') || ' For this approved metric, normalize an empty numeric aggregate to 0 in the returned result. Preserve the template join from CZN_CUSTOM_GOODS_CHANGE_TXN to CZN_COMN_USER_MST, apply u.EXPT_USER_YN=''N'', and count distinct u.GUID.',
inspection_note = 'Customer QA verified: empty total Ether usage is reported as numeric zero with the template user-master join, excluded-user filter, and user population.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-13';
COMMIT;

View File

@@ -1,21 +0,0 @@
-- Approve the remaining customer-provided standard QA references for exact-question Few-shot retrieval.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
inspection_status = 'VERIFIED',
inspection_note = 'Customer QA benchmark approved for exact-question Few-shot retrieval.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id LIKE 'STD-%'
AND reference_status <> 'APPROVED';
UPDATE sg_qa_vector_example
SET answer_text = NVL(answer_text, '') || ' This unavailable-object case must not fabricate a DUAL/NULL result row. Return no result rows and explain that no approved physical object is available for the resolved game.',
inspection_note = 'Customer QA verified: unavailable game objects return no result rows; no synthetic DUAL result.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'STD-01';
COMMIT;

View File

@@ -1,64 +0,0 @@
-- Customer QA is evaluation data, never production Few-shot context.
-- Preserve it for SG_AI_QA_* baseline/history audit while retiring its vector copies.
UPDATE sg_qa_vector_example
SET reference_status = 'RETIRED',
inspection_note = 'Evaluation-only customer QA. Excluded from production Few-shot retrieval.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_EVALUATION_SEPARATION'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK';
COMMIT;
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
p_question IN CLOB,
p_top_k IN PLS_INTEGER DEFAULT 3,
p_target_type IN VARCHAR2 DEFAULT 'ANY'
) RETURN SYS_REFCURSOR
AUTHID DEFINER
IS
v_query_vector VECTOR;
v_results SYS_REFCURSOR;
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
BEGIN
IF p_question IS NULL THEN
RAISE_APPLICATION_ERROR(-20003, 'question is required.');
END IF;
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
END IF;
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
RAISE_APPLICATION_ERROR(-20005, 'target_type must be NONE, SINGLE, MULTI, ALL, or ANY.');
END IF;
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
p_question,
JSON(sg_qa_vector_params('search_query'))
);
OPEN v_results FOR
SELECT example_id,
question,
answer_sql,
answer_text,
embedding_model,
reference_kind,
target_type,
object_role,
source_case_id,
source_type,
vector_distance(embedding, v_query_vector, COSINE) AS cosine_distance
FROM sg_qa_vector_example
WHERE reference_status = 'APPROVED'
AND (source_type IS NULL OR source_type <> 'CUSTOMER_QA_BENCHMARK')
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
ORDER BY vector_distance(embedding, v_query_vector, COSINE), example_id
FETCH FIRST p_top_k ROWS ONLY;
RETURN v_results;
END;
/
SELECT source_type, reference_status, COUNT(*) AS example_count
FROM sg_qa_vector_example
GROUP BY source_type, reference_status
ORDER BY source_type, reference_status;

View File

@@ -1,165 +0,0 @@
-- Build one generalized runtime Few-shot pattern for every customer QA case.
-- The source benchmark remains evaluation-only; this derived record contains
-- no customer game name, date literal, expected result, or physical CZN object.
DECLARE
v_pattern_question CLOB;
v_pattern_sql CLOB;
v_embedding_input CLOB;
v_embedding VECTOR;
v_object_role VARCHAR2(64);
v_exists NUMBER;
FUNCTION generalized_question(p_question CLOB) RETURN CLOB IS
v_value CLOB := p_question;
BEGIN
-- Resolved names/aliases become a semantic game placeholder.
FOR token IN (
SELECT column_value AS value
FROM TABLE(sys.odcivarchar2list(
'카오스 제로 나이트메어', '카오스제로나이트메어', 'Chaos Zero Nightmare',
'STOVE_CHAOSZERO', '카제나', 'CZN', 'Bubblyz', '버블리즈',
'로드나인', '로나', '테스트게임', 'BUBBLYZ', 'LORDNINE'
))
) LOOP
v_value := REPLACE(v_value, token.value, '<게임>');
END LOOP;
v_value := REGEXP_REPLACE(v_value, '[0-9]{4}년[[:space:]]*[0-9]{1,2}월[[:space:]]*[0-9]{1,2}일', '<기준일>');
v_value := REGEXP_REPLACE(v_value, '[0-9]{4}-[0-9]{2}-[0-9]{2}', '<기준일>');
RETURN v_value;
END;
FUNCTION generalized_sql(p_sql CLOB) RETURN CLOB IS
v_value CLOB := p_sql;
BEGIN
-- Physical game objects become logical roles. Common dimensions remain
-- logical as well so the current metadata/plan selects real objects.
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_COMN_USER_MST"', '<RESOLVED_GAME_USER_MASTER>');
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_COMN_CHARACTER_MST"', '<RESOLVED_GAME_CHARACTER_MASTER>');
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_CUSTOM_GOODS_HAVE_TXN"', '<RESOLVED_GAME_GOODS_HOLDINGS>');
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_CUSTOM_GOODS_CHANGE_TXN"', '<RESOLVED_GAME_GOODS_CHANGE>');
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_CUSTOM_BIZ_USER_TXN"', '<RESOLVED_GAME_BUSINESS_USER>');
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_CUSTOM_USER_GOODS_TXN"', '<RESOLVED_GAME_USER_GOODS>');
v_value := REPLACE(v_value, '"SGMP_POC"."COMN_SALES_TXN"', '<APPROVED_SALES_TRANSACTION>');
v_value := REPLACE(v_value, '"SGMP_POC"."COMN_REFUND_TXN"', '<APPROVED_REFUND_TRANSACTION>');
v_value := REPLACE(v_value, '"SGMP_POC"."COMN_GAME_ALIAS_BAS"', '<GAME_ALIAS_CATALOG>');
v_value := REPLACE(v_value, '"SGMP_POC"."COMN_COUNTRY_BAS"', '<COUNTRY_DIMENSION>');
v_value := REPLACE(v_value, '"SGMP_POC"."CZN_COMN_SVC_DIM_BAS"', '<RESOLVED_GAME_SERVICE_DIMENSION>');
v_value := REPLACE(v_value, 'STOVE_CHAOSZERO', '<RESOLVED_GAME_ID>');
v_value := REPLACE(v_value, '''카제나''', '<RESOLVED_GAME_NAME>');
v_value := REPLACE(v_value, '''CZN''', '<RESOLVED_GAME_PREFIX>');
v_value := REGEXP_REPLACE(v_value, 'CZN_[A-Z0-9_]+', '<RESOLVED_GAME_OBJECT>');
v_value := REPLACE(v_value, '카제나', '<RESOLVED_GAME_NAME>');
v_value := REPLACE(v_value, '카오스 제로 나이트메어', '<RESOLVED_GAME_NAME>');
v_value := REPLACE(v_value, '카오스제로나이트메어', '<RESOLVED_GAME_NAME>');
v_value := REPLACE(v_value, 'CZN', '<RESOLVED_GAME_PREFIX>');
v_value := REPLACE(v_value, 'BUBBLYZ', '<RESOLVED_GAME_ID>');
v_value := REPLACE(v_value, 'Bubblyz', '<RESOLVED_GAME_NAME>');
v_value := REPLACE(v_value, '버블리즈', '<RESOLVED_GAME_NAME>');
v_value := REPLACE(v_value, 'LORDNINE', '<RESOLVED_GAME_ID>');
v_value := REPLACE(v_value, '로드나인', '<RESOLVED_GAME_NAME>');
v_value := REPLACE(v_value, '테스트게임', '<RESOLVED_GAME_NAME>');
v_value := REGEXP_REPLACE(v_value, 'TO_DATE\(''[0-9]{4}-[0-9]{2}-[0-9]{2}'', ''YYYY-MM-DD''\)', '<BUSINESS_DATE>');
v_value := REGEXP_REPLACE(v_value, 'TO_DATE\(''[0-9]{8}'', ''YYYYMMDD''\)', '<BUSINESS_DATE>');
v_value := REGEXP_REPLACE(v_value, 'DATE ''[0-9]{4}-[0-9]{2}-[0-9]{2}''', '<BUSINESS_DATE>');
RETURN v_value;
END;
FUNCTION role_of(p_sql CLOB) RETURN VARCHAR2 IS
BEGIN
IF DBMS_LOB.INSTR(p_sql, 'CZN_COMN_CHARACTER_MST') > 0 THEN
RETURN 'GAME_CHARACTER_MASTER';
ELSIF DBMS_LOB.INSTR(p_sql, 'CZN_CUSTOM_GOODS_HAVE_TXN') > 0 THEN
RETURN 'GAME_GOODS_HOLDINGS';
ELSIF DBMS_LOB.INSTR(p_sql, 'CZN_CUSTOM_GOODS_CHANGE_TXN') > 0 THEN
RETURN 'GAME_GOODS_CHANGE';
ELSIF DBMS_LOB.INSTR(p_sql, 'CZN_CUSTOM_BIZ_USER_TXN') > 0 THEN
RETURN 'GAME_BUSINESS_USER';
ELSIF DBMS_LOB.INSTR(p_sql, 'COMN_SALES_TXN') > 0 THEN
RETURN 'SALES_TRANSACTION';
ELSIF DBMS_LOB.INSTR(p_sql, 'COMN_REFUND_TXN') > 0 THEN
RETURN 'REFUND_TRANSACTION';
ELSIF DBMS_LOB.INSTR(p_sql, 'CZN_COMN_USER_MST') > 0 THEN
RETURN 'GAME_USER_MASTER';
END IF;
RETURN 'METADATA_OR_OPERATION';
END;
BEGIN
FOR source_row IN (
SELECT example_id, source_case_id, question, answer_sql
FROM sg_qa_vector_example
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
ORDER BY source_case_id
) LOOP
v_pattern_question := generalized_question(source_row.question);
v_pattern_sql := generalized_sql(source_row.answer_sql);
v_object_role := role_of(source_row.answer_sql);
v_embedding_input := TO_CLOB('Generalized question pattern: ') || v_pattern_question
|| CHR(10) || 'Logical object role: ' || v_object_role
|| CHR(10) || 'Structural SQL template: ' || v_pattern_sql
|| CHR(10) || 'Use only current game scope metadata and replace placeholders from the current request.';
-- A generalized runtime pattern must not contain known customer answer
-- identifiers or fixed business-date literals.
IF REGEXP_LIKE(v_pattern_question,
'카제나|버블리즈|Bubblyz|로드나인|테스트게임|[0-9]{4}년|[0-9]{4}-[0-9]{2}-[0-9]{2}', 'i')
OR REGEXP_LIKE(v_pattern_sql,
'CZN_|STOVE_CHAOSZERO|카제나|버블리즈|Bubblyz|[0-9]{4}-[0-9]{2}-[0-9]{2}', 'i') THEN
RAISE_APPLICATION_ERROR(-20061, 'Generalization leak in ' || source_row.source_case_id);
END IF;
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
v_embedding_input,
JSON(sg_qa_vector_params('search_document'))
);
SELECT COUNT(*)
INTO v_exists
FROM sg_qa_vector_example
WHERE source_type = 'GENERALIZED_QUESTION_PATTERN'
AND source_case_id = 'PAT-' || source_row.source_case_id;
IF v_exists = 0 THEN
INSERT INTO sg_qa_vector_example (
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
reference_status, reference_kind, target_type, object_role,
inspection_status, inspection_note, verified_at, verified_by,
source_case_id, source_type
) VALUES (
v_pattern_question,
v_pattern_sql,
'Question-specific generalized Few-shot. Structural only: it contains no customer game, date, result, or executable answer. First decide whether this pattern is applicable; then apply the authoritative NONE/SINGLE/MULTI/ALL game plan and replace placeholders from current metadata.',
v_embedding_input,
v_embedding,
'cohere.embed-v4.0',
'APPROVED', 'SQL_TEMPLATE', 'ANY', v_object_role,
'VERIFIED',
'Derived from a customer QA structure after game/date/result/object leakage validation; runtime uses this generalized pattern only.',
SYSTIMESTAMP, 'SGMP_POC_PATTERN_REVIEW',
'PAT-' || source_row.source_case_id, 'GENERALIZED_QUESTION_PATTERN'
);
ELSE
UPDATE sg_qa_vector_example
SET question = v_pattern_question,
answer_sql = v_pattern_sql,
answer_text = 'Question-specific generalized Few-shot. Structural only: it contains no customer game, date, result, or executable answer. First decide whether this pattern is applicable; then apply the authoritative NONE/SINGLE/MULTI/ALL game plan and replace placeholders from current metadata.',
embedding_input = v_embedding_input,
embedding = v_embedding,
object_role = v_object_role,
reference_status = 'APPROVED',
inspection_status = 'VERIFIED',
inspection_note = 'Derived from a customer QA structure after game/date/result/object leakage validation; runtime uses this generalized pattern only.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_PATTERN_REVIEW'
WHERE source_type = 'GENERALIZED_QUESTION_PATTERN'
AND source_case_id = 'PAT-' || source_row.source_case_id;
END IF;
END LOOP;
COMMIT;
END;
/
SELECT source_type, reference_status, COUNT(*) AS example_count
FROM sg_qa_vector_example
GROUP BY source_type, reference_status
ORDER BY source_type, reference_status;

View File

@@ -1,358 +0,0 @@
-- Generate one reusable, question-specific Few-shot pattern per customer QA
-- benchmark without promoting the benchmark answer itself. Game identity is
-- deliberately not inferred here: sg_game_query_plan owns that through OCI
-- GenAI chat + the current game catalog.
CREATE OR REPLACE FUNCTION sg_qa_genai_generalize_pattern(
p_question IN CLOB,
p_answer_sql IN CLOB,
p_target_type IN VARCHAR2
) RETURN CLOB AUTHID DEFINER
IS
v_prompt CLOB;
v_result CLOB;
BEGIN
v_prompt :=
'Create one reusable, question-specific Few-shot SQL pattern from the source example. '
|| 'This is training guidance, never an answer key. Return exactly these tagged sections and nothing else: '
|| '[[PATTERN_QUESTION]], [[STRUCTURAL_SQL_PATTERN]], [[OBJECT_ROLE]], [[TARGET_TYPE]], '
|| '[[APPLICABILITY]], [[END]]. '
|| 'Preserve only the query intent and structural operations such as aggregation, joins, '
|| 'grouping, ordering, date semantics, and filters. Replace every game name, alias, game ID, '
|| 'schema name, physical object name, column name, literal date, literal number, user ID, '
|| 'currency amount, and expected output with semantic placeholders such as <GAME_SCOPE>, '
|| '<LOGICAL_FACT>, <LOGICAL_DIMENSION>, <METRIC>, <AS_OF_DATE>, <FILTER>, and <GROUPING>. '
|| 'In STRUCTURAL_SQL_PATTERN, every non-SQL identifier must be an angle-bracket placeholder: '
|| 'do not retain any source column, alias, table, schema, literal, code, or business value. '
|| 'Do not include executable SQL. Do not include a game name or a customer answer. '
|| 'The current game scope is supplied separately at runtime by a database OCI GenAI chat '
|| 'resolver, therefore never choose or imply a game. The TARGET_TYPE section must be one of NONE, '
|| 'SINGLE, MULTI, ALL, ANY and must describe applicability, not a game identity. '
|| 'Source target type from the current resolver: ' || NVL(p_target_type, 'ANY') || CHR(10)
|| 'Source question:' || CHR(10) || DBMS_LOB.SUBSTR(p_question, 4000, 1) || CHR(10)
|| 'Source SQL (structure only; do not copy identifiers or values):' || CHR(10)
|| DBMS_LOB.SUBSTR(p_answer_sql, 12000, 1);
v_result := DBMS_CLOUD_AI.GENERATE(
prompt => v_prompt,
profile_name => 'SGMP_POC_OCI_GPT54MINI',
action => 'chat'
);
RETURN v_result;
END;
/
CREATE OR REPLACE FUNCTION sg_qa_genai_validate_pattern(
p_pattern_json IN CLOB
) RETURN CLOB AUTHID DEFINER
IS
v_prompt CLOB;
v_result CLOB;
BEGIN
v_prompt :=
'Inspect only concrete-answer leakage in this reusable Few-shot pattern. Return exactly '
|| '[[CONCRETE_LEAKAGE]] YES or NO, then [[REASON]] and a short reason, then [[END]]. '
|| 'Return YES only when a customer answer, concrete game identity, physical schema/table/column '
|| 'identifier, literal date, literal business result, or executable SQL against a real object remains. '
|| 'Return NO when all such references are semantic angle-bracket placeholders. A pseudo-SQL pattern '
|| 'using SELECT/FROM/JOIN/GROUP BY, generic game-scope checks, EXISTS, UNION, or equality with '
|| 'angle-bracket placeholders is not concrete leakage and must return NO. Do not judge usefulness or '
|| 'completeness; classify leakage only. '
|| 'Candidate:' || CHR(10) || DBMS_LOB.SUBSTR(p_pattern_json, 16000, 1);
v_result := DBMS_CLOUD_AI.GENERATE(
prompt => v_prompt,
profile_name => 'SGMP_POC_OCI_GPT54MINI',
action => 'chat'
);
RETURN v_result;
END;
/
CREATE OR REPLACE FUNCTION sg_qa_generate_generalized_patterns
RETURN NUMBER AUTHID DEFINER
IS
PRAGMA AUTONOMOUS_TRANSACTION;
v_plan_raw CLOB;
v_plan JSON_OBJECT_T;
v_target_type VARCHAR2(16);
v_pattern_raw CLOB;
v_validation_raw CLOB;
v_status VARCHAR2(16);
v_validation_note CLOB;
v_question CLOB;
v_sql_pattern CLOB;
v_answer_text CLOB;
v_object_role VARCHAR2(64);
v_embedding_input CLOB;
v_embedding VECTOR;
v_count NUMBER := 0;
FUNCTION parse_json_result(p_value CLOB) RETURN JSON_OBJECT_T IS
v_text CLOB := TRIM(p_value);
BEGIN
IF DBMS_LOB.SUBSTR(v_text, 7, 1) = '```json' THEN
v_text := REGEXP_REPLACE(v_text, '^```json[[:space:]]*', '');
v_text := REGEXP_REPLACE(v_text, '[[:space:]]*```[[:space:]]*$', '');
ELSIF DBMS_LOB.SUBSTR(v_text, 3, 1) = '```' THEN
v_text := REGEXP_REPLACE(v_text, '^```[[:space:]]*', '');
v_text := REGEXP_REPLACE(v_text, '[[:space:]]*```[[:space:]]*$', '');
END IF;
RETURN JSON_OBJECT_T.parse(v_text);
END;
FUNCTION section_value(
p_raw IN CLOB, p_start_tag IN VARCHAR2, p_end_tag IN VARCHAR2
) RETURN CLOB IS
v_start PLS_INTEGER;
v_end PLS_INTEGER;
BEGIN
v_start := DBMS_LOB.INSTR(p_raw, p_start_tag, 1, 1);
IF v_start = 0 THEN
RAISE_APPLICATION_ERROR(-20071, 'OCI GenAI response is missing ' || p_start_tag);
END IF;
v_start := v_start + LENGTH(p_start_tag);
v_end := DBMS_LOB.INSTR(p_raw, p_end_tag, v_start, 1);
IF v_end = 0 OR v_end <= v_start THEN
RAISE_APPLICATION_ERROR(-20072, 'OCI GenAI response is missing ' || p_end_tag);
END IF;
RETURN TRIM(DBMS_LOB.SUBSTR(p_raw, LEAST(v_end - v_start, 32767), v_start));
END;
PROCEDURE upsert_pattern(
p_case_id IN VARCHAR2,
p_status IN VARCHAR2,
p_note IN CLOB
) IS
BEGIN
UPDATE sg_qa_vector_example
SET question = v_question,
answer_sql = v_sql_pattern,
answer_text = v_answer_text,
embedding_input = v_embedding_input,
embedding = v_embedding,
embedding_model = 'cohere.embed-v4.0',
reference_status = p_status,
reference_kind = 'SQL_PATTERN',
target_type = v_target_type,
object_role = v_object_role,
inspection_status = CASE WHEN p_status = 'APPROVED' THEN 'GENAI_VERIFIED' ELSE 'GENAI_REJECTED' END,
inspection_note = p_note,
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_OCI_GENAI_PATTERN'
WHERE source_type = 'GENERALIZED_QUESTION_PATTERN'
AND source_case_id = 'PAT-' || p_case_id;
IF SQL%ROWCOUNT = 0 THEN
INSERT INTO sg_qa_vector_example (
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
reference_status, reference_kind, target_type, object_role, inspection_status,
inspection_note, verified_at, verified_by, source_case_id, source_type
) VALUES (
v_question, v_sql_pattern, v_answer_text, v_embedding_input, v_embedding, 'cohere.embed-v4.0',
p_status, 'SQL_PATTERN', v_target_type, v_object_role,
CASE WHEN p_status = 'APPROVED' THEN 'GENAI_VERIFIED' ELSE 'GENAI_REJECTED' END,
p_note, SYSTIMESTAMP, 'SGMP_POC_OCI_GENAI_PATTERN',
'PAT-' || p_case_id, 'GENERALIZED_QUESTION_PATTERN'
);
END IF;
END;
BEGIN
FOR source_row IN (
SELECT source.source_case_id, source.question, source.answer_sql
FROM sg_qa_vector_example source
WHERE source.source_type = 'CUSTOMER_QA_BENCHMARK'
AND NOT EXISTS (
SELECT 1
FROM sg_qa_vector_example pattern
WHERE pattern.source_type = 'GENERALIZED_QUESTION_PATTERN'
AND pattern.source_case_id = 'PAT-' || source.source_case_id
AND pattern.reference_status = 'APPROVED'
)
ORDER BY source_case_id
) LOOP
BEGIN
-- The target category comes from the existing OCI GenAI game resolver;
-- no alias, prefix, table, or name is transformed in this migration.
v_plan_raw := sg_game_query_plan(source_row.question, 5);
v_plan := parse_json_result(v_plan_raw);
v_target_type := UPPER(NVL(v_plan.get_string('targetType'), 'ANY'));
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL') THEN
v_target_type := 'ANY';
END IF;
v_pattern_raw := sg_qa_genai_generalize_pattern(
source_row.question, source_row.answer_sql, v_target_type
);
v_question := section_value(v_pattern_raw, '[[PATTERN_QUESTION]]', '[[STRUCTURAL_SQL_PATTERN]]');
v_sql_pattern := section_value(v_pattern_raw, '[[STRUCTURAL_SQL_PATTERN]]', '[[OBJECT_ROLE]]');
v_object_role := SUBSTR(section_value(v_pattern_raw, '[[OBJECT_ROLE]]', '[[TARGET_TYPE]]'), 1, 64);
IF section_value(v_pattern_raw, '[[TARGET_TYPE]]', '[[APPLICABILITY]]')
IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
v_target_type := section_value(v_pattern_raw, '[[TARGET_TYPE]]', '[[APPLICABILITY]]');
END IF;
v_answer_text := TO_CLOB('Generalized, question-specific structural pattern. '
|| 'Current game scope must be supplied only by sg_game_query_plan. Applicability: ')
|| section_value(v_pattern_raw, '[[APPLICABILITY]]', '[[END]]');
v_embedding_input := TO_CLOB('Question-specific generalized Few-shot pattern:' || CHR(10))
|| v_question || CHR(10) || 'Logical role: ' || v_object_role || CHR(10)
|| 'Structural SQL pattern:' || CHR(10) || v_sql_pattern;
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
v_embedding_input, JSON(sg_qa_vector_params('search_document'))
);
v_validation_raw := sg_qa_genai_validate_pattern(v_pattern_raw);
v_status := CASE
WHEN REGEXP_SUBSTR(
UPPER(section_value(v_validation_raw, '[[CONCRETE_LEAKAGE]]', '[[REASON]]')),
'[A-Z]+'
) = 'NO'
THEN 'APPROVE'
ELSE 'REJECT'
END;
v_validation_note := section_value(v_validation_raw, '[[REASON]]', '[[END]]');
IF v_status = 'APPROVE' THEN
upsert_pattern(source_row.source_case_id, 'APPROVED',
'ADB OCI GenAI generated and independently validated a generalized pattern. '
|| 'The original customer QA remains evaluation-only. ' || v_validation_note);
v_count := v_count + 1;
ELSE
upsert_pattern(source_row.source_case_id, 'DRAFT',
'ADB OCI GenAI rejected the generalized pattern: ' || v_validation_note);
END IF;
EXCEPTION
WHEN OTHERS THEN
-- Persist an auditable non-runtime draft and continue with the other
-- customer questions; one malformed LLM response must not block all 47.
v_question := source_row.question;
v_sql_pattern := TO_CLOB('<PATTERN_GENERATION_FAILED>');
v_answer_text := TO_CLOB('No runtime Few-shot pattern: OCI GenAI generalization failed.');
v_object_role := 'UNSPECIFIED';
v_target_type := 'ANY';
v_embedding_input := TO_CLOB('Failed generalized pattern: ') || source_row.question;
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
v_embedding_input, JSON(sg_qa_vector_params('search_document'))
);
upsert_pattern(source_row.source_case_id, 'DRAFT',
'OCI GenAI pattern generation error: ' || SQLERRM);
END;
END LOOP;
COMMIT;
RETURN v_count;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
/
-- Re-run only the independent OCI Chat safety review after its policy changes.
-- It never reads a customer benchmark and never changes the generated pattern.
CREATE OR REPLACE FUNCTION sg_qa_revalidate_generalized_patterns
RETURN NUMBER AUTHID DEFINER
IS
PRAGMA AUTONOMOUS_TRANSACTION;
v_raw CLOB;
v_status VARCHAR2(16);
v_reason CLOB;
v_start PLS_INTEGER;
v_end PLS_INTEGER;
v_count NUMBER := 0;
FUNCTION section_value(
p_raw IN CLOB, p_start_tag IN VARCHAR2, p_end_tag IN VARCHAR2
) RETURN CLOB IS
v_from PLS_INTEGER;
v_to PLS_INTEGER;
BEGIN
v_from := DBMS_LOB.INSTR(p_raw, p_start_tag, 1, 1);
IF v_from = 0 THEN RAISE_APPLICATION_ERROR(-20073, 'Missing ' || p_start_tag); END IF;
v_from := v_from + LENGTH(p_start_tag);
v_to := DBMS_LOB.INSTR(p_raw, p_end_tag, v_from, 1);
IF v_to = 0 OR v_to <= v_from THEN RAISE_APPLICATION_ERROR(-20074, 'Missing ' || p_end_tag); END IF;
RETURN TRIM(DBMS_LOB.SUBSTR(p_raw, LEAST(v_to - v_from, 32767), v_from));
END;
BEGIN
FOR item IN (
SELECT example_id, question, answer_sql, answer_text
FROM sg_qa_vector_example
WHERE source_type = 'GENERALIZED_QUESTION_PATTERN'
ORDER BY source_case_id
) LOOP
BEGIN
v_raw := sg_qa_genai_validate_pattern(
TO_CLOB('[[PATTERN_QUESTION]]') || item.question
|| TO_CLOB(CHR(10) || '[[STRUCTURAL_SQL_PATTERN]]') || item.answer_sql
|| TO_CLOB(CHR(10) || '[[APPLICABILITY]]') || item.answer_text || CHR(10) || '[[END]]'
);
v_status := CASE
WHEN REGEXP_SUBSTR(
UPPER(section_value(v_raw, '[[CONCRETE_LEAKAGE]]', '[[REASON]]')),
'[A-Z]+'
) = 'NO'
THEN 'APPROVE'
ELSE 'REJECT'
END;
v_reason := section_value(v_raw, '[[REASON]]', '[[END]]');
UPDATE sg_qa_vector_example
SET reference_status = CASE WHEN v_status = 'APPROVE' THEN 'APPROVED' ELSE 'DRAFT' END,
inspection_status = CASE WHEN v_status = 'APPROVE' THEN 'GENAI_VERIFIED' ELSE 'GENAI_REJECTED' END,
inspection_note = 'ADB OCI GenAI independent revalidation: ' || v_reason,
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_OCI_GENAI_PATTERN'
WHERE example_id = item.example_id;
IF v_status = 'APPROVE' THEN v_count := v_count + 1; END IF;
EXCEPTION
WHEN OTHERS THEN
v_reason := TO_CLOB('OCI GenAI revalidation error: ' || SQLERRM);
UPDATE sg_qa_vector_example
SET reference_status = 'DRAFT',
inspection_status = 'GENAI_REJECTED',
inspection_note = v_reason,
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_OCI_GENAI_PATTERN'
WHERE example_id = item.example_id;
END;
END LOOP;
COMMIT;
RETURN v_count;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END;
/
-- Production retrieval accepts only independently generalized patterns or
-- policy templates. Customer QA benchmarks remain evaluation-only forever.
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
p_question IN CLOB,
p_top_k IN PLS_INTEGER DEFAULT 3,
p_target_type IN VARCHAR2 DEFAULT 'ANY'
) RETURN SYS_REFCURSOR AUTHID DEFINER
IS
v_query_vector VECTOR;
v_results SYS_REFCURSOR;
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
BEGIN
IF p_question IS NULL THEN RAISE_APPLICATION_ERROR(-20003, 'question is required.'); END IF;
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
END IF;
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
END IF;
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
p_question, JSON(sg_qa_vector_params('search_query'))
);
OPEN v_results FOR
SELECT example_id, question, answer_sql, answer_text, embedding_model,
reference_kind, target_type, object_role, source_case_id, source_type,
vector_distance(embedding, v_query_vector, COSINE) AS cosine_distance
FROM sg_qa_vector_example
WHERE reference_status = 'APPROVED'
AND source_type IN ('GENERALIZED_QUESTION_PATTERN', 'POLICY_TEMPLATE')
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
ORDER BY vector_distance(embedding, v_query_vector, COSINE), example_id
FETCH FIRST p_top_k ROWS ONLY;
RETURN v_results;
END;
/

View File

@@ -1,9 +0,0 @@
-- The MCP DB account owns the OCI GenAI planning functions while the game
-- catalog is owned by the data schema. Definer-rights PL/SQL needs direct
-- object grants; role grants are not sufficient at compile time.
BEGIN
EXECUTE IMMEDIATE 'GRANT SELECT ON SGMP_POC.SG_GAME_CATALOG TO ADMIN';
EXECUTE IMMEDIATE 'GRANT SELECT ON SGMP_POC.COMN_GAME_ALIAS_BAS TO ADMIN';
EXECUTE IMMEDIATE 'GRANT EXECUTE ON SGMP_POC.SG_GAME_CATALOG_SEARCH TO ADMIN';
END;
/

View File

@@ -1,148 +0,0 @@
-- Deterministic daily-AU lookup for an already resolved game query plan.
-- Physical user-master objects are selected only from SG_GAME_CATALOG.
-- No game name, alias, prefix, or object name is embedded in this function.
CREATE OR REPLACE FUNCTION sg_game_daily_au_lookup(
p_query_plan IN CLOB,
p_base_date IN DATE DEFAULT NULL
) RETURN CLOB AUTHID DEFINER IS
v_plan JSON_OBJECT_T;
v_targets JSON_ARRAY_T;
v_target JSON_OBJECT_T;
v_result JSON_OBJECT_T := JSON_OBJECT_T();
v_items JSON_ARRAY_T := JSON_ARRAY_T();
v_item JSON_OBJECT_T;
v_game_key VARCHAR2(128);
v_game_id VARCHAR2(128);
v_game_name VARCHAR2(512);
v_object_name VARCHAR2(128);
v_safe_object_name VARCHAR2(128);
v_effective_date DATE;
v_au_count NUMBER;
v_column_count PLS_INTEGER;
v_object_count PLS_INTEGER;
v_seen SYS.ODCIVARCHAR2LIST := SYS.ODCIVARCHAR2LIST();
v_target_count PLS_INTEGER := 0;
FUNCTION is_seen(p_game_key IN VARCHAR2) RETURN BOOLEAN IS
BEGIN
FOR i IN 1 .. v_seen.COUNT LOOP
IF v_seen(i) = p_game_key THEN
RETURN TRUE;
END IF;
END LOOP;
RETURN FALSE;
END;
PROCEDURE add_status(
p_game_key IN VARCHAR2,
p_status IN VARCHAR2,
p_reason IN VARCHAR2
) IS
BEGIN
v_item := JSON_OBJECT_T();
v_item.put('gameKey', p_game_key);
v_item.put('status', p_status);
v_item.put('reason', p_reason);
v_items.append(v_item);
END;
BEGIN
IF p_query_plan IS NULL THEN
RAISE_APPLICATION_ERROR(-20001, 'queryPlan is required');
END IF;
v_plan := JSON_OBJECT_T.parse(p_query_plan);
v_targets := v_plan.get_array('dataEligibleTargets');
IF v_targets IS NULL THEN
v_targets := v_plan.get_array('targets');
END IF;
IF v_targets IS NOT NULL AND v_targets.get_size > 0 THEN
FOR i IN 0 .. v_targets.get_size - 1 LOOP
v_target := TREAT(v_targets.get(i) AS JSON_OBJECT_T);
IF v_target IS NULL OR NOT v_target.has('gameKey') THEN
CONTINUE;
END IF;
v_game_key := v_target.get_string('gameKey');
IF v_game_key IS NULL OR is_seen(v_game_key) THEN
CONTINUE;
END IF;
v_seen.EXTEND;
v_seen(v_seen.COUNT) := v_game_key;
v_target_count := v_target_count + 1;
BEGIN
SELECT game_id, game_nm, user_master_object_name
INTO v_game_id, v_game_name, v_object_name
FROM sg_game_catalog
WHERE game_key = v_game_key
AND active_yn = 'Y';
EXCEPTION
WHEN NO_DATA_FOUND THEN
add_status(v_game_key, 'UNAVAILABLE', 'Catalog target is not active.');
CONTINUE;
END;
IF v_object_name IS NULL THEN
add_status(v_game_key, 'UNAVAILABLE', 'No approved user-master object is registered.');
CONTINUE;
END IF;
v_safe_object_name := DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(v_object_name));
SELECT COUNT(*) INTO v_object_count
FROM user_objects
WHERE object_name = v_safe_object_name
AND object_type IN ('TABLE', 'VIEW', 'MATERIALIZED VIEW')
AND status = 'VALID';
SELECT COUNT(*) INTO v_column_count
FROM user_tab_columns
WHERE table_name = v_safe_object_name
AND column_name IN ('GUID', 'BASE_DT', 'AU_FLAG', 'EXPT_USER_YN');
IF v_object_count = 0 OR v_column_count <> 4 THEN
add_status(v_game_key, 'UNAVAILABLE', 'Approved user-master object is not query-ready.');
CONTINUE;
END IF;
IF p_base_date IS NULL THEN
EXECUTE IMMEDIATE 'SELECT MAX(BASE_DT) FROM ' || v_safe_object_name
INTO v_effective_date;
ELSE
v_effective_date := TRUNC(p_base_date);
END IF;
IF v_effective_date IS NULL THEN
add_status(v_game_key, 'NO_DATA', 'No available base date in the selected object.');
CONTINUE;
END IF;
EXECUTE IMMEDIATE
'SELECT COUNT(DISTINCT GUID) FROM ' || v_safe_object_name
|| ' WHERE BASE_DT = :1 AND AU_FLAG = 1 AND EXPT_USER_YN = ''N'''
INTO v_au_count USING v_effective_date;
v_item := JSON_OBJECT_T();
v_item.put('gameKey', v_game_key);
v_item.put('gameId', v_game_id);
v_item.put('gameName', v_game_name);
v_item.put('objectName', v_safe_object_name);
v_item.put('baseDate', TO_CHAR(v_effective_date, 'YYYY-MM-DD'));
v_item.put('auCount', v_au_count);
v_item.put('status', 'READY');
v_item.put('sqlTemplate',
'SELECT COUNT(DISTINCT GUID) AS AU_COUNT FROM <catalog_user_master_object> '
|| 'WHERE BASE_DT = :baseDate AND AU_FLAG = 1 AND EXPT_USER_YN = ''N''');
v_items.append(v_item);
END LOOP;
END IF;
v_result.put('status', CASE WHEN v_target_count = 0 THEN 'NO_GAME_TARGET' ELSE 'GAME_AU_LOOKUP' END);
v_result.put('targetType', NVL(v_plan.get_string('targetType'), 'NONE'));
IF p_base_date IS NULL THEN
v_result.put_null('requestedBaseDate');
ELSE
v_result.put('requestedBaseDate', TO_CHAR(TRUNC(p_base_date), 'YYYY-MM-DD'));
END IF;
v_result.put('targetCount', v_target_count);
v_result.put('items', v_items);
RETURN v_result.to_clob;
END;
/

View File

@@ -1,68 +0,0 @@
-- Customer-managed score policy for vector-only game identity resolution.
BEGIN
EXECUTE IMMEDIATE q'[
CREATE TABLE sg_game_scope_policy (
policy_key VARCHAR2(128) PRIMARY KEY,
number_value NUMBER,
text_value VARCHAR2(4000),
description VARCHAR2(1000) NOT NULL,
active_yn CHAR(1) DEFAULT 'Y' NOT NULL,
updated_at TIMESTAMP(6) DEFAULT SYSTIMESTAMP NOT NULL,
CONSTRAINT sg_game_scope_policy_active_ck CHECK (active_yn IN ('Y', 'N'))
)]';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
MERGE INTO sg_game_scope_policy t
USING (
SELECT 'GAME_ALIAS_MAX_COSINE_DISTANCE' AS policy_key,
0.500000 AS number_value,
CAST(NULL AS VARCHAR2(4000)) AS text_value,
'Maximum cosine distance for accepting the closest independently embedded game alias.' AS description
FROM dual
UNION ALL
SELECT 'SCOPE_GUIDANCE_NONE', NULL,
'{"mode":"GAME_UNSPECIFIED","allowGameScopedObjects":false,"targetExecution":"COMMON_OBJECTS_OR_ZERO_ROW","instruction":"No game was selected. Do not use a game-scoped object. Use only a game-neutral common object when it answers the question; otherwise return a zero-row result."}',
'Prompt guidance for a question without a selected game.'
FROM dual
UNION ALL
SELECT 'SCOPE_GUIDANCE_SINGLE', NULL,
'{"mode":"EXACT_TARGETS","allowGameScopedObjects":true,"targetExecution":"ONLY_RESOLVED_TARGETS","instruction":"Use only the resolved target in targets. Do not select another game-scoped object."}',
'Prompt guidance for exactly one resolved game target.'
FROM dual
UNION ALL
SELECT 'SCOPE_GUIDANCE_MULTI', NULL,
'{"mode":"MULTIPLE_TARGETS","allowGameScopedObjects":true,"targetExecution":"ALL_RESOLVED_TARGETS","instruction":"Return results for all resolved available targets. Preserve unresolved targets as unavailable; do not replace them with another game."}',
'Prompt guidance for multiple game targets.'
FROM dual
UNION ALL
SELECT 'SCOPE_GUIDANCE_ALL', NULL,
'{"mode":"ALL_CATALOG_TARGETS","allowGameScopedObjects":true,"targetExecution":"ALL_AVAILABLE_CATALOG_TARGETS","instruction":"Use all available catalog targets. Do not invent games or game-scoped objects outside the catalog."}',
'Prompt guidance for every catalog game.'
FROM dual
) s
ON (t.policy_key = s.policy_key)
WHEN MATCHED THEN UPDATE SET
t.text_value = CASE
WHEN s.policy_key LIKE 'SCOPE_GUIDANCE_%' THEN s.text_value
ELSE t.text_value
END,
t.description = s.description,
t.active_yn = 'Y',
t.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
policy_key, number_value, text_value, description, active_yn
) VALUES (
s.policy_key, s.number_value, s.text_value, s.description, 'Y'
);
/
COMMENT ON TABLE sg_game_scope_policy IS
'Customer-managed game scope policy values; changing a value requires no application deployment.';
COMMENT ON COLUMN sg_game_scope_policy.number_value IS
'Numeric policy value. GAME_ALIAS_MAX_COSINE_DISTANCE applies to the closest alias vector.';
/

View File

@@ -1,82 +0,0 @@
-- Customer-managed quality floor for runtime Few-shot retrieval.
-- Lower cosine distance is more similar. The value is data, not application code.
MERGE INTO sg_game_scope_policy t
USING (
SELECT 'QA_VECTOR_MAX_COSINE_DISTANCE' AS policy_key,
0.650000 AS number_value,
'Maximum cosine distance accepted for a runtime approved Few-shot example.' AS description
FROM dual
) s
ON (t.policy_key = s.policy_key)
WHEN MATCHED THEN UPDATE SET
t.number_value = s.number_value,
t.description = s.description,
t.active_yn = 'Y',
t.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
policy_key, number_value, text_value, description, active_yn
) VALUES (
s.policy_key, s.number_value, NULL, s.description, 'Y'
);
/
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
p_question IN CLOB,
p_top_k IN PLS_INTEGER DEFAULT 3,
p_target_type IN VARCHAR2 DEFAULT 'ANY'
) RETURN SYS_REFCURSOR AUTHID DEFINER
IS
v_query_vector VECTOR;
v_results SYS_REFCURSOR;
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
v_max_cosine_distance NUMBER;
BEGIN
IF p_question IS NULL THEN
RAISE_APPLICATION_ERROR(-20003, 'question is required.');
END IF;
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
END IF;
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
END IF;
SELECT number_value
INTO v_max_cosine_distance
FROM sg_game_scope_policy
WHERE policy_key = 'QA_VECTOR_MAX_COSINE_DISTANCE'
AND active_yn = 'Y'
AND number_value IS NOT NULL;
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
p_question, JSON(sg_qa_vector_params('search_query'))
);
OPEN v_results FOR
SELECT example_id, question, answer_sql, answer_text, embedding_model,
reference_kind, target_type, object_role, source_case_id, source_type,
cosine_distance
FROM (
SELECT example_id, question, answer_sql, answer_text, embedding_model,
reference_kind, target_type, object_role, source_case_id, source_type,
VECTOR_DISTANCE(embedding, v_query_vector, COSINE) AS cosine_distance
FROM sg_qa_vector_example
WHERE reference_status = 'APPROVED'
-- A generated structural pattern is review material, not a runtime
-- Few-shot. Runtime examples must have human verification and an
-- executable SQL body rather than unresolved logical placeholders.
AND inspection_status = 'VERIFIED'
AND answer_sql IS NOT NULL
AND NOT REGEXP_LIKE(answer_sql, '<[A-Z][A-Z0-9_]*>', 'i')
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
)
WHERE cosine_distance <= v_max_cosine_distance
ORDER BY cosine_distance, example_id
FETCH FIRST p_top_k ROWS ONLY;
RETURN v_results;
END;
/
COMMENT ON TABLE sg_game_scope_policy IS
'Customer-managed game scope and runtime retrieval policy values; changing a value requires no application deployment.';
/

View File

@@ -1,38 +0,0 @@
-- STD-12 is a multi-target orchestration case, not a reusable SQL few-shot.
-- Preserve SG_AI_QA_QUESTION as the customer benchmark; remove only its
-- invalid vector-example row so it cannot be managed as a few-shot.
DECLARE
v_count PLS_INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_count
FROM sg_qa_vector_example
WHERE example_id = 51
AND source_case_id = 'STD-12'
AND source_type = 'CUSTOMER_QA_BENCHMARK';
IF v_count <> 1 THEN
RAISE_APPLICATION_ERROR(-20051, 'Expected exactly one invalid STD-12 few-shot row.');
END IF;
DELETE FROM sg_qa_vector_example
WHERE example_id = 51
AND source_case_id = 'STD-12'
AND source_type = 'CUSTOMER_QA_BENCHMARK';
IF SQL%ROWCOUNT <> 1 THEN
RAISE_APPLICATION_ERROR(-20052, 'Invalid STD-12 few-shot row was not deleted.');
END IF;
COMMIT;
END;
/
SELECT COUNT(*) AS remaining_fewshot_rows
FROM sg_qa_vector_example
WHERE example_id = 51;
SELECT COUNT(*) AS preserved_question_rows
FROM sg_ai_qa_question
WHERE question_code = 'STD-12';

View File

@@ -1,84 +0,0 @@
-- One DB-owned JSON identity document per game. No game value is hardcoded.
-- The relational alias child makes aliases a nested Duality View array.
BEGIN
EXECUTE IMMEDIATE q'[
CREATE TABLE sg_game_catalog_identity_alias (
game_key VARCHAR2(128) NOT NULL,
alias_value VARCHAR2(512) NOT NULL,
CONSTRAINT sg_game_catalog_identity_alias_pk PRIMARY KEY (game_key, alias_value),
CONSTRAINT sg_game_catalog_identity_alias_fk FOREIGN KEY (game_key)
REFERENCES sg_game_catalog (game_key)
)]';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN RAISE; END IF;
END;
/
MERGE INTO sg_game_catalog_identity_alias target
USING (
SELECT c.game_key, aliases.alias_value
FROM sg_game_catalog c,
JSON_TABLE(
c.aliases_json,
'$[*]' COLUMNS (alias_value VARCHAR2(512) PATH '$')
) aliases
WHERE c.active_yn = 'Y'
) source
ON (target.game_key = source.game_key AND target.alias_value = source.alias_value)
WHEN NOT MATCHED THEN INSERT (game_key, alias_value)
VALUES (source.game_key, source.alias_value);
/
DELETE FROM sg_game_catalog_identity_alias target
WHERE NOT EXISTS (
SELECT 1
FROM sg_game_catalog c,
JSON_TABLE(
c.aliases_json,
'$[*]' COLUMNS (alias_value VARCHAR2(512) PATH '$')
) aliases
WHERE c.game_key = target.game_key
AND c.active_yn = 'Y'
AND aliases.alias_value = target.alias_value
);
/
CREATE OR REPLACE JSON RELATIONAL DUALITY VIEW sg_game_catalog_identity_dv AS
SELECT JSON {
'_id' : c.game_key,
'gameId' : c.game_id,
'gamePrefix' : c.game_prefix,
'gameName' : c.game_nm,
'gameAliases' : [
SELECT JSON {
'_id' : { 'gameKey' : a.game_key, 'value' : a.alias_value }
}
FROM sg_game_catalog_identity_alias a
WHERE a.game_key = c.game_key
]
}
FROM sg_game_catalog c
WHERE c.active_yn = 'Y'
WITH CHECK OPTION;
/
-- Serialize the DB JSON document itself before embedding. GAME_ID, GAME_PREFIX,
-- names and every alias therefore share one vector search document.
UPDATE sg_game_catalog c
SET c.embedding = DBMS_VECTOR.UTL_TO_EMBEDDING(
(
SELECT JSON_SERIALIZE(d.data RETURNING CLOB)
FROM sg_game_catalog_identity_dv d
WHERE JSON_VALUE(d.data, '$._id') = c.game_key
),
JSON(sg_qa_vector_params('search_document'))
),
c.updated_at = SYSTIMESTAMP
WHERE c.active_yn = 'Y';
/
COMMENT ON TABLE sg_game_catalog_identity_dv IS
'DB JSON identity document for each active game; the canonical embedding source for game-name, alias, GAME_ID and GAME_PREFIX resolution.';
/

View File

@@ -1,144 +0,0 @@
-- STD-18 asks for an aggregate over qualifying orders, not an individual
-- transaction list. Remove the invalid customer-derived references and keep
-- one reusable, data-neutral aggregate pattern for runtime retrieval.
DELETE FROM sg_qa_vector_example
WHERE source_case_id IN ('STD-18', 'PAT-STD-18')
AND source_type IN ('CUSTOMER_QA_BENCHMARK', 'GENERALIZED_QUESTION_PATTERN');
/
DECLARE
v_input CLOB;
v_embedding VECTOR;
v_exists NUMBER;
BEGIN
v_input := TO_CLOB('질문 패턴: 전체 매출에서 금액 조건을 만족하는 주문을 집계해줘.')
|| CHR(10) || 'Question pattern: summarize whole-scope sales after a payment amount filter.'
|| CHR(10) || 'Logical object role: SALES_TRANSACTION'
|| CHR(10) || 'Required result grain: one aggregate row with total sales amount, distinct buyer count, and order count.'
|| CHR(10) || 'A reference to orders does not by itself request individual order detail.';
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
v_input,
JSON(sg_qa_vector_params('search_document'))
);
SELECT COUNT(*)
INTO v_exists
FROM sg_qa_vector_example
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'FILTERED_SALES_AGGREGATE';
IF v_exists = 0 THEN
INSERT INTO sg_qa_vector_example (
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
reference_status, reference_kind, target_type, object_role,
inspection_status, inspection_note, verified_at, verified_by,
source_case_id, source_type
) VALUES (
'전체 매출에서 금액 조건을 만족하는 주문을 집계해줘.',
TO_CLOB('SELECT SUM(CAST(s."PAYMT_AMT" AS NUMBER)) AS "TOTAL_SALES_AMOUNT",' || CHR(10)
|| ' COUNT(DISTINCT s."GUID") AS "BUYER_COUNT",' || CHR(10)
|| ' COUNT(*) AS "ORDER_COUNT"' || CHR(10)
|| 'FROM "SGMP_POC"."COMN_SALES_TXN" s' || CHR(10)
|| 'WHERE s."PAYMT_DTM" >= <BUSINESS_DATE_START>' || CHR(10)
|| ' AND s."PAYMT_DTM" < <BUSINESS_DATE_END>' || CHR(10)
|| ' AND CAST(s."PAYMT_AMT" AS NUMBER) <AMOUNT_CONDITION>' || CHR(10)
|| ' AND s."EXPT_USER_YN" = ''N'''),
'Structural Few-shot: return one aggregate row containing total sales amount, distinct buyer count, and order count after the requested payment-amount filter. Do not return individual orders unless the user explicitly asks for a list or detail rows.',
v_input,
v_embedding,
'cohere.embed-v4.0',
'APPROVED', 'SQL_TEMPLATE', 'NONE', 'SALES_TRANSACTION',
'VERIFIED',
'Reusable whole-scope filtered-sales aggregate. No customer date, amount, game, or expected result is stored.',
SYSTIMESTAMP, 'SGMP_POC_METADATA_REVIEW',
'FILTERED_SALES_AGGREGATE', 'POLICY_TEMPLATE'
);
ELSE
UPDATE sg_qa_vector_example
SET question = '전체 매출에서 금액 조건을 만족하는 주문을 집계해줘.',
answer_sql = TO_CLOB('SELECT SUM(CAST(s."PAYMT_AMT" AS NUMBER)) AS "TOTAL_SALES_AMOUNT",' || CHR(10)
|| ' COUNT(DISTINCT s."GUID") AS "BUYER_COUNT",' || CHR(10)
|| ' COUNT(*) AS "ORDER_COUNT"' || CHR(10)
|| 'FROM "SGMP_POC"."COMN_SALES_TXN" s' || CHR(10)
|| 'WHERE s."PAYMT_DTM" >= <BUSINESS_DATE_START>' || CHR(10)
|| ' AND s."PAYMT_DTM" < <BUSINESS_DATE_END>' || CHR(10)
|| ' AND CAST(s."PAYMT_AMT" AS NUMBER) <AMOUNT_CONDITION>' || CHR(10)
|| ' AND s."EXPT_USER_YN" = ''N'''),
answer_text = 'Structural Few-shot: return one aggregate row containing total sales amount, distinct buyer count, and order count after the requested payment-amount filter. Do not return individual orders unless the user explicitly asks for a list or detail rows.',
embedding_input = v_input,
embedding = v_embedding,
reference_status = 'APPROVED',
reference_kind = 'SQL_TEMPLATE',
target_type = 'NONE',
object_role = 'SALES_TRANSACTION',
inspection_status = 'VERIFIED',
inspection_note = 'Reusable whole-scope filtered-sales aggregate. No customer date, amount, game, or expected result is stored.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'FILTERED_SALES_AGGREGATE';
END IF;
COMMIT;
END;
/
-- A SQL template is prompt context, never an executable statement. Permit
-- reviewed policy templates to retain logical placeholders while continuing
-- to require executable SQL for automatically generalized patterns.
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
p_question IN CLOB,
p_top_k IN PLS_INTEGER DEFAULT 3,
p_target_type IN VARCHAR2 DEFAULT 'ANY'
) RETURN SYS_REFCURSOR AUTHID DEFINER
IS
v_query_vector VECTOR;
v_results SYS_REFCURSOR;
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
v_max_cosine_distance NUMBER;
BEGIN
IF p_question IS NULL THEN RAISE_APPLICATION_ERROR(-20003, 'question is required.'); END IF;
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
END IF;
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
END IF;
SELECT number_value INTO v_max_cosine_distance
FROM sg_game_scope_policy
WHERE policy_key = 'QA_VECTOR_MAX_COSINE_DISTANCE'
AND active_yn = 'Y'
AND number_value IS NOT NULL;
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
p_question, JSON(sg_qa_vector_params('search_query'))
);
OPEN v_results FOR
SELECT example_id, question, answer_sql, answer_text, embedding_model,
reference_kind, target_type, object_role, source_case_id, source_type,
cosine_distance
FROM (
SELECT example_id, question, answer_sql, answer_text, embedding_model,
reference_kind, target_type, object_role, source_case_id, source_type,
VECTOR_DISTANCE(embedding, v_query_vector, COSINE) AS cosine_distance
FROM sg_qa_vector_example
WHERE reference_status = 'APPROVED'
AND inspection_status = 'VERIFIED'
AND answer_sql IS NOT NULL
AND (source_type = 'POLICY_TEMPLATE'
OR NOT REGEXP_LIKE(answer_sql, '<[A-Z][A-Z0-9_]*>', 'i'))
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
)
WHERE cosine_distance <= v_max_cosine_distance
ORDER BY cosine_distance, example_id
FETCH FIRST p_top_k ROWS ONLY;
RETURN v_results;
END;
/
SELECT example_id, source_case_id, source_type, reference_status, inspection_status
FROM sg_qa_vector_example
WHERE source_case_id IN ('STD-18', 'PAT-STD-18', 'FILTERED_SALES_AGGREGATE')
ORDER BY example_id;

View File

@@ -1,33 +0,0 @@
-- This customer question is game-unscoped. Keep the existing NONE-compatible
-- Few-shot path; game_query_plan remains responsible for scope resolution.
UPDATE sg_qa_vector_example
SET question = '매출에서 금액 조건을 만족하는 주문의 총액, 구매자 수, 주문 수를 집계해줘.',
answer_sql = TO_CLOB('SELECT' || CHR(10)
|| ' SUM(CAST(s."PAYMT_AMT" AS NUMBER)) AS "TOTAL_SALES_AMOUNT",' || CHR(10)
|| ' COUNT(DISTINCT s."GUID") AS "BUYER_COUNT",' || CHR(10)
|| ' COUNT(*) AS "ORDER_COUNT"' || CHR(10)
|| ' FROM <COMMON_SALES_TRANSACTION> s' || CHR(10)
|| ' WHERE s."PAYMT_DTM" >= <BUSINESS_DATE_START>' || CHR(10)
|| ' AND s."PAYMT_DTM" < <BUSINESS_DATE_END>' || CHR(10)
|| ' AND CAST(s."PAYMT_AMT" AS NUMBER) <AMOUNT_CONDITION>' || CHR(10)
|| ' AND s."EXPT_USER_YN" = ''N'''),
answer_text = 'Aggregate result-shape reference: return one row with total sales amount, distinct buyer count, and order count. The game plan separately supplies any game scope; use this pattern only when the question is semantically similar.',
embedding_input = TO_CLOB('Question pattern: summarize sales after a payment amount condition.' || CHR(10)
|| 'Logical object role: SALES_TRANSACTION' || CHR(10)
|| 'Result grain: one aggregate row with total sales amount, distinct buyer count, and order count.'),
embedding = DBMS_VECTOR.UTL_TO_EMBEDDING(
TO_CLOB('Question pattern: summarize sales after a payment amount condition.' || CHR(10)
|| 'Logical object role: SALES_TRANSACTION' || CHR(10)
|| 'Result grain: one aggregate row with total sales amount, distinct buyer count, and order count.'),
JSON(sg_qa_vector_params('search_document'))),
target_type = 'NONE',
source_case_id = 'PORTAL-STD-18',
inspection_note = 'Generalized aggregate pattern for the current game-unscoped question; game_query_plan controls scope separately.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE example_id = 141
AND source_type = 'POLICY_TEMPLATE';
/
COMMIT;
/

View File

@@ -1,23 +0,0 @@
-- The reviewed customer benchmark answer is part of this Few-shot guidance.
-- It clarifies that the requested result is one aggregate row, not detail rows.
UPDATE sg_qa_vector_example
SET answer_text = TO_CLOB('Expected answer shape: return exactly one aggregate row, not individual order rows.' || CHR(10)
|| 'Expected answer:' || CHR(10)
|| 'TOTAL_SALES_AMOUNT BUYER_COUNT ORDER_COUNT' || CHR(10)
|| '------------------ ----------- -----------' || CHR(10)
|| ' 204720 6 6'),
embedding_input = TO_CLOB('Question pattern: summarize sales after a payment amount condition.' || CHR(10)
|| 'Expected output: TOTAL_SALES_AMOUNT, BUYER_COUNT, ORDER_COUNT as one aggregate row.' || CHR(10)
|| 'Expected result example: 204720, 6, 6.'),
embedding = DBMS_VECTOR.UTL_TO_EMBEDDING(
TO_CLOB('Question pattern: summarize sales after a payment amount condition.' || CHR(10)
|| 'Expected output: TOTAL_SALES_AMOUNT, BUYER_COUNT, ORDER_COUNT as one aggregate row.' || CHR(10)
|| 'Expected result example: 204720, 6, 6.'),
JSON(sg_qa_vector_params('search_document'))),
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE example_id = 141
AND source_type = 'POLICY_TEMPLATE';
/
COMMIT;
/

View File

@@ -1,29 +0,0 @@
-- STD-25 is a period AU metric, not the daily AU_FLAG metric.
-- Keep the evaluation evidence explicit so the LLM judge accepts the valid
-- weekly result shape produced by the NL2SQL tool.
UPDATE sg_ai_qa_question
SET expected_focus = 'Weekly AU: use one as-of snapshot (BASE_DT=2026-07-15), '
|| 'count DISTINCT GUID whose LAST_CONN_DT is in the inclusive seven-day window '
|| '(2026-07-09 through 2026-07-15), with STD_USER_YN=''Y'' and EXPT_USER_YN=''N''. '
|| 'This is one aggregate result, not daily rows. Do not substitute daily AU_FLAG=1 for the period definition.',
baseline_sql = TO_CLOB('SELECT COUNT(DISTINCT u."GUID") AS "RECENT_7DAY_AU"' || CHR(10)
|| 'FROM "SGMP_POC"."CZN_COMN_USER_MST" u' || CHR(10)
|| 'WHERE u."BASE_DT" = DATE ''2026-07-15''' || CHR(10)
|| ' AND u."LAST_CONN_DT" BETWEEN DATE ''2026-07-09'' AND DATE ''2026-07-15''' || CHR(10)
|| ' AND u."STD_USER_YN" = ''Y''' || CHR(10)
|| ' AND u."EXPT_USER_YN" = ''N'''),
baseline_answer = 'RECENT_7DAY_AU=0',
evaluation_rule_json = '{"required_sql_terms":["CZN_COMN_USER_MST","LAST_CONN_DT","STD_USER_YN","EXPT_USER_YN","COUNT"],"recommended_sql_terms":["BASE_DT"],"forbidden_sql_terms":["AU_FLAG"],"required_result_shape":"SINGLE_AGGREGATE"}',
updated_at = SYSTIMESTAMP
WHERE question_code = 'STD-25';
/
COMMIT;
/
SELECT question_code, expected_focus, baseline_sql, baseline_answer, evaluation_rule_json
FROM sg_ai_qa_question
WHERE question_code = 'STD-25';

View File

@@ -1,40 +0,0 @@
-- CZN-02 customer sample marks STD_USER_YN='Y' as optional for daily
-- standard-AU reporting. It must not turn an otherwise correct AU query into
-- a failure merely because the condition is present.
UPDATE sg_ai_qa_question
SET expected_focus = 'Daily standard AU: COUNT(DISTINCT GUID) from CZN_COMN_USER_MST '
|| 'for BASE_DT=2026-07-15 with AU_FLAG=1 and EXPT_USER_YN=''N''. '
|| 'STD_USER_YN=''Y'' is an allowed optional cohort filter in the customer sample; '
|| 'its presence or absence is not a contradiction to this baseline.',
evaluation_rule_json = '{"required_sql_terms":["CZN_COMN_USER_MST","AU_FLAG","EXPT_USER_YN","COUNT"],"recommended_sql_terms":["BASE_DT","STD_USER_YN"],"optional_sql_terms":["STD_USER_YN"]}',
updated_at = SYSTIMESTAMP
WHERE question_code = 'CZN-02';
/
UPDATE sg_qa_vector_example
SET answer_text = 'Expected focus: daily standard AU uses CZN_COMN_USER_MST, BASE_DT=2026-07-15, '
|| 'AU_FLAG=1 and EXPT_USER_YN=''N''. The customer sample permits STD_USER_YN=''Y'' '
|| 'as an optional standard-user cohort filter; do not treat its presence as a conflicting condition. '
|| 'Historical answer: STD_AU_COUNT=0',
inspection_note = 'Customer sample permits optional STD_USER_YN filtering for daily standard AU; AU_FLAG and excluded-user filtering remain mandatory.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-02'
AND reference_status = 'APPROVED';
/
COMMIT;
/
SELECT q.question_code, q.expected_focus, q.evaluation_rule_json,
e.example_id, e.answer_text
FROM sg_ai_qa_question q
LEFT JOIN sg_qa_vector_example e
ON e.source_type = 'CUSTOMER_QA_BENCHMARK'
AND e.source_case_id = q.question_code
WHERE q.question_code = 'CZN-02';

View File

@@ -1,90 +0,0 @@
-- Reusable SINGLE-scope pattern: two AU populations must be aggregated
-- independently before comparison. A user-master LEFT JOIN may erase valid
-- business-user rows and must not define the business population.
UPDATE sg_ai_qa_question
SET expected_focus = 'Compare standard AU and business AU as two independent single-row aggregates for the same as-of date. '
|| 'Standard AU uses the resolved game user master with AU_FLAG=1 and EXPT_USER_YN=''N''. '
|| 'Business AU uses the resolved game business-user fact with BIZ_AU_FLAG=1 and EXPT_USER_YN=''N''. '
|| 'Do not make the business count depend on a LEFT JOIN from the user-master population. '
|| 'STD_USER_YN=''Y'' is an allowed optional cohort filter, not a reason to reject the result.',
evaluation_rule_json = '{"required_sql_terms":["CZN_COMN_USER_MST","CZN_CUSTOM_BIZ_USER_TXN","AU_FLAG","BIZ_AU_FLAG","EXPT_USER_YN","COUNT"],"recommended_sql_terms":["BASE_DT","STD_USER_YN"],"optional_sql_terms":["STD_USER_YN"],"required_result_shape":"SINGLE_COMPARISON"}',
updated_at = SYSTIMESTAMP
WHERE question_code = 'CZN-03';
/
DECLARE
v_input CLOB;
v_embedding VECTOR;
v_exists NUMBER;
BEGIN
v_input := TO_CLOB('Question pattern: compare daily standard active users and business active users for one resolved game and one business date.')
|| CHR(10) || 'Question pattern Korean: 한 게임의 기준일 일간 표준 AU와 사업 AU를 비교해줘.'
|| CHR(10) || 'Logical object role: USER_BUSINESS_AU_COMPARISON'
|| CHR(10) || 'Required result shape: one row with two independent aggregate metrics.'
|| CHR(10) || 'Business population must be aggregated independently; a LEFT JOIN from the user-master population may not define it.';
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
v_input, JSON(sg_qa_vector_params('search_document'))
);
SELECT COUNT(*) INTO v_exists
FROM sg_qa_vector_example
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'STD_BIZ_AU_COMPARE';
IF v_exists = 0 THEN
INSERT INTO sg_qa_vector_example (
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
reference_status, reference_kind, target_type, object_role,
inspection_status, inspection_note, verified_at, verified_by,
source_case_id, source_type
) VALUES (
'한 게임의 기준일 일간 표준 AU와 사업 AU를 비교해줘.',
TO_CLOB('SELECT' || CHR(10)
|| ' (SELECT COUNT(DISTINCT u."GUID")' || CHR(10)
|| ' FROM <RESOLVED_GAME_USER_MASTER> u' || CHR(10)
|| ' WHERE u."BASE_DT" = <BUSINESS_DATE>' || CHR(10)
|| ' AND u."AU_FLAG" = 1' || CHR(10)
|| ' AND u."EXPT_USER_YN" = ''N'') AS "STANDARD_AU_COUNT",' || CHR(10)
|| ' (SELECT COUNT(DISTINCT b."GUID")' || CHR(10)
|| ' FROM <RESOLVED_GAME_BUSINESS_USER> b' || CHR(10)
|| ' WHERE b."BASE_DT" = <BUSINESS_DATE>' || CHR(10)
|| ' AND b."BIZ_AU_FLAG" = 1' || CHR(10)
|| ' AND b."EXPT_USER_YN" = ''N'') AS "BUSINESS_AU_COUNT"' || CHR(10)
|| 'FROM DUAL'),
'Applicable metric reference: for this daily AU comparison, the standard metric must use AU_FLAG=1 and EXPT_USER_YN=''N''; do not replace it with LAST_CONN_DT period logic or STD_USER_YN alone. The business metric must use BIZ_AU_FLAG=1 and EXPT_USER_YN=''N''. Produce one row from two independent aggregate subqueries, and do not count business users through a LEFT JOIN from the standard-user population. STD_USER_YN may be added only as an optional cohort filter.',
v_input, v_embedding, 'cohere.embed-v4.0',
'APPROVED', 'SQL_TEMPLATE', 'SINGLE', 'USER_BUSINESS_AU_COMPARISON',
'VERIFIED',
'Reusable comparison pattern with logical placeholders only; no customer game, date, result, or physical object is embedded.',
SYSTIMESTAMP, 'SGMP_POC_METADATA_REVIEW',
'STD_BIZ_AU_COMPARE', 'POLICY_TEMPLATE'
);
ELSE
UPDATE sg_qa_vector_example
SET question = '한 게임의 기준일 일간 표준 AU와 사업 AU를 비교해줘.',
answer_sql = TO_CLOB('SELECT' || CHR(10)
|| ' (SELECT COUNT(DISTINCT u."GUID")' || CHR(10)
|| ' FROM <RESOLVED_GAME_USER_MASTER> u' || CHR(10)
|| ' WHERE u."BASE_DT" = <BUSINESS_DATE>' || CHR(10)
|| ' AND u."AU_FLAG" = 1' || CHR(10)
|| ' AND u."EXPT_USER_YN" = ''N'') AS "STANDARD_AU_COUNT",' || CHR(10)
|| ' (SELECT COUNT(DISTINCT b."GUID")' || CHR(10)
|| ' FROM <RESOLVED_GAME_BUSINESS_USER> b' || CHR(10)
|| ' WHERE b."BASE_DT" = <BUSINESS_DATE>' || CHR(10)
|| ' AND b."BIZ_AU_FLAG" = 1' || CHR(10)
|| ' AND b."EXPT_USER_YN" = ''N'') AS "BUSINESS_AU_COUNT"' || CHR(10)
|| 'FROM DUAL'),
answer_text = 'Applicable metric reference: for this daily AU comparison, the standard metric must use AU_FLAG=1 and EXPT_USER_YN=''N''; do not replace it with LAST_CONN_DT period logic or STD_USER_YN alone. The business metric must use BIZ_AU_FLAG=1 and EXPT_USER_YN=''N''. Produce one row from two independent aggregate subqueries, and do not count business users through a LEFT JOIN from the standard-user population. STD_USER_YN may be added only as an optional cohort filter.',
embedding_input = v_input,
embedding = v_embedding,
reference_status = 'APPROVED', inspection_status = 'VERIFIED',
verified_at = SYSTIMESTAMP, verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'STD_BIZ_AU_COMPARE';
END IF;
COMMIT;
END;
/

View File

@@ -1,58 +0,0 @@
-- Reusable SINGLE-scope pattern for country-grouped daily AU.
UPDATE sg_ai_qa_question
SET expected_focus = 'Country-grouped daily standard AU uses the resolved game user master with BASE_DT, AU_FLAG=1 and EXPT_USER_YN=''N''. '
|| 'Group by LAST_CONN_COUNTRY_CD and use the approved country dimension only for display/classification. '
|| 'STD_USER_YN is optional and cannot replace AU_FLAG for the daily metric.',
evaluation_rule_json = '{"required_sql_terms":["CZN_COMN_USER_MST","AU_FLAG","EXPT_USER_YN","LAST_CONN_COUNTRY_CD","COUNT"],"recommended_sql_terms":["BASE_DT","COMN_COUNTRY_BAS","STD_USER_YN"],"optional_sql_terms":["STD_USER_YN"],"required_result_shape":"COUNTRY_GROUPED"}',
updated_at = SYSTIMESTAMP
WHERE question_code = 'CZN-06';
/
DECLARE
v_input CLOB;
v_embedding VECTOR;
v_exists NUMBER;
BEGIN
v_input := TO_CLOB('Question pattern: show daily active-user counts by country for one resolved game and one business date.')
|| CHR(10) || 'Question pattern Korean: 한 게임의 기준일 주요 국가별 표준 AU 수를 알려줘.'
|| CHR(10) || 'Logical object role: COUNTRY_GROUPED_DAILY_AU'
|| CHR(10) || 'Required metric: AU_FLAG=1 and excluded-user filtering; group by the last connection country.';
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(v_input, JSON(sg_qa_vector_params('search_document')));
SELECT COUNT(*) INTO v_exists FROM sg_qa_vector_example
WHERE source_type = 'POLICY_TEMPLATE' AND source_case_id = 'COUNTRY_DAILY_AU';
IF v_exists = 0 THEN
INSERT INTO sg_qa_vector_example (
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
reference_status, reference_kind, target_type, object_role,
inspection_status, inspection_note, verified_at, verified_by, source_case_id, source_type
) VALUES (
'한 게임의 기준일 주요 국가별 표준 AU 수를 알려줘.',
TO_CLOB('SELECT u."LAST_CONN_COUNTRY_CD" AS "COUNTRY_CD",' || CHR(10)
|| ' c."COUNTRY_KR_NM" AS "COUNTRY_NAME",' || CHR(10)
|| ' COUNT(DISTINCT u."GUID") AS "STANDARD_AU_COUNT"' || CHR(10)
|| 'FROM <RESOLVED_GAME_USER_MASTER> u' || CHR(10)
|| 'LEFT JOIN <APPROVED_COUNTRY_DIMENSION> c' || CHR(10)
|| ' ON c."COUNTRY_2CHAR_CD" = u."LAST_CONN_COUNTRY_CD"' || CHR(10)
|| 'WHERE u."BASE_DT" = <BUSINESS_DATE>' || CHR(10)
|| ' AND u."AU_FLAG" = 1' || CHR(10)
|| ' AND u."EXPT_USER_YN" = ''N''' || CHR(10)
|| 'GROUP BY u."LAST_CONN_COUNTRY_CD", c."COUNTRY_KR_NM"'),
'Applicable metric reference: country-grouped daily AU must use AU_FLAG=1 and EXPT_USER_YN=''N''; do not replace AU_FLAG with STD_USER_YN alone. Group by LAST_CONN_COUNTRY_CD. Use an approved country dimension for country display or a current approved major-country classification when the request requires it.',
v_input, v_embedding, 'cohere.embed-v4.0',
'APPROVED', 'SQL_TEMPLATE', 'SINGLE', 'COUNTRY_GROUPED_DAILY_AU',
'VERIFIED', 'Reusable country-grouped daily-AU pattern; no customer game, date, result, or physical object is embedded.',
SYSTIMESTAMP, 'SGMP_POC_METADATA_REVIEW', 'COUNTRY_DAILY_AU', 'POLICY_TEMPLATE'
);
ELSE
UPDATE sg_qa_vector_example
SET embedding_input = v_input, embedding = v_embedding,
reference_status = 'APPROVED', inspection_status = 'VERIFIED',
verified_at = SYSTIMESTAMP, verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'POLICY_TEMPLATE' AND source_case_id = 'COUNTRY_DAILY_AU';
END IF;
COMMIT;
END;
/

View File

@@ -1,17 +0,0 @@
-- CZN-06 is a verified, exact customer question/SQL pair. It must be a
-- runtime Few-shot when approved; RETIRED is the DB switch that excludes it.
-- This is a single-game reference, so keep the retrieval scope explicit.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
reference_kind = 'SQL_TEMPLATE',
target_type = 'SINGLE',
object_role = 'COUNTRY_GROUPED_DAILY_AU',
inspection_status = 'VERIFIED',
inspection_note = 'Verified exact CZN-06 Few-shot restored for runtime retrieval. Daily country AU requires AU_FLAG=1 and EXPT_USER_YN=''N''; STD_USER_YN alone is insufficient.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-06'
/
COMMIT

View File

@@ -1,69 +0,0 @@
-- Runtime Few-shots remain semantic vector retrieval. Customer examples are
-- governed by their DB approval state, not restricted to exact text matches.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
reference_kind = 'SQL_TEMPLATE',
target_type = 'SINGLE',
object_role = 'GAME_GOODS_HOLDINGS',
inspection_status = 'VERIFIED',
inspection_note = 'Verified CZN-07 semantic Few-shot. Use goods holdings, crystal dimension, RU_FLAG=1, excluded-user filter, nonzero holdings, and daily grouping.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-07'
/
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
p_question IN CLOB,
p_top_k IN PLS_INTEGER DEFAULT 3,
p_target_type IN VARCHAR2 DEFAULT 'ANY'
) RETURN SYS_REFCURSOR AUTHID DEFINER
IS
v_query_vector VECTOR;
v_results SYS_REFCURSOR;
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
v_max_cosine_distance NUMBER;
BEGIN
IF p_question IS NULL THEN RAISE_APPLICATION_ERROR(-20003, 'question is required.'); END IF;
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
END IF;
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
END IF;
SELECT number_value INTO v_max_cosine_distance
FROM sg_game_scope_policy
WHERE policy_key = 'QA_VECTOR_MAX_COSINE_DISTANCE'
AND active_yn = 'Y'
AND number_value IS NOT NULL;
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
p_question, JSON(sg_qa_vector_params('search_query'))
);
OPEN v_results FOR
SELECT example_id, question, answer_sql, answer_text, embedding_model,
reference_kind, target_type, object_role, source_case_id, source_type,
cosine_distance
FROM (
SELECT example_id, question, answer_sql, answer_text, embedding_model,
reference_kind, target_type, object_role, source_case_id, source_type,
VECTOR_DISTANCE(embedding, v_query_vector, COSINE) AS cosine_distance
FROM sg_qa_vector_example
WHERE reference_status = 'APPROVED'
AND inspection_status = 'VERIFIED'
AND answer_sql IS NOT NULL
AND (source_type = 'POLICY_TEMPLATE'
OR NOT REGEXP_LIKE(answer_sql, '<[A-Z][A-Z0-9_]*>', 'i'))
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
)
WHERE cosine_distance <= v_max_cosine_distance
ORDER BY cosine_distance, example_id
FETCH FIRST p_top_k ROWS ONLY;
RETURN v_results;
END;
/
COMMIT

View File

@@ -1,87 +0,0 @@
-- Keep semantic vector retrieval, but do not inject weak trailing neighbours
-- when a materially stronger example has already been found.
MERGE INTO sg_game_scope_policy t
USING (
SELECT 'QA_VECTOR_NEIGHBOR_DISTANCE_MARGIN' AS policy_key,
0.120000 AS number_value,
'Maximum additional cosine distance from the best runtime Few-shot candidate.' AS description
FROM dual
) s
ON (t.policy_key = s.policy_key)
WHEN MATCHED THEN UPDATE SET
t.number_value = s.number_value,
t.description = s.description,
t.active_yn = 'Y',
t.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
policy_key, number_value, text_value, description, active_yn
) VALUES (
s.policy_key, s.number_value, NULL, s.description, 'Y'
)
/
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
p_question IN CLOB,
p_top_k IN PLS_INTEGER DEFAULT 3,
p_target_type IN VARCHAR2 DEFAULT 'ANY'
) RETURN SYS_REFCURSOR AUTHID DEFINER
IS
v_query_vector VECTOR;
v_results SYS_REFCURSOR;
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
v_max_cosine_distance NUMBER;
v_neighbor_margin NUMBER;
BEGIN
IF p_question IS NULL THEN RAISE_APPLICATION_ERROR(-20003, 'question is required.'); END IF;
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
END IF;
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
END IF;
SELECT number_value INTO v_max_cosine_distance
FROM sg_game_scope_policy
WHERE policy_key = 'QA_VECTOR_MAX_COSINE_DISTANCE'
AND active_yn = 'Y'
AND number_value IS NOT NULL;
SELECT number_value INTO v_neighbor_margin
FROM sg_game_scope_policy
WHERE policy_key = 'QA_VECTOR_NEIGHBOR_DISTANCE_MARGIN'
AND active_yn = 'Y'
AND number_value IS NOT NULL;
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
p_question, JSON(sg_qa_vector_params('search_query'))
);
OPEN v_results FOR
SELECT example_id, question, answer_sql, answer_text, embedding_model,
reference_kind, target_type, object_role, source_case_id, source_type,
cosine_distance
FROM (
SELECT c.*,
MIN(c.cosine_distance) OVER () AS best_cosine_distance
FROM (
SELECT example_id, question, answer_sql, answer_text, embedding_model,
reference_kind, target_type, object_role, source_case_id, source_type,
VECTOR_DISTANCE(embedding, v_query_vector, COSINE) AS cosine_distance
FROM sg_qa_vector_example
WHERE reference_status = 'APPROVED'
AND inspection_status = 'VERIFIED'
AND answer_sql IS NOT NULL
AND (source_type = 'POLICY_TEMPLATE'
OR NOT REGEXP_LIKE(answer_sql, '<[A-Z][A-Z0-9_]*>', 'i'))
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
) c
WHERE c.cosine_distance <= v_max_cosine_distance
)
WHERE cosine_distance <= best_cosine_distance + v_neighbor_margin
ORDER BY cosine_distance, example_id
FETCH FIRST p_top_k ROWS ONLY;
RETURN v_results;
END;
/
COMMIT

View File

@@ -1,16 +0,0 @@
-- CZN-08 is the reviewed semantic reference for daily standard-AU crystal
-- total and per-user average holdings.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
reference_kind = 'SQL_TEMPLATE',
target_type = 'SINGLE',
object_role = 'GAME_GOODS_HOLDINGS',
inspection_status = 'VERIFIED',
inspection_note = 'Verified CZN-08 Few-shot. Standard-AU crystal holdings require AU_FLAG=1, excluded-user filtering, and per-user average as SUM(HAVE_CNT) / COUNT(DISTINCT GUID), not AVG(HAVE_CNT).',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-08'
/
COMMIT

View File

@@ -1,11 +0,0 @@
-- Calibrate the semantic-neighbour window using reviewed CZN patterns:
-- retain close paraphrases, exclude adjacent metric shapes.
UPDATE sg_game_scope_policy
SET number_value = 0.100000,
description = 'Maximum additional cosine distance from the best runtime Few-shot candidate.',
active_yn = 'Y',
updated_at = SYSTIMESTAMP
WHERE policy_key = 'QA_VECTOR_NEIGHBOR_DISTANCE_MARGIN'
/
COMMIT

View File

@@ -1,15 +0,0 @@
-- CZN-05 is the reviewed reference for country-grouped business AU.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
reference_kind = 'SQL_TEMPLATE',
target_type = 'SINGLE',
object_role = 'COUNTRY_GROUPED_BUSINESS_AU',
inspection_status = 'VERIFIED',
inspection_note = 'Verified CZN-05 Few-shot. Join business-user data to user master on GUID and BASE_DT before grouping by user country; filter BIZ_AU_FLAG=1 and EXPT_USER_YN=''N''.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-05'
/
COMMIT

View File

@@ -1,95 +0,0 @@
-- Hybrid retrieval remains database-driven: dense vector similarity handles
-- paraphrases, while lexical similarity distinguishes decisive request terms.
MERGE INTO sg_game_scope_policy t
USING (
SELECT 'QA_VECTOR_LEXICAL_WEIGHT' AS policy_key, 0.350000 AS number_value,
'Weight of normalized lexical question similarity in runtime Few-shot reranking.' AS description
FROM dual
UNION ALL
SELECT 'QA_VECTOR_HYBRID_SCORE_MARGIN', 0.050000,
'Maximum hybrid-score difference from the best runtime Few-shot candidate.'
FROM dual
) s
ON (t.policy_key = s.policy_key)
WHEN MATCHED THEN UPDATE SET
t.number_value = s.number_value,
t.description = s.description,
t.active_yn = 'Y',
t.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
policy_key, number_value, text_value, description, active_yn
) VALUES (
s.policy_key, s.number_value, NULL, s.description, 'Y'
)
/
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
p_question IN CLOB,
p_top_k IN PLS_INTEGER DEFAULT 3,
p_target_type IN VARCHAR2 DEFAULT 'ANY'
) RETURN SYS_REFCURSOR AUTHID DEFINER
IS
v_query_vector VECTOR;
v_results SYS_REFCURSOR;
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
v_max_cosine_distance NUMBER;
v_lexical_weight NUMBER;
v_hybrid_margin NUMBER;
BEGIN
IF p_question IS NULL THEN RAISE_APPLICATION_ERROR(-20003, 'question is required.'); END IF;
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
END IF;
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
RAISE_APPLICATION_ERROR(-20005, 'invalid target type.');
END IF;
SELECT number_value INTO v_max_cosine_distance FROM sg_game_scope_policy
WHERE policy_key = 'QA_VECTOR_MAX_COSINE_DISTANCE' AND active_yn = 'Y';
SELECT number_value INTO v_lexical_weight FROM sg_game_scope_policy
WHERE policy_key = 'QA_VECTOR_LEXICAL_WEIGHT' AND active_yn = 'Y';
SELECT number_value INTO v_hybrid_margin FROM sg_game_scope_policy
WHERE policy_key = 'QA_VECTOR_HYBRID_SCORE_MARGIN' AND active_yn = 'Y';
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
p_question, JSON(sg_qa_vector_params('search_query'))
);
OPEN v_results FOR
SELECT example_id, question, answer_sql, answer_text, embedding_model,
reference_kind, target_type, object_role, source_case_id, source_type,
cosine_distance
FROM (
SELECT s.*,
MAX(s.hybrid_score) OVER () AS best_hybrid_score
FROM (
SELECT c.*,
((1 - v_lexical_weight) * (1 - c.cosine_distance)
+ v_lexical_weight * c.lexical_similarity) AS hybrid_score
FROM (
SELECT example_id, question, answer_sql, answer_text, embedding_model,
reference_kind, target_type, object_role, source_case_id, source_type,
VECTOR_DISTANCE(embedding, v_query_vector, COSINE) AS cosine_distance,
UTL_MATCH.JARO_WINKLER_SIMILARITY(
DBMS_LOB.SUBSTR(question, 4000, 1),
DBMS_LOB.SUBSTR(p_question, 4000, 1)
) / 100 AS lexical_similarity
FROM sg_qa_vector_example
WHERE reference_status = 'APPROVED'
AND inspection_status = 'VERIFIED'
AND answer_sql IS NOT NULL
AND (source_type = 'POLICY_TEMPLATE'
OR NOT REGEXP_LIKE(answer_sql, '<[A-Z][A-Z0-9_]*>', 'i'))
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
) c
WHERE c.cosine_distance <= v_max_cosine_distance
) s
)
WHERE hybrid_score >= best_hybrid_score - v_hybrid_margin
ORDER BY hybrid_score DESC, cosine_distance, example_id
FETCH FIRST p_top_k ROWS ONLY;
RETURN v_results;
END;
/
COMMIT

View File

@@ -1,15 +0,0 @@
-- CZN-13 is the reviewed reference for Ether usage and distinct users.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
reference_kind = 'SQL_TEMPLATE',
target_type = 'SINGLE',
object_role = 'GAME_GOODS_CHANGE',
inspection_status = 'VERIFIED',
inspection_note = 'Verified CZN-13 Few-shot. Ether usage requires goods-change data joined to the goods dimension and user master by GUID and BASE_DT, CHANGE_TYPE_CD=''USE'', active Ether dimension, excluded-user filter, and GOODS_CHANGE_CNT aggregation.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-13'
/
COMMIT

View File

@@ -1,15 +0,0 @@
-- CZN-16 is the reviewed reference for purchasers of a named package.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
reference_kind = 'SQL_TEMPLATE',
target_type = 'SINGLE',
object_role = 'SALES_PRODUCT_PURCHASER',
inspection_status = 'VERIFIED',
inspection_note = 'Verified CZN-16 Few-shot. Join sales transactions to product display by GAME_ID and PRODUCT_ID, filter the resolved package name and excluded users, and use the payment business date when counting distinct purchasers.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-16'
/
COMMIT

View File

@@ -1,597 +0,0 @@
-- ============================================================
-- 72_hmm_leave_team_vpd.sql
--
-- HMM HR leave row security:
-- * HMM_HR_MANAGER -> self and directly managed employees
-- * HMM_HR_VIEWER -> self only
-- * HMM_HR_ADMIN -> all rows when the role is explicitly assigned
--
-- Run as ADMIN after the HMM access-management schema is installed.
-- This script is idempotent and never stores a plain bearer token.
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET ECHO OFF
SET FEEDBACK ON
SET DEFINE OFF
ALTER SESSION DISABLE PARALLEL DML;
PROMPT === 1. Ensuring HMM roles and demo role assignments ===
MERGE INTO hmm_access_roles dst
USING (
SELECT 1 role_id, 'HMM_HR_VIEWER' role_name,
'HMM employee self-service leave access' description,
'PUBLIC' max_sensitivity_level FROM dual
UNION ALL
SELECT 2, 'HMM_HR_MANAGER',
'HMM manager access to self and direct reports',
'INTERNAL' FROM dual
UNION ALL
SELECT 3, 'HMM_HR_ADMIN',
'HMM HR administrator full leave access',
'CONFIDENTIAL' FROM dual
) src
ON (dst.role_name = src.role_name)
WHEN MATCHED THEN UPDATE SET
dst.description = src.description,
dst.max_sensitivity_level = src.max_sensitivity_level,
dst.active_yn = 'Y',
dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
role_id, role_name, description, max_sensitivity_level,
active_yn, created_at, updated_at
) VALUES (
src.role_id, src.role_name, src.description, src.max_sensitivity_level,
'Y', SYSTIMESTAMP, SYSTIMESTAMP
);
MERGE INTO hmm_employee_access_roles dst
USING (
SELECT e.employee_id, r.role_id
FROM hmm_hr_employees e
JOIN hmm_access_roles r ON r.role_name = 'HMM_HR_MANAGER'
WHERE e.employee_code = 'E1001'
AND e.employment_status = 'ACTIVE'
) src
ON (dst.employee_id = src.employee_id AND dst.role_id = src.role_id)
WHEN NOT MATCHED THEN INSERT (employee_id, role_id, created_at)
VALUES (src.employee_id, src.role_id, SYSTIMESTAMP);
MERGE INTO hmm_employee_access_roles dst
USING (
SELECT e.employee_id, r.role_id
FROM hmm_hr_employees e
JOIN hmm_access_roles r ON r.role_name = 'HMM_HR_VIEWER'
WHERE e.manager_employee_id = (
SELECT manager.employee_id
FROM hmm_hr_employees manager
WHERE manager.employee_code = 'E1001'
)
AND e.employment_status = 'ACTIVE'
) src
ON (dst.employee_id = src.employee_id AND dst.role_id = src.role_id)
WHEN NOT MATCHED THEN INSERT (employee_id, role_id, created_at)
VALUES (src.employee_id, src.role_id, SYSTIMESTAMP);
COMMIT;
PROMPT === 2. Ensuring protected leave objects and columns ===
DECLARE
PROCEDURE ensure_object(
p_object_name IN VARCHAR2,
p_description IN VARCHAR2
) AS
v_object_id NUMBER;
BEGIN
BEGIN
SELECT object_id
INTO v_object_id
FROM hmm_access_objects
WHERE owner = 'ADMIN'
AND object_name = p_object_name;
UPDATE hmm_access_objects
SET enabled_yn = 'Y',
description = p_description,
ords_path = 'cb-ords/cb-object-query/admin/' || LOWER(p_object_name)
WHERE object_id = v_object_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT NVL(MAX(object_id), 0) + 1
INTO v_object_id
FROM hmm_access_objects;
INSERT INTO hmm_access_objects (
object_id, owner, object_name, ords_path, enabled_yn, description
) VALUES (
v_object_id,
'ADMIN',
p_object_name,
'cb-ords/cb-object-query/admin/' || LOWER(p_object_name),
'Y',
p_description
);
END;
FOR c IN (
SELECT column_name
FROM user_tab_columns
WHERE table_name = p_object_name
ORDER BY column_id
) LOOP
MERGE INTO hmm_access_object_columns dst
USING (
SELECT v_object_id object_id, c.column_name column_name
FROM dual
) src
ON (
dst.object_id = src.object_id
AND dst.column_name = src.column_name
)
WHEN NOT MATCHED THEN INSERT (
column_id, object_id, column_name, sensitive_yn,
sensitivity_level, redaction_method
) VALUES (
(SELECT NVL(MAX(column_id), 0) + 1 FROM hmm_access_object_columns),
src.object_id,
src.column_name,
'N',
'PUBLIC',
'NONE'
);
END LOOP;
END;
BEGIN
ensure_object(
'HMM_LEAVE_BALANCES',
'HMM employee leave balance protected by employee hierarchy'
);
ensure_object(
'HMM_LEAVE_REQUESTS',
'HMM employee leave requests protected by employee hierarchy'
);
END;
/
COMMIT;
PROMPT === 3. Ensuring role permission rules ===
DECLARE
PROCEDURE ensure_permission(
p_role_name IN VARCHAR2,
p_target_name IN VARCHAR2,
p_rule_type IN VARCHAR2
) AS
v_role_id NUMBER;
v_perm_id NUMBER;
BEGIN
SELECT role_id
INTO v_role_id
FROM hmm_access_roles
WHERE role_name = p_role_name
AND active_yn = 'Y';
BEGIN
SELECT perm_id
INTO v_perm_id
FROM hmm_access_permissions
WHERE role_id = v_role_id
AND target_name = p_target_name
AND action_name = 'SELECT';
UPDATE hmm_access_permissions
SET permission_effect = 'ALLOW'
WHERE perm_id = v_perm_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
SELECT cb_permission_seq.NEXTVAL
INTO v_perm_id
FROM dual;
INSERT INTO hmm_access_permissions (
perm_id, role_id, target_name, action_name, permission_effect
) VALUES (
v_perm_id, v_role_id, p_target_name, 'SELECT', 'ALLOW'
);
END;
DELETE FROM hmm_access_permission_rules
WHERE perm_id = v_perm_id;
INSERT INTO hmm_access_permission_rules (
rule_id, perm_id, rule_column, rule_type, rule_value
) VALUES (
cb_permission_rule_seq.NEXTVAL,
v_perm_id,
'EMPLOYEE_ID',
p_rule_type,
NULL
);
END;
BEGIN
ensure_permission('HMM_HR_VIEWER', 'HMM_LEAVE_BALANCES', 'SELF');
ensure_permission('HMM_HR_VIEWER', 'HMM_LEAVE_REQUESTS', 'SELF');
ensure_permission('HMM_HR_MANAGER', 'HMM_LEAVE_BALANCES', 'MANAGED_TEAM');
ensure_permission('HMM_HR_MANAGER', 'HMM_LEAVE_REQUESTS', 'MANAGED_TEAM');
ensure_permission('HMM_HR_ADMIN', 'HMM_LEAVE_BALANCES', 'ALL');
ensure_permission('HMM_HR_ADMIN', 'HMM_LEAVE_REQUESTS', 'ALL');
END;
/
COMMIT;
PROMPT === 4. Creating secure HMM access context ===
CREATE OR REPLACE PACKAGE hmm_access_ctx_pkg AUTHID DEFINER AS
PROCEDURE clear_user;
PROCEDURE set_user_by_bearer(p_bearer_token IN VARCHAR2);
END;
/
CREATE OR REPLACE PACKAGE BODY hmm_access_ctx_pkg AS
PROCEDURE clear_user AS
BEGIN
DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID', NULL);
DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_CODE', NULL);
DBMS_SESSION.SET_CONTEXT('HMM_ACCESS_CTX', 'TEAM_ID', NULL);
END;
PROCEDURE set_user_by_bearer(p_bearer_token IN VARCHAR2) AS
v_employee_id hmm_hr_employees.employee_id%TYPE;
v_employee_code hmm_hr_employees.employee_code%TYPE;
v_team_id hmm_hr_employees.team_id%TYPE;
BEGIN
IF p_bearer_token IS NULL OR TRIM(p_bearer_token) IS NULL THEN
clear_user;
RAISE_APPLICATION_ERROR(-20101, 'Bearer token is required');
END IF;
SELECT e.employee_id,
e.employee_code,
e.team_id
INTO v_employee_id,
v_employee_code,
v_team_id
FROM hmm_access_bearer_tokens t
JOIN hmm_hr_employees e
ON e.employee_id = t.employee_id
WHERE t.key_hash = STANDARD_HASH(p_bearer_token, 'SHA256')
AND t.revoked_at IS NULL
AND t.expires_at > CAST(SYSTIMESTAMP AS TIMESTAMP)
AND e.employment_status = 'ACTIVE';
DBMS_SESSION.SET_CONTEXT(
'HMM_ACCESS_CTX', 'EMPLOYEE_ID', TO_CHAR(v_employee_id)
);
DBMS_SESSION.SET_CONTEXT(
'HMM_ACCESS_CTX', 'EMPLOYEE_CODE', v_employee_code
);
DBMS_SESSION.SET_CONTEXT(
'HMM_ACCESS_CTX', 'TEAM_ID', TO_CHAR(v_team_id)
);
EXCEPTION
WHEN NO_DATA_FOUND THEN
clear_user;
RAISE_APPLICATION_ERROR(-20102, 'Invalid or expired bearer token');
WHEN TOO_MANY_ROWS THEN
clear_user;
RAISE_APPLICATION_ERROR(-20103, 'Bearer token mapping is not unique');
END;
END;
/
CREATE OR REPLACE CONTEXT hmm_access_ctx USING hmm_access_ctx_pkg;
PROMPT === 5. Creating the HMM leave VPD filter ===
CREATE OR REPLACE FUNCTION hmm_leave_vpd_filter(
p_schema IN VARCHAR2,
p_object IN VARCHAR2
) RETURN VARCHAR2
AUTHID DEFINER
AS
v_employee_id NUMBER;
v_target VARCHAR2(128);
v_allow_predicate VARCHAR2(32767);
v_deny_predicate VARCHAR2(32767);
v_permission_clause VARCHAR2(32767);
FUNCTION quote_literal(p_value IN VARCHAR2) RETURN VARCHAR2 IS
BEGIN
RETURN '''' || REPLACE(NVL(p_value, ''), '''', '''''') || '''';
END;
FUNCTION safe_column(p_column_name IN VARCHAR2) RETURN VARCHAR2 IS
v_column VARCHAR2(128);
v_count NUMBER;
BEGIN
v_column := DBMS_ASSERT.SIMPLE_SQL_NAME(UPPER(TRIM(p_column_name)));
SELECT COUNT(*)
INTO v_count
FROM all_tab_columns
WHERE owner = UPPER(p_schema)
AND table_name = UPPER(p_object)
AND column_name = v_column;
RETURN CASE WHEN v_count = 1 THEN v_column ELSE NULL END;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
PROCEDURE append_and(p_clause IN VARCHAR2) IS
BEGIN
IF p_clause IS NULL THEN
v_permission_clause := NULL;
ELSIF v_permission_clause IS NULL THEN
v_permission_clause := '(' || p_clause || ')';
ELSE
v_permission_clause := v_permission_clause || ' AND (' || p_clause || ')';
END IF;
END;
PROCEDURE append_permission(
p_effect IN VARCHAR2,
p_clause IN VARCHAR2
) IS
BEGIN
IF p_clause IS NULL THEN
RETURN;
END IF;
IF p_effect = 'DENY' THEN
IF v_deny_predicate IS NULL THEN
v_deny_predicate := '(' || p_clause || ')';
ELSE
v_deny_predicate := v_deny_predicate || ' OR (' || p_clause || ')';
END IF;
ELSIF v_allow_predicate IS NULL THEN
v_allow_predicate := '(' || p_clause || ')';
ELSE
v_allow_predicate := v_allow_predicate || ' OR (' || p_clause || ')';
END IF;
END;
BEGIN
BEGIN
v_employee_id := TO_NUMBER(
SYS_CONTEXT('HMM_ACCESS_CTX', 'EMPLOYEE_ID')
);
EXCEPTION
WHEN OTHERS THEN
RETURN '1 = 0';
END;
IF v_employee_id IS NULL OR p_schema IS NULL OR p_object IS NULL THEN
RETURN '1 = 0';
END IF;
v_target := UPPER(TRIM(p_object));
IF v_target NOT IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS') THEN
RETURN '1 = 0';
END IF;
FOR permission_row IN (
SELECT DISTINCT
p.perm_id,
NVL(UPPER(TRIM(p.permission_effect)), 'ALLOW') permission_effect
FROM (
SELECT er.role_id
FROM hmm_employee_access_roles er
WHERE er.employee_id = v_employee_id
UNION
SELECT gr.role_id
FROM hmm_access_group_members gm
JOIN hmm_access_groups g
ON g.group_id = gm.group_id
AND g.active_yn = 'Y'
JOIN hmm_group_access_roles gr
ON gr.group_id = gm.group_id
WHERE gm.employee_id = v_employee_id
) effective_role
JOIN hmm_access_roles role
ON role.role_id = effective_role.role_id
AND role.active_yn = 'Y'
JOIN hmm_access_permissions p
ON p.role_id = role.role_id
AND p.target_name = v_target
AND p.action_name = 'SELECT'
ORDER BY p.perm_id
) LOOP
v_permission_clause := NULL;
FOR rule_row IN (
SELECT UPPER(TRIM(rule_type)) rule_type,
UPPER(TRIM(rule_column)) rule_column,
rule_value
FROM hmm_access_permission_rules
WHERE perm_id = permission_row.perm_id
ORDER BY rule_id
) LOOP
DECLARE
v_column VARCHAR2(128);
v_clause VARCHAR2(32767);
BEGIN
v_column := safe_column(NVL(rule_row.rule_column, 'EMPLOYEE_ID'));
v_clause := NULL;
IF rule_row.rule_type = 'ALL' THEN
v_clause := '1 = 1';
ELSIF rule_row.rule_type = 'SELF' AND v_column IS NOT NULL THEN
v_clause := v_column
|| ' = TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID''))';
ELSIF rule_row.rule_type = 'MANAGED_TEAM' AND v_column IS NOT NULL THEN
v_clause := v_column || ' IN ('
|| 'SELECT employee.employee_id '
|| 'FROM ADMIN.HMM_HR_EMPLOYEES employee '
|| 'WHERE employee.employee_id = '
|| 'TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID'')) '
|| 'OR employee.manager_employee_id = '
|| 'TO_NUMBER(SYS_CONTEXT(''HMM_ACCESS_CTX'', ''EMPLOYEE_ID''))'
|| ')';
ELSIF rule_row.rule_type = '='
AND v_column IS NOT NULL
AND rule_row.rule_value IS NOT NULL THEN
v_clause := 'TO_CHAR(' || v_column || ') = '
|| quote_literal(rule_row.rule_value);
ELSIF rule_row.rule_type IN ('!=', '<>')
AND v_column IS NOT NULL
AND rule_row.rule_value IS NOT NULL THEN
v_clause := 'TO_CHAR(' || v_column || ') <> '
|| quote_literal(rule_row.rule_value);
END IF;
IF v_clause IS NULL THEN
v_permission_clause := NULL;
EXIT;
END IF;
append_and(v_clause);
END;
END LOOP;
append_permission(
permission_row.permission_effect,
v_permission_clause
);
END LOOP;
IF v_allow_predicate IS NULL THEN
RETURN '1 = 0';
END IF;
IF v_deny_predicate IS NULL THEN
RETURN v_allow_predicate;
END IF;
RETURN '((' || v_allow_predicate || ') AND NOT ('
|| v_deny_predicate || '))';
EXCEPTION
WHEN OTHERS THEN
RETURN '1 = 0';
END;
/
SHOW ERRORS PACKAGE hmm_access_ctx_pkg
SHOW ERRORS PACKAGE BODY hmm_access_ctx_pkg
SHOW ERRORS FUNCTION hmm_leave_vpd_filter
PROMPT === 6. Attaching context-sensitive VPD policies ===
DECLARE
PROCEDURE replace_policy(p_object_name IN VARCHAR2) AS
BEGIN
BEGIN
DBMS_RLS.DROP_POLICY(
object_schema => 'ADMIN',
object_name => p_object_name,
policy_name => 'HMM_LEAVE_SCOPE_POLICY'
);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -28102 THEN
RAISE;
END IF;
END;
DBMS_RLS.ADD_POLICY(
object_schema => 'ADMIN',
object_name => p_object_name,
policy_name => 'HMM_LEAVE_SCOPE_POLICY',
function_schema => 'ADMIN',
policy_function => 'HMM_LEAVE_VPD_FILTER',
statement_types => 'SELECT',
update_check => FALSE,
enable => TRUE,
policy_type => DBMS_RLS.CONTEXT_SENSITIVE
);
END;
BEGIN
replace_policy('HMM_LEAVE_BALANCES');
replace_policy('HMM_LEAVE_REQUESTS');
END;
/
PROMPT === 7. Recording backoffice descriptions ===
MERGE INTO hmm_access_vpd_filter_notes dst
USING (
SELECT 'ADMIN' function_owner,
'HMM_LEAVE_VPD_FILTER' function_name,
'Builds self, managed-team, and administrator leave predicates from HMM roles and access rules.'
description
FROM dual
) src
ON (
dst.function_owner = src.function_owner
AND dst.function_name = src.function_name
)
WHEN MATCHED THEN UPDATE SET
dst.description = src.description,
dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
function_owner, function_name, description, updated_at
) VALUES (
src.function_owner, src.function_name, src.description, SYSTIMESTAMP
);
MERGE INTO hmm_access_vpd_policy_notes dst
USING (
SELECT 'ADMIN' object_owner,
object_name,
'HMM_LEAVE_SCOPE_POLICY' policy_name,
'Restricts leave rows to self, direct reports, or explicit HR administrator access.'
description
FROM (
SELECT 'HMM_LEAVE_BALANCES' object_name FROM dual
UNION ALL
SELECT 'HMM_LEAVE_REQUESTS' FROM dual
)
) src
ON (
dst.object_owner = src.object_owner
AND dst.object_name = src.object_name
AND dst.policy_name = src.policy_name
)
WHEN MATCHED THEN UPDATE SET
dst.description = src.description,
dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
object_owner, object_name, policy_name, description, updated_at
) VALUES (
src.object_owner, src.object_name, src.policy_name,
src.description, SYSTIMESTAMP
);
COMMIT;
PROMPT === 8. Deployment inventory ===
SELECT object_name, object_type, status
FROM user_objects
WHERE object_name IN (
'HMM_ACCESS_CTX',
'HMM_ACCESS_CTX_PKG',
'HMM_LEAVE_VPD_FILTER'
)
ORDER BY object_name, object_type;
SELECT object_name, policy_name, function, sel, enable, policy_type
FROM all_policies
WHERE object_owner = 'ADMIN'
AND object_name IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS')
AND policy_name = 'HMM_LEAVE_SCOPE_POLICY'
ORDER BY object_name;
SELECT e.employee_code, r.role_name
FROM hmm_employee_access_roles er
JOIN hmm_hr_employees e ON e.employee_id = er.employee_id
JOIN hmm_access_roles r ON r.role_id = er.role_id
WHERE r.role_name IN ('HMM_HR_VIEWER', 'HMM_HR_MANAGER')
ORDER BY e.employee_code, r.role_name;
SELECT r.role_name,
p.target_name,
pr.rule_column,
pr.rule_type
FROM hmm_access_permissions p
JOIN hmm_access_roles r ON r.role_id = p.role_id
JOIN hmm_access_permission_rules pr ON pr.perm_id = p.perm_id
WHERE p.target_name IN ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS')
ORDER BY r.role_name, p.target_name;
PROMPT === HMM leave team VPD ready ===

View File

@@ -1,159 +0,0 @@
-- #740 HMM MCP VPD execution boundary
-- Run as ADMIN with SQLcl. The password is prompted without echo and is used
-- only when CB_ORDS does not already exist.
set define on verify off echo off feedback on serveroutput on
whenever sqlerror exit sql.sqlcode rollback
accept hmm_cb_ords_password char prompt 'CB_ORDS initial password: ' hide
declare
v_user_count number;
v_password varchar2(4000) := q'~&hmm_cb_ords_password~';
begin
select count(*)
into v_user_count
from dba_users
where username = 'CB_ORDS';
if v_user_count = 0 then
if length(v_password) < 16
or instr(v_password, '"') > 0
or instr(v_password, chr(10)) > 0
or instr(v_password, chr(13)) > 0
or not regexp_like(v_password, '[A-Z]')
or not regexp_like(v_password, '[a-z]')
or not regexp_like(v_password, '[0-9]')
or not regexp_like(v_password, '[^A-Za-z0-9]') then
raise_application_error(
-20001,
'CB_ORDS password must be 16+ chars with upper/lower/digit/special and no quote/newline.');
end if;
execute immediate
'create user CB_ORDS identified by "' || v_password || '" '
|| 'default tablespace DATA temporary tablespace TEMP quota 0 on DATA';
dbms_output.put_line('Created CB_ORDS.');
else
dbms_output.put_line('CB_ORDS already exists; password was not changed.');
end if;
end;
/
alter user CB_ORDS account unlock;
grant create session to CB_ORDS;
create or replace package CB_ORDS_HANDLER_PKG
authid definer
as
procedure SET_VPD_CONTEXT(p_authorization in varchar2);
procedure CLEAR_VPD_CONTEXT;
end CB_ORDS_HANDLER_PKG;
/
create or replace package body CB_ORDS_HANDLER_PKG
as
c_bearer_prefix constant varchar2(7) := 'Bearer ';
procedure CLEAR_VPD_CONTEXT
is
begin
HMM_ACCESS_CTX_PKG.CLEAR_USER;
dbms_session.clear_identifier;
end CLEAR_VPD_CONTEXT;
procedure SET_VPD_CONTEXT(p_authorization in varchar2)
is
v_authorization varchar2(2048) := trim(p_authorization);
v_token varchar2(512);
begin
CLEAR_VPD_CONTEXT;
if v_authorization is null
or length(v_authorization) > 2048
or upper(substr(v_authorization, 1, length(c_bearer_prefix)))
<> upper(c_bearer_prefix) then
raise_application_error(-20002, 'A valid Bearer authorization header is required.');
end if;
v_token := trim(substr(v_authorization, length(c_bearer_prefix) + 1));
if v_token is null or length(v_token) > 512 then
raise_application_error(-20003, 'A valid Bearer token is required.');
end if;
HMM_ACCESS_CTX_PKG.SET_USER_BY_BEARER(v_token);
dbms_session.set_identifier(
sys_context('HMM_ACCESS_CTX', 'EMPLOYEE_CODE'));
exception
when others then
CLEAR_VPD_CONTEXT;
raise;
end SET_VPD_CONTEXT;
end CB_ORDS_HANDLER_PKG;
/
grant execute on CB_ORDS_HANDLER_PKG to CB_ORDS;
grant select on HMM_ORG_TEAMS to CB_ORDS;
grant select on HMM_HR_EMPLOYEES to CB_ORDS;
grant select on HMM_LEAVE_BALANCES to CB_ORDS;
grant select on HMM_LEAVE_REQUESTS to CB_ORDS;
grant select on HMM_ATTENDANCE_DAILY to CB_ORDS;
grant select on HMM_HR_TERMS to CB_ORDS;
create or replace synonym CB_ORDS.CB_ORDS_HANDLER_PKG
for ADMIN.CB_ORDS_HANDLER_PKG;
create or replace synonym CB_ORDS.HMM_ORG_TEAMS
for ADMIN.HMM_ORG_TEAMS;
create or replace synonym CB_ORDS.HMM_HR_EMPLOYEES
for ADMIN.HMM_HR_EMPLOYEES;
create or replace synonym CB_ORDS.HMM_LEAVE_BALANCES
for ADMIN.HMM_LEAVE_BALANCES;
create or replace synonym CB_ORDS.HMM_LEAVE_REQUESTS
for ADMIN.HMM_LEAVE_REQUESTS;
create or replace synonym CB_ORDS.HMM_ATTENDANCE_DAILY
for ADMIN.HMM_ATTENDANCE_DAILY;
create or replace synonym CB_ORDS.HMM_HR_TERMS
for ADMIN.HMM_HR_TERMS;
declare
v_exempt_count number;
v_policy_count number;
begin
select count(*)
into v_exempt_count
from dba_sys_privs
where grantee = 'CB_ORDS'
and privilege = 'EXEMPT ACCESS POLICY';
if v_exempt_count <> 0 then
raise_application_error(-20004, 'CB_ORDS must not have EXEMPT ACCESS POLICY.');
end if;
select count(*)
into v_policy_count
from dba_policies
where object_owner = 'ADMIN'
and object_name in ('HMM_LEAVE_BALANCES', 'HMM_LEAVE_REQUESTS')
and policy_name = 'HMM_LEAVE_SCOPE_POLICY'
and enable = 'YES'
and sel = 'YES';
if v_policy_count <> 2 then
raise_application_error(-20005, 'Both HMM leave VPD policies must be enabled.');
end if;
end;
/
select username, account_status
from dba_users
where username = 'CB_ORDS';
select privilege
from dba_sys_privs
where grantee = 'CB_ORDS'
order by privilege;
select object_name, object_type, status
from dba_objects
where owner = 'ADMIN'
and object_name = 'CB_ORDS_HANDLER_PKG'
order by object_type;
undefine hmm_cb_ords_password
prompt HMM MCP VPD runtime boundary is ready.

View File

@@ -1,32 +0,0 @@
-- ============================================================
-- HMM ADB -> AWS RDS PostgreSQL federation demo
-- Step 0: precheck
-- Run in Database Actions SQL Worksheet as ADMIN.
-- Redmine: #730
-- ============================================================
SELECT
USER AS session_user,
SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') AS current_schema,
SYS_CONTEXT('USERENV', 'DB_NAME') AS database_name
FROM dual;
SELECT credential_name, username
FROM all_credentials
WHERE credential_name = 'HMM_RDS_PG_CRED';
SELECT db_link, username, host, created
FROM user_db_links
WHERE db_link = 'HMM_RDS_PG_LINK';
SELECT object_name, object_type, status
FROM user_objects
WHERE object_name IN (
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
ORDER BY object_name;
-- First recording: the last three queries should return no rows.
-- If objects already exist, run 80_hmm_rds_pg_99_cleanup.sql first.

View File

@@ -1,20 +0,0 @@
-- ============================================================
-- Step 1: create the PostgreSQL credential.
-- Run only this statement after replacing <RDS_PASSWORD> in the
-- Worksheet. Never save the actual password in this file.
-- Redmine: #730
-- ============================================================
BEGIN
DBMS_CLOUD.CREATE_CREDENTIAL(
credential_name => 'HMM_RDS_PG_CRED',
username => 'postgres',
password => '<RDS_PASSWORD>'
);
END;
/
-- Passwords are never returned by ALL_CREDENTIALS.
SELECT credential_name, username
FROM all_credentials
WHERE credential_name = 'HMM_RDS_PG_CRED';

View File

@@ -1,61 +0,0 @@
-- ============================================================
-- Step 2: register and verify the host-specific ACL.
-- Run as ADMIN.
--
-- Important:
-- Oracle-managed heterogeneous Database Link traffic uses the
-- managed gateway. This ACL records SQL-user host permissions;
-- it does not replace the AWS RDS Security Group rule.
-- Redmine: #730
-- ============================================================
-- DNS resolution privilege must not have a port range.
BEGIN
DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
host => 'database-1.czaaygccsncp.ap-northeast-2.rds.amazonaws.com',
ace => XS$ACE_TYPE(
privilege_list => XS$NAME_LIST('resolve'),
principal_name => SYS_CONTEXT('USERENV', 'SESSION_USER'),
principal_type => XS_ACL.PTYPE_DB
)
);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -24243 THEN
RAISE;
END IF;
END;
/
-- Permit only the PostgreSQL service port.
BEGIN
DBMS_NETWORK_ACL_ADMIN.APPEND_HOST_ACE(
host => 'database-1.czaaygccsncp.ap-northeast-2.rds.amazonaws.com',
lower_port => 5432,
upper_port => 5432,
ace => XS$ACE_TYPE(
privilege_list => XS$NAME_LIST('connect'),
principal_name => SYS_CONTEXT('USERENV', 'SESSION_USER'),
principal_type => XS_ACL.PTYPE_DB
)
);
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -24243 THEN
RAISE;
END IF;
END;
/
SELECT
host,
lower_port,
upper_port,
principal,
privilege,
grant_type
FROM dba_host_aces
WHERE host = 'database-1.czaaygccsncp.ap-northeast-2.rds.amazonaws.com'
AND principal = USER
AND privilege IN ('resolve', 'connect')
ORDER BY privilege, lower_port;

View File

@@ -1,24 +0,0 @@
-- ============================================================
-- Step 3: create an Oracle-managed PostgreSQL Database Link.
-- Based on FY26 internal training, section 3.2.3.
-- Run as ADMIN.
-- Redmine: #730
-- ============================================================
BEGIN
DBMS_CLOUD_ADMIN.CREATE_DATABASE_LINK(
db_link_name => 'HMM_RDS_PG_LINK',
hostname => 'database-1.czaaygccsncp.ap-northeast-2.rds.amazonaws.com',
port => 5432,
service_name => 'postgres',
credential_name => 'HMM_RDS_PG_CRED',
ssl_server_cert_dn => NULL,
private_target => FALSE,
gateway_params => JSON_OBJECT('db_type' VALUE 'postgres')
);
END;
/
SELECT db_link, username, host, created
FROM user_db_links
WHERE db_link = 'HMM_RDS_PG_LINK';

View File

@@ -1,34 +0,0 @@
-- ============================================================
-- Step 4: query PostgreSQL directly through the Database Link.
-- PostgreSQL schema, table, view, and column identifiers are
-- lower-case and must be double quoted.
-- Redmine: #730
-- ============================================================
SELECT COUNT(*) AS carrier_count
FROM "hmm_demo"."carriers"@HMM_RDS_PG_LINK;
SELECT
COUNT(*) AS performance_count,
MIN("performance_month") AS first_month,
MAX("performance_month") AS latest_month
FROM "hmm_demo"."carrier_monthly_performance"@HMM_RDS_PG_LINK;
SELECT
"carrier_code",
"carrier_name",
"service_region",
"performance_tier"
FROM "hmm_demo"."carriers"@HMM_RDS_PG_LINK
ORDER BY "carrier_code";
SELECT
"carrier_code",
"carrier_name",
"performance_month",
"shipped_teu",
"revenue_usd",
"schedule_reliability_pct",
"risk_level"
FROM "hmm_demo"."carrier_performance_latest_v"@HMM_RDS_PG_LINK
ORDER BY "carrier_code";

View File

@@ -1,109 +0,0 @@
-- ============================================================
-- Step 5: create local Oracle Views and Select AI metadata.
-- Explicit aliases normalize PostgreSQL lower-case identifiers
-- into ordinary Oracle identifiers.
-- Redmine: #730
-- ============================================================
CREATE OR REPLACE VIEW HMM_RDS_CARRIERS_V AS
SELECT
"carrier_code" AS carrier_code,
"carrier_name" AS carrier_name,
"country_code" AS country_code,
"alliance_name" AS alliance_name,
"service_region" AS service_region,
"performance_tier" AS performance_tier
FROM "hmm_demo"."carriers"@HMM_RDS_PG_LINK;
COMMENT ON TABLE HMM_RDS_CARRIERS_V IS
'HMM federation demo fictional carrier master. Primary Key: CARRIER_CODE. Referenced by HMM_RDS_CARRIER_PERF_V(CARRIER_CODE).';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.CARRIER_CODE IS
'Primary Key. Fictional carrier identifier such as C001.';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.CARRIER_NAME IS
'Fictional carrier display name; not an actual carrier.';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.COUNTRY_CODE IS
'Two-letter demo country code.';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.ALLIANCE_NAME IS
'Fictional carrier cooperation network.';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.SERVICE_REGION IS
'Primary service region for the demo carrier.';
COMMENT ON COLUMN HMM_RDS_CARRIERS_V.PERFORMANCE_TIER IS
'Demo portfolio tier: CORE, GROWTH, or WATCH.';
CREATE OR REPLACE VIEW HMM_RDS_CARRIER_PERF_V AS
SELECT
"carrier_code" AS carrier_code,
"performance_month" AS performance_month,
"contracted_teu" AS contracted_teu,
"shipped_teu" AS shipped_teu,
"revenue_usd" AS revenue_usd,
"gross_margin_usd" AS gross_margin_usd,
"schedule_reliability_pct" AS schedule_reliability_pct,
"vessel_utilization_pct" AS vessel_utilization_pct,
"claim_rate_pct" AS claim_rate_pct,
"yoy_growth_pct" AS yoy_growth_pct,
"risk_level" AS risk_level
FROM "hmm_demo"."carrier_monthly_performance"@HMM_RDS_PG_LINK;
COMMENT ON TABLE HMM_RDS_CARRIER_PERF_V IS
'HMM federation demo monthly carrier KPI. Composite Primary Key: CARRIER_CODE and PERFORMANCE_MONTH. Foreign Key: CARRIER_CODE references HMM_RDS_CARRIERS_V(CARRIER_CODE). All values are fictional.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.CARRIER_CODE IS
'Foreign Key referencing HMM_RDS_CARRIERS_V(CARRIER_CODE).';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.PERFORMANCE_MONTH IS
'First calendar day of the KPI month; part of the composite Primary Key.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.CONTRACTED_TEU IS
'Contracted container volume in TEU.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.SHIPPED_TEU IS
'Actually shipped container volume in TEU.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.REVENUE_USD IS
'Fictional monthly revenue in USD.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.GROSS_MARGIN_USD IS
'Fictional monthly gross margin in USD; negative values indicate a loss.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.SCHEDULE_RELIABILITY_PCT IS
'Schedule reliability percentage from 0 through 100.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.VESSEL_UTILIZATION_PCT IS
'Vessel capacity utilization percentage from 0 through 100.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.CLAIM_RATE_PCT IS
'Cargo claim rate percentage from 0 through 100.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.YOY_GROWTH_PCT IS
'Year-over-year performance growth percentage.';
COMMENT ON COLUMN HMM_RDS_CARRIER_PERF_V.RISK_LEVEL IS
'Derived demo risk category: GREEN, AMBER, or RED.';
CREATE OR REPLACE VIEW HMM_RDS_CARRIER_LATEST_V AS
SELECT
"carrier_code" AS carrier_code,
"carrier_name" AS carrier_name,
"country_code" AS country_code,
"alliance_name" AS alliance_name,
"service_region" AS service_region,
"performance_tier" AS performance_tier,
"performance_month" AS performance_month,
"contracted_teu" AS contracted_teu,
"shipped_teu" AS shipped_teu,
"revenue_usd" AS revenue_usd,
"gross_margin_usd" AS gross_margin_usd,
"schedule_reliability_pct" AS schedule_reliability_pct,
"vessel_utilization_pct" AS vessel_utilization_pct,
"claim_rate_pct" AS claim_rate_pct,
"yoy_growth_pct" AS yoy_growth_pct,
"risk_level" AS risk_level
FROM "hmm_demo"."carrier_performance_latest_v"@HMM_RDS_PG_LINK;
COMMENT ON TABLE HMM_RDS_CARRIER_LATEST_V IS
'Latest fictional monthly KPI for each HMM federation demo carrier. Primary Key: CARRIER_CODE.';
COMMENT ON COLUMN HMM_RDS_CARRIER_LATEST_V.CARRIER_CODE IS
'Primary Key for the latest carrier KPI row.';
COMMENT ON COLUMN HMM_RDS_CARRIER_LATEST_V.PERFORMANCE_MONTH IS
'Latest KPI month available in the PostgreSQL source.';
COMMENT ON COLUMN HMM_RDS_CARRIER_LATEST_V.RISK_LEVEL IS
'Latest derived risk category: GREEN, AMBER, or RED.';
SELECT object_name, object_type, status
FROM user_objects
WHERE object_name IN (
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
ORDER BY object_name;

View File

@@ -1,86 +0,0 @@
-- ============================================================
-- Step 6: verify the local federation Views.
-- Expected: carrier/performance/latest = 8/144/8.
-- Redmine: #730
-- ============================================================
DECLARE
v_carrier_count PLS_INTEGER;
v_performance_count PLS_INTEGER;
v_latest_count PLS_INTEGER;
v_risk_count PLS_INTEGER;
BEGIN
SELECT COUNT(*) INTO v_carrier_count
FROM HMM_RDS_CARRIERS_V;
SELECT COUNT(*) INTO v_performance_count
FROM HMM_RDS_CARRIER_PERF_V;
SELECT COUNT(*), COUNT(DISTINCT risk_level)
INTO v_latest_count, v_risk_count
FROM HMM_RDS_CARRIER_LATEST_V;
IF v_carrier_count != 8 THEN
RAISE_APPLICATION_ERROR(
-20001,
'Expected 8 carriers, found ' || v_carrier_count
);
END IF;
IF v_performance_count != 144 THEN
RAISE_APPLICATION_ERROR(
-20002,
'Expected 144 performance rows, found ' || v_performance_count
);
END IF;
IF v_latest_count != 8 OR v_risk_count != 3 THEN
RAISE_APPLICATION_ERROR(
-20003,
'Expected 8 latest rows and 3 risk levels, found '
|| v_latest_count || ' rows and ' || v_risk_count || ' levels'
);
END IF;
END;
/
SELECT
(SELECT COUNT(*) FROM HMM_RDS_CARRIERS_V) AS carrier_count,
(SELECT COUNT(*) FROM HMM_RDS_CARRIER_PERF_V) AS performance_count,
(SELECT COUNT(*) FROM HMM_RDS_CARRIER_LATEST_V) AS latest_count
FROM dual;
SELECT
MIN(performance_month) AS first_month,
MAX(performance_month) AS latest_month,
COUNT(DISTINCT carrier_code) AS carrier_count
FROM HMM_RDS_CARRIER_PERF_V;
SELECT risk_level, COUNT(*) AS carrier_count
FROM HMM_RDS_CARRIER_LATEST_V
GROUP BY risk_level
ORDER BY risk_level;
SELECT
carrier_code,
carrier_name,
performance_month,
shipped_teu,
revenue_usd,
gross_margin_usd,
schedule_reliability_pct,
claim_rate_pct,
risk_level
FROM HMM_RDS_CARRIER_LATEST_V
ORDER BY carrier_code;
SELECT
table_name,
comments
FROM user_tab_comments
WHERE table_name IN (
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
ORDER BY table_name;

View File

@@ -1,63 +0,0 @@
-- ============================================================
-- Optional pre-recording reset.
-- Drops only the three local Views, Database Link, and
-- Credential owned by this demo. Host ACL entries are retained.
-- Redmine: #730
-- ============================================================
DECLARE
v_count PLS_INTEGER;
BEGIN
FOR object_row IN (
SELECT object_name
FROM user_objects
WHERE object_type = 'VIEW'
AND object_name IN (
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
ORDER BY CASE object_name
WHEN 'HMM_RDS_CARRIER_LATEST_V' THEN 1
WHEN 'HMM_RDS_CARRIER_PERF_V' THEN 2
ELSE 3
END
) LOOP
EXECUTE IMMEDIATE 'DROP VIEW ' || object_row.object_name;
END LOOP;
SELECT COUNT(*) INTO v_count
FROM user_db_links
WHERE db_link = 'HMM_RDS_PG_LINK';
IF v_count = 1 THEN
EXECUTE IMMEDIATE 'DROP DATABASE LINK HMM_RDS_PG_LINK';
END IF;
SELECT COUNT(*) INTO v_count
FROM all_credentials
WHERE credential_name = 'HMM_RDS_PG_CRED';
IF v_count = 1 THEN
DBMS_CLOUD.DROP_CREDENTIAL(
credential_name => 'HMM_RDS_PG_CRED'
);
END IF;
END;
/
SELECT credential_name, username
FROM all_credentials
WHERE credential_name = 'HMM_RDS_PG_CRED';
SELECT db_link, username, host
FROM user_db_links
WHERE db_link = 'HMM_RDS_PG_LINK';
SELECT object_name, object_type
FROM user_objects
WHERE object_name IN (
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
);

View File

@@ -1,209 +0,0 @@
-- ============================================================
-- HMM carrier assignments using existing access-group tables.
--
-- Existing tables:
-- HMM_HR_EMPLOYEES
-- HMM_ACCESS_GROUPS
-- HMM_ACCESS_GROUP_MEMBERS
--
-- No new assignment table is created.
-- Korean values are reconstructed from UTF-8 base64 so SQLcl
-- cannot corrupt the literals.
-- Redmine: #730
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
SET SERVEROUTPUT ON
MERGE INTO hmm_access_groups dst
USING (
SELECT
base.max_group_id + carrier.seq_no AS group_id,
'CARRIER_' || carrier.carrier_code AS group_code,
carrier.carrier_code || ' ' ||
utl_i18n.raw_to_char(
utl_encode.base64_decode(
utl_raw.cast_to_raw('7ISg7IKsIOuLtOuLuSDqt7jro7k=')
),
'AL32UTF8'
) AS group_name,
utl_i18n.raw_to_char(
utl_encode.base64_decode(
utl_raw.cast_to_raw(
'642w66qo7JqpIOyEoOyCrCDsoJHqt7wg67CPIOuLtOuLueyekCDqt7jro7k='
)
),
'AL32UTF8'
) || ' (' || carrier.carrier_code || ')' AS description
FROM (
SELECT 1 seq_no, 'C001' carrier_code FROM dual
UNION ALL SELECT 2, 'C002' FROM dual
UNION ALL SELECT 3, 'C003' FROM dual
UNION ALL SELECT 4, 'C004' FROM dual
UNION ALL SELECT 5, 'C005' FROM dual
UNION ALL SELECT 6, 'C006' FROM dual
UNION ALL SELECT 7, 'C007' FROM dual
UNION ALL SELECT 8, 'C008' FROM dual
) carrier
CROSS JOIN (
SELECT NVL(MAX(group_id), 0) max_group_id
FROM hmm_access_groups
) base
) src
ON (dst.group_code = src.group_code)
WHEN MATCHED THEN UPDATE SET
dst.group_name = src.group_name,
dst.description = src.description,
dst.active_yn = 'Y',
dst.updated_at = SYSTIMESTAMP
WHEN NOT MATCHED THEN INSERT (
group_id,
group_code,
group_name,
description,
active_yn,
created_at,
updated_at
) VALUES (
src.group_id,
src.group_code,
src.group_name,
src.description,
'Y',
SYSTIMESTAMP,
SYSTIMESTAMP
);
MERGE INTO hmm_access_group_members dst
USING (
SELECT
access_group.group_id,
employee.employee_id
FROM (
SELECT 'E1002' employee_code, 'CARRIER_C001' group_code FROM dual
UNION ALL SELECT 'E1002', 'CARRIER_C002' FROM dual
UNION ALL SELECT 'E1003', 'CARRIER_C003' FROM dual
UNION ALL SELECT 'E1004', 'CARRIER_C004' FROM dual
UNION ALL SELECT 'E1005', 'CARRIER_C005' FROM dual
UNION ALL SELECT 'E1006', 'CARRIER_C006' FROM dual
UNION ALL SELECT 'E1006', 'CARRIER_C007' FROM dual
UNION ALL SELECT 'E1007', 'CARRIER_C008' FROM dual
) assignment
JOIN hmm_hr_employees employee
ON employee.employee_code = assignment.employee_code
AND employee.employment_status = 'ACTIVE'
JOIN hmm_access_groups access_group
ON access_group.group_code = assignment.group_code
AND access_group.active_yn = 'Y'
) src
ON (
dst.group_id = src.group_id
AND dst.employee_id = src.employee_id
)
WHEN NOT MATCHED THEN INSERT (
group_id,
employee_id,
created_at
) VALUES (
src.group_id,
src.employee_id,
SYSTIMESTAMP
);
CREATE OR REPLACE VIEW hmm_carrier_assignments_v AS
SELECT
employee.employee_id,
employee.employee_code,
employee.full_name AS employee_name,
employee.job_title,
employee.team_id,
employee.manager_employee_id,
manager.employee_code AS manager_employee_code,
manager.full_name AS manager_name,
SUBSTR(access_group.group_code, LENGTH('CARRIER_') + 1) AS carrier_code,
access_group.group_code,
access_group.group_name
FROM hmm_access_group_members membership
JOIN hmm_access_groups access_group
ON access_group.group_id = membership.group_id
AND access_group.active_yn = 'Y'
JOIN hmm_hr_employees employee
ON employee.employee_id = membership.employee_id
AND employee.employment_status = 'ACTIVE'
LEFT JOIN hmm_hr_employees manager
ON manager.employee_id = employee.manager_employee_id
WHERE access_group.group_code LIKE 'CARRIER_C%';
COMMENT ON TABLE hmm_carrier_assignments_v IS
'ADB employee-to-carrier assignments from HMM_HR_EMPLOYEES, HMM_ACCESS_GROUPS, and HMM_ACCESS_GROUP_MEMBERS. Join relationship: HMM_CARRIER_ASSIGNMENTS_V.CARRIER_CODE joins HMM_RDS_CARRIERS_V.CARRIER_CODE, HMM_RDS_CARRIER_PERF_V.CARRIER_CODE, and HMM_RDS_CARRIER_LATEST_V.CARRIER_CODE.';
COMMENT ON COLUMN hmm_carrier_assignments_v.EMPLOYEE_CODE IS
'HMM employee code sourced from HMM_HR_EMPLOYEES.EMPLOYEE_CODE. Example: E1002.';
COMMENT ON COLUMN hmm_carrier_assignments_v.MANAGER_EMPLOYEE_CODE IS
'Direct manager employee code sourced from HMM_HR_EMPLOYEES. Use E1001 to identify the HMM demo manager team.';
COMMENT ON COLUMN hmm_carrier_assignments_v.CARRIER_CODE IS
'Join Key and Foreign Key to HMM_RDS_CARRIERS_V(CARRIER_CODE), HMM_RDS_CARRIER_PERF_V(CARRIER_CODE), and HMM_RDS_CARRIER_LATEST_V(CARRIER_CODE).';
COMMENT ON COLUMN hmm_carrier_assignments_v.GROUP_CODE IS
'Existing access-group code in the form CARRIER_C001.';
COMMIT;
DECLARE
v_group_count PLS_INTEGER;
v_member_count PLS_INTEGER;
v_assignment_count PLS_INTEGER;
v_federated_count PLS_INTEGER;
BEGIN
SELECT COUNT(*)
INTO v_group_count
FROM hmm_access_groups
WHERE group_code BETWEEN 'CARRIER_C001' AND 'CARRIER_C008'
AND active_yn = 'Y';
SELECT COUNT(*)
INTO v_member_count
FROM hmm_access_group_members membership
JOIN hmm_access_groups access_group
ON access_group.group_id = membership.group_id
WHERE access_group.group_code BETWEEN 'CARRIER_C001' AND 'CARRIER_C008';
SELECT COUNT(*)
INTO v_assignment_count
FROM hmm_carrier_assignments_v;
SELECT COUNT(*)
INTO v_federated_count
FROM hmm_carrier_assignments_v assignment
JOIN hmm_rds_carrier_latest_v performance
ON performance.carrier_code = assignment.carrier_code;
IF v_group_count != 8
OR v_member_count != 8
OR v_assignment_count != 8
OR v_federated_count != 8 THEN
RAISE_APPLICATION_ERROR(
-20081,
'Expected group/member/assignment/federated counts 8/8/8/8; found '
|| v_group_count || '/'
|| v_member_count || '/'
|| v_assignment_count || '/'
|| v_federated_count
);
END IF;
END;
/
SELECT
assignment.employee_code,
assignment.employee_name,
assignment.manager_employee_code,
assignment.carrier_code,
performance.carrier_name,
performance.performance_month,
performance.revenue_usd,
performance.risk_level
FROM hmm_carrier_assignments_v assignment
JOIN hmm_rds_carrier_latest_v performance
ON performance.carrier_code = assignment.carrier_code
ORDER BY assignment.employee_code, assignment.carrier_code;

View File

@@ -1,67 +0,0 @@
-- ============================================================
-- Explicit Select AI relationship metadata for ADB + RDS Views.
-- Redmine: #730
-- ============================================================
WHENEVER SQLERROR EXIT SQL.SQLCODE
SET DEFINE OFF
SET FEEDBACK ON
COMMENT ON TABLE hmm_carrier_assignments_v IS
'ADB employee-to-carrier assignments from HMM_HR_EMPLOYEES, HMM_ACCESS_GROUPS, and HMM_ACCESS_GROUP_MEMBERS. Join relationship: HMM_CARRIER_ASSIGNMENTS_V.CARRIER_CODE joins HMM_RDS_CARRIERS_V.CARRIER_CODE, HMM_RDS_CARRIER_PERF_V.CARRIER_CODE, and HMM_RDS_CARRIER_LATEST_V.CARRIER_CODE.';
COMMENT ON COLUMN hmm_carrier_assignments_v.EMPLOYEE_CODE IS
'HMM employee code sourced from HMM_HR_EMPLOYEES.EMPLOYEE_CODE. Example: E1002.';
COMMENT ON COLUMN hmm_carrier_assignments_v.MANAGER_EMPLOYEE_CODE IS
'Direct manager employee code sourced from HMM_HR_EMPLOYEES. Use E1001 to identify the HMM demo manager team.';
COMMENT ON COLUMN hmm_carrier_assignments_v.CARRIER_CODE IS
'Join Key and Foreign Key to HMM_RDS_CARRIERS_V(CARRIER_CODE), HMM_RDS_CARRIER_PERF_V(CARRIER_CODE), and HMM_RDS_CARRIER_LATEST_V(CARRIER_CODE).';
COMMENT ON TABLE hmm_rds_carriers_v IS
'PostgreSQL fictional carrier master exposed through HMM_RDS_PG_LINK. Primary Key: CARRIER_CODE. Join relationship: HMM_RDS_CARRIERS_V.CARRIER_CODE joins HMM_CARRIER_ASSIGNMENTS_V.CARRIER_CODE and HMM_RDS_CARRIER_PERF_V.CARRIER_CODE.';
COMMENT ON COLUMN hmm_rds_carriers_v.CARRIER_CODE IS
'Primary Key. Join Key to HMM_CARRIER_ASSIGNMENTS_V(CARRIER_CODE), HMM_RDS_CARRIER_PERF_V(CARRIER_CODE), and HMM_RDS_CARRIER_LATEST_V(CARRIER_CODE).';
COMMENT ON TABLE hmm_rds_carrier_perf_v IS
'PostgreSQL monthly carrier KPI exposed through HMM_RDS_PG_LINK. Composite Primary Key: CARRIER_CODE and PERFORMANCE_MONTH. Foreign Key: CARRIER_CODE references HMM_RDS_CARRIERS_V(CARRIER_CODE). Join relationship: CARRIER_CODE joins HMM_CARRIER_ASSIGNMENTS_V.CARRIER_CODE.';
COMMENT ON COLUMN hmm_rds_carrier_perf_v.CARRIER_CODE IS
'Foreign Key to HMM_RDS_CARRIERS_V(CARRIER_CODE). Join Key to HMM_CARRIER_ASSIGNMENTS_V(CARRIER_CODE).';
COMMENT ON COLUMN hmm_rds_carrier_perf_v.PERFORMANCE_MONTH IS
'Part of the composite Primary Key with CARRIER_CODE. First calendar day of the KPI month.';
COMMENT ON TABLE hmm_rds_carrier_latest_v IS
'Latest PostgreSQL KPI for each fictional carrier exposed through HMM_RDS_PG_LINK. Primary Key: CARRIER_CODE. Join relationship: HMM_RDS_CARRIER_LATEST_V.CARRIER_CODE joins HMM_CARRIER_ASSIGNMENTS_V.CARRIER_CODE and HMM_RDS_CARRIERS_V.CARRIER_CODE.';
COMMENT ON COLUMN hmm_rds_carrier_latest_v.CARRIER_CODE IS
'Primary Key. Join Key to HMM_CARRIER_ASSIGNMENTS_V(CARRIER_CODE) and HMM_RDS_CARRIERS_V(CARRIER_CODE).';
SELECT table_name, comments
FROM user_tab_comments
WHERE table_name IN (
'HMM_CARRIER_ASSIGNMENTS_V',
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
ORDER BY table_name;
SELECT table_name, column_name, comments
FROM user_col_comments
WHERE table_name IN (
'HMM_CARRIER_ASSIGNMENTS_V',
'HMM_RDS_CARRIERS_V',
'HMM_RDS_CARRIER_PERF_V',
'HMM_RDS_CARRIER_LATEST_V'
)
AND column_name IN (
'EMPLOYEE_CODE',
'MANAGER_EMPLOYEE_CODE',
'CARRIER_CODE',
'PERFORMANCE_MONTH'
)
ORDER BY table_name, column_name;

View File

@@ -1,19 +0,0 @@
-- Select AI profile instructions contain only common SQL-generation guidance.
-- Game target routing and execution policy are supplied at runtime by
-- SG_GAME_QUERY_PLAN; they do not belong in the profile-wide prompt.
BEGIN
DBMS_CLOUD_AI.SET_ATTRIBUTE(
profile_name => 'SGMP_POC_OCI_GPT54MINI',
attribute_name => 'additional_instructions',
attribute_value => q'~Generate Oracle SQL only for the listed approved objects. Do not reference external tables. Use English aliases only. Use database comments and annotations as the source of business rules.
~'
);
END;
/
SELECT attribute_name, attribute_value
FROM user_cloud_ai_profile_attributes
WHERE profile_name = 'SGMP_POC_OCI_GPT54MINI'
AND attribute_name = 'additional_instructions';

View File

@@ -1,63 +0,0 @@
-- Benchmark correction: common-fact eligibility is determined by the active
-- alias source. Registry-only games are reported separately while eligible
-- targets continue through the common-fact query.
-- Korean baseline text and SQL are reconstructed from UTF-8 base64 for SQLcl safety.
UPDATE sg_ai_qa_question
SET expected_focus = utl_i18n.raw_to_char(
utl_encode.base64_decode(utl_raw.cast_to_raw(
'7Jes65+sIOqyjOyehCDruYTqtZDsl5DshJwg7Lm07YOI66Gc6re466GcIO2ZleyduOuQnCDrjIDsg4HsnYAg67OE7LmtIOy5tO2TiOuhnOq3uOulvCDqtazrj5kg7KeR7ZWp7Jy866GcIO2VmOqzoCDqs7XthrUg7IKs7IukIO2FjOydtOu4lOydhCBMRUZUIEpPSU7tlZjsl6wg6rKM7J6E67OEIOynkeqzhO2VnOuLpC4g7IKs7IukIO2WieydtCDsl4bripQg64yA7IOB7J2AIDDsnLzroZwg67O07KG07ZWY6rOgLCDrp6Tsua3rkJwg64uk66W4IOuMgOyDgeydmCDqsrDqs7zrpbwg7IOd65617ZWY7KeAIOyViuuKlOuLpC4gRFVBTCBVTklPTuycvOuhnCDrjIDsg4HrqoXqs7wg6rCS7J2EIO2VqeyEse2VmOyngCDslYrripTri6Qu'
)),
'AL32UTF8'
),
baseline_answer = utl_i18n.raw_to_char(
utl_encode.base64_decode(utl_raw.cast_to_raw(
'7Lm07YOI66Gc6re4IOunpOy5rSDrjIDsg4Hrs4Qg66ek7Lac7J2EIOuwmO2ZmO2VnOuLpC4g7IKs7IukIO2WieydtCDsl4bripQg64yA7IOB7J2AIDAsIOuLpOuluCDrp6Tsua0g64yA7IOB7J2AIO2VtOuLuSDsnbzsnpDsnZgg7KeR6rOE6rCS7J2EIOuwmO2ZmO2VnOuLpC4='
)),
'AL32UTF8'
),
baseline_sql = utl_i18n.raw_to_char(
utl_encode.base64_decode(utl_raw.cast_to_raw(
'V0lUSCByZXF1ZXN0ZWRfZ2FtZXMgQVMgKAogIFNFTEVDVCBESVNUSU5DVCBnLiJHQU1FX0lEIiwgZy4iR0FNRV9OTSIKICBGUk9NICJTR01QX1BPQyIuIkNPTU5fR0FNRV9BTElBU19CQVMiIGcKICBXSEVSRSBnLiJVU0VfWU4iID0gJ1knCiAgICBBTkQgKAogICAgICBVUFBFUihnLiJHQU1FX05NIikgTElLRSBVUFBFUignJUJ1YmJseXolJykKICAgICAgT1IgVVBQRVIoZy4iR0FNRV9BTElBU19OTSIpIExJS0UgVVBQRVIoJyVCdWJibHl6JScpCiAgICAgIE9SIFVQUEVSKGcuIkdBTUVfTk0iKSBMSUtFIFVQUEVSKCcl7Lm07KCc64KYJScpCiAgICAgIE9SIFVQUEVSKGcuIkdBTUVfQUxJQVNfTk0iKSBMSUtFIFVQUEVSKCcl7Lm07KCc64KYJScpCiAgICApCikKU0VMRUNUIHJnLiJHQU1FX05NIiBBUyAiR0FNRV9OQU1FIiwKICAgICAgIE5WTChTVU0oQ0FTRQogICAgICAgICBXSEVOIHMuIlBBWU1UX0RUTSIgPj0gVE9fREFURSgnMjAyNjA3MTUnLCAnWVlZWU1NREQnKQogICAgICAgICAgQU5EIHMuIlBBWU1UX0RUTSIgPCBUT19EQVRFKCcyMDI2MDcxNicsICdZWVlZTU1ERCcpCiAgICAgICAgICBBTkQgcy4iRVhQVF9VU0VSX1lOIiA9ICdOJwogICAgICAgICBUSEVOIENBU1Qocy4iUEFZTVRfQU1UIiBBUyBOVU1CRVIpIEVMU0UgMCBFTkQpLCAwKSBBUyAiU0FMRVNfQU1UIgpGUk9NIHJlcXVlc3RlZF9nYW1lcyByZwpMRUZUIEpPSU4gIlNHTVBfUE9DIi4iQ09NTl9TQUxFU19UWE4iIHMKICBPTiBzLiJHQU1FX0lEIiA9IHJnLiJHQU1FX0lEIgpHUk9VUCBCWSByZy4iR0FNRV9OTSIKT1JERVIgQlkgcmcuIkdBTUVfTk0iCg=='
)),
'AL32UTF8'
)
WHERE question_code = 'STD-11';
UPDATE sg_qa_vector_example
SET inspection_status = 'REVIEW',
inspection_note = 'Customer benchmark criterion updated: multi-target common-fact comparisons preserve catalog-resolved zero-fact targets through a catalog-driven left join.'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'STD-11';
COMMIT;
-- Correct the earlier catalog-only interpretation in this same migration. A
-- registry can identify a game to the operator, but does not by itself make it
-- an approved source for a common fact query.
UPDATE sg_ai_qa_question
SET expected_focus = utl_i18n.raw_to_char(
utl_encode.base64_decode(utl_raw.cast_to_raw(
'67O17IiYIOqyjOyehCDqs7XthrUg7IKs7IukIOuNsOydtO2EsCDsp4jsnZjripQg7Zmc7ISxIOqyjOyehCDrs4Tsua0g7JuQ7LKc7JeQ7IScIO2ZleyduOuQnCDrjIDsg4Hrp4wg7IKs7IukIOyhsO2ajCDrjIDsg4HsnLzroZwg7IKs7Jqp7ZWY6rOgLCDrs4Tsua0g7JuQ7LKc7J20IOyXhuuKlCDroIjsp4DsiqTtirjrpqwg7KCE7JqpIOuMgOyDgeydgCDsobDtmowg67aI6rCAIOyDge2DnOuhnCDrs4Trj4Qg7JWI64K07ZWc64ukLiDsobDtmowg6rCA64ql7ZWcIOuMgOyDgeydmCDqsrDqs7zripQg7Jyg7KeA7ZWY66mwIOyghOyytCDsmpTssq3snYQg7LCo64uo7ZWY7KeAIOyViuuKlOuLpC4gRFVBTCBVTklPTuycvOuhnCDrjIDsg4Eg7ZaJ7J2064KYIOqwkuydhCDrp4zrk6Tsp4Ag7JWK64qU64ukLg=='
)),
'AL32UTF8'
),
baseline_answer = utl_i18n.raw_to_char(
utl_encode.base64_decode(utl_raw.cast_to_raw(
'7Lm07KCc64KYIOunpOy2nOydgCAyMjcsNjgx7J6F64uI64ukLiBCdWJibHl664qUIO2ZnOyEsSDqsozsnoQg67OE7LmtIOybkOyynOydtCDsl4bslrQg66ek7LacIOyhsO2ajCDrjIDsg4HsnbQg7JWE64uZ64uI64ukLg=='
)),
'AL32UTF8'
)
WHERE question_code = 'STD-11';
UPDATE sg_qa_vector_example
SET inspection_status = 'REVIEW',
inspection_note = 'Customer benchmark criterion updated: common-fact comparison queries use active-alias eligible targets; registry-only targets are separately unavailable and do not become synthetic fact rows.'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'STD-11';
COMMIT;
SELECT question_code, expected_focus, baseline_answer
FROM sg_ai_qa_question
WHERE question_code = 'STD-11';

View File

@@ -1,22 +0,0 @@
-- Object-specific, data-driven guidance for comparison queries on a common fact table.
-- No game, prefix, ID, or physical per-game object is embedded in this annotation.
DECLARE
v_result VARCHAR2(4000);
BEGIN
v_result := sgmp_set_annotation(
'SGMP_POC',
'TABLE',
'COMN_SALES_TXN',
NULL,
'Game fact scope: query this table only for plan targets marked ACTIVE_ALIAS. Derive target GAME_ID values through active COMN_GAME_ALIAS_BAS aliases rather than direct identifier or prefix literals. For a mixed request, retain the eligible target results and report other plan statuses separately; do not substitute or manufacture a target result.',
'MULTI_TARGET_COMPARISON'
);
DBMS_OUTPUT.PUT_LINE(v_result);
END;
/
SELECT annotation_name, annotation_value
FROM user_annotations_usage
WHERE object_name = 'COMN_SALES_TXN'
AND annotation_name = 'MULTI_TARGET_COMPARISON';

View File

@@ -1,92 +0,0 @@
-- Generic Few-shot structure for multi-target comparisons on a common fact object.
-- The template is intentionally logical: no current game, prefix, ID, date, or result value is embedded.
DECLARE
v_exists NUMBER;
v_input CLOB;
v_embedding VECTOR;
BEGIN
SELECT COUNT(*)
INTO v_exists
FROM sg_qa_vector_example
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'MULTI_COMMON_FACT_LEFT_JOIN';
IF v_exists = 0 THEN
v_input := TO_CLOB('Question pattern: Compare a common fact metric across multiple resolved games. Use only fact-query-eligible targets and separately report known registry-only or unavailable targets.')
|| CHR(10) || 'Logical object role: SALES_TRANSACTION'
|| CHR(10) || 'Required structure: active alias catalog distinct game set, left join fact, aggregate by catalog display identifier.';
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
v_input,
JSON(sg_qa_vector_params('search_document'))
);
INSERT INTO sg_qa_vector_example (
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
reference_status, reference_kind, target_type, object_role,
inspection_status, inspection_note, verified_at, verified_by,
source_case_id, source_type
) VALUES (
'Compare a common fact metric across multiple resolved games, including games with no fact rows.',
TO_CLOB('WITH resolved_games AS (' || CHR(10)
|| ' SELECT DISTINCT a."GAME_ID", a."GAME_NM"' || CHR(10)
|| ' FROM "SGMP_POC"."COMN_GAME_ALIAS_BAS" a' || CHR(10)
|| ' WHERE a."USE_YN" = ''Y''' || CHR(10)
|| ' AND (<ACTIVE_ALIAS_MATCHES_FOR_EACH_REQUESTED_GAME_TERM>)' || CHR(10)
|| ')' || CHR(10)
|| 'SELECT g."GAME_NM" AS "GAME_NAME",' || CHR(10)
|| ' NVL(SUM(CASE WHEN <FACT_DATE_AND_EXCLUSION_CONDITION>' || CHR(10)
|| ' THEN CAST(f."<METRIC_COLUMN>" AS NUMBER) ELSE 0 END), 0) AS "METRIC_VALUE"' || CHR(10)
|| 'FROM resolved_games g' || CHR(10)
|| 'LEFT JOIN "SGMP_POC"."<APPROVED_COMMON_FACT_OBJECT>" f' || CHR(10)
|| ' ON f."GAME_ID" = g."GAME_ID"' || CHR(10)
|| 'GROUP BY g."GAME_NM"' || CHR(10)
|| 'ORDER BY g."GAME_NM"'),
'Structural Few-shot only. Replace every angle-bracket placeholder from the current approved object metadata, the current game query plan, and the original question. Only ACTIVE_ALIAS targets enter the alias-driven LEFT JOIN and grouping; report registry-only or unavailable targets from the plan without manufacturing fact rows with DUAL/UNION.',
v_input,
v_embedding,
'cohere.embed-v4.0',
'APPROVED', 'SQL_TEMPLATE', 'MULTI', 'SALES_TRANSACTION',
'VERIFIED',
'Generic, non-customer-specific comparison structure. Verified against the game-alias and common-fact metadata contract; not an executable answer key.',
SYSTIMESTAMP, 'SGMP_POC_METADATA_REVIEW',
'MULTI_COMMON_FACT_LEFT_JOIN', 'POLICY_TEMPLATE'
);
END IF;
COMMIT;
END;
/
-- Keep the approved template current when the policy text evolves. The vector
-- is rebuilt from its generic retrieval text; no customer answer is embedded.
DECLARE
v_input CLOB;
v_embedding VECTOR;
BEGIN
v_input := TO_CLOB('Question pattern: Compare a common fact metric across multiple resolved games. Use only fact-query-eligible targets and separately report known registry-only or unavailable targets.')
|| CHR(10) || 'Logical object role: SALES_TRANSACTION'
|| CHR(10) || 'Required structure: active alias catalog distinct game set, left join fact, aggregate by catalog display identifier.';
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
v_input,
JSON(sg_qa_vector_params('search_document'))
);
UPDATE sg_qa_vector_example
SET question = 'Compare a common fact metric across multiple resolved games, using only fact-query-eligible targets.',
answer_text = 'Structural Few-shot only. Replace every angle-bracket placeholder from the current approved object metadata, the current game query plan, and the original question. Only ACTIVE_ALIAS targets enter the alias-driven LEFT JOIN and grouping; report registry-only or unavailable targets from the plan without manufacturing fact rows with DUAL/UNION.',
embedding_input = v_input,
embedding = v_embedding,
inspection_note = 'Generic, non-customer-specific comparison structure. Active-alias targets are fact-query eligible; registry-only targets are reported separately. Not an executable answer key.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'MULTI_COMMON_FACT_LEFT_JOIN';
COMMIT;
END;
/
SELECT example_id, reference_status, reference_kind, target_type, object_role,
source_case_id, source_type
FROM sg_qa_vector_example
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'MULTI_COMMON_FACT_LEFT_JOIN';

View File

@@ -1,13 +0,0 @@
-- Retire the overly specific no-target template. The profile and table
-- metadata carry this general scope policy without a case-shaped example.
DELETE FROM sg_qa_vector_example
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'NO_ELIGIBLE_FACT_TARGET';
COMMIT;
SELECT COUNT(*) AS remaining_template_count
FROM sg_qa_vector_example
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'NO_ELIGIBLE_FACT_TARGET';

View File

@@ -1,70 +0,0 @@
-- Generic Few-shot structure for a filtered transaction/order detail request.
-- It fixes the output grain through an approved object pattern, not a global
-- instruction or a customer-specific answer.
DECLARE
v_input CLOB;
v_embedding VECTOR;
v_exists NUMBER;
BEGIN
v_input := TO_CLOB('Question pattern: List individual payment orders that match a business date and an amount condition.')
|| CHR(10) || 'Logical object role: SALES_TRANSACTION'
|| CHR(10) || 'Required output: transaction identifiers, game, user, payment timestamp, and payment amount.';
v_embedding := DBMS_VECTOR.UTL_TO_EMBEDDING(
v_input,
JSON(sg_qa_vector_params('search_document'))
);
SELECT COUNT(*)
INTO v_exists
FROM sg_qa_vector_example
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'FILTERED_TRANSACTION_DETAIL';
IF v_exists = 0 THEN
INSERT INTO sg_qa_vector_example (
question, answer_sql, answer_text, embedding_input, embedding, embedding_model,
reference_status, reference_kind, target_type, object_role,
inspection_status, inspection_note, verified_at, verified_by,
source_case_id, source_type
) VALUES (
'List payment orders matching a date and amount condition.',
TO_CLOB('SELECT t."PAYMT_TRANSAC_ID" AS "PAYMENT_TRANSACTION_ID",' || CHR(10)
|| ' t."PAYMT_TRANSAC_DTL_ID" AS "PAYMENT_TRANSACTION_DETAIL_ID",' || CHR(10)
|| ' t."GAME_ID" AS "GAME_ID",' || CHR(10)
|| ' t."GUID" AS "USER_ID",' || CHR(10)
|| ' t."PAYMT_DTM" AS "PAYMENT_DATETIME",' || CHR(10)
|| ' t."PAYMT_AMT" AS "PAYMENT_AMOUNT"' || CHR(10)
|| 'FROM "SGMP_POC"."COMN_SALES_TXN" t' || CHR(10)
|| 'WHERE t."PAYMT_DTM" >= <BUSINESS_DATE_START>' || CHR(10)
|| ' AND t."PAYMT_DTM" < <BUSINESS_DATE_END>' || CHR(10)
|| ' AND CAST(t."PAYMT_AMT" AS NUMBER) <AMOUNT_CONDITION>' || CHR(10)
|| ' AND t."EXPT_USER_YN" = ''N''' || CHR(10)
|| 'ORDER BY t."PAYMT_DTM", t."PAYMT_TRANSAC_ID", t."PAYMT_TRANSAC_DTL_ID"'),
'Structural Few-shot only. Replace placeholders using the original request and approved metadata. Use the business payment timestamp for a payment-date condition. This pattern is for individual transaction detail; do not substitute an aggregate-only result for a requested order list.',
v_input,
v_embedding,
'cohere.embed-v4.0',
'APPROVED', 'SQL_TEMPLATE', 'NONE', 'SALES_TRANSACTION',
'VERIFIED',
'Generic transaction-detail output shape with no customer date, amount, game, or result value; not an executable answer key.',
SYSTIMESTAMP, 'SGMP_POC_METADATA_REVIEW',
'FILTERED_TRANSACTION_DETAIL', 'POLICY_TEMPLATE'
);
ELSE
UPDATE sg_qa_vector_example
SET embedding_input = v_input,
embedding = v_embedding,
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'FILTERED_TRANSACTION_DETAIL';
END IF;
COMMIT;
END;
/
SELECT example_id, reference_status, target_type, object_role, source_case_id
FROM sg_qa_vector_example
WHERE source_type = 'POLICY_TEMPLATE'
AND source_case_id = 'FILTERED_TRANSACTION_DETAIL';

View File

@@ -1,57 +0,0 @@
-- Customer question wording requests individual orders. Align the benchmark
-- with the transaction-detail output pattern rather than forcing KPI summary.
UPDATE sg_ai_qa_question
SET expected_focus = utl_i18n.raw_to_char(
utl_encode.base64_decode(utl_raw.cast_to_raw(
'Q09NTl9TQUxFU19UWE7sl5DshJwgUEFZTVRfRFRNIOq4sOykgOydvCwgUEFZTVRfQU1UID4gMTAwMDAsIEVYUFRfVVNFUl9ZTj0nTicg7KGw6rG07J2YIOqwnOuzhCDso7zrrLgg7IOB7IS466W8IOyhsO2ajO2VnOuLpC4g7KO866y4IOyLneuzhOyekCwg6rKM7J6ELCDsgqzsmqnsnpAsIOqysOygnCDsnbzsi5zsmYAg6riI7JWh7J2EIOygnOqzte2VnOuLpC4='
)),
'AL32UTF8'
),
baseline_sql = TO_CLOB('SELECT s."PAYMT_TRANSAC_ID" AS "PAYMENT_TRANSACTION_ID",' || CHR(10)
|| ' s."PAYMT_TRANSAC_DTL_ID" AS "PAYMENT_TRANSACTION_DETAIL_ID",' || CHR(10)
|| ' s."GAME_ID" AS "GAME_ID",' || CHR(10)
|| ' s."GUID" AS "USER_ID",' || CHR(10)
|| ' s."PAYMT_DTM" AS "PAYMENT_DATETIME",' || CHR(10)
|| ' s."PAYMT_AMT" AS "PAYMENT_AMOUNT"' || CHR(10)
|| 'FROM "SGMP_POC"."COMN_SALES_TXN" s' || CHR(10)
|| 'WHERE s."PAYMT_DTM" >= DATE ''2026-07-15''' || CHR(10)
|| ' AND s."PAYMT_DTM" < DATE ''2026-07-16''' || CHR(10)
|| ' AND CAST(s."PAYMT_AMT" AS NUMBER) > 10000' || CHR(10)
|| ' AND s."EXPT_USER_YN" = ''N''' || CHR(10)
|| 'ORDER BY s."PAYMT_DTM", s."PAYMT_TRANSAC_ID", s."PAYMT_TRANSAC_DTL_ID"'),
baseline_answer = utl_i18n.raw_to_char(
utl_encode.base64_decode(utl_raw.cast_to_raw(
'6rKw7KCc6riI7JWhIDHrp4zsm5Ag7LSI6rO8IOyjvOusuCA26rG07J2EIOyjvOusuCDsi53rs4TsnpAsIOqyjOyehCwg7IKs7Jqp7J6QLCDqsrDsoJwg7J287IucLCDqsrDsoJzquIjslaHqs7wg7ZWo6ruYIOuwmO2ZmO2VnOuLpC4='
)),
'AL32UTF8'
)
WHERE question_code = 'STD-18';
UPDATE sg_qa_vector_example
SET answer_sql = TO_CLOB('SELECT s."PAYMT_TRANSAC_ID" AS "PAYMENT_TRANSACTION_ID",' || CHR(10)
|| ' s."PAYMT_TRANSAC_DTL_ID" AS "PAYMENT_TRANSACTION_DETAIL_ID",' || CHR(10)
|| ' s."GAME_ID" AS "GAME_ID",' || CHR(10)
|| ' s."GUID" AS "USER_ID",' || CHR(10)
|| ' s."PAYMT_DTM" AS "PAYMENT_DATETIME",' || CHR(10)
|| ' s."PAYMT_AMT" AS "PAYMENT_AMOUNT"' || CHR(10)
|| 'FROM "SGMP_POC"."COMN_SALES_TXN" s' || CHR(10)
|| 'WHERE s."PAYMT_DTM" >= DATE ''2026-07-15''' || CHR(10)
|| ' AND s."PAYMT_DTM" < DATE ''2026-07-16''' || CHR(10)
|| ' AND CAST(s."PAYMT_AMT" AS NUMBER) > 10000' || CHR(10)
|| ' AND s."EXPT_USER_YN" = ''N''' || CHR(10)
|| 'ORDER BY s."PAYMT_DTM", s."PAYMT_TRANSAC_ID", s."PAYMT_TRANSAC_DTL_ID"'),
answer_text = 'Approved customer Few-shot: return individual qualifying payment orders with transaction identifiers, game, user, payment timestamp, and payment amount. Use PAYMT_DTM for the payment business date.',
reference_status = 'APPROVED',
inspection_status = 'VERIFIED',
inspection_note = 'Customer benchmark aligned to detailed qualifying orders and the payment business timestamp.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'STD-18';
COMMIT;
SELECT question_code, expected_focus, baseline_answer
FROM sg_ai_qa_question
WHERE question_code = 'STD-18';

View File

@@ -1,19 +0,0 @@
-- Customer benchmark evidence is retained for evaluation only. It is not a
-- runtime Few-shot because the vector store must not become a collection of
-- case-specific benchmark overrides.
UPDATE sg_qa_vector_example
SET reference_status = 'RETIRED',
inspection_status = 'RETIRED',
inspection_note = 'Retired from runtime Few-shot retrieval; retained as customer QA evaluation evidence.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'STD-21';
COMMIT;
SELECT example_id, reference_status, inspection_status, source_case_id
FROM sg_qa_vector_example
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'STD-21';

View File

@@ -1,17 +0,0 @@
-- Promote the reviewed customer QA example for the NRU metric.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
inspection_status = 'VERIFIED',
inspection_note = 'Customer QA reviewed: NRU is measured with NRU_FLAG, with the stated date and excluded-user condition.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'STD-22';
COMMIT;
SELECT example_id, reference_status, inspection_status, source_case_id
FROM sg_qa_vector_example
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'STD-22';

View File

@@ -1,17 +0,0 @@
-- Promote the reviewed customer QA example for the standard AU metric.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
inspection_status = 'VERIFIED',
inspection_note = 'Customer QA reviewed: standard AU is measured with AU_FLAG and excluded-user filtering.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-02';
COMMIT;
SELECT example_id, reference_status, inspection_status, source_case_id
FROM sg_qa_vector_example
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-02';

View File

@@ -1,57 +0,0 @@
-- Prefer an exact approved customer QA question over semantically adjacent
-- vector neighbours. This is a general retrieval rule; it does not encode
-- a game, metric, table, or customer-case-specific SQL policy.
CREATE OR REPLACE FUNCTION sg_qa_vector_search(
p_question IN CLOB,
p_top_k IN PLS_INTEGER DEFAULT 3,
p_target_type IN VARCHAR2 DEFAULT 'ANY'
) RETURN SYS_REFCURSOR
AUTHID DEFINER
IS
v_query_vector VECTOR;
v_results SYS_REFCURSOR;
v_target_type VARCHAR2(16) := UPPER(TRIM(NVL(p_target_type, 'ANY')));
BEGIN
IF p_question IS NULL THEN
RAISE_APPLICATION_ERROR(-20003, 'question is required.');
END IF;
IF p_top_k IS NULL OR p_top_k < 1 OR p_top_k > 20 THEN
RAISE_APPLICATION_ERROR(-20004, 'top_k must be between 1 and 20.');
END IF;
IF v_target_type NOT IN ('NONE', 'SINGLE', 'MULTI', 'ALL', 'ANY') THEN
RAISE_APPLICATION_ERROR(-20005, 'target_type must be NONE, SINGLE, MULTI, ALL, or ANY.');
END IF;
v_query_vector := DBMS_VECTOR.UTL_TO_EMBEDDING(
p_question,
JSON(sg_qa_vector_params('search_query'))
);
OPEN v_results FOR
SELECT example_id,
question,
answer_sql,
answer_text,
embedding_model,
reference_kind,
target_type,
object_role,
source_case_id,
source_type,
vector_distance(embedding, v_query_vector, COSINE) AS cosine_distance
FROM sg_qa_vector_example
WHERE reference_status = 'APPROVED'
AND (target_type = 'ANY' OR v_target_type = 'ANY' OR target_type = v_target_type)
ORDER BY CASE
WHEN DBMS_LOB.COMPARE(
LOWER(TRIM(question)), LOWER(TRIM(p_question))
) = 0 THEN 0
ELSE 1
END,
vector_distance(embedding, v_query_vector, COSINE),
example_id
FETCH FIRST p_top_k ROWS ONLY;
RETURN v_results;
END;
/

View File

@@ -1,20 +0,0 @@
-- Strengthen the approved customer QA example itself. The wording belongs
-- to the benchmark Few-shot record, not to a global Select AI profile rule.
UPDATE sg_qa_vector_example
SET answer_text = 'Expected focus: CZN_COMN_USER_MST, BASE_DT=2026-07-15, AU_FLAG=1, EXPT_USER_YN=''N''. '
|| 'The phrase standard AU is the report metric label; do not add STD_USER_YN unless the question separately asks for the standard-user cohort. '
|| 'Historical answer: STD_AU_COUNT=0',
inspection_note = 'Customer QA verified: standard AU uses the AU flag and excluded-user filtering; standard-user cohort is a separate request.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-02'
AND reference_status = 'APPROVED';
COMMIT;
SELECT example_id, reference_status, inspection_status, answer_text
FROM sg_qa_vector_example
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-02';

View File

@@ -1,23 +0,0 @@
-- Approve the reviewed customer QA comparison example. Metric definitions
-- stay in the exact Few-shot example rather than becoming global profile text.
UPDATE sg_qa_vector_example
SET reference_status = 'APPROVED',
inspection_status = 'VERIFIED',
answer_text = 'Expected focus: compare two independently aggregated metrics for the same resolved game and date. '
|| 'Standard AU: CZN_COMN_USER_MST with AU_FLAG=1 and EXPT_USER_YN=''N''. '
|| 'Business AU: CZN_CUSTOM_BIZ_USER_TXN with BIZ_AU_FLAG=1 and EXPT_USER_YN=''N''. '
|| 'The labels standard AU and business AU do not imply STD_USER_YN. '
|| 'Historical answer: STD_AU_COUNT=0, BIZ_AU_COUNT=1.',
inspection_note = 'Customer QA verified: standard and business AU are separate aggregates with their respective AU flags.',
verified_at = SYSTIMESTAMP,
verified_by = 'SGMP_POC_METADATA_REVIEW'
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-03';
COMMIT;
SELECT example_id, reference_status, inspection_status, source_case_id, answer_text
FROM sg_qa_vector_example
WHERE source_type = 'CUSTOMER_QA_BENCHMARK'
AND source_case_id = 'CZN-03';

View File

@@ -1,300 +0,0 @@
-- ============================================================
-- HMM demo carrier performance source data for PostgreSQL.
--
-- Target: AWS RDS for PostgreSQL, database postgres
-- Schema: hmm_demo
-- Redmine: #729
--
-- This script is idempotent. It owns only C001..C008 and their
-- 2025-02 through 2026-07 monthly demo rows. It never stores an
-- RDS endpoint, password, or certificate path.
--
-- Example:
-- PGPASSWORD="$PG_PASSWORD" psql \
-- "host=$PG_HOST port=5432 dbname=postgres user=$PG_USER \
-- sslmode=verify-full sslrootcert=$PG_SSL_ROOT_CERT" \
-- -X -f database/source/postgres_hmm_carrier_performance.sql
-- ============================================================
\set ON_ERROR_STOP on
\echo === HMM carrier performance demo: begin ===
BEGIN;
CREATE SCHEMA IF NOT EXISTS hmm_demo AUTHORIZATION postgres;
COMMENT ON SCHEMA hmm_demo IS
'HMM federation demonstration data. All carrier names and KPI values are fictional.';
REVOKE CREATE ON SCHEMA hmm_demo FROM PUBLIC;
CREATE TABLE IF NOT EXISTS hmm_demo.carriers (
carrier_code VARCHAR(10) PRIMARY KEY,
carrier_name VARCHAR(120) NOT NULL UNIQUE,
country_code CHAR(2) NOT NULL CHECK (country_code ~ '^[A-Z]{2}$'),
alliance_name VARCHAR(80),
service_region VARCHAR(40) NOT NULL,
performance_tier VARCHAR(10) NOT NULL
CHECK (performance_tier IN ('CORE', 'GROWTH', 'WATCH')),
active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE hmm_demo.carriers IS
'Fictional carrier master data for the HMM ADB and PostgreSQL federation demo.';
COMMENT ON COLUMN hmm_demo.carriers.carrier_code IS 'Fictional carrier identifier.';
COMMENT ON COLUMN hmm_demo.carriers.performance_tier IS
'Demo portfolio tier: CORE, GROWTH, or WATCH.';
CREATE TABLE IF NOT EXISTS hmm_demo.carrier_monthly_performance (
carrier_code VARCHAR(10) NOT NULL
REFERENCES hmm_demo.carriers (carrier_code),
performance_month DATE NOT NULL
CHECK (performance_month = date_trunc('month', performance_month)::DATE),
contracted_teu INTEGER NOT NULL CHECK (contracted_teu >= 0),
shipped_teu INTEGER NOT NULL CHECK (shipped_teu >= 0),
revenue_usd NUMERIC(16,2) NOT NULL CHECK (revenue_usd >= 0),
gross_margin_usd NUMERIC(16,2) NOT NULL,
schedule_reliability_pct NUMERIC(5,2) NOT NULL
CHECK (schedule_reliability_pct BETWEEN 0 AND 100),
vessel_utilization_pct NUMERIC(5,2) NOT NULL
CHECK (vessel_utilization_pct BETWEEN 0 AND 100),
claim_rate_pct NUMERIC(5,2) NOT NULL
CHECK (claim_rate_pct BETWEEN 0 AND 100),
yoy_growth_pct NUMERIC(7,2) NOT NULL,
risk_level VARCHAR(10) NOT NULL
CHECK (risk_level IN ('GREEN', 'AMBER', 'RED')),
updated_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (carrier_code, performance_month)
);
COMMENT ON TABLE hmm_demo.carrier_monthly_performance IS
'Deterministic fictional monthly carrier KPIs for 2025-02 through 2026-07.';
COMMENT ON COLUMN hmm_demo.carrier_monthly_performance.performance_month IS
'First calendar day of the KPI month.';
COMMENT ON COLUMN hmm_demo.carrier_monthly_performance.risk_level IS
'Derived demo risk level based on margin, reliability, and claim rate.';
CREATE INDEX IF NOT EXISTS ix_carrier_perf_month
ON hmm_demo.carrier_monthly_performance (performance_month, carrier_code);
CREATE INDEX IF NOT EXISTS ix_carrier_perf_risk
ON hmm_demo.carrier_monthly_performance (risk_level, performance_month);
INSERT INTO hmm_demo.carriers (
carrier_code,
carrier_name,
country_code,
alliance_name,
service_region,
performance_tier,
active
) VALUES
('C001', 'Bluewave Maritime', 'SG', 'North Pacific Network', 'ASIA-PACIFIC', 'CORE', TRUE),
('C002', 'Pacific Star Lines', 'JP', 'East Ocean Connect', 'ASIA-PACIFIC', 'CORE', TRUE),
('C003', 'Korea Ocean Link', 'KR', 'East Ocean Connect', 'ASIA-PACIFIC', 'GROWTH', TRUE),
('C004', 'Nordic Seaways', 'DK', 'Northern Trade Group', 'EUROPE', 'CORE', TRUE),
('C005', 'Orient Bridge Shipping', 'TW', 'North Pacific Network', 'ASIA-PACIFIC', 'WATCH', TRUE),
('C006', 'Atlantic Merchant Line', 'DE', 'Atlantic Cargo Forum', 'EUROPE-AMERICAS', 'WATCH', TRUE),
('C007', 'Global Horizon Cargo', 'US', 'Atlantic Cargo Forum', 'AMERICAS', 'GROWTH', TRUE),
('C008', 'Southern Cross Marine', 'FR', 'Southern Ocean Group', 'OCEANIA', 'WATCH', TRUE)
ON CONFLICT (carrier_code) DO UPDATE SET
carrier_name = EXCLUDED.carrier_name,
country_code = EXCLUDED.country_code,
alliance_name = EXCLUDED.alliance_name,
service_region = EXCLUDED.service_region,
performance_tier = EXCLUDED.performance_tier,
active = EXCLUDED.active;
WITH month_series AS (
SELECT
month_index,
(DATE '2025-02-01' + make_interval(months => month_index))::DATE AS performance_month,
CASE month_index % 6
WHEN 0 THEN -1
WHEN 1 THEN 0
WHEN 2 THEN 1
WHEN 3 THEN 2
WHEN 4 THEN 1
ELSE -1
END::NUMERIC AS seasonal_factor
FROM generate_series(0, 17) AS series(month_index)
),
profiles (
carrier_code,
base_teu,
monthly_teu_step,
seasonal_teu,
utilization_base,
utilization_step,
revenue_per_teu,
revenue_step,
margin_pct_base,
margin_pct_step,
reliability_base,
reliability_step,
claim_rate_base,
claim_rate_step,
yoy_growth_base,
yoy_growth_step
) AS (
VALUES
('C001', 1800::NUMERIC, 25::NUMERIC, 80::NUMERIC, 91.5::NUMERIC, 0.10::NUMERIC, 1850::NUMERIC, 4::NUMERIC, 15.0::NUMERIC, 0.10::NUMERIC, 95.0::NUMERIC, 0.05::NUMERIC, 0.35::NUMERIC, -0.005::NUMERIC, 8.0::NUMERIC, 0.25::NUMERIC),
('C002', 1500::NUMERIC, 5::NUMERIC, 60::NUMERIC, 88.5::NUMERIC, 0.02::NUMERIC, 1780::NUMERIC, 3::NUMERIC, 12.0::NUMERIC, 0.00::NUMERIC, 92.5::NUMERIC, 0.00::NUMERIC, 0.45::NUMERIC, 0.005::NUMERIC, 3.0::NUMERIC, 0.05::NUMERIC),
('C003', 1050::NUMERIC, 15::NUMERIC, 50::NUMERIC, 83.0::NUMERIC, 0.20::NUMERIC, 1920::NUMERIC, 2::NUMERIC, 8.0::NUMERIC, 0.15::NUMERIC, 88.5::NUMERIC, 0.25::NUMERIC, 1.20::NUMERIC, -0.040::NUMERIC, -4.0::NUMERIC, 0.60::NUMERIC),
('C004', 1350::NUMERIC, 0::NUMERIC, 180::NUMERIC, 86.0::NUMERIC, 0.00::NUMERIC, 1810::NUMERIC, 3::NUMERIC, 10.0::NUMERIC, 0.00::NUMERIC, 93.0::NUMERIC, 0.00::NUMERIC, 0.55::NUMERIC, 0.010::NUMERIC, 1.0::NUMERIC, 0.10::NUMERIC),
('C005', 1300::NUMERIC, -20::NUMERIC, 70::NUMERIC, 88.0::NUMERIC, -0.10::NUMERIC, 1850::NUMERIC, -2::NUMERIC, 8.0::NUMERIC, -0.20::NUMERIC, 91.0::NUMERIC, -0.15::NUMERIC, 0.80::NUMERIC, 0.030::NUMERIC, -2.0::NUMERIC, -0.50::NUMERIC),
('C006', 1250::NUMERIC, -10::NUMERIC, 90::NUMERIC, 84.0::NUMERIC, -0.12::NUMERIC, 1800::NUMERIC, -3::NUMERIC, 6.0::NUMERIC, -0.15::NUMERIC, 87.0::NUMERIC, -0.25::NUMERIC, 1.50::NUMERIC, 0.050::NUMERIC, -5.0::NUMERIC, -0.70::NUMERIC),
('C007', 800::NUMERIC, 30::NUMERIC, 100::NUMERIC, 82.0::NUMERIC, 0.25::NUMERIC, 2050::NUMERIC, 5::NUMERIC, 9.0::NUMERIC, 0.18::NUMERIC, 90.5::NUMERIC, 0.15::NUMERIC, 0.70::NUMERIC, -0.020::NUMERIC, 12.0::NUMERIC, 0.50::NUMERIC),
('C008', 1150::NUMERIC, -25::NUMERIC, 60::NUMERIC, 80.0::NUMERIC, -0.15::NUMERIC, 1750::NUMERIC, -4::NUMERIC, 3.0::NUMERIC, -0.20::NUMERIC, 85.0::NUMERIC, -0.30::NUMERIC, 2.00::NUMERIC, 0.080::NUMERIC, -8.0::NUMERIC, -0.80::NUMERIC)
),
base_metrics AS (
SELECT
p.carrier_code,
m.performance_month,
ROUND(
p.base_teu
+ p.monthly_teu_step * m.month_index
+ p.seasonal_teu * m.seasonal_factor
)::INTEGER AS contracted_teu,
LEAST(
98.0::NUMERIC,
GREATEST(
60.0::NUMERIC,
p.utilization_base
+ p.utilization_step * m.month_index
+ m.seasonal_factor * 0.50
)
) AS utilization_pct,
p.revenue_per_teu + p.revenue_step * m.month_index AS unit_revenue,
p.margin_pct_base + p.margin_pct_step * m.month_index AS margin_pct,
LEAST(
99.5::NUMERIC,
GREATEST(
60.0::NUMERIC,
p.reliability_base
+ p.reliability_step * m.month_index
+ m.seasonal_factor * 0.20
)
) AS reliability_pct,
LEAST(
100.0::NUMERIC,
GREATEST(
0.0::NUMERIC,
p.claim_rate_base + p.claim_rate_step * m.month_index
)
) AS claim_pct,
p.yoy_growth_base + p.yoy_growth_step * m.month_index AS yoy_pct
FROM profiles p
CROSS JOIN month_series m
),
volume_metrics AS (
SELECT
b.*,
ROUND(b.contracted_teu * b.utilization_pct / 100.0)::INTEGER AS shipped_teu
FROM base_metrics b
),
financial_metrics AS (
SELECT
v.*,
ROUND(v.shipped_teu * v.unit_revenue, 2) AS revenue_usd
FROM volume_metrics v
),
final_metrics AS (
SELECT
f.*,
ROUND(f.revenue_usd * f.margin_pct / 100.0, 2) AS gross_margin_usd
FROM financial_metrics f
)
INSERT INTO hmm_demo.carrier_monthly_performance (
carrier_code,
performance_month,
contracted_teu,
shipped_teu,
revenue_usd,
gross_margin_usd,
schedule_reliability_pct,
vessel_utilization_pct,
claim_rate_pct,
yoy_growth_pct,
risk_level,
updated_at
)
SELECT
f.carrier_code,
f.performance_month,
f.contracted_teu,
f.shipped_teu,
f.revenue_usd,
f.gross_margin_usd,
ROUND(f.reliability_pct, 2),
ROUND(f.utilization_pct, 2),
ROUND(f.claim_pct, 2),
ROUND(f.yoy_pct, 2),
CASE
WHEN f.gross_margin_usd < 0
OR f.reliability_pct < 87
OR f.claim_pct >= 2 THEN 'RED'
WHEN f.margin_pct < 7
OR f.reliability_pct < 92
OR f.claim_pct >= 1 THEN 'AMBER'
ELSE 'GREEN'
END,
TIMESTAMPTZ '2026-07-24 00:00:00+00'
FROM final_metrics f
ON CONFLICT (carrier_code, performance_month) DO UPDATE SET
contracted_teu = EXCLUDED.contracted_teu,
shipped_teu = EXCLUDED.shipped_teu,
revenue_usd = EXCLUDED.revenue_usd,
gross_margin_usd = EXCLUDED.gross_margin_usd,
schedule_reliability_pct = EXCLUDED.schedule_reliability_pct,
vessel_utilization_pct = EXCLUDED.vessel_utilization_pct,
claim_rate_pct = EXCLUDED.claim_rate_pct,
yoy_growth_pct = EXCLUDED.yoy_growth_pct,
risk_level = EXCLUDED.risk_level,
updated_at = EXCLUDED.updated_at;
CREATE OR REPLACE VIEW hmm_demo.carrier_performance_latest_v AS
SELECT
c.carrier_code,
c.carrier_name,
c.country_code,
c.alliance_name,
c.service_region,
c.performance_tier,
p.performance_month,
p.contracted_teu,
p.shipped_teu,
p.revenue_usd,
p.gross_margin_usd,
p.schedule_reliability_pct,
p.vessel_utilization_pct,
p.claim_rate_pct,
p.yoy_growth_pct,
p.risk_level
FROM hmm_demo.carriers c
JOIN hmm_demo.carrier_monthly_performance p
ON p.carrier_code = c.carrier_code
WHERE p.performance_month = (
SELECT MAX(latest.performance_month)
FROM hmm_demo.carrier_monthly_performance latest
);
COMMENT ON VIEW hmm_demo.carrier_performance_latest_v IS
'Latest fictional monthly KPI for each active demo carrier.';
DO $role$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'hmm_federation_reader') THEN
CREATE ROLE hmm_federation_reader NOLOGIN;
END IF;
END
$role$;
GRANT USAGE ON SCHEMA hmm_demo TO hmm_federation_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA hmm_demo TO hmm_federation_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA hmm_demo
GRANT SELECT ON TABLES TO hmm_federation_reader;
COMMIT;
\echo === HMM carrier performance demo: complete ===
\echo Next: run database/source/postgres_hmm_carrier_performance_verify.sql

View File

@@ -1,324 +0,0 @@
-- ============================================================
-- Verification for HMM demo carrier performance data.
-- Redmine: #729
-- ============================================================
\set ON_ERROR_STOP on
\pset pager off
\echo === HMM carrier performance demo: verification begin ===
DO $verify$
DECLARE
carrier_count INTEGER;
performance_count INTEGER;
invalid_month_count INTEGER;
latest_month DATE;
earliest_month DATE;
wrong_series_count INTEGER;
latest_risk_count INTEGER;
wrong_trend_count INTEGER;
reader_can_login BOOLEAN;
BEGIN
SELECT COUNT(*)
INTO carrier_count
FROM hmm_demo.carriers
WHERE carrier_code BETWEEN 'C001' AND 'C008';
SELECT COUNT(*), MIN(performance_month), MAX(performance_month)
INTO performance_count, earliest_month, latest_month
FROM hmm_demo.carrier_monthly_performance
WHERE carrier_code BETWEEN 'C001' AND 'C008'
AND performance_month BETWEEN DATE '2025-02-01' AND DATE '2026-07-01';
SELECT COUNT(*)
INTO invalid_month_count
FROM hmm_demo.carrier_monthly_performance
WHERE performance_month <> date_trunc('month', performance_month)::DATE;
SELECT COUNT(*)
INTO wrong_series_count
FROM (
SELECT carrier_code, COUNT(*) AS month_count
FROM hmm_demo.carrier_monthly_performance
WHERE carrier_code BETWEEN 'C001' AND 'C008'
AND performance_month BETWEEN DATE '2025-02-01' AND DATE '2026-07-01'
GROUP BY carrier_code
HAVING COUNT(*) <> 18
) wrong_series;
SELECT COUNT(DISTINCT risk_level)
INTO latest_risk_count
FROM hmm_demo.carrier_performance_latest_v;
WITH boundaries AS (
SELECT
carrier_code,
MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2025-02-01')
AS first_revenue,
MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2026-07-01')
AS latest_revenue
FROM hmm_demo.carrier_monthly_performance
WHERE carrier_code BETWEEN 'C001' AND 'C008'
GROUP BY carrier_code
)
SELECT COUNT(*)
INTO wrong_trend_count
FROM boundaries
WHERE (carrier_code IN ('C001', 'C002', 'C003', 'C004', 'C007')
AND latest_revenue <= first_revenue)
OR (carrier_code IN ('C005', 'C006', 'C008')
AND latest_revenue >= first_revenue);
SELECT rolcanlogin
INTO reader_can_login
FROM pg_roles
WHERE rolname = 'hmm_federation_reader';
IF carrier_count <> 8 THEN
RAISE EXCEPTION 'Expected 8 carriers, found %', carrier_count;
END IF;
IF performance_count <> 144 THEN
RAISE EXCEPTION 'Expected 144 monthly performance rows, found %', performance_count;
END IF;
IF earliest_month <> DATE '2025-02-01' OR latest_month <> DATE '2026-07-01' THEN
RAISE EXCEPTION 'Unexpected period: % through %', earliest_month, latest_month;
END IF;
IF invalid_month_count <> 0 OR wrong_series_count <> 0 THEN
RAISE EXCEPTION
'Invalid monthly series: invalid dates %, wrong carrier series %',
invalid_month_count,
wrong_series_count;
END IF;
IF latest_risk_count <> 3 THEN
RAISE EXCEPTION 'Expected GREEN, AMBER, RED latest risks, found % levels',
latest_risk_count;
END IF;
IF wrong_trend_count <> 0 THEN
RAISE EXCEPTION 'Unexpected first-to-latest trend for % carriers', wrong_trend_count;
END IF;
IF reader_can_login IS DISTINCT FROM FALSE THEN
RAISE EXCEPTION 'hmm_federation_reader must remain a NOLOGIN role';
END IF;
IF NOT has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'USAGE') THEN
RAISE EXCEPTION 'hmm_federation_reader lacks schema USAGE';
END IF;
IF has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'CREATE') THEN
RAISE EXCEPTION 'hmm_federation_reader unexpectedly has schema CREATE';
END IF;
IF NOT has_table_privilege(
'hmm_federation_reader',
'hmm_demo.carrier_monthly_performance',
'SELECT'
) THEN
RAISE EXCEPTION 'hmm_federation_reader lacks SELECT';
END IF;
IF has_table_privilege(
'hmm_federation_reader',
'hmm_demo.carrier_monthly_performance',
'INSERT'
) THEN
RAISE EXCEPTION 'hmm_federation_reader unexpectedly has INSERT';
END IF;
END
$verify$;
DO $constraint_tests$
BEGIN
BEGIN
INSERT INTO hmm_demo.carrier_monthly_performance (
carrier_code,
performance_month,
contracted_teu,
shipped_teu,
revenue_usd,
gross_margin_usd,
schedule_reliability_pct,
vessel_utilization_pct,
claim_rate_pct,
yoy_growth_pct,
risk_level,
updated_at
) VALUES (
'C001',
DATE '2026-07-02',
1,
1,
1,
1,
90,
90,
1,
1,
'GREEN',
CURRENT_TIMESTAMP
);
RAISE EXCEPTION 'Month-first-day constraint did not reject invalid input';
EXCEPTION
WHEN check_violation THEN
NULL;
END;
BEGIN
INSERT INTO hmm_demo.carrier_monthly_performance (
carrier_code,
performance_month,
contracted_teu,
shipped_teu,
revenue_usd,
gross_margin_usd,
schedule_reliability_pct,
vessel_utilization_pct,
claim_rate_pct,
yoy_growth_pct,
risk_level,
updated_at
) VALUES (
'UNKNOWN',
DATE '2026-08-01',
1,
1,
1,
1,
90,
90,
1,
1,
'GREEN',
CURRENT_TIMESTAMP
);
RAISE EXCEPTION 'Foreign key constraint did not reject unknown carrier';
EXCEPTION
WHEN foreign_key_violation THEN
NULL;
END;
BEGIN
INSERT INTO hmm_demo.carrier_monthly_performance (
carrier_code,
performance_month,
contracted_teu,
shipped_teu,
revenue_usd,
gross_margin_usd,
schedule_reliability_pct,
vessel_utilization_pct,
claim_rate_pct,
yoy_growth_pct,
risk_level,
updated_at
) VALUES (
'C001',
DATE '2026-08-01',
1,
1,
1,
1,
101,
90,
1,
1,
'GREEN',
CURRENT_TIMESTAMP
);
RAISE EXCEPTION 'Percentage constraint did not reject invalid input';
EXCEPTION
WHEN check_violation THEN
NULL;
END;
END
$constraint_tests$;
\echo --- Row counts and period ---
SELECT
COUNT(DISTINCT carrier_code) AS carrier_count,
COUNT(*) AS monthly_row_count,
MIN(performance_month) AS first_month,
MAX(performance_month) AS latest_month
FROM hmm_demo.carrier_monthly_performance
WHERE carrier_code BETWEEN 'C001' AND 'C008'
AND performance_month BETWEEN DATE '2025-02-01' AND DATE '2026-07-01';
\echo --- Per-carrier series ---
SELECT
c.carrier_code,
c.carrier_name,
COUNT(p.*) AS month_count,
MIN(p.performance_month) AS first_month,
MAX(p.performance_month) AS latest_month
FROM hmm_demo.carriers c
JOIN hmm_demo.carrier_monthly_performance p
ON p.carrier_code = c.carrier_code
WHERE c.carrier_code BETWEEN 'C001' AND 'C008'
GROUP BY c.carrier_code, c.carrier_name
ORDER BY c.carrier_code;
\echo --- Latest KPI and risk distribution ---
SELECT
carrier_code,
carrier_name,
performance_month,
shipped_teu,
revenue_usd,
gross_margin_usd,
schedule_reliability_pct,
claim_rate_pct,
risk_level
FROM hmm_demo.carrier_performance_latest_v
ORDER BY carrier_code;
SELECT risk_level, COUNT(*) AS carrier_count
FROM hmm_demo.carrier_performance_latest_v
GROUP BY risk_level
ORDER BY risk_level;
\echo --- First-to-latest revenue trend ---
WITH boundaries AS (
SELECT
carrier_code,
MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2025-02-01') AS first_revenue,
MAX(revenue_usd) FILTER (WHERE performance_month = DATE '2026-07-01') AS latest_revenue
FROM hmm_demo.carrier_monthly_performance
WHERE carrier_code BETWEEN 'C001' AND 'C008'
GROUP BY carrier_code
)
SELECT
carrier_code,
first_revenue,
latest_revenue,
CASE
WHEN latest_revenue > first_revenue THEN 'UP'
WHEN latest_revenue < first_revenue THEN 'DOWN'
ELSE 'FLAT'
END AS trend
FROM boundaries
ORDER BY carrier_code;
\echo --- TLS session ---
SELECT
ssl,
version AS tls_version,
cipher,
bits
FROM pg_stat_ssl
WHERE pid = pg_backend_pid();
\echo --- Read-only role privileges ---
SELECT
(SELECT NOT rolcanlogin
FROM pg_roles
WHERE rolname = 'hmm_federation_reader') AS no_login,
has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'USAGE') AS schema_usage,
has_schema_privilege('hmm_federation_reader', 'hmm_demo', 'CREATE') AS schema_create,
has_table_privilege(
'hmm_federation_reader',
'hmm_demo.carrier_monthly_performance',
'SELECT'
) AS can_select,
has_table_privilege(
'hmm_federation_reader',
'hmm_demo.carrier_monthly_performance',
'INSERT'
) AS can_insert;
\echo === HMM carrier performance demo: verification complete ===

View File

@@ -56,10 +56,10 @@ ORDS Handler가 Bearer 값을 문자열로 바꾸는 것만으로 DDS Context가
먼저 DDS 데모 전용 로컬 데이터셋과 권한 객체를 적용합니다. 이 스크립트는 VPD VIEW나 외부 RDS DB Link를 사용하지 않습니다.
```bash
sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @database/adb/31_dds_standalone_demo_setup.sql
sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @database/adb/32_dds_vector_tag_setup.sql
sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @sql/adb/31_dds_standalone_demo_setup.sql
sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @sql/adb/32_dds_vector_tag_setup.sql
bash scripts/setup-dds-token-data-grant.sh
sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @database/adb/36_dds_sales_knowledge_scenario.sql
sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @sql/adb/36_dds_sales_knowledge_scenario.sql
```
`32_dds_vector_tag_setup.sql`은 공통 청크·태그 저장소를 읽는 DDS 전용 VIEW를 만들고, 동일한 `CB_PERMISSION`/`CB_PERMISSION_RULE`에 역할별 TAG 규칙을 등록합니다. `SPRING_BOOT`, `ORDS`, `ORACLE_VPD`, `MCP`처럼 한 역할에 여러 TAG가 있으면 `/dds-provision`이 OR 조건으로 합쳐 DDS DATA GRANT를 다시 만듭니다. 벡터 검색에 필요한 `EMBEDDING`은 DDS 엔진이 거리 계산에 사용하므로 Grant에서 제외하지 않고, 검색 응답에서는 애플리케이션이 반환하지 않습니다. 그 외 민감 컬럼은 애플리케이션 권한의 원문 허용 목록에 없으면 `ALL COLUMNS EXCEPT`로 제외됩니다. 검색 화면에서는 같은 문서를 청킹·임베딩한 뒤 선택한 END USER로 직접 조회하므로, DDS Grant가 없는 주체는 보호 VIEW 자체를 볼 수 없습니다.
@@ -68,7 +68,7 @@ sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @database/adb/36_dds_sales_knowledge_
`36_dds_sales_knowledge_scenario.sql``agent_sales``SALES_KNOWLEDGE_ROLE`을 만들고 `TECH_TAG=SALES`인 세일즈 청크(28004)를 추가합니다. 짧은 데모 토큰 `dds_sales_demo_token`을 사용하면 결과는 28004만 남아야 합니다. 운영에서는 이 토큰 대신 Bearer 관리 화면에서 발급한 임시 토큰을 사용합니다.
토큰 경로를 제거할 때는 사용 중인 애플리케이션이 없는지 확인한 뒤 `database/adb/37_dds_sales_knowledge_scenario_cleanup.sql``database/adb/35_dds_token_data_grant_cleanup.sql`을 별도로 승인해 실행합니다.
토큰 경로를 제거할 때는 사용 중인 애플리케이션이 없는지 확인한 뒤 `sql/adb/37_dds_sales_knowledge_scenario_cleanup.sql``sql/adb/35_dds_token_data_grant_cleanup.sql`을 별도로 승인해 실행합니다.
그 다음 DDS 인스턴스를 실행합니다.

View File

@@ -1,41 +0,0 @@
location = /auth/check {
internal;
proxy_pass http://127.0.0.1:8621/auth/check;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header Cookie $http_cookie;
proxy_set_header X-Real-IP $remote_addr;
}
location /auth/ {
proxy_pass http://127.0.0.1:8621;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location @hmm_portal_login {
return 302 /auth/login;
}
location / {
auth_request /auth/check;
error_page 401 = @hmm_portal_login;
auth_request_set $hmm_auth_user $upstream_http_x_auth_user;
auth_request_set $hmm_auth_expires $upstream_http_x_auth_expires;
proxy_pass http://127.0.0.1:8622;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-HMM-Authenticated-User $hmm_auth_user;
proxy_set_header X-HMM-Auth-Expires $hmm_auth_expires;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300;
proxy_send_timeout 300;
}

View File

@@ -1,2 +0,0 @@
[Service]
EnvironmentFile=/etc/hmm-poc4-audit.env

View File

@@ -1,29 +0,0 @@
[Unit]
Description=HMM Portal HttpOnly Cookie Authentication
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=opc
Group=opc
WorkingDirectory=/opt/hmm-poc4
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=/opt/hmm-poc4/.env
ExecStart=/opt/hmm-poc4/.venv/bin/python -m ai_web_agent_console.auth_gateway
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
LockPersonality=true
RestrictAddressFamilies=AF_INET AF_INET6
UMask=0077
[Install]
WantedBy=multi-user.target

View File

@@ -1,20 +0,0 @@
[Unit]
Description=Smilegate PoC4 Portal Authentication Gateway
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=opc
Group=opc
WorkingDirectory=/home/opc/workspaces/vpd-permission-poc-20260628213409/poc4_active_source_20260714
EnvironmentFile=/etc/smilegate/backoffice.env
EnvironmentFile=/etc/smilegate/poc4-console.env
ExecStart=/opt/smilegate/poc4-console/venv/bin/python /home/opc/workspaces/vpd-permission-poc-20260628213409/poc4_active_source_20260714/apps/poc4/portal_auth_gateway.py
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

View File

@@ -1,3 +0,0 @@
[Service]
ExecStart=
ExecStart=/opt/smilegate/poc4-console/venv/bin/streamlit run /home/opc/workspaces/vpd-permission-poc-20260628213409/poc4_active_source_20260714/apps/smilegate_demo/main.py --server.address 127.0.0.1 --server.port 8622 --server.headless true --server.enableCORS false --server.enableXsrfProtection false --browser.serverAddress smilegate.cloud-handson.com --browser.serverPort 443 --browser.gatherUsageStats false --logger.level=warn

View File

@@ -1,20 +0,0 @@
# HMM deployment profile for /etc/vpd-backoffice.env.
# Append these non-secret values to the existing file. Keep DB passwords,
# remember-me keys and user Bearer tokens outside Git.
BACKOFFICE_CATALOG_OWNER=ADMIN
BACKOFFICE_CATALOG_OBJECTS='[{"key":"teams","tableName":"HMM_ORG_TEAMS","objectType":"TABLE","businessName":"조직 원장","description":"HMM HR 조직·팀 기본정보"},{"key":"employees","tableName":"HMM_HR_EMPLOYEES","objectType":"TABLE","businessName":"직원 원장","description":"직원·매니저·소속팀 정보"},{"key":"leave-balances","tableName":"HMM_LEAVE_BALANCES","objectType":"TABLE","businessName":"휴가 잔여 원장","description":"직원별 연도·휴가 유형별 부여·사용·잔여 일수"},{"key":"leave-requests","tableName":"HMM_LEAVE_REQUESTS","objectType":"TABLE","businessName":"휴가 신청 원장","description":"직원별 휴가 신청·승인 상태와 기간"},{"key":"attendance-daily","tableName":"HMM_ATTENDANCE_DAILY","objectType":"TABLE","businessName":"일별 근태 원장","description":"직원별 출퇴근·근무 상태와 근무 시간"},{"key":"hr-terms","tableName":"HMM_HR_TERMS","objectType":"TABLE","businessName":"HR 표준 용어 원장","description":"휴가·근태 표준 코드, 명칭과 유사 표현","previewColumns":["TERM_ID","TERM_CODE","TERM_KIND","CANONICAL_NAME","TERM_NAME","IS_CANONICAL","DESCRIPTION","EMBEDDED_AT"]}]'
BACKOFFICE_PRODUCT_NAME='HMM HR Access Console'
BACKOFFICE_PRODUCT_TITLE='HMM HR Access Console'
BACKOFFICE_PRODUCT_DATA_LABEL='HMM HR 데이터'
BACKOFFICE_MCP_PUBLIC_URL='https://hmm-backoffice.cloud-handson.com/mcp'
BACKOFFICE_MCP_SERVER_NAME='hmm-hr-backoffice'
# AGENT_TOOL targetName must exist with STATUS=ENABLED in USER_AI_AGENT_TOOLS.
# The application fails startup before advertising an invalid configured contract.
BACKOFFICE_MCP_TOOLS='[{"name":"resolve_hr_term","label":"HMM HR 용어 표준화","description":"휴가·근태 표현을 HMM 표준 용어와 코드로 변환합니다. 모호한 표현은 데이터 조회 전에 이 도구를 사용합니다.","argumentName":"term","argumentDescription":"확인할 휴가·근태 용어, 동의어 또는 코드입니다.","executionType":"AGENT_TOOL","targetName":"HMM_HR_TERM_RESOLVER","targetParameterName":"P_TERM"},{"name":"search_hr_data","label":"HMM HR 데이터 조회","description":"조직, 직원, 휴가 잔여·신청, 근태 데이터를 읽기 전용 Select AI로 조회합니다.","argumentName":"query","argumentDescription":"조직, 직원, 휴가 또는 근태에 대한 완전한 자연어 질문입니다.","executionType":"SELECT_AI"},{"name":"search_hr_policy","label":"HMM HR 규정 검색","description":"HR 규정 PDF의 문서 메타데이터, Abstract, 관련 청크를 계층형 벡터 검색으로 조회합니다.","argumentName":"query","argumentDescription":"HR 규정에 대한 완전한 자연어 질문입니다.","executionType":"AGENT_TOOL","targetName":"HMM_HR_POLICY_SEARCH","targetParameterName":"P_QUERY"}]'
BACKOFFICE_MASKING_POLICIES='[{"objectName":"HMM_HR_EMPLOYEES","policyName":"HMM_EMPLOYEE_PII_REDACT"},{"objectName":"HMM_LEAVE_BALANCES","policyName":"HMM_LEAVE_BALANCE_REDACT"},{"objectName":"HMM_LEAVE_REQUESTS","policyName":"HMM_LEAVE_REQUEST_REDACT"},{"objectName":"HMM_ATTENDANCE_DAILY","policyName":"HMM_ATTENDANCE_REDACT"}]'
BACKOFFICE_SECURITY_SQL_SCRIPTS='[{"scriptId":"hmm-leave-vpd","category":"HMM / VPD","fileName":"72_hmm_leave_team_vpd.sql","title":"HMM 휴가 팀 접근 정책","description":"직원 본인과 팀장의 직접 보고 팀원 휴가 행 접근 정책 및 토큰 컨텍스트를 구성합니다."}]'

View File

@@ -1,2 +0,0 @@
[Service]
EnvironmentFile=/etc/vpd-backoffice.env

View File

@@ -8,7 +8,7 @@
> 단위 매트릭스** 입니다 (README 참고). 본 상세 가이드는 그 위에 얹을 수 있는 **행
> 단위 region 필터링** 변형(`KR_ANALYSTS → APAC`, `GLOBAL_ADMINS → '*'`) 을 예시로
> 사용합니다 — VPD 메커니즘 자체는 동일하므로 개념 이해에는 차이가 없습니다.
> region 필터를 실제로 켜려면 `database/adb/03_seed.sql` 하단의 주석을 해제하세요.
> region 필터를 실제로 켜려면 `sql/adb/03_seed.sql` 하단의 주석을 해제하세요.
---
@@ -543,7 +543,7 @@ SQL> SELECT COUNT(*) FROM admin.v_customers_pg; -- 0
SQL> SELECT COUNT(*) FROM admin.v_customers_my; -- 0
# region 필터 변형을 켰을 때 — vpduser_both 에게 APAC 만 허용한 경우
# (database/adb/03_seed.sql 하단 UPDATE 주석 해제 후)
# (sql/adb/03_seed.sql 하단 UPDATE 주석 해제 후)
sqlplus "vpduser_both/\"${VPDUSER_BOTH_PASSWORD}\"@$ADB_TNS"
SQL> SELECT region, COUNT(*) FROM admin.v_customers_pg GROUP BY region;
-- 결과: APAC 만 보임

View File

@@ -4,7 +4,7 @@
---
## Postgres (`database/source/postgres_setup.sql`)
## Postgres (`sql/source/postgres_setup.sql`)
```
DB : 사용자가 .env 에서 지정 (PG_DB, 기본값 vpdpoc)
@@ -22,12 +22,12 @@ seed rows : 12 (APAC 4 / EMEA 4 / AMER 4)
```bash
PGPASSWORD=$PG_PASSWORD psql \
-h $PG_HOST -p $PG_PORT -U $PG_USER -d $PG_DB \
-f database/source/postgres_setup.sql
-f sql/source/postgres_setup.sql
```
---
## MySQL (`database/source/mysql_setup.sql`)
## MySQL (`sql/source/mysql_setup.sql`)
```
DB : MY_DB (기본 ecommerce_poc)
@@ -47,12 +47,12 @@ PK 범위를 PG (1~12) 와 다르게 가져간 이유:
```bash
mysql -h $MY_HOST -P $MY_PORT -u $MY_USER -p"$MY_PASSWORD" $MY_DB \
< database/source/mysql_setup.sql
< sql/source/mysql_setup.sql
```
---
## ADB → 원격 DB Link (`database/adb/01_dblinks.sql`)
## ADB → 원격 DB Link (`sql/adb/01_dblinks.sql`)
`DBMS_CLOUD_ADMIN.CREATE_DATABASE_LINK` 가 ADB 안에서 직접 heterogeneous 연결을
처리합니다. 별도 Database Gateway 설치 불필요.

View File

@@ -2,7 +2,7 @@
> 본 문서는 **선택사항** 입니다. 메인 데모(`run.sh all`) 는 전통적 **VPD** 경로로
> 그대로 동작합니다. 이 문서는 동일한 4-user 매트릭스를 Oracle AI Database
> **26ai 신기능 Deep Data Security (DDS)** 로 재구현한 `database/adb/13_dds_variant.sql`
> **26ai 신기능 Deep Data Security (DDS)** 로 재구현한 `sql/adb/13_dds_variant.sql`
> 의 사용법과, VPD ↔ DDS 1:1 매핑을 다룹니다.
>
> 본 변형은 **2026-05-26 실제 ADB (23.26.2.2.0) 에서 E2E 검증** 됨 (8장 결과).
@@ -84,7 +84,7 @@ Oracle 이 **2026-04-09** 에 VPD/RAS 의 공식 후계자로 발표한 native a
```bash
source .env # DDSUSER_*_PASSWORD 로드
sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @database/adb/13_dds_variant.sql
sqlplus "$ADB_USER/$ADB_PASSWORD@$ADB_TNS" @sql/adb/13_dds_variant.sql
```
스크립트가 만드는 객체:
@@ -218,7 +218,7 @@ DROP VIEW v_dds_customers_my;
## 8. E2E 검증 결과 (2026-05-26, ADB 23.26.2.2.0)
`database/adb/13_dds_variant.sql` 실행 후 4명의 ddsuser 로 `v_dds_customers_*` 조회:
`sql/adb/13_dds_variant.sql` 실행 후 4명의 ddsuser 로 `v_dds_customers_*` 조회:
| 사용자 | `v_dds_customers_pg` | `v_dds_customers_my` | 판정 |
|---|---|---|---|

View File

@@ -1165,15 +1165,15 @@ ORDER BY rg.grantee, dg.object_name, dg.grant_name;
| 파일 | 역할 |
|---|---|
| `scripts/run_agent_ords_security_adb_local.sh` | 전체 실행 |
| `database/adb/16_agent_ords_security_local_cleanup.sql` | `CB_*` 예제 객체 정리 |
| `database/adb/17_agent_ords_security_local_vpd_setup.sql` | VPD용 로컬 테이블, 권한 테이블, context, redaction, 정책 생성 |
| `database/adb/18_agent_ords_security_local_vpd_test.sql` | `CB_ORDS`로 Bearer Key 기반 VPD 테스트 |
| `database/adb/19_agent_ords_security_local_dds_setup.sql` | DDS용 로컬 테이블, END USER, DATA ROLE, DATA GRANT 생성 |
| `database/adb/20_agent_ords_security_local_dds_test.sql` | DDS end user별 조회 테스트 |
| `database/adb/21_agent_ords_security_ords_enable_schema.sql` | `CB_ORDS` schema를 ORDS에 enable |
| `database/adb/22_agent_ords_security_ords_handler_setup.sql` | ORDS Module/Handler와 Handler Package 생성 |
| `database/adb/23_agent_ords_security_ords_handler_test.sql` | Handler Package 직접 실행으로 VPD/DDS Bearer 경로 검증 |
| `database/adb/24_agent_ords_security_inventory.sql` | VPD/DDS 정책과 권한을 중앙 조회 |
| `sql/adb/16_agent_ords_security_local_cleanup.sql` | `CB_*` 예제 객체 정리 |
| `sql/adb/17_agent_ords_security_local_vpd_setup.sql` | VPD용 로컬 테이블, 권한 테이블, context, redaction, 정책 생성 |
| `sql/adb/18_agent_ords_security_local_vpd_test.sql` | `CB_ORDS`로 Bearer Key 기반 VPD 테스트 |
| `sql/adb/19_agent_ords_security_local_dds_setup.sql` | DDS용 로컬 테이블, END USER, DATA ROLE, DATA GRANT 생성 |
| `sql/adb/20_agent_ords_security_local_dds_test.sql` | DDS end user별 조회 테스트 |
| `sql/adb/21_agent_ords_security_ords_enable_schema.sql` | `CB_ORDS` schema를 ORDS에 enable |
| `sql/adb/22_agent_ords_security_ords_handler_setup.sql` | ORDS Module/Handler와 Handler Package 생성 |
| `sql/adb/23_agent_ords_security_ords_handler_test.sql` | Handler Package 직접 실행으로 VPD/DDS Bearer 경로 검증 |
| `sql/adb/24_agent_ords_security_inventory.sql` | VPD/DDS 정책과 권한을 중앙 조회 |
### 7.1 VPD + Redaction 실행 검증
@@ -1634,7 +1634,7 @@ ORA-20101: Authorization header must be Bearer <key>
### 7.4 중앙 권한 인벤토리
VPD와 DDS 모두 적용 결과를 Dictionary View로 확인할 수 있다. 이 예제에서는 `database/adb/24_agent_ords_security_inventory.sql`을 실행해 정책 연결과 DDS Grant Matrix를 확인한다.
VPD와 DDS 모두 적용 결과를 Dictionary View로 확인할 수 있다. 이 예제에서는 `sql/adb/24_agent_ords_security_inventory.sql`을 실행해 정책 연결과 DDS Grant Matrix를 확인한다.
VPD 정책 연결 확인:

View File

@@ -3,7 +3,7 @@
> **상태**: Draft
> **작성**: [AI] Architect · **최종수정**: 2026-06-23
> **추적성** — Redmine: #424 · 관련 ADR: 없음
> · 구현 파일: `vpd-backoffice/pom.xml`, `vpd-backoffice/src/main/**`, `vpd-backoffice/src/test/**` · 테스트: `mvn test`
> · 구현 파일: `pom.xml`, `src/main/**`, `src/test/**` · 테스트: `mvn test`
## 1. 목적 (Why)
@@ -58,8 +58,8 @@
### 모듈/파일 구조
```text
vpd-backoffice/pom.xml
vpd-backoffice/src/main/java/com/cloudhandson/vpdbackoffice/
pom.xml
src/main/java/com/cloudhandson/vpdbackoffice/
VpdBackofficeApplication.java
config/
DataSourceConfig.java
@@ -88,13 +88,13 @@ vpd-backoffice/src/main/java/com/cloudhandson/vpdbackoffice/
TokenController.java
ProbeController.java
DashboardController.java
vpd-backoffice/src/main/resources/
src/main/resources/
application.yml
mapper/*.xml
templates/**/*.html
static/css/app.css
static/js/app.js
vpd-backoffice/src/test/java/com/cloudhandson/vpdbackoffice/
src/test/java/com/cloudhandson/vpdbackoffice/
```
### 데이터 흐름
@@ -251,7 +251,7 @@ MCP-style reasoning
## 12. 미해결 질문 (Open Questions)
- ORDS 검증 API는 기존 `database/adb/22_agent_ords_security_ords_handler_setup.sql`의 Handler를 그대로 사용할지, 백오피스 전용 Handler를 추가할지 결정이 필요하다.
- ORDS 검증 API는 기존 `sql/adb/22_agent_ords_security_ords_handler_setup.sql`의 Handler를 그대로 사용할지, 백오피스 전용 Handler를 추가할지 결정이 필요하다.
- 백오피스 관리자 로그인은 초기에는 local user로 둘지, 사내 인증과 연결할지 후속 결정이 필요하다.
- 컬럼 정책을 Redaction DDL까지 자동 생성할지, 관리 테이블 저장 후 DBA 적용으로 둘지 결정이 필요하다.
- 실제 구현 issue를 별도 Redmine 하위 이슈로 나눌지, #424를 Developer 단계로 계속 이동할지 결정이 필요하다.

View File

@@ -3,7 +3,7 @@
> **상태**: Approved
> **작성**: [AI] Architect · **최종수정**: 2026-06-25
> **추적성** — Redmine: #456 · 관련 ADR: 없음
> · 구현 파일: `database/adb/26_agent_ords_security_dynamic_vpd_filter.sql`, `database/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql` · 테스트: `database/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql`, `mvn test`
> · 구현 파일: `sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql`, `sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql` · 테스트: `sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql`, `mvn test`
## 1. 목적 (Why)
@@ -78,8 +78,8 @@ I/O와 순수 로직 경계:
## 10. 테스트 계획
- SQLcl: `@database/adb/26_agent_ords_security_dynamic_vpd_filter.sql`
- SQLcl: `@database/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql`
- SQLcl: `@sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql`
- SQLcl: `@sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql`
- Maven: `mvn test`
- ORDS smoke: 기존 HR=3, SELF=1, ALL=6 결과 유지 확인.

View File

@@ -1,7 +1,7 @@
# 함수 설계서: `cb_agent_doc_vpd_filter` (#456)
> **부모 설계서**: ./README.md · **상태**: Approved
> **작성**: [AI] Architect · **구현**: `database/adb/26_agent_ords_security_dynamic_vpd_filter.sql:cb_agent_doc_vpd_filter` · **테스트**: `database/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql`
> **작성**: [AI] Architect · **구현**: `sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql:cb_agent_doc_vpd_filter` · **테스트**: `sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql`
## 1. 시그니처

View File

@@ -3,7 +3,7 @@
> **상태**: Approved
> **작성**: [AI] Architect · **최종수정**: 2026-06-25
> **추적성** — Redmine: #462 · 관련 ADR: 없음
> · 구현 파일: `database/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql` · 테스트: SQLcl `27`, `./run.sh backoffice-vpd-ords-test`, `mvn test`
> · 구현 파일: `sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql` · 테스트: SQLcl `27`, `./run.sh backoffice-vpd-ords-test`, `mvn test`
## 1. 목적 (Why)
@@ -67,8 +67,8 @@ malicious cb_permission_rule rows
## 10. 테스트 계획
- `@database/adb/26_agent_ords_security_dynamic_vpd_filter.sql`
- `@database/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql`
- `@sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql`
- `@sql/adb/27_agent_ords_security_dynamic_vpd_filter_test.sql`
- `./run.sh backoffice-vpd-ords-test`
- `mvn test`

View File

@@ -3,7 +3,7 @@
> **상태**: Approved
> **작성**: [AI] Architect · **최종수정**: 2026-06-28
> **추적성** — Redmine: #547 · 관련 ADR: 없음
> · 구현 파일: `vpd-backoffice/src/main/resources/application.yml`, `SecurityConfig.java`, `scripts/deploy-backoffice-vm.sh`, `scripts/configure-backoffice-https-vm.sh`, `deploy/vpd-backoffice/caddy/Caddyfile.template`
> · 구현 파일: `src/main/resources/application.yml`, `SecurityConfig.java`, `scripts/deploy-backoffice-vm.sh`, `scripts/configure-backoffice-https-vm.sh`, `deploy/caddy/Caddyfile.template`
> · 테스트: `TransportSecurityTest.java`, `scripts/test-backoffice-https-config.sh`
## 1. 목적 (Why)
@@ -49,7 +49,7 @@ Internet client
Internet client ── HTTP :8082 ──X (loopback bind + firewalld/NSG deny)
```
- `deploy/vpd-backoffice/caddy/Caddyfile.template`: TLS/redirect/HSTS/reverse proxy의 선언적 설정.
- `deploy/caddy/Caddyfile.template`: TLS/redirect/HSTS/reverse proxy의 선언적 설정.
- `scripts/configure-backoffice-https-vm.sh`: 입력 검증, 원격 설정 검증·백업·적용·확인.
- `scripts/deploy-backoffice-vm.sh`: 앱 배포 시 운영 보안 환경값을 강제하고 루프백 헬스체크.
- Spring 설정: proxy가 전달한 HTTPS scheme을 인식하고 직접 HTTP 요청을 거부한다.

View File

@@ -2,7 +2,7 @@
> **상태**: VPD·DDS 병행 검증 완료, 토큰 기반 객체별 Data Grant 적용 완료
> **추적성**: Redmine #565, #566 · DDS 구현 커밋 `1e48864`, `a5e70bc`, `04cca64`, `8978be4`
> **기준 구현**: `database/adb/32_dds_vector_tag_setup.sql`, `database/adb/34_dds_token_data_grant_common_auth.sql`, `database/adb/36_dds_sales_knowledge_scenario.sql`, `dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/service/DdsGrantPublisher.java`, `dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/service/DdsVectorKnowledgeService.java`
> **기준 구현**: `sql/adb/32_dds_vector_tag_setup.sql`, `sql/adb/34_dds_token_data_grant_common_auth.sql`, `sql/adb/36_dds_sales_knowledge_scenario.sql`, `dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/service/DdsGrantPublisher.java`, `dds-backoffice/src/main/java/com/cloudhandson/ddsbackoffice/service/DdsVectorKnowledgeService.java`
## 목적

View File

@@ -77,8 +77,8 @@ Authorization: Bearer <masked>
## 설치와 운영 절차
1. `ADMIN`으로 [42_agent_ords_fga_execution_audit.sql](../../../database/adb/42_agent_ords_fga_execution_audit.sql)을 실행해 활성 보호 객체마다 FGA `SELECT` 정책을 만든다.
2. `CB_ORDS`로 [22_agent_ords_security_ords_handler_setup.sql](../../../database/adb/22_agent_ords_security_ords_handler_setup.sql), [29_agent_ords_vector_search_ords.sql](../../../database/adb/29_agent_ords_vector_search_ords.sql), [43_agent_ords_probe_id_handler_patch.sql](../../../database/adb/43_agent_ords_probe_id_handler_patch.sql)을 순서대로 실행한다.
1. `ADMIN`으로 [42_agent_ords_fga_execution_audit.sql](../../../sql/adb/42_agent_ords_fga_execution_audit.sql)을 실행해 활성 보호 객체마다 FGA `SELECT` 정책을 만든다.
2. `CB_ORDS`로 [22_agent_ords_security_ords_handler_setup.sql](../../../sql/adb/22_agent_ords_security_ords_handler_setup.sql), [29_agent_ords_vector_search_ords.sql](../../../sql/adb/29_agent_ords_vector_search_ords.sql), [43_agent_ords_probe_id_handler_patch.sql](../../../sql/adb/43_agent_ords_probe_id_handler_patch.sql)을 순서대로 실행한다.
3. 새 권한 결과 확인을 실행한다. 이전 요청에는 `CLIENT_ID`가 없으므로 소급해 매칭하지 않는다.
4. 백오피스 실행 계정이 Autonomous의 `UNIFIED_AUDIT_TRAIL`을 조회할 수 있어야 한다. 전통 FGA 환경은 `DBA_FGA_AUDIT_TRAIL`을 fallback으로 사용한다. 일반 계정이면 감사 조회 권한을 가진 전용 observer 계정을 사용한다.

View File

@@ -1,7 +1,7 @@
# 함수 설계서: `addLocalSqlTrace` (#567)
> **부모 설계서**: ./README.md · **상태**: Approved
> **작성**: [AI] Architect · **구현**: `vpd-backoffice/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java:addLocalSqlTrace` · **테스트**: `ProbeResultTest`, `mvn test`
> **작성**: [AI] Architect · **구현**: `src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java:addLocalSqlTrace` · **테스트**: `ProbeResultTest`, `mvn test`
## 1. 시그니처

View File

@@ -1,7 +1,7 @@
# 함수 설계서: `findVpdPredicate` (#567)
> **부모 설계서**: ./README.md · **상태**: Approved
> **작성**: [AI] Architect · **구현**: `vpd-backoffice/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java:findVpdPredicate` · **테스트**: `mvn test`, 운영 smoke
> **작성**: [AI] Architect · **구현**: `src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java:findVpdPredicate` · **테스트**: `mvn test`, 운영 smoke
## 1. 시그니처

Some files were not shown because too many files have changed in this diff Show More