/** * ContextRank Phase 0 (ADR-048 섹션 6.1) — 엣지 그래프 구축 (shadow, 액션 0). * * 56b sim: brute-force k-NN cosine (corpus ~5.5k라 ANN 불요, 섹션 0.1 #3). * 56c cooc: injection_hits 2+ PPMI-후보 + usage session co-use + tag-cooc(hub 필터). * * 본 모듈은 memory_edges 테이블에 엣지를 기록만 한다. 삭제/병합/rerank 없음. * 가중 정규화(PPMI/행 정규화)와 PageRank는 후속 sub-task(56e~56f). */ import type { MemoryDatabase } from '../db/database.js'; /** sim 엣지 노드당 top-k 이웃 수 (ADR 섹션 5.2c 근사). */ export declare const SIM_TOP_K = 25; /** * sim 엣지 최소 cosine. * * 🔴 `#208` (ADR-090) 실측: 이 값은 **아무것도 거르지 않는다**. 전쌍 히스토그램(n=1,500)에서 * `sim >= 0.5` 통과율이 **100.000%** 였다([0.8,0.9) 구간에만 78.2%). E5 임베딩의 anisotropy 때문에 * 모든 벡터가 좁은 원뿔에 몰린다. 그래서 ANN·LSH·**정적** 조기종료의 근거(「후보를 줄인다」)가 * 원리적으로 성립하지 않고, 실제 지배항은 dot 이 아니라 **top-k 저장**이었다(측정: 저장 81% / dot 19%). * 값을 올리려면 엣지 수가 함께 줄어드는 것을 감수해야 하므로 그대로 둔다 — 대신 `buildSimEdges` 가 * **동적** 임계(노드별 현재 k번째 값)로 가지를 친다. */ export declare const SIM_MIN = 0.5; /** * `#208` (ADR-090 D4): 코시-슈바르츠 상한 검사 주기(차원 청크 크기). * 384 = 64×6, 1536 = 64×24 로 이 저장소의 두 임베딩 차원 모두 나누어떨어진다. * 작을수록 가지치기가 촘촘해지지만 검사 자체의 비용이 는다. */ export declare const SIM_CHUNK = 64; /** * `#216` (ADR-092 D4-a): 코시-슈바르츠 상한의 **부동소수 여유**. * * 🔴 상한은 이론상 `|a|·|b| = 1` 이지만 384차원 누적에서 ~1e-14 오차가 난다. * 그래서 **sim 이 정확히 경계와 같은 동점 쌍**에서 상한이 `thr` 에 1 ulp 못 미쳐 * **정답 쌍이 잘린다**. 전건은 `minv` 가 점진적으로 차올라 그 쌍을 먼저 계산해 버리므로 * 대개 안 드러나는데, 증분은 warm-seed 가 `minv` 를 **처음부터 최대치**로 만들어 * 같은 쌍을 자른다 → 전건과 결과가 갈린다. * * [검증] 2026-09-14 — 동일 벡터 30개 + 신규 1개 fixture 에서 **5건 불일치**로 실측. * 전건은 `DUP → NEW`(dst 인덱스가 작아 동점에서 이김)를 넣는데 증분은 그 쌍을 가지쳐 * `DUP → DUP` 를 남겼다. ⚠️ **이 여유가 없으면 등가성이 깨진다** — 성능 최적화가 아니라 * **정확성 조건**이다. * * 값: cosine 범위가 [-1,1] 이고 실측 오차가 ~1e-14 이므로 1e-9 는 5 자릿수 여유다. * 가지치기 손실은 무시할 수준이다(대다수 쌍의 상한은 thr 보다 훨씬 작다). */ export declare const SIM_PRUNE_EPS = 1e-9; /** * #142 (ADR-070): tag-cooc hub 임계의 **절대 상한**. * * 기존 임계는 `max(50, floor(totalMems * 0.1))` 로 **코퍼스에 비례**했다. 그러면 코퍼스가 자랄수록 * 더 많은 generic tag 가 통과하고, 통과한 tag 마다 `coocFromSet` 이 `O(df²)` 쌍을 만들어 * 엣지가 **초선형**으로 폭발한다. 실측(2026-08-23): 엣지 7,500,674 = DB 2.76GB 의 93%, * 그 결과 모든 hook 이 시작 시 정합 스캔에 **31.4초**를 지불했다. * * 상수 상한을 AND 로 묶으면 tag 1개의 엣지가 `C(ABS_CAP, 2) = 4,950` 으로 **캡핑**되어 * `O(df²)` 축이 원천 소멸한다(tag 종류 증가는 선형). 비례식은 남겨 둔다 — * 코퍼스가 작을 때(N < 1,000)는 그쪽이 더 엄격하다. * * 값 근거 — A/B 3-arm 실측(같은 코퍼스·같은 시점, `docs/adr/ADR-070`): * 기본(1,659) cooc 7,940,832 / **100 → 235,470(−97.0%)** / 50 → 145,588(−98.2%) * 100 과 50 은 지연 개선이 같고(둘 다 스캔이 무시할 수준으로 하락) 제외 tag 가 42 vs 61 이라 * **신호를 더 유지하는 100** 을 택했다. */ export declare const TAG_COOC_ABS_MAX_DF = 100; /** * #142 (ADR-070): tag-cooc hub 임계 산식. 호출부가 `hubMaxDf` 를 넘기면 그대로, * 아니면 **절대 상한 AND 비례식**. * * 순수 함수로 분리한 이유 — 기본 산식 경로가 **어떤 테스트에도 안 걸리고 있었다** * (`tests/contextrank/edge-builder.test.ts:94,106` 이 `hubMaxDf` 를 명시 전달하고, * 프로덕션 호출 2곳은 전부 미전달이라 **테스트가 지키는 경로와 실행되는 경로가 정반대**였다). */ export declare function resolveHubMaxDf(totalMems: number, hubMaxDf?: number): number; export interface SimBuildResult { nodes: number; edges: number; elapsedMs: number; /** * #207 (ADR-089 C4): 리스 소유권을 잃어 중도 종료했다. * 호출자는 이 값이 true 면 **뒤 단계를 돌리지 말고 즉시 반환**해야 한다 — * 값 없는 `return` 으로는 함수만 빠져나와 cooc→temp→ref→signals→rank 가 계속 돌고 * (실측 약 170초 + `replaceEdges` 동시 쓰기) goal「한 번에 하나」가 깨진다. */ aborted?: boolean; /** * `#208` (ADR-090 F1): D4 동적 임계 조기 종료가 **실제로 가지를 친 쌍 수**. * * 🔴 이 필드가 없으면 D4 가 조용히 no-op 이 되어도 어떤 게이트도 울지 않는다 — * D4 는 「버리기만」 하므로 무력화돼도 **결과가 같고** 정확도 회귀가 원리적으로 못 본다. * (실증: `rest[blk]` → `rest[blk-1]` 한 글자 오타로 발화 0 이 되는데 엣지 집합은 동일) * 회귀가 이 값이 0 보다 큰지 단언해 「최적화가 살아 있는가」 축을 연다. */ prunedPairs?: number; /** `#216` (ADR-092 DoD 2): 실제로 dot 을 계산한 쌍 수. 이론값 `Σ_g [k·m − k(k+1)/2]` 과 일치해야 한다. */ pairsConsidered?: number; /** * `#221` (ADR-093 DoD 3): 이번 실행이 **교체 대상으로 삼은 src 수**. * 전건이면 방출한 src 전부, 증분이면 `Δ⁺ ∪ 계산참여 ∪ danglingDstSrc ∪ staleSrc`. * * ⚠️ **이 값은 코드가 스스로 센 것이라 DoD 판정의 오라클이 아니다**([[verification-essence]] 68.1 — * 분모가 판정 대상 안에서 나오면 순환). 실제 판정은 DB 가 한다: * `SELECT COUNT(*) FROM memory_edges WHERE type='sim' AND computed_at = <이번 실행 시각>`. * 이 필드는 **관측용**(로그에 찍어 추세를 본다)이다. */ srcsReplaced?: number; /** * `#221` (ADR-093 D2): 이번 실행이 **실제로 INSERT 한 엣지 수**. * 🔴 `edges`(= DB 총량)와 **다르다** — 증분에서는 `edgesWritten ≪ edges` 가 정상이다. * 둘을 섞으면 회귀가 깨진다(초판이 그렇게 깨졌다). */ edgesWritten?: number; } /** * #207 (ADR-089 D-A/M4): 리스 핸들을 **함수로** 주입받는다. * 경로 문자열을 받으면 이 순수 계산 모듈이 `fs` 를 직접 import 해야 한다 — * 함수 주입이면 fs import 0 이 유지되고, 회귀가 클로저 스왑으로 검증된다. */ export interface SimLease { renew(): boolean; } /** * `#216` (ADR-092 D3-a): top-k 고정 슬롯 삽입 — **(sim 내림, dst 오름) 엄격 전순서**. * * 🔴 **삽입 순서와 무관해야 한다.** 구 규약은 *"동점이면 먼저 들어온 쪽 유지"* 였고, * 전건은 도착이 dst 오름차순이라 두 규약이 **같은 답**을 냈다. 그런데 증분(`since`)은 * warm-seed 가 기존 엣지를 **prefix 로** 밀어 넣어 도착 순서를 깨뜨리므로, * 순서 의존 규약이면 전건과 결과가 갈린다. * * 🔴 **왜 클로저가 아니라 export 순수 함수인가** — 인라인 클로저면 이 규약을 **직접 시험할 수 * 없어서**, fault injection 이 미검출을 냈을 때 「가드가 약한가 / fixture 가 약한가」를 * 가르지 못한다([[testing]] 37(c-2)). 실측: 클로저 시절 구 규약 주입이 **3회 연속 미검출**이었고 * 그때마다 fixture 를 고쳤는데(차원 2종 → 동점 그룹 → `ORDER BY id` → 가지치기 여유) * 정작 필요한 것은 **검증 위치를 바꾸는 것**이었다. * * @param nbSim/nbDst 노드당 `topK` 슬롯(내림차순 유지) — `a * topK` 오프셋 * @param cnt 노드별 보유 수 / @param minv 꽉 찬 노드의 k번째 sim(미달이면 −2) */ export declare function insertTopK(nbSim: Float64Array, nbDst: Int32Array, cnt: Int32Array, minv: Float64Array, a: number, topK: number, dst: number, s: number): void; /** `#221` (ADR-093 P1): 증분 진입 시점의 기존 sim 엣지 스냅샷(src rows 인덱스 기준). */ export interface ExistingSimSnapshot { /** src 별 **dangling 포함** 총 엣지 수. */ count: Int32Array; /** src 별 살아 있는 dst 의 rows 인덱스. */ dst: Array; /** `dst` 와 같은 순서의 weight. */ w: Array; } /** * `#221` (ADR-093 P1): 이 src 의 새 top-k 가 **DB 에 이미 있는 것과 완전히 같은가**. * * 🔴 **왜 필요한가**: D2 는 교체 대상을 「계산에 참여했나」로 정하는데, 비퇴화 회차에서 그 집합이 * 전체의 **99%** 라 쓰기 절감이 0.9% 뿐이었다. 계산에 참여해도 top-k 가 안 바뀐 src 가 대부분이다. * * 🔴 **왜 이것이 안전한가 (등가성)**: `true` 를 반환하면 그 src 는 DELETE·INSERT 를 둘 다 건너뛰고 * **DB 의 기존 행이 그대로 남는다**. 그 행이 새 계산 결과와 (dst, weight) 가 전부 같으므로 * 최종 상태가 동일하다. 반대 방향(다른데 같다고 판정)만 위험한데, weight 를 `===` 로 **정확 비교** * 하므로 미세한 차이도 `false` 가 된다 — 즉 **오차는 항상 「다시 쓴다」 쪽**으로만 난다. * * 🔴 **dangling 가드**: `count` 는 dst 가 죽은 엣지까지 센 총량이다. 그런 엣지가 남아 있으면 * `count !== k` 가 되어 반드시 다시 쓰인다 — 그것이 없으면 죽은 dst 를 가리키는 행이 * **영구 잔존**한다(DoD 3b 고아 0 이 그 방어다). * * 🔴 **왜 클로저가 아니라 export 순수 함수인가** — `insertTopK` 와 같은 이유다([[testing]] 37(c-2)). * 클로저 안에 두면 이 판정을 직접 시험할 수 없어, fault injection 이 미검출을 냈을 때 * 「가드가 약한가 / fixture 가 약한가」를 가르지 못한다. * * @param gi src 의 **전역** rows 인덱스 * @param k 새 top-k 보유 수 (`cnt[a]`) * @param offset `nbDst`/`nbSim` 의 시작 오프셋 (`a * topK`) * @param idx 그룹 로컬 인덱스 → 전역 rows 인덱스 */ export declare function sameAsExisting(snap: ExistingSimSnapshot, gi: number, k: number, offset: number, nbDst: Int32Array, nbSim: Float64Array, idx: number[]): boolean; /** * 56b: k-NN sim 엣지. 대칭성 이용(i void; }): SimBuildResult; export interface CoocBuildResult { edges: number; injectionSets: number; usageSets: number; tagStats: { totalTags: number; hubTags: number; validTags: number; }; elapsedMs: number; } /** * 56c: cooc 엣지 — injection matched + usage session co-use + tag-cooc(hub 필터). * shadow 단계: raw co-occurrence count를 weight로 기록. PPMI/행 정규화는 56f 통합 시. */ export declare function buildCoocEdges(db: MemoryDatabase, opts?: { hubMaxDf?: number; nowMs?: number; }): CoocBuildResult; /** temp 엣지에서 제외할 대형 세션 상한 (O(size²) 쌍 폭발 방지 — cooc hub 필터 정신). */ export declare const TEMP_MAX_SET = 200; export interface TempBuildResult { edges: number; sessionSets: number; skippedLargeSets: number; elapsedMs: number; } /** * 56d temp: 동일 session_id 공유 메모리 쌍 (대칭, 무방향). * 대형 세션(> TEMP_MAX_SET)은 쌍 폭발 회피로 제외 + 카운트(shadow 관찰). */ export declare function buildTempEdges(db: MemoryDatabase, opts?: { maxSetSize?: number; nowMs?: number; }): TempBuildResult; /** slug DF 상한 — resolve dst 수가 초과하면 generic 토큰(substring 과매칭)으로 보고 skip (56f). */ export declare const REF_DF_MAX = 100; export interface RefBuildResult { edges: number; sources: number; slugRefs: number; resolvedSlugs: number; skippedHighDf: number; elapsedMs: number; /** #212 (ADR-091 DoD 5): 매처 패턴 수 = distinct slug. `slugRefs` 와의 비가 곧 중복 배수다. */ patterns: number; /** * #212 (ADR-091 DoD 5): 단일 패스로 읽은 문서 수. * 🔴 **이 필드가 「단일 패스가 실제로 돌았는가」를 보는 유일한 런타임 채널이다** — * 0 이거나 노드 수와 크게 다르면 스캔이 안 돈 것이다 * (`ADR-090` 이 같은 이유로 `prunedPairs` 를 넣었다). */ scanned: number; } /** * 56d ref: content `[[slug]]` → slug substring 역매칭으로 dst memory resolve (방향성 src→dst). * memories에 name/slug 컬럼 부재(ADR 섹션 15 Q3) → content substring resolve. shadow: resolve율 관찰. * * 56f slug DF 필터: substring resolve 과매칭(fanout 평균 37.7, 56d 실측)으로 high-DF slug의 * 의도 신호가 약함 → DF > REF_DF_MAX slug는 skip + weight = 1/(1+log2(df)) IDF 다운웨이트 * (df=1 → 1.0 정확 참조, df=37 → ~0.16). */ export declare function buildRefEdges(db: MemoryDatabase, opts?: { nowMs?: number; dfMax?: number; }): RefBuildResult; //# sourceMappingURL=edge-builder.d.ts.map