Sno Station
Sno Station은 AI 에이전트를 하나의 팀으로 묶는 오픈소스 워크스테이션으로, 로컬 머신에서 에이전트 협업을 가능하게 함
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기법고개 — LLM·문서 속 한국 판례/법령 인용의 진위를 실시간 검증하는 Chrome 확장 · Chrome extension that flags AI-fabricated Korean case-law citations in real time
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗**법고개(Bupgogae)**는 대형 언어 모델(LLM)의 법률 답변에 표시된 한국 사건번호를 찾아 현재 브라우저의 공개 판례 DB와 대조하는 크롬 확장프로그램입니다.
법률 답변의 판례 인용은 원문과 추가 자료로 직접 확인해야 합니다. 법고개는 사건번호 확인을 빠르게 시작할 수 있도록 보조하며, 판례의 진위나 AI 답변 전체의 정확성을 보장하지 않습니다.
확인 범위는 법원·헌법재판소 판례입니다. 조세심판원·특허심판원 자료는 포함하지 않으며, 법원의 조세 사건과 특허법원 판결은 포함합니다.
chatgpt.com)claude.ai)gemini.google.com)copilot.microsoft.com)perplexity.ai)grok.com) — 실험적 지원www.law.go.kr 원문을 우측 뷰어에서 조회합니다. (Shadow DOM 캡슐화로 호스트 UI와 충돌 방지)Alt+C)도 지원하지만, 일부 서비스가 단축키를 가로채는 경우 버튼이 안정적으로 동작합니다. 팝업에서 안내 배너 표시 여부를 켜고 끌 수 있습니다.adapters.json)에서 최신 셀렉터를 자동으로 가져와 적용합니다.방법 1: Chrome 웹 스토어 설치 (권장)
방법 2: 개발자 모드 설치 (수동 최신 빌드)
chrome://extensions/ 에 접속합니다.extension/ 디렉토리를 선택합니다.nash (개발하는 IP 변호사)
기술과 법률의 경계에서 새로운 가치를 만들어갑니다.
이 프로젝트는 GNU GPLv3 라이선스를 따릅니다.
판례·헌재결정례 메타데이터의 제공 출처는 **법제처 국가법령정보센터의 국가법령정보 공동활용 Open API**이며, 재판·결정의 작성 기관은 각 법원과 헌법재판소입니다. 법고개는 이들 기관이 운영하거나 판례의 진위·법적 효력을 보증하는 서비스가 아닙니다.
법고개가 수행한 사건번호 정규화·검증된 오류 정정은 원천 자료 자체의 공식 수정과 구분합니다. 원문·원본 필드와 정정 근거를 보존하며, 최종 내용은 공공 출처의 원문을 확인해야 합니다. GPLv3 고지는 소프트웨어에 적용되며, 제3자 자료의 권리까지 법고개가 새로 허락한다는 의미는 아닙니다.
bupgogae
Copyright 2026 nash-dir
이 소프트웨어의 코드 일부는 LLM 기반 AI 도구(Google Gemini, Anthropic Claude)의
보조를 받아 생성되었습니다. 최종 구현은 저자가 직접 검토, 테스트 및 승인하였습니다.
Bupgogae (법고개) is a Chrome extension that finds Korean case numbers displayed in LLM answers and compares them with the public case-law database currently active in the browser.
Case citations in AI answers still require direct review of the source. Bupgogae helps start that review; it does not guarantee authenticity, legal validity, or the accuracy of the surrounding answer.
Coverage is limited to courts and the Constitutional Court. Court judgments on taxation and patent disputes remain included; administrative tribunal decisions are excluded.
chatgpt.com)claude.ai)gemini.google.com)copilot.microsoft.com)perplexity.ai)grok.com) — experimentalwww.law.go.kr in a sliding sidebar. Uses Shadow DOM to prevent CSS conflicts.Alt+C shortcut also works, but the button stays reliable when a site overrides the shortcut. The banner can be toggled on/off from the popup.adapters.json).Method 1: Chrome Web Store (Recommended)
Method 2: Developer Mode (Manual latest build)
chrome://extensions/ in Chrome.extension/ directory.This project is licensed under GNU GPLv3.
bupgogae
Copyright 2026 nash-dir
This product includes code that was partially generated with the assistance
of LLM-based AI tools (Google Gemini, Anthropic Claude).
The final implementation was reviewed, tested, and approved by the author.
Defensive Publication Notice (방어적 공개 고지)
This document is a deliberate public disclosure of the technical concepts embodied in this project, first published under GPLv3 on March 22, 2026. The authors intend for this disclosure to constitute prior art under patent law (35 U.S.C. §102; 대한민국 특허법 제29조), thereby preventing any party — including the authors — from obtaining patent protection over the methods described herein.
Each LLM chatbot platform (ChatGPT, Claude, Gemini, Copilot, Perplexity, Grok, etc.) renders AI-generated responses in a unique DOM structure. A browser extension that locates case-number citations and compares them with a local public database must navigate these heterogeneous DOMs without coupling its core detection logic to any single platform.
The extension implements a Site Adapter Pattern — a variant of the Strategy pattern — that decouples site-specific DOM traversal from the shared detection pipeline. The architecture consists of:
Abstract Base Class (SiteAdapter): Defines the interface contract:
siteId — unique site identifier stringdisplayName — human-readable platform namegetResponseSelectors() — returns an ordered array of CSS selectors targeting AI response containersfindResponseContainers() — executes the selector resolution algorithm (inherited from base class)Concrete Adapters: Each platform has a dedicated adapter (e.g., GeminiAdapter, ChatGPTAdapter) that only overrides getResponseSelectors() with platform-specific CSS selectors. No adapter contains detection logic.
Hostname-Based Registry: A static ADAPTER_MAP maps hostnames to adapter class names. On page load, getAdapter() performs an exact hostname match against the registry and instantiates the corresponding adapter.
Shared Core Pipeline: Operates identically regardless of which adapter is active:
MutationObserver → settle (process after the response stops mutating)
→ adapter.findResponseContainers() [adapter-specific]
→ processContainer(container) [TreeWalker, cooperative scan, filters editables]
→ extractCaseNumbers(text) [shared, whitelist regex]
→ validateCaseNumber(parsed) [shared, Red filter cascade]
→ compressCaseKey(parsed) [shared, Hangul→ASCII]
→ chrome.runtime.sendMessage(LOOKUP_BATCH) [shared, IPC to SW]
→ renderPrecedentBadges(textNode) [shared, one-pass: Green/Orange/Red/Gray]
Render timing (settle + single-pass). Some platforms (e.g., Gemini, Copilot, Grok) re-render the response subtree while streaming, discarding externally injected badge nodes. To avoid flicker, the pipeline waits for the subtree to go quiet (a settle window) before processing, then renders once. Within a text node, all matches are split and badged in a single pass (
renderPrecedentBadges), so case numbers listed across consecutive lines — which some platforms place in one text node — all appear together instead of one-per-cycle.
The base class findResponseContainers() tries remote selectors followed by bundled selectors, skipping duplicates. It excludes explicit user messages, editable roots, and whole-page roots such as main. The first selector with usable response containers determines the set for that scan cycle. A stale remote selector that finds no usable replies cannot mask the shipped configuration.
Each LLM platform's frontend changes frequently, potentially invalidating hardcoded CSS selectors. The extension addresses this through a Remote Config mechanism:
adapters.json from the server (alongside the case law DB sync) and stores it in chrome.storage.localfindResponseContainers() checks remote config first, falling back to bundled selectors:
Priority: Remote Config (server) → Bundled adapters.json
FETCH_ADAPTERS message to the Service Worker (throttled to once per page load to prevent infinite loops)res.json(); no eval(), innerHTML, or remote JS execution (MV3 CSP compliant)Remote configuration can deliver selector corrections without a Store release. Bundled selectors remain available offline. Changes to a service's response structure still require compatibility checks.
The createTextWalker() filter used by processContainer() rejects text nodes inside editable areas, user messages, and extension badges. Editable exclusions include:
parent.isContentEditable === trueparent.closest('[contenteditable="true"], [contenteditable=""], textarea, [role="textbox"]') returns non-nullThe adapter selects response containers first. Explicit user message markers are excluded even when a broad site selector matches them. Selector compatibility still requires live review when a service changes its DOM.
┌─────────────────────────────────────────────────────────────┐
│ Core Pipeline (shared) │
│ │
│ MutationObserver → extractCaseNumbers → validate → lookup │
│ → renderBadge │
│ ↑ │
│ findResponseContainers() │
│ (delegated to adapter) │
└──────────────────────────┬──────────────────────────────────┘
│
┌─────────┬─────────┼─────────┬─────────┐
│ │ │ │ │
┌────┴───┐ ┌──┴────┐ ┌──┴────┐ ┌──┴─────┐ ┌─┴──────┐
│ Gemini │ │ChatGPT│ │Claude │ │Copilot │ │Perplex.│ ...
│Adapter │ │Adapter│ │Adapter│ │Adapter │ │Adapter │
└────────┘ └───────┘ └───────┘ └────────┘ └────────┘
Naively querying a database for every potential case number extracted from LLM text incurs unnecessary latency and produces ambiguous results. Some candidate strings fail the structural rules of Korean case-number formatting and can be rejected without a database lookup; that format result alone does not determine whether the surrounding answer is truthful.
The extension implements a Multi-Tier Pre-Database Filtering system — a 3-level format-check cascade applied before any database query. Red / Orange / Green / Gray report only format-check failure, current-local-DB miss, current-local-DB hit, or lookup-not-completed; they are not confidence scores.
| Class | Meaning | Determined By |
|---|---|---|
| 🔴 Red (Format/support-range warning) | Candidate failed a structural or current product-range check; this does not prove legal nonexistence | Pre-DB cascade (zero latency) |
| 🟠 Orange (Not found in local DB) | Format checks passed; no match in the currently active local DB | DB lookup: no match |
| 🟢 Green (Found in local DB) | Match in the currently active public DB snapshot | DB lookup: match found |
| ⚪ Gray (Verification incomplete) | Lookup did not complete; this is not a DB miss | Timeout or system error after retries |
Before any IndexedDB query, each candidate passes through this cascade in strict order. The first failing check immediately classifies the candidate as Red and halts further processing:
| Level | Check | Rule | Example | Rationale |
|---|---|---|---|---|
| 🔴 L1 | Future year | fullYear > currentYear | 2030다12345 | No case can exist in the future |
| 🔴 L2 | Below product range | < 1945 (court), < 1988 (헌재) | 1940다100, 1987헌마1 | Current lookup-policy floor; not a legal nonexistence finding |
| 🔴 L3 | Zero serial | parseInt(serial) === 0 | 2020다0 | Serial numbers are 1-indexed |
Why there is no "invalid code" check in this cascade: case-code validity is enforced upstream, at the extraction stage, not here. The whitelist extraction regex (see Core Concept below) only emits candidates whose code is in case_code_map (~159 registered types). An unregistered code — a hallucinated 뿡, or another agency's code such as 부해 (labor commission) / 형제 (prosecution) — is therefore never extracted and receives no badge at all. That silence is the signal: the extension highlights only the citations it can actually reason about, rather than flagging every unknown token Red. Because all reachable candidates already carry a registered code, this cascade only needs to check the year and serial.
Key insight: Red badges with strikethrough appear before a database query, while Green/Orange classifications require an asynchronous LOOKUP_BATCH IPC round-trip. Gray is reserved for a lookup that still failed after retries.
Legacy Korean case numbers use 2-digit years (e.g., 99다카34567). The system expands these relative to the current year with a sliding window (expandTwoDigitYear): a 2-digit year resolves to the most recent past year not exceeding the current year. For example, in 2026: 26 → 2026, 27 → 1927, 99 → 1999, 00 → 2000. This avoids a hardcoded pivot that drifts over time — a fixed 30 pivot would wrongly read 30다1 as 1930 from 2030 onward.
Korean case numbers follow a strict [Year][Code][Serial] structure:
2015 다 6302
──── ── ────
Year Code Serial
4-digit Hangul 1-7 digit
1-4 char
| Component | Rules | Examples |
|---|---|---|
| Year | 4 digits (19xx/20xx), or legacy 2-digit | 2015, 99 |
| Case Code (사건부호) | 1–4 Hangul characters from ~159 valid types | 다, 가합, 다카, 준재가단 |
| Serial | 1–7 digit sequential number | 6302, 215133 |
Challenges making regex design non-trivial:
2015다6302 has no spaces or punctuation separating components다 vs 준재가단)99다카34567 must be disambiguatedcase_code_map) solves this at the extraction stage.Instead of a broad regex that captures all Hangul combinations and filters afterward, the extension builds a dynamic whitelist regex from the case_code_map at initialization:
Static (fallback): /[가-힣]{1,4}/ ← matches ANY Hangul (high false positives)
Dynamic (primary): /(가합|가단|다|나|마|카|...)/ ← matches ONLY registered codes
Built from case_code_map keys, sorted longest-first
At initMeta(), the extension sorts all ~159 case codes by length (descending) to ensure greedy matching (가합 before 가), then builds a single RegExp:
// Built dynamically from case_code_map keys.
// (?<![0-9]) — left boundary so a run of digits (phone/account numbers)
// isn't mis-split into a bogus 2-digit-year case number.
_courtCaseRegex = new RegExp(
`(?<![0-9])((?:19|20)\\d{2}|\\d{2})(${sortedCodesPattern})(\\d{1,7})(?![0-9])`, 'g'
);
This whitelist approach eliminates false positives from non-court case codes (e.g., 부해 from labor commissions, 형제 from prosecutors) at the extraction stage, rather than requiring a downstream Red filter. The same dynamic regex also covers Constitutional Court codes (헌가, 헌마, …) since they live in case_code_map. The leading (?<![0-9]) and trailing (?![0-9]) boundaries prevent surrounding digit runs from being truncated into a different case number.
An additional snapshot-derived allowlist applies only to identifiers that are exactly four
characters long (YY + one-Hangul case code + one-digit serial). Such a candidate is accepted
only when its compressed key exists in the active DB snapshot. Isolated four-character text
nodes are checked against an in-memory Set before regex execution; identifiers of five or
more characters, including 89헌마1 and four-digit-year citations such as 2026헌가1, keep
the normal code-whitelist path.
Analyzing user conversations with AI chatbots is inherently privacy-sensitive. A server-based verification system would require transmitting conversation text — including prompts and responses — to an external API, creating privacy risks, network dependency, and latency.
Prompt/response pattern matching and case-number database lookup occur inside the user's browser. The public database is stored in IndexedDB, and prompt/response text is not sent to the update server. This local matching path is separate from supporting requests for database/manifest/selector updates and user-requested original documents.
User's Browser (prompt/response text stays in this local path)
┌──────────────────────────────────────────────────────────────────┐
│ │
│ LLM Response Text │
│ │ │
│ ▼ │
│ [Whitelist Regex] case_code_map 기반 동적 정규식 (법원 + 헌재) │
│ │ { year, code, serial, type } │
│ ▼ │
│ [Red Filter Cascade] L1→L2→L3 │
│ │ pass? ──── fail → 🔴 Red badge (instant, zero latency) │
│ ▼ │
│ [Key Compression] "2015다6302" → "15Da6302" │
│ │ ASCII key │
│ ▼ │
│ [LOOKUP_BATCH IPC] Content Script → Service Worker │
│ │ │
│ ▼ │
│ [IndexedDB Query] Single readonly transaction │
│ │ found? ── yes → 🟢 Green badge (+ law.go.kr link) │
│ │ no → 🟠 Orange badge (manual check prompt) │
│ │ error → ⚪ Gray badge (verification incomplete) │
│ ▼ │
│ [Badge Render] Inline highlight + tooltip with metadata │
│ │
└──────────────────────────────────────────────────────────────────┘
Supporting network path (no prompt/response text attached):
api.bup.live ← db.json.gz / manifest.json / adapters.json
www.law.go.kr ← selected original document, only after a user request
Case numbers are compressed from variable-length Hangul into compact fixed-structure ASCII keys for fast IndexedDB lookups:
Original: 2015다6302 → Compressed: 15Da6302
┌────┐ ┌──┐
Year suffix (2 digits)
┌─┐ ┌──┐
Romanized code (case_code_map)
┌────┐ ┌────┐
Serial (as-is)
The case_code_map maps ~159 Hangul case codes to short romanized abbreviations (~40% reduction in average key length).
Each entry uses positional arrays instead of named fields to minimize JSON overhead:
{
"15Da6302": [[612529, 1, 230101, "양도소득세부과처분취소"]],
"24Du56290": [[612529, 1, 251204, "부당이득금"]]
}
| Index | Field | Encoding |
|---|---|---|
[0] | Serial ID | Numeric → direct law.go.kr link |
[1] | Court code | Integer → court_code_map reverse lookup |
[2] | Decision date | YYMMDD compact (6 digits) |
[3] | Case name | Raw string (plain text) |
Case names are stored as raw strings without tokenization. We evaluated two compression strategies and rejected both:
| Tokens | Strategy | Raw Saved | After gzip | Decode Collisions |
|---|---|---|---|---|
| 34 | Flat | 8.6% | ~0.2 MB | 0 |
| 400 | BPE | 48.0% | ~0.5 MB | 0 |
Decision: Store raw text. gzip (applied by .crx packaging and R2 transfer) already compresses JSON efficiently. The marginal ~0.2–0.5 MB savings do not justify the added encoding complexity, debugging difficulty, or decode overhead.
The automatic update path downloads static public data and configuration. It does not attach prompt/response text, telemetry, or analytics payloads:
Serverless Pipeline (daily 24:00) Extension
┌──────────────────────────┐ ┌──────────────────────────┐
│ 법제처 API → master.db │ │ Install: load bundled DB │
│ (date-modulus scan) │ │ from extension/data/ │
│ ↓ │ │ ↓ │
│ master.db → db.json.gz │ │ Every 6h: fetch manifest │
│ ↓ │ │ then hash-addressed DB │
│ object → manifest → │──────────→ │ Hash match? stage : keep │
│ legacy fixed-key mirror │ │ last-known-good snapshot │
│ ↓ │ │ ↓ │
│ Telegram report │ │ Inactive-store staging │
│ │ │ + active pointer switch │
└──────────────────────────┘ └──────────────────────────┘
objects/<sha256>.json.gz is uploaded first, manifest.json commits that object second, and the legacy db.json.gz mirror is updated last. A failed manifest upload leaves old clients untouched; a failed mirror upload leaves new clients on the committed immutable object and old clients on their previous fixed-key snapshot.cases store active until the first complete v2 snapshot is readyadapters.json is fetched during sync; packaged/last-known-good selectors remain available on failureThe backend data pipeline runs as a serverless cron job on GitHub Actions, scheduled daily at 22:00 KST (UTC 13:00):
Trusted state job ──→ isolated-secret crawl job ──→ Trusted finalize job
R2 download law.go.kr API SQLite backup + R2 state pointer
BUPGOGAE_API_KEY object → manifest → mirror publish
(no R2/Telegram) separate Telegram-only notification
Because GitHub Actions operates in an ephemeral environment, master.db and the durable failed_dates.json retry backlog are synchronized through Cloudflare R2 before and after each execution. State upload uses the SQLite backup API to include committed WAL content, verifies the snapshot, and in normal incremental mode requires at least 100,000 precedents plus historical, recent, and Constitutional Court coverage before any R2 write. A partial state is allowed only by the trusted manual full_bootstrap=true workflow path. The uploader writes immutable generation-scoped DB/backlog objects, preserves the validated old pointer as bounded previous.json, and updates current.json last. Download validates the complete current pair and falls back to the complete previous pair if the pointer, object, checksum, SQLite schema, or backlog is damaged; neither candidate can partially replace local state. Missing baseline state fails closed, and a full historical bootstrap is available only through the explicit manual boolean input. Do not apply a blanket age-expiration rule to bupgogae/state/generations/: after a prolonged outage it could delete a generation still referenced by current.json or previous.json. Garbage collection is safe only when a separate verified procedure first protects both pointers and at least one additional validated generation, then deletes objects not in that protected set. Without such a procedure, retain all generations; the pipeline intentionally does not delete them so a failed run cannot erase the last-known-good recovery state.
A failed page remains in the backlog. The crawler is stopped by an OS-level 75-minute timeout inside the 90-minute job budget so the always() resume-artifact step normally has time to preserve DB/WAL/backlog state. The run may back up resume state, but it does not publish a user-facing DB or manifest unless every scheduled/backlog item completed successfully.
No marketplace WARP action or manually provisioned WireGuard identity runs in the crawl job. A short checked-in script downloads the official Cloudflare Ubuntu 24.04 package at the reviewed version (2026.6.880.0), verifies its pinned SHA-256, creates an anonymous ephemeral consumer registration, selects MASQUE, and fails closed unless cdn-cgi/trace reports an active WARP route. WARP_PRIVATE_KEY and any replacement Cloudflare credential are therefore unnecessary. An authorized repository owner must first review Cloudflare's Application Terms and set the non-secret repository variable CLOUDFLARE_WARP_TOS_ACCEPTED=true; the workflow will not install or register the client otherwise. The PR smoke runs a local-proxy connection on a separate GitHub-hosted runner with no repository secrets, so it does not replace the runner's default route or DNS. The daily crawler uses the same pinned client in full-tunnel mode; its first production run remains the full-route parity check.
The official package starts warp-svc as a privileged daemon, which remains alive while the crawler uses WARP. It is therefore an explicit part of the crawl job's trusted computing base and could technically observe the later law.go.kr API-key process; ending the setup step alone is not a process-isolation boundary. Only BUPGOGAE_API_KEY is available to the crawler step, and the workflow deletes the ephemeral registration and stops the daemon immediately after crawling. R2 and Telegram credentials remain in separate jobs/steps and are never available on the WARP runner. Production Python dependencies, including transitives, are exact-version and wheel-hash locked, and the Docker base image is digest-pinned.
Residual trust is explicit: the GitHub-hosted runner, pinned official GitHub Actions, Ubuntu package archive, reviewed Cloudflare package artifact, Python wheel artifacts, and Cloudflare WARP service remain in the trusted computing/network base. Package availability, consumer-registration limits, route changes, Cloudflare outages, and law.go.kr 403/429 responses can still stop the crawler. WARP is not anonymity technology and does not guarantee a chosen egress country. The durable higher-isolation replacement is a controlled Korean/allowed-egress ephemeral runner or a minimal law.go.kr relay with job-scoped authentication; adopting either requires external infrastructure and credential authority outside this repository. Until then, route verification fails closed and those failures require operational monitoring. The trusted finalize job checks the SQLite state, raw/gzip equality, payload size, and full record schema before publication, but it cannot independently prove the semantic correctness of every upstream public record.
Normal mode requires a healthy existing /app/data/master.db and preserves the current mounted volume for incremental updates. A new or empty Docker/NAS volume is not initialized implicitly; run the historical bootstrap flag once, review that it completes, and use normal mode thereafter:
# Existing healthy master.db volume: normal incremental run
docker compose -f docker-compose.crawler.yml run --rm crawler
# New empty volume only: explicit full historical bootstrap
docker compose -f docker-compose.crawler.yml run --rm crawler --full-bootstrap
The equivalent direct command for a new local data directory is python pipeline/crawler_runner.py --data-dir ./pipeline/data --full-bootstrap. Do not add this flag to a recurring schedule; after a successful bootstrap, omit it so the existing master.db remains the incremental baseline.
Instead of scanning all dates daily, the crawler uses a 3-tier modulus strategy to distribute the scanning load:
| Tier | Window | Modulus | Dates/Day |
|---|---|---|---|
| T1 | 1948–1999 | date_offset % 56 | ~366 |
| T2 | 2000–year-before-last | date_offset % 15 | ~609 |
| T3 | Last year–today | Full scan (every day) | ~445 |
This scans ~1,420 dates/day (~21 min) while covering the full historical range every 56 days at most.
The crawler maintains a blacklist.json of 326 known-bad serial numbers (corrupted court names, garbled case numbers from the API). Blacklisted serials are:
Green (found-in-DB) badges use the public serial identifier to request or link to the corresponding law.go.kr page. The badge reports a database match, not a guarantee about authenticity or the surrounding answer.
.github/
└── workflows/
└── daily-pipeline.yml # GHA Serverless Cron Scheduler
extension/
├── background/
│ └── db-sync.js # Service Worker — IndexedDB sync via R2
├── content/
│ ├── adapters/ # Site-specific DOM adapters
│ │ ├── base-adapter.js # SiteAdapter base class
│ │ └── registry.js # Inline adapters & Hostname → adapter mapping
│ ├── case-regex.js # Case number extraction & validation
│ ├── precedent-badge.js # 3-level inline highlight + tooltip
│ ├── sidebar.js # Shadow DOM sidebar for case law documents
│ └── bupgogae-content.js # Main controller (adapter-agnostic)
├── popup/ # Extension popup UI (enable/disable toggle)
├── icons/ # Extension icons (16/48/128 px)
├── data/ # Bundled case law database (140K+ entries)
└── manifest.json # Chrome Extension Manifest V3
pipeline/
├── api.py # 법제처 API client
├── compress.py # Shared case-number/date compression and code maps
├── master_db.py # SQLite master DB with blacklist
├── crawler_runner.py # Crawler orchestrator (date-modulus)
├── sync_state.py # Local ↔ R2 serverless state synchronizer
├── upload_r2.py # R2 upload helper
└── blacklist.json # 326 known-bad serial numbers
To add support for a new chatbot in v0.8.0+, you only need to update two files.
extension/data/adapters.json:"hosts": {
"newsite.com": "newsite"
},
"adapters": {
"newsite": {
"responseSelectors": [
"[data-role=\"assistant\"] .prose",
".response-container"
]
}
}
SITE_DEFS in extension/content/adapters/registry.js:{ adapterName: 'NewSiteAdapter', siteId: 'newsite', displayName: 'New Site AI', hosts: ['newsite.com'] },
matches and host_permissions in extension/manifest.json, then run registry and DOM fixture tests.