메인 콘텐츠로 건너뛰기

fact 정의란?

**fact 정의(Fact Definition)**는 규칙이 조건과 액션에서 참조할 수 있는 입력 변수를 선언합니다. 함수 파라미터처럼 이름과 타입을 정의합니다. fact는 테넌트 전체에서 공유됩니다.

사전 등록 fact

모든 테넌트에는 계정 생성 시 아래 fact가 미리 등록되어 있습니다: userId, userTags는 편의를 위해 제공되는 fact일 뿐, 특별히 보호되지 않습니다. 이름 변경·타입 변경·삭제가 커스텀 fact와 똑같은 규칙을 따릅니다(fact 관리 참고). 등록 없이 즉시 조건과 액션에서 쓸 수 있습니다. isBlocked, blockReason은 시스템 fact입니다. BLOCK 액션이 쓰는 값을 담으므로 모양을 엔진이 정합니다.
  • 삭제하거나 타입을 바꿀 수 없습니다(FD-003).
  • SET_FACT나 MUTATE_FACT의 대상으로 쓸 수 없습니다(ACT-033). 값은 BLOCK만 씁니다.
  • 이름과 설명, PII 표시는 바꿀 수 있고, blockReason에는 값 제약을 달 수 있습니다.
조건에서 쓸 수 있지만, 조건은 요청에 담아 보낸 값만 읽습니다. 같은 요청에서 BLOCK이 쓴 값은 보지 못합니다(요청이 실행되는 순서 참고). 앞 호출의 차단 결과를 조건에 쓰려면 previouslyBlocked 같은 다른 키로 보내세요. isBlocked: true를 보내고 BLOCK이 다시 true를 쓰면 값이 바뀌지 않아 mutatedFacts에도 generatedVariables에도 나오지 않습니다.

커스텀 fact

사전 등록 fact 외의 모든 입력 변수는 사용 전에 등록하는 것이 좋습니다 (미등록 fact 참고). 예를 들어 customerTier, orderRegion, productCategory 등은 커스텀 fact입니다.

지원 타입

값 제약 (Value Domain)

타입은 fact가 숫자를 담는다는 것만 말합니다. 값 제약은 그중 어떤 숫자인지를 말합니다. 값 제약을 선언하면 절대 맞을 수 없는 규칙을 저장 단에서 막고, 규칙 편집기가 직접 입력 대신 드롭다운을 띄웁니다. BOOLEAN에는 쓸 수 없습니다. 값이 true와 false 둘뿐이라 좁힐 것이 없습니다. LIST 타입에서 값 제약은 리스트 전체가 아니라 각 원소에 걸립니다.
세 칸이 전부 선택입니다. 하나도 안 채우면 fact는 종전과 똑같이 동작합니다.

선언하면 무엇이 달라지나

  • 규칙을 저장할 때. 제약 밖 값을 쓴 조건이 거부됩니다. tier == "PLATNUM" 같은 오타가 저장 시점에 걸리고, 조용히 아무것도 안 걸리는 규칙이 배포되지 않습니다.
  • 규칙을 쓸 때. 스칼라 타입이면 콘솔이 값 입력칸을 드롭다운으로 바꿉니다.
  • 실행할 때. 제약 밖 값이 들어오면 400입니다. 종전에는 제약 밖 값을 보내는 것이 값을 아예 안 보내는 것보다 관대했습니다. 규칙이 읽는 fact를 빠뜨리면 이미 400이었으니까요.
비교 연산자는 min과 max만 봅니다. 경계값이 허용값 목록의 원소일 필요는 없습니다. 허용값이 [0, 100, 200]이어도 loyaltyPoints > 50은 정당한 조건입니다. 50은 비교 기준이지 그 fact가 가질 값이 아니기 때문입니다. CONTAINS는 아예 제외됩니다. tier CONTAINS "OLD"는 "GOLD"에 걸리는 정당한 조건인데 "OLD"는 허용값이 아닙니다.

나중에 제약을 바꾸면

실행은 오늘의 정의가 아니라 발행 시점에 뜬 사본을 봅니다.
  • 제약을 바꿔도 이미 발행한 버전은 흔들리지 않습니다. 나중에 그 버전을 배포하거나 그 버전으로 롤백해도 같습니다. 바뀐 제약은 새 버전을 발행해 배포해야 실행에 반영됩니다.
  • 제약을 바꿔도 규칙은 고쳐지지 않고, 새 제약 밖의 값을 쓰는 규칙이 있어도 fact 수정은 막히지 않습니다. 대신 수정 결과가 그 규칙을 알려 줍니다.
값 제약은 fact를 만들 때 함께 선언하세요. 그 뒤에 쓰는 규칙이 드롭다운을 받고, 값 제약이 잡아주는 오타는 알아채기 가장 어려운 부류입니다. 저장은 깨끗하게 되는데 실행에서 영영 안 걸리는 규칙이니까요.

새 제약 밖의 값을 쓰는 규칙

fact를 수정하면서 값 제약이 바뀌면(처음 선언하거나, 좁히거나, 다른 값으로 바꾸면) LexQ는 그 fact를 비교하는 규칙들을 규칙 저장과 같은 검사(P-051)로 다시 봅니다. 새 제약에 없는 값을 조건에 쓴 규칙이 수정 결과에 함께 옵니다.
  • 어느 버전인가. 초안, 그리고 지금 배포 중인 버전입니다. 그룹마다 라이브 버전과 A/B 테스트 버전이 여기에 들어갑니다. 발행만 한 버전, 유효 기간이 끝난 버전, 삭제한 그룹의 버전은 뺍니다. 고칠 수 없고, 발행할 때의 사본을 계속 쓰기 때문입니다.
  • 얼마나 싣나. 버전은 전부 싣습니다. 버전마다 규칙을 우선순위 순서로 5개까지 보여 주고, 걸린 조건은 규칙에 적힌 그대로 전부 싣습니다. 나머지 규칙은 수로 셉니다. 비활성 규칙도 들어갑니다.
  • 언제 나오나. 저장된 제약이 실제로 바뀐 수정에서만 나옵니다. 같은 제약을 다시 보내면 아무것도 나오지 않습니다. 제약을 지울 때도 나오지 않습니다. 그 밖에 있는 값이 없기 때문입니다.
걸린 버전마다 할 일이 다릅니다. 허용 값으로 바꿔 넣기만 하지는 마세요. tier에서 BRONZE를 빼면 tier NOT_EQUALS "BRONZE"는 허용된 모든 등급에서 참입니다. 이것을 tier NOT_EQUALS "SILVER"로 바꾸면 실버 고객을 빼는 조건이 됩니다. 목록이 나오는 곳: tier의 허용 값에서 BRONZE를 빼면 stdout에 수정된 fact가, stderr에 아래 문장이 나옵니다. CLI의 문장은 영어입니다.
같은 예의 수정 응답 meta입니다. data에는 수정된 fact가 담깁니다.

명명 규칙

fact 키는 영문자로 시작하고, 그 뒤에 영문자, 숫자, 언더스코어를 쓸 수 있습니다. 키는 대소문자를 구분하므로 paymentAmount와 paymentamount는 서로 다른 fact입니다. 표기는 직접 정하세요. LexQ가 만드는 fact와 이 문서의 예시는 camelCase를 쓰지만, 엔진은 어떤 표기든 받고 이미 만든 키는 그대로 동작합니다.
  • ✅ paymentAmount, customerTier, userRegion
  • ✅ payment_amount, PAYMENT_AMOUNT (유효합니다. 이 문서가 쓰지 않을 뿐입니다)
  • ❌ Payment-Amount, PAYMENT AMOUNT, 1stPurchase, _internal

필수 vs 선택 fact

요청이 어떤 키를 보내야 하는지는 fact 정의가 아니라 규칙이 정합니다. 실행하는 버전의 활성 규칙이 읽는 키는 모두 요청에 있어야 합니다. 규칙의 조건 필드, 그리고 MUTATE_FACT의 targetVar와 refVar가 여기에 해당합니다. 하나라도 빠지면 요청이 P-015로 거절됩니다. 일부러 값을 비우려면 키를 null로 보내세요. 값이 null인 조건은 매칭되지 않습니다. MUTATE_FACT의 대상은 예외입니다. 액션이 실행될 때 값이 숫자여야 하고, 그때 null이면 요청이 P-003으로 실패합니다.
fact 정의의 필수 여부(Required) 표시는 안내용입니다. 실행에는 아무 영향이 없고, 없어질 예정입니다.

PII fact

fact는 콘솔의 관리 → Fact Definitions에서, 또는 CLI로 PII(개인식별정보)로 지정할 수 있습니다. CLI는 lexq facts create나 lexq facts update에 --pii를 주고, 끌 때는 --no-pii를 줍니다. update에서는 두 플래그 다 CLI 1.1.6 이상이 필요합니다. 지정은 값을 읽는 방식을 바꿀 뿐, 규칙 실행에는 영향이 없습니다.
  • 규칙은 평소처럼 실제 값으로 평가합니다. 마스킹은 사람과 통합을 향한 것이지 엔진을 향한 것이 아닙니다.
  • 모든 읽기 표면(의사결정 출처, 실행 이력 상세, export)에서 값은 ••••••로 마스킹되어 도착하고, maskedKeys가 마스킹된 키 목록을 알려줍니다.
  • 마스킹된 값의 열람은 콘솔 전용이며 감사됩니다. 값이 보이기 전에 감사 행이 먼저 기록됩니다. 전체 계약은 의사결정 출처(Decision Provenance)를 참조하세요.
이메일, 전화번호, 계정 식별자 같은 fact는 생성 시점에 PII로 지정하세요. 플래그를 켜는 순간부터 마스킹이 적용되고, 규칙 변경은 필요 없습니다.

미등록 fact

규칙이 읽기만 하는 키(조건 필드, refVar)는 fact 정의로 등록하기 전에도 참조할 수 있습니다. LexQ는 이런 키를 먼저 등록하도록 강제하지 않지만, 규칙을 저장할 때마다 스키마에 없는 참조 키를 알려줍니다. 액션 대상은 다릅니다. SET_FACT와 MUTATE_FACT의 targetVar는 등록된 fact여야 합니다. 등록되지 않은 키에 값을 쓰는 규칙은 저장과 발행이 ACT-032로 거절되므로, fact를 먼저 등록하세요. 규칙을 create 또는 update 할 때마다 LexQ는 규칙의 조건과 액션을 스캔해 미등록 fact(어떤 fact 정의에도 선언되지 않은 키)를 반환합니다. 각 항목에는 추론된 타입, 제안된 표시 이름, 참조 위치(조건/액션)가 포함됩니다. 규칙이 읽기만 하는 키는 일부러 막지 않습니다. 저장·발행·배포가 그대로 진행되고, 규칙은 작성된 그대로 저장됩니다. 다만 규칙이 읽는 키는 등록 여부와 상관없이 실행 때 보내야 합니다(필수 vs 선택 fact 참고). 등록 전까지 비활성화되는 것: fact를 등록하면 실행 전 타입 검사도 켜집니다. 버전이 읽는 등록된 fact에 선언과 다른 타입의 값을 보내면 규칙을 보기 전에 P-060으로 거절됩니다. 예를 들어 NUMBER에 "85"를 보낸 경우입니다. null은 타입 불일치가 아닙니다. 값 제약처럼 이 검사도 발행할 때 뜬 선언의 사본을 보므로, 버전을 발행한 뒤에 등록한 fact는 그 버전에서 검사하지 않습니다. 등록하지 않은 키는 검사하지 않습니다. 조건이 매칭되지 않고, 그 규칙의 decisionTraces[].reasonDetail에 Evaluation error: FACT_TYPE_MISMATCH가 남습니다. 미등록 fact 경고는 규칙을 작성하는 모든 곳에 표시됩니다: CLI(stderr), 콘솔 규칙 편집기와 배포 다이얼로그, MCP 도구.

확인 및 등록

저장 응답에 미등록 fact가 포함되지만, 버전 전체를 언제든 조회할 수도 있습니다:
유지할 fact를 등록하세요:
등록은 항상 명시적 단계입니다. LexQ는 규칙이 키를 참조한다고 해서 fact를 자동으로 생성하지 않습니다. 스키마는 규칙 작성의 부산물이 아니라 의도적인 계약으로 유지됩니다.

fact 관리

수정은 보낸 칸만 바꿉니다. API와 MCP 도구 lexq_facts_update도 같습니다. --name이나 --description을 빼면 그 칸은 그대로입니다. 설명을 지우려면 빈 문자열을 주세요.
표시명은 지울 수 없습니다. 비었거나 공백뿐인 name은 C-001로 거절됩니다.
lexq analytics requirements (CLI) 또는 Engine API의 GET /groups/{groupId}/requirements 엔드포인트를 사용하여 배포된 버전이 필요로 하는 fact를 확인하세요.
규칙이 참조하는 fact는 삭제할 수 없고 타입도 바꿀 수 없습니다. 둘 다 409로 거절되며(삭제는 FD-006, 타입 변경은 FD-007) 참조 중인 규칙 수를 함께 알려줍니다. 참조를 먼저 걷거나 새 fact를 만드세요.버전 상태와 무관합니다. 초안이 참조해도 배포된 버전과 똑같이 막힙니다. 삭제한 그룹은 고칠 수도 배포할 수도 없어서 그 그룹의 규칙은 세지 않습니다. fact를 지우거나 타입을 바꾼 뒤에도 삭제한 그룹은 그대로 재실행할 수 있습니다. 이름 변경과 필수 여부 전환은 언제나 열려 있습니다.시스템 fact는 예외입니다. 참조하는 규칙이 있든 없든 삭제와 타입 변경이 FD-003으로 거절됩니다.

다음 단계

정책 규칙

규칙 조건과 액션에서 fact를 사용하세요.

정책 실행

실행 시 fact가 어떻게 전달되는지 확인하세요.