release: ship v1.5.0 with on-device writing suggestions
Some checks failed
deploy-site / deploy (push) Failing after 33s
portable-unsigned / portable-windows (push) Failing after 4m7s
release / release-windows (push) Failing after 3m16s

Adds next-sentence suggestions while typing, weekly input insights and a
personal phrase memory to the desktop app, and fixes custom instructions so
they process the text instead of inserting the instruction's own wording.
Local model requests are now bounded and individually cancellable.

Bumps the product version to 1.5.0 (Android/iOS build 1050000), refreshes the
landing and web download links, and records the new INPUT feature rows and the
open verification gaps in the infrastructure map.
This commit is contained in:
Yun Chan 2026-09-23 16:04:27 +09:00
parent 99f06c253c
commit 5c11ee2fde
104 changed files with 14410 additions and 174 deletions

View file

@ -0,0 +1,123 @@
// packages/core/__tests__/personal-graph.test.ts
// 개인 그래프 순수 함수 테스트 — 용어 추출/문장 분해/관계/순위.
import { describe, expect, it } from 'vitest'
import {
SHARES_TERMS_MIN_SIMILARITY,
buildSequenceEdges,
continuationsFrom,
extractTerms,
jaccard,
rankRelated,
splitSentences
} from '../src/personal-graph'
describe('extractTerms', () => {
it('소문자화하고 조사/불용어를 걷어낸다', () => {
const terms = extractTerms('오늘 회의에서 논의한 내용을 정리해서 공유드립니다')
expect(terms).toContain('회의에서')
expect(terms).toContain('정리해서')
expect(terms).not.toContain('그리고')
})
it('영어 불용어를 제거한다', () => {
const terms = extractTerms('Thanks for the quick turnaround on the report')
expect(terms).toContain('quick')
expect(terms).toContain('turnaround')
expect(terms).toContain('report')
expect(terms).not.toContain('the')
})
it('빈도 순으로 상위 limit 개만 돌려준다', () => {
const terms = extractTerms('배포 배포 배포 점검 점검 테스트', 2)
expect(terms).toEqual(['배포', '점검'])
})
it('1자 토큰은 버린다', () => {
expect(extractTerms('a b c 배포')).toEqual(['배포'])
})
})
describe('splitSentences', () => {
it('종결 부호와 개행으로 나누고 짧은 조각은 버린다', () => {
const sentences = splitSentences('오늘 회의는 여기서 끝. 짧음. 내일 일정을 공유드릴게요!')
expect(sentences).toContain('오늘 회의는 여기서 끝')
expect(sentences).toContain('내일 일정을 공유드릴게요')
expect(sentences).not.toContain('짧음')
})
})
describe('buildSequenceEdges', () => {
it('인접한 문장 쌍만 만든다', () => {
const edges = buildSequenceEdges(['A 문장입니다', 'B 문장입니다', 'C 문장입니다'])
expect(edges).toEqual([
{ from: 'A 문장입니다', to: 'B 문장입니다' },
{ from: 'B 문장입니다', to: 'C 문장입니다' }
])
})
it('같은 문장이 반복되면 만들지 않는다', () => {
expect(buildSequenceEdges(['같은 문장입니다', '같은 문장입니다'])).toEqual([])
})
})
describe('jaccard', () => {
it('겹침 비율을 계산한다', () => {
expect(jaccard(['a', 'b'], ['a', 'b'])).toBe(1)
expect(jaccard(['a', 'b'], ['c', 'd'])).toBe(0)
expect(jaccard(['a', 'b', 'c'], ['a', 'b', 'd'])).toBeCloseTo(0.5, 5)
})
it('빈 집합은 0 이다', () => {
expect(jaccard([], ['a'])).toBe(0)
})
})
describe('continuationsFrom', () => {
it('꼬리 뒤에 실제로 이어 쓴 텍스트만 뽑는다', () => {
const out = continuationsFrom(
['오늘 회의에서 논의한 내용을 정리해서 공유드립니다', '관계 없는 문장입니다'],
'논의한 내용을'
)
expect(out).toEqual(['정리해서 공유드립니다'])
})
it('꼬리가 너무 짧으면 아무것도 만들지 않는다', () => {
expect(continuationsFrom(['아무 문장'], '아무')).toEqual([])
})
it('길이를 제한하고 중복을 없앤다', () => {
const out = continuationsFrom(
['접두 뒤에 이어지는 아주 긴 문장이 계속 이어집니다 그리고 더 이어집니다', '접두 뒤에 이어지는 아주 긴 문장이 계속 이어집니다'],
'접두 뒤에',
2,
20
)
expect(out).toHaveLength(1)
expect(out[0].length).toBeLessThanOrEqual(20)
})
})
describe('rankRelated', () => {
it('관계 가중치와 용어 유사도, 최근성을 함께 본다', () => {
const now = Date.now()
const ranked = rankRelated(
['회의', '일정'],
[
{ text: '회의 일정을 공유드립니다', terms: ['회의', '일정'], weight: 0, lastUsedAt: now },
{ text: '전혀 상관 없는 문장입니다', terms: ['바다', '산'], weight: 0, lastUsedAt: now },
{ text: '강하게 이어지는 문장입니다', terms: [], weight: 10, lastUsedAt: null }
],
3
)
// 관련도 순: 강한 엣지 → 용어 유사 + 최근성 → 무관(마지막)
expect(ranked[0]).toBe('강하게 이어지는 문장입니다')
expect(ranked[1]).toBe('회의 일정을 공유드립니다')
expect(ranked[2]).toBe('전혀 상관 없는 문장입니다')
})
it('용어 유사도 임계값 상수는 0과 1 사이이다', () => {
expect(SHARES_TERMS_MIN_SIMILARITY).toBeGreaterThan(0)
expect(SHARES_TERMS_MIN_SIMILARITY).toBeLessThan(1)
})
})