{"slug": "how-macos-summarize-works-reverse-engineered-with-kimi-k3", "title": "How macOS Summarize Works (Reverse-Engineered with Kimi K3)", "summary": "A developer reverse-engineered macOS's Summarize feature, revealing that the extractive summarization algorithm uses TF-IDF cosine similarity via the SearchKit framework. The Python reimplementation, built from dynamic analysis of SKSummary, produces byte-identical output on 9 of 12 test files, with minor floating-point noise on 2 and a CJK tokenization gap on 1. The project, hosted on GitHub, documents the algorithm's tokenization quirks, sentence segmentation rules, and summary length formulas for macOS 26.5.2.", "body_md": "Please note: the code and content in this repository were generated by an LLM (Kimi K3).\n\n**English** | [中文](/fenxer/macos-summarize-internals/blob/main/README.zh-CN.md)\n\nEver wondered what happens when you right-click text in macOS and choose **Services → Summarize**? This repository contains a complete answer: a dynamic-analysis reverse engineering of `SKSummary`\n\n(the SearchKit engine behind `Summary Service.app`\n\n), plus a Python reimplementation that is **byte-verified against the system output** on a 12-file corpus — 9 files byte-identical, 2 differing only in exact-tie floating-point noise, and 1 covering the CJK tokenization gap (scoring formula itself verified).\n\n-\n`Summary Service.app`\n\nis a thin shell. The real algorithm lives in**SearchKit**(`SKSummary*`\n\n— a public API family dating back to the Carbon era). -\nIt is\n\n**extractive summarization as self-retrieval**: build an in-memory inverted index over the sentences, then run the*whole document*as a ranked query against it, and pick the top sentences by TF-IDF cosine similarity:score(s) = Σ q(t)·w(t) / √Σ w(t)², where w(t) = idf·ln(1+tf), q(t) = idf·ln(1+qtf)/|Q|, idf(t) = 1 + ln(N/df)\n\n-\nDefault summary length:\n\n**k = max(1, ⌊3.0·ln n⌋)**; the UI slider switches to** k = max(1, ⌊n·pct⌋)**. -\n**No stopword removal, no stemming.** Digits are terms (even single digits), case is folded, single-character words are indexed. -\nTokenization quirks pinned by probes: decimals split at the dot (\n\n`3.5`\n\n→`3`\n\n,`5`\n\n); e-mail splits at`@`\n\nbut the domain stays whole (`admin@example.com`\n\n→`admin`\n\n+`example.com`\n\n); hyphens split; apostrophes are kept. -\nSentence segmentation: no break when a period is followed by a lowercase letter;\n\n`。！？`\n\nbreak unconditionally. -\nTies: the\n\n**later** sentence ranks first; zero-term sentences sink to the bottom in document order. -\nSummary assembly keeps raw trailing whitespace: adjacent sentences are joined with 2 extra spaces, skipped ranges get an ellipsis (\n\n`...`\n\n), paragraph crossings use`\\n\\n`\n\n/`\\n\\n...`\n\n. -\nTest environment:\n\n**macOS 26.5.2 (arm64)**. These are private behaviors; re-verify on other versions.\n\n| Path | What it is |\n|---|---|\n`tools/pysummary.py` |\nThe Python reimplementation (segmentation, tokenization, scoring, ranking, assembly) |\n`tools/sk_dump.c` |\nGround-truth dumper: calls system `SKSummary` and prints sentence count, per-sentence ranks, sentence texts, and summaries for k=1..8 |\n`tools/tok_dump.c` |\nDumper for real `CFStringTokenizer` sentence/word tokenization |\n`tools/compare.py` |\nRegression harness: diffs `sk_dump` vs `pysummary.py` over `corpus/*.txt` |\n`corpus/` |\n12 regression texts (prose, repeats, ties, paragraphs, numbers, news, abbreviations, punctuation, long text, Chinese, hyphen/e-mail, full-tie) |\n`probes/` |\nAll probes & experiments from the reverse-engineering process (C probes, lldb scripts, hypothesis tests) |\n`dumps/` |\nRaw SearchKit segment dumps captured during analysis |\n`docs/article.md` |\nThe full write-up (Chinese) |\n`docs/article.en.md` |\nThe full write-up (English), also appended at the bottom of this README |\n\n- macOS with Xcode Command Line Tools (\n`clang`\n\n) — the dumpers link against system frameworks `python3`\n\n(standard library only;`probes/exp3.py`\n\nadditionally needs`numpy`\n\n)\n\n```\n# build the ground-truth dumpers\nclang -O1 -framework CoreServices tools/sk_dump.c -o tools/sk_dump\nclang -O1 -framework CoreFoundation tools/tok_dump.c -o tools/tok_dump\n\n# run the full regression (expect: PASS 9 / PASS△ 2 / known-deviation 1)\npython3 tools/compare.py\n\n# summarize any text file with the reimplementation\npython3 tools/pysummary.py your_text.txt\npython3 tools/pysummary.py <file> [--scores]\n```\n\nPrints, in the exact same format as `sk_dump`\n\n:\n\n`N <n>`\n\n— sentence count`RANK <i> <r>`\n\n— rank of sentence i (1 = best)`SENT <i> <text>`\n\n— raw sentence span (trailing whitespace preserved, newlines escaped)`SUMMARY <k> <text>`\n\n— the assembled summary string for k = 1..min(n, 8)\n\n`--scores`\n\nadditionally writes per-sentence TF-IDF cosine scores to stderr.\n\n```\npython3 tools/compare.py\n```\n\nFor every `corpus/*.txt`\n\nit runs `tools/sk_dump`\n\n(system ground truth) and `python3 tools/pysummary.py`\n\n, then diffs line by line:\n\n`PASS`\n\n— byte-identical output`PASS△`\n\n— rank differences only between mathematically exact ties (float noise inside the system's accumulation order; not reproducible without replicating internals)`FAIL△`\n\n— known tokenization gap (CJK: Apple's dictionary-based segmenter is a private black box; the scoring formula itself is validated by feeding`CFStringTokenizer`\n\ntokens into the reimplementation — ranks match the system exactly)\n\nRun from the repository root. Notable ones:\n\n`probe7.c`\n\n— decisive probes for case folding / single-character terms`probe8.c`\n\n— decimal/domain/e-mail tokenization probes`exp_variants.py`\n\n,`exp2.py`\n\n— tokenization-hypothesis tests against corpus 05/11`exp3.py`\n\n— float32 pipeline simulation of the tie-noise hypothesis (needs`numpy`\n\n)`exp4.py`\n\n— Chinese scoring validation using real`CFStringTokenizer`\n\ntokens`dis_funcs*.py`\n\n,`dump_*.py`\n\n,`lldb_*.txt`\n\n,`sk_spy.py`\n\n— lldb dynamic-analysis scripts from the formula-extraction phase\n\nThe complete story, from disassembly to verification (also available as [article.en.md](/fenxer/macos-summarize-internals/blob/main/docs/article.en.md); [中文版](/fenxer/macos-summarize-internals/blob/main/docs/article.md)):\n\nFrom a pile of assembly instructions to a Python reimplementation that matches the system's output byte for byte. There were detours and misreadings along the way — and at the end, a 12-file regression report card.\n\nBuried in the macOS Services menu sits an ancient feature: select some text, right-click → Services → \"Summarize\". It has existed since the Mac OS X days, with a UI austere enough to pass for a last-century artifact, yet it has quietly done its job for twenty years: give it an article, and it hands you back a plausible-looking summary.\n\nThe question is, how does it actually \"think\"? A machine-learning model, or some primordial rule set?\n\nWith the bundle contents of `/System/Library/Services/Summary Service.app`\n\nin front of me, the answer had to be dug out of the binary.\n\nCracking the app open brought a small disappointment first: `Summary Service.app`\n\nitself is just a shell. Its code is tiny — receive text, call an API, display the result. The real algorithm hides inside the **SearchKit** framework in the system dyld shared cache. Yes, the same search-engine framework that powers Spotlight.\n\nWorth a side note: the `SKSummary`\n\nfamily is nothing new — it's a public SearchKit API dating back to the Carbon era. Summary Service merely wraps it in a service shell. That also explains the design's thoroughly pre-neural IR temperament.\n\nThis was the first big clue. Text summarization on macOS is, at its core, **a search engine querying itself**.\n\nWhat does that mean? An analogy. Imagine walking into a library, wanting \"the most important sentences of this article.\" A plain approach: make an index card for every sentence, recording which words it contains; then take the *entire article* as a search query and ask the card catalog — which sentences are most similar to \"the full text\"? The sentences that best match the whole are naturally the best summary candidates.\n\nThat is the entire core idea of Summary Service, known in the trade as **extractive summarization**. It doesn't understand semantics and never writes a new sentence; it scores every sentence and picks the top scorers verbatim. The pipeline is four steps: split the text into sentences and terms; build a tiny in-memory inverted index over the sentences, just like a search engine indexing web pages; run the full document as a ranked query against that mini index; then sort by score, take the top N, and stitch them back in original order.\n\nThe idea was easy to guess; the formula had to be pried out. The ranking math is buried in SearchKit's assembly, where static disassembly shows only an ocean of SIMD instructions. So I switched to dynamic analysis: set lldb breakpoints on the key functions and pulled the intermediate data out of memory, one piece at a time.\n\nThe call chain revealed itself under tracing:\n\n```\nVectorAccessor::RankedSearch\n  → ComputeDocTFMaps        (per-sentence term-frequency maps)\n  → ComputeTermScaleFactors (6 calls recorded in this trace — the idf computation)\n  → AddHit                  (dot-product accumulation)\n```\n\nCross-checking the captured numbers by hand yielded the full scoring formula. For sentence\n\nwhere:\n\n-\n$w(t) = \\text{idf}(t)\\cdot\\ln(1+\\text{tf})$ — the in-sentence weight, tf being the term's frequency within the sentence; -\n$q(t) = \\text{idf}(t)\\cdot\\ln(1+\\text{qtf}_t),/,|Q|$ — the query weight, qtf being the term's frequency in the*whole document*,$|Q|$ the query vector's norm; -\n$\\text{idf}(t) = 1 + \\ln(N/\\text{df}(t))$ — N the total sentence count, df the number of sentences containing the term.\n\nAnyone familiar with information retrieval will recognize it instantly: classic **TF-IDF cosine similarity** — every sentence measuring its angle against the \"whole document\" query.\n\nTwo details are worth pointing out. First, term frequency is sublinearly compressed with `logf`\n\nbeyond that. Twenty-year-old code — save a logarithm wherever you can.\n\nThe default summary length turned out to be a logarithmic curve:\n\nA 20-sentence article gets 8 sentences by default; a 6-sentence one gets 5. The log curve keeps short texts from being guillotined down to a headline. Dragging the slider in the Summary window switches to a linear formula,\n\nA lesson worth recording here. I initially read this constant from the assembly as 2.0; only after measuring the produced sentence counts on texts of different lengths — which stubbornly refused to match — did I realize an address misalignment had fooled me. Both architectures actually carry 3.0. In reverse engineering, dynamic measurement beats static guesswork. I paid for that sentence with a redo.\n\nThe formula is only the skeleton. What made the reimplementation genuinely hard was the old algorithm's collection of quirks — each had to be pinned down with its own dedicated probe.\n\nIt does **no stopword removal**. Feed it sentences made purely of function words alongside sentences full of content words, and a function-word sentence can still take first place. There is no notion of \"empty words\" here: rarity is the only virtue, even if the word is \"the\".\n\nIt does **no stemming**. `runs`\n\nand `running`\n\nare complete strangers to it. **Digits are first-class terms** — `1995`\n\nranks equal with `zebra`\n\n, and even a single-digit `7`\n\ncounts.\n\n**Case is folded** (`Apple`\n\nand `apple`\n\nare the same word), and **single-character words are indexed** — `I`\n\nis not noise. These two took a purpose-built \"decisive probe\" to confirm: construct a text where the \"folding\" and \"sensitive\" hypotheses predict entirely different rankings, and let the system vote.\n\nThe sentence segmenter has a notorious trap: CFStringTokenizer **refuses to break** when a period is followed by a lowercase letter — it suspects an abbreviation. So `\"...beta. alpha...\"`\n\ncounts as one sentence. Several of my probes capsized on this rock. The house rule became: every test sentence starts with a capital letter. Don't argue with it.\n\n**Ties go to the later sentence.** This was settled with 8 structurally identical sentences (4 mutually distinct words each): the system returned the clean ranking `[8,7,6,5,4,3,2,1]`\n\n— a perfect reversal, no ambiguity (the 3-sentence version kept in the corpus likewise converges to `[3,2,1]`\n\n).\n\nThe biggest surprise was **two unwritten tokenization rules**. During regression, two corpus files had rankings that just wouldn't line up, with gaps far too large to be floating-point error. A \"term-sharing probe\" finally forced out the truth: **decimals are split at the dot** — `3.5`\n\nis indexed as `3`\n\nand `5`\n\n; **e-mail addresses split at the @ but the domain stays whole** — `admin@example.com`\n\nbecomes `admin`\n\nand `example.com`\n\n, while a bare domain `example.com`\n\ngets split into `example`\n\nand `com`\n\n. Why does the same string split or not depending on context? Don't ask. Historical baggage.\n\nEven the final summary string's assembly has rules — byte-level rules. Each sentence's internal representation **keeps its original trailing whitespace**; the space after a period belongs to the preceding sentence. In a summary, two **adjacent** sentences get 2 extra spaces between them (3 total with the inherited one). When sentences were **skipped** in between, an ellipsis plus two spaces is inserted. **Across paragraphs**, the separator becomes `\"\\n\\n...\"`\n\n— and the ellipsis is *not* followed by spaces.\n\nSo a `k=2`\n\nsummary can look like this:\n\n```\nA fisherman mended his nets by hand. ...  By noon the catch would be sold.\n```\n\nThat ellipsis is not rhetoric; it's the algorithm honestly telling you, \"I skipped something between these two sentences.\" A small kindness left by some engineer twenty years ago.\n\nWith every rule pinned, I built three tools into a verification loop. `sk_dump`\n\ncalls the system `SKSummary`\n\nand prints the sentence count, per-sentence ranks, sentence texts, and the k=1..8 summary strings — the gold standard. `pysummary.py`\n\nis the complete Python reimplementation: segmentation, tokenization, scoring, ranking, assembly. `compare.py`\n\ndiffs the two line by line across the corpus.\n\nThe corpus has 12 files covering every pressure point I could think of: plain prose, repeated terms, tie constructions, multiple paragraphs, mixed-case numbers, news-style text, an abbreviation barrage, quotes and semicolons, a 20-sentence long text, Chinese, hyphens and e-mail addresses, and an all-tie construction.\n\n**Final report card: 9 of 12 byte-identical.** Per-sentence ranks, sentence boundaries, summary strings — not one byte out of place.\n\nThe remaining three deserve individual hearings.\n\n**Two differ only on mathematically exact ties** (04 paragraphs, 09 long text). Take 04: it actually contains *two* tied pairs (s0/s3 and s1/s4) — within each pair, the term multisets are fully isomorphic, right down to identical floating-point sums. Interestingly, the system's ordering of s0/s3 agrees with mine, while s1/s4 came out flipped — a deterministic tie-break rule would have gone the same way for both pairs, so the inconsistency is itself the best evidence for noise. The most plausible explanation is the system's internal float accumulation order: floating-point addition is not associative, and term traversal order plus vectorized dot-product paths can nudge two mathematically equal sums apart by ±1 ulp. I simulated the whole float32 pipeline under several hypothesized accumulation orders and never reproduced the system's direction; I filed it as \"float details tied to internal traversal/accumulation order\" and stopped digging. These deviations only affect mathematically exact ties — a measure-zero event in real texts — so they are classified as known noise.\n\n**One is Chinese.** Segmentation at `。！？`\n\nposed no problem, but Chinese word segmentation is dictionary-grade (`委员会/在/周二/上午/举行/了/会议`\n\n), and that dictionary is Apple's private black box. So I validated from a different angle: feed the tokens produced by the system's own public tokenizer, CFStringTokenizer, into my scorer — the resulting ranks `[3,2,6,5,1,4]`\n\nmatched the system exactly. Strictly speaking, this proves the scoring formula is *compatible* with that token stream; it cannot prove SearchKit uses the identical token sequence internally. Python could in principle call CFStringTokenizer directly to align with the public segmentation behavior, but not with the indexer's private lexicon and its cross-version drift. This is the one gap this article leaves open.\n\nThe complete portrait of Summary Service: an extractive summarizer living inside a search engine. It indexes sentences into a mini inverted index, queries it with the full document, scores every sentence by TF-IDF cosine, and stitches back the top\n\nNo neural networks, no semantic understanding — and yet, looking back, there is a certain austere elegance to this last-century design. It believes exactly one thing: *\"the sentences that best represent the whole are the ones most similar to it.\"* As an answer to the word \"summary\", that assumption is almost brutally simple — and surprisingly effective. It still sits in the macOS context menu today, twenty-year-old lookup tables and all, earnestly scoring every paragraph it meets.\n\nAnd its mind? We verified it with 12 files — the byte-for-byte kind of verification.\n\n*All conclusions in this article were validated through lldb dynamic analysis and controlled experiments, on macOS 26.5.2 (arm64). Apple does not guarantee these private behaviors remain stable across versions — re-verify before generalizing. The reimplementation and regression corpus live in this repository ( pysummary.py, sk_dump.c, compare.py, corpus/); feel free to rerun everything.*", "url": "https://wpnews.pro/news/how-macos-summarize-works-reverse-engineered-with-kimi-k3", "canonical_source": "https://github.com/fenxer/macos-summarize-internals", "published_at": "2026-07-23 06:22:21+00:00", "updated_at": "2026-07-23 06:52:41.325541+00:00", "lang": "en", "topics": ["natural-language-processing", "developer-tools"], "entities": ["macOS", "SearchKit", "SKSummary", "Summary Service.app", "Kimi K3", "Python"], "alternates": {"html": "https://wpnews.pro/news/how-macos-summarize-works-reverse-engineered-with-kimi-k3", "markdown": "https://wpnews.pro/news/how-macos-summarize-works-reverse-engineered-with-kimi-k3.md", "text": "https://wpnews.pro/news/how-macos-summarize-works-reverse-engineered-with-kimi-k3.txt", "jsonld": "https://wpnews.pro/news/how-macos-summarize-works-reverse-engineered-with-kimi-k3.jsonld"}}