Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4d9f23c
fix(tools): remove lexical pseudo-topic models
seonghobae Aug 9, 2026
6f58d3d
fix(docs): restore complete changelog
seonghobae Aug 9, 2026
3f1803b
docs: start canonical architecture decision index
seonghobae Aug 9, 2026
77ff173
docs: record topic measurement authority ADR
seonghobae Aug 9, 2026
26bbceb
docs: link topic measurement authority decision
seonghobae Aug 9, 2026
0e1fc6f
docs: scope topic measurement ADR to Naruon
seonghobae Aug 9, 2026
a1a4676
test(tools): tighten topic boundary evidence
seonghobae Aug 9, 2026
3906b0e
docs: complete topic intelligence decision package
seonghobae Aug 9, 2026
cb8a752
fix(docs): tighten topic contract evidence
seonghobae Aug 9, 2026
6e3f613
Merge develop into fix/remove-lexical-topic-heuristics
seonghobae Aug 9, 2026
59bfd5a
Merge develop into fix/remove-lexical-topic-heuristics
seonghobae Aug 9, 2026
4fe5c9a
merge: sync with develop
seonghobae Aug 13, 2026
682c32d
ci: add one-shot conflict-safe sync for PR 1297
seonghobae Aug 14, 2026
c906692
Merge develop into fix/remove-lexical-topic-heuristics
github-actions[bot] Aug 14, 2026
0b0f6ac
chore: retrigger exact-head checks after develop sync
seonghobae Aug 14, 2026
01fbdac
ci: add one-shot AGENTS guidance repair
seonghobae Aug 14, 2026
b99558d
ci: fix one-shot AGENTS repair trigger
seonghobae Aug 14, 2026
61e9de0
ci: simplify one-shot AGENTS repair
seonghobae Aug 14, 2026
6c5dbaa
ci: repair one-shot workflow YAML
seonghobae Aug 14, 2026
7f2d34a
docs: clarify scoped identity dimensions
github-actions[bot] Aug 14, 2026
e8c8f43
chore: retrigger exact-head gates after scoped identity repair
seonghobae Aug 14, 2026
13a6e66
Merge protected develop into topic boundary branch
seonghobae Aug 14, 2026
d417bcd
merge(develop): synchronize topic-intelligence candidate
seonghobae Aug 15, 2026
fd8a71f
Merge branch 'develop' into fix/remove-lexical-topic-heuristics
opencode-agent[bot] Aug 15, 2026
c8a785f
merge(develop): integrate current protected base
seonghobae Aug 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/one-shot-sync-pr-1297.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: One-shot sync PR 1297 with develop

on:
push:
branches:
- fix/remove-lexical-topic-heuristics

permissions:
contents: write

concurrency:
group: one-shot-sync-pr-1297
cancel-in-progress: false

jobs:
sync:
if: >-
github.repository == 'ContextualWisdomLab/naruon' &&
github.ref == 'refs/heads/fix/remove-lexical-topic-heuristics'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit

- name: Checkout exact PR branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: true
ref: fix/remove-lexical-topic-heuristics

- name: Merge current develop and remove this one-shot workflow
shell: bash
run: |
set -euo pipefail

branch='fix/remove-lexical-topic-heuristics'
workflow='.github/workflows/one-shot-sync-pr-1297.yml'
expected_pr_parent='4fe5c9accd0864af8c11c54f1103d120280ae9b1'
email_detail_entry='- EmailDetail 테스트가 지원하지 않는 스레드 병합/분리 버튼을 `textContent`뿐 아니라 `aria-label`과 `title` 접근 가능 이름으로도 검출하도록 바꿔, 아이콘 전용 버튼 회귀를 놓치지 않습니다.'

test "$(git rev-parse HEAD^)" = "$expected_pr_parent"
git fetch --no-tags origin develop

git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'

if ! git merge-base --is-ancestor origin/develop HEAD; then
set +e
git merge --no-commit --no-ff origin/develop
merge_status=$?
set -e

mapfile -t conflicts < <(git diff --name-only --diff-filter=U)
if (( merge_status != 0 )); then
if (( ${#conflicts[@]} != 1 )) || [[ "${conflicts[0]}" != 'CHANGELOG.md' ]]; then
printf 'Unexpected merge conflicts:\n' >&2
printf ' %s\n' "${conflicts[@]}" >&2
exit 1
fi

git checkout --ours -- CHANGELOG.md
EMAIL_DETAIL_ENTRY="$email_detail_entry" python - <<'PY'
import os
from pathlib import Path

path = Path('CHANGELOG.md')
text = path.read_text(encoding='utf-8')
heading = '## [Unreleased]\n'
entry = os.environ['EMAIL_DETAIL_ENTRY'] + '\n'
if not text.startswith(heading):
raise SystemExit('CHANGELOG.md does not start with the Unreleased heading')
if entry not in text:
text = heading + entry + text[len(heading):]
path.write_text(text, encoding='utf-8')
PY
git add CHANGELOG.md
Comment thread
seonghobae marked this conversation as resolved.
Outdated
fi
fi

rm -- "$workflow"
git add -A
git diff --cached --check

if git diff --cached --quiet; then
echo 'No synchronization commit was required.'
exit 0
fi

git commit -m 'Merge develop into fix/remove-lexical-topic-heuristics'
git push origin "HEAD:${branch}"
Comment thread
seonghobae marked this conversation as resolved.
Outdated
18 changes: 18 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ in this repo.
knowledge-graph pipeline (DOM decomposition, entity/relation extraction,
grounded graph retrieval) should ground itself in the relevant layout-analysis
and knowledge-graph / grounded-retrieval literature.

### Structural topic-model boundary

- Do not implement or describe hard-coded term lists, term frequency,
embeddings, or LLM-assigned labels as structural topic modeling (STM).
Fixed business labels are not topic-posterior estimates, and the explicitly
lexical `keyword_extractor` must not be used as topic evidence.
- Topic inference requires a versioned fitted TEPP model and its frozen
preprocessing and vocabulary contract. If that fitted model is unavailable,
fail closed; do not return a default label, template agenda, or substitute
keyword/embedding/LLM result presented as STM.
<!-- END cwl-agent-guidance -->

## Release governance defaults
Expand Down Expand Up @@ -425,6 +436,13 @@ in this repo.
- Public audit/event identifiers that may use human-readable prefixes must not
be stored in artificially short `varchar(n)` columns; use opaque source UIDs
that fit seeded smoke data and provider evidence without truncation.
- Conceptual ERDs, API schemas, persistence models, and fixtures must not mark a
reusable business identifier such as `document_ref`, `model_id`, `topic_id`,
or `label_id` as an unscoped primary or foreign key. Use an opaque immutable
reference that binds the full scope or an explicit composite identity with the
required snapshot revision, model version, request/result scope, and label
version. Never join snapshots, model artifacts, topic components, or label
evidence by a bare document, model, topic, rank, label, or display value.
Comment thread
seonghobae marked this conversation as resolved.
Outdated
- When reviews find public/private identifier leaks, stale API fixture shapes, or recurring bug patterns, update tests, frontend mocks, E2E mocks, README examples, architecture docs, and explicitly record the anti-pattern in `AGENTS.md` so the same bug pattern does not reappear in copied examples.
- When reviews find missing browser security headers or tabnabbing hardening,
update both backend header tests and frontend link tests. Global backend
Expand Down
15 changes: 15 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,21 @@ Runtime database connectivity is secret-injected: `backend/core/config.py` has
no fallback `DATABASE_URL`, so missing database configuration fails at startup
rather than silently using shared development credentials.

## Topic-intelligence boundary

Naruon has no live Structural Topic Modeling endpoint, fitted topic artifact,
or topic-result persistence. The retained `keyword_extractor` is deterministic
lexical metadata and must not feed topic, agenda, search, or norm-group
inference. A future adapter may consume a separately accepted, versioned TEPP
artifact/API only when frozen preprocessing and vocabulary, covariate design,
mixed-membership posterior uncertainty, diagnostics, provenance, and explicit
abstention are all available. Missing or incompatible scientific authority
fails closed. Naruon owns authentication, authorization, request validation,
the adapter envelope, and disclosure policy; TEPP would own the scientific
payload. See the canonical documentation graph in
[`docs/topic-intelligence/README.md`](docs/topic-intelligence/README.md) and
[`ADR-0001`](docs/adr/0001-topic-measurement-authority.md).

## Workspace navigation boundary

The Next.js shell opens the Today execution dashboard for first-run sessions and
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
## [Unreleased]
### 주제 측정 경계 (Topic Measurement)

- STM 결과로 오인될 수 있었던 하드코딩 용어표 기반
`email_categorizer`와 `meeting_agenda_generator`를 도구 레지스트리에서
제거했습니다. `keyword_extractor`는 결정론적 단어 빈도 유틸리티로 유지하되
주제 posterior 근거로 사용하지 않는 경계를 문서화했습니다. 현재 Naruon에는
fitted TEPP 모델 기반 production 주제 측정 API가 없으므로, 모델 부재 시
기본 라벨이나 템플릿으로 대체하지 않고 fail closed 합니다.
- 이 경계의 PRD, TRD, ADR, Architecture, API 계약, JSON Schema, UML,
개념 ERD, 보안·위협 모델, 테스트·운영 전략, 추적성 및 문서 적합성 평가를
`docs/topic-intelligence/`에 하나의 상태 표시 문서 그래프로 정리했습니다.
이는 미래 계약의 설계 근거이며, 현재 runtime 구현이나 물리 DB 엔터티가
존재한다는 주장이 아닙니다.
- UUID V4 제너레이터(`uuid_v4_generator`) 도구를 추가하여 런타임에서 범용 고유 식별자 버전 4를 랜덤으로 생성할 수 있게 하였습니다. 테스트 커버리지 100%를 보장합니다.

### 보안 패치 (CodeQL extended current-head)

- `cryptography`를 `50.0.0`으로 갱신해 공격자 제공 PKCS#7 EnvelopedData 복호화 결과의 오류·타이밍 차이로 발생하는 Bleichenbacher oracle(`CVE-2026-69247`, `GHSA-g6cj-pr64-35w5`)을 제거하고, backend·uv lock·hash lock·Strix CI 의존성 증거를 같은 버전으로 동기화했습니다. Strix 잠금은 `google-cloud-aiplatform==1.160.0`의 `<7` 제약을 위반하던 `protobuf==7.35.1`을 이미 검증된 `6.33.6`으로 복구해 다시 해석·설치 가능하게 했습니다.
Expand Down
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ Next.js frontend ──> FastAPI backend (control plane) ──> Postgres + pgve
test/lint/build), plus `bandit`, `codeql`, `trivy`, `scorecard`,
`pr-governance`, `docker-publish` (GHCR on `v*` tags matching `VERSION`), and
`mail-smoke`. Actions are pinned to full commit SHAs.
- Topic intelligence is **not implemented**. Never use lexical frequencies,
embeddings, zero-shot labels, or request-time LLM labels as an STM result.
The retained `keyword_extractor` is lexical metadata only. Any future adapter
is blocked on a versioned fitted TEPP artifact/API with frozen preprocessing,
mixed-membership uncertainty and diagnostics; absence or incompatibility
fails closed. Start at `docs/topic-intelligence/README.md` and
`docs/adr/0001-topic-measurement-authority.md`.

## Key conventions

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ mail/calendar/file systems.
## Quick Links
- [Installation & Setup](#five-minute-local-path)
- [Architecture](docs/architecture/)
- [Topic-intelligence documentation set](docs/topic-intelligence/README.md)
- [Architecture decisions](docs/adr/README.md)
- [Contributing](CONTRIBUTING.md)
- [Code of Conduct](CODE_OF_CONDUCT.md)
- [Security Policy](SECURITY.md)
Expand Down Expand Up @@ -48,6 +50,12 @@ mail/calendar/file systems.
auto-merge, and mechanical merge actions run as the target repository's
`github-actions[bot]` through the central workflow. Pending CodeRabbit or
required-check evidence is a wait state, not a hard blocker.
- Topic intelligence is not currently a live Naruon capability. The lexical
`keyword_extractor` is metadata only; Naruon fails closed rather than present
keyword, embedding, or LLM labels as Structural Topic Modeling. The product,
technical, architecture, contract, security, UML, conceptual ERD, test, and
operability records are indexed in
[`docs/topic-intelligence/`](docs/topic-intelligence/README.md).
- Security governance is source-backed through signed
`/api/security/access-surface`. The endpoint reads scoped WebDAV, CalDAV, and
connector evidence plus durable `security_audit_events`, reuses the deny-first
Expand Down
93 changes: 3 additions & 90 deletions backend/api/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,26 +706,6 @@ async def base64_decoder_handler(params: Dict[str, Any]) -> Dict[str, str]:
"합니다",
}
)
_CATEGORY_TERMS = (
("Urgent", ("urgent", "asap", "immediate", "긴급", "시급", "빨리")),
("Finance", ("invoice", "billing", "payment", "결제", "청구", "송금")),
("Scheduling", ("meeting", "schedule", "appointment", "회의", "일정", "약속")),
)
_AGENDA_TOPICS = (
("Project Status Update", ("project", "프로젝트", "과제")),
("Discuss Pending Issues", ("issue", "bug", "blocker", "문제", "오류", "장애")),
("Decisions Required", ("decision", "approve", "결정", "승인")),
(
"Timeline and Milestones",
("deadline", "milestone", "timeline", "마감", "기한", "일정"),
),
(
"Budget and Resource Review",
("budget", "cost", "resource", "예산", "비용", "자원"),
),
)


def _normalize_analysis_text(value: str) -> str:
"""Normalize user text for deterministic, multilingual rule matching."""
if len(value) > ANALYSIS_TEXT_MAX_CHARS:
Expand All @@ -740,44 +720,8 @@ def _analysis_tokens(value: str) -> list[str]:
return _ANALYSIS_TOKEN_PATTERN.findall(_normalize_analysis_text(value))


def _contains_analysis_term(normalized_text: str, term: str) -> bool:
"""Match ASCII terms on word boundaries and Korean terms as morpheme stems."""
normalized_term = _normalize_analysis_text(term)
if normalized_term.isascii():
pattern = rf"(?<![a-z0-9]){re.escape(normalized_term)}(?![a-z0-9])"
return re.search(pattern, normalized_text) is not None
return normalized_term in normalized_text


async def email_categorizer_handler(params: Dict[str, Any]) -> Any:
"""Categorize email text with deterministic Korean and English rules."""
content = _normalize_analysis_text(params.get("email_content", ""))
categories = [
category
for category, terms in _CATEGORY_TERMS
if any(_contains_analysis_term(content, term) for term in terms)
]

if not categories:
categories = ["General"]

return {"categories": categories, "primary_category": categories[0]}


registry.register(
ToolInfo(
code="email_categorizer",
name="이메일 자동 분류기 (Email Categorizer)",
description="이메일 내용을 분석하여 알맞은 카테고리로 자동 분류합니다.",
category="이메일 분석",
parameters={"email_content": "string"},
),
email_categorizer_handler,
)


async def keyword_extractor_handler(params: Dict[str, Any]) -> Any:
"""Extract stable keywords ranked by frequency and first occurrence."""
"""Extract deterministic lexical terms by frequency and first occurrence."""
candidates = [
token
for token in _analysis_tokens(params.get("text", ""))
Expand All @@ -801,46 +745,14 @@ async def keyword_extractor_handler(params: Dict[str, Any]) -> Any:
ToolInfo(
code="keyword_extractor",
name="주요 키워드 추출기 (Keyword Extractor)",
description="텍스트 본문에서 가장 중요한 키워드를 추출합니다.",
description="텍스트 본문에서 빈도와 최초 출현 순으로 반복 용어를 추출합니다.",
category="이메일 분석",
parameters={"text": "string"},
),
keyword_extractor_handler,
)


async def meeting_agenda_generator_handler(params: Dict[str, Any]) -> Any:
"""Generate a deterministic agenda from Korean or English discussion topics."""
context = _normalize_analysis_text(params.get("discussion_context", ""))
if len(_analysis_tokens(context)) < 2:
return {
"agenda_items": ["Introductions", "Open Discussion"],
"estimated_duration_minutes": 30,
}

items = ["Review previous action items"]
items.extend(
agenda_item
for agenda_item, terms in _AGENDA_TOPICS
if any(_contains_analysis_term(context, term) for term in terms)
)
items.append("Next Steps and Action Items")

return {"agenda_items": items, "estimated_duration_minutes": len(items) * 15}


registry.register(
ToolInfo(
code="meeting_agenda_generator",
name="회의 아젠다 생성기 (Meeting Agenda Generator)",
description="논의 컨텍스트를 바탕으로 적절한 회의 아젠다를 자동으로 생성합니다.",
category="일정 관리",
parameters={"discussion_context": "string"},
),
meeting_agenda_generator_handler,
)


async def uuid_v4_generator_handler(params: Dict[str, Any]) -> Dict[str, str]:
return {"uuid": str(uuid.uuid4())}

Expand All @@ -857,6 +769,7 @@ async def uuid_v4_generator_handler(params: Dict[str, Any]) -> Dict[str, str]:
)



@router.get("/tools", response_model=list[ToolInfo])
def get_tools() -> list[ToolInfo]:
"""
Expand Down
Loading
Loading