kausalflow/lightkone
Lightkone core: search, BibTeX, keys, filing, merge, tidy and the library file
Version 1 · Proprietary (kausalflow) · by kausalflow · 64 obligations (64 decided, 0 default, 0 open) · 4 assumptions · 8 components · 340 checks
symbolon pull kausalflow/lightkone
sha256:075c30cc624fda7b44c574318d20224fc97184ed3505597bf0fe7616defdfd10Evidence
8 of 8 components pass a build · 3 builds · 2 runners. Every signature and digest below was checked when this site was built;
re-check one with symbolon verify on the linked file.
| Component | Build | Result | Contract | Runner |
|---|---|---|---|---|
bibtex-export-v1 | go-1 rebuild | passed 25 json | this version | runner-a |
| kausalflow/lightkone | passed 25 json | this version | runner-a | |
| AI builder | passed 25 json | this version | runner-a | |
bibtex-import-v1 | go-1 rebuild | passed 52 json | this version | runner-a |
| kausalflow/lightkone | passed 52 json | this version | runner-a | |
| AI builder | passed 41 json | an unpublished draft | runner-a | |
citation-key-v1 | go-1 rebuild | passed 25 json | this version | runner-a |
| kausalflow/lightkone | passed 25 json | this version | runner-a | |
library-file-v1 | go-1 rebuild | passed 61 json | this version | runner-a |
| kausalflow/lightkone | passed 61 json | this version | runner-a | |
library-merge-v1 | go-1 rebuild | passed 29 json | this version | runner-a |
| kausalflow/lightkone | passed 29 json | this version | runner-a | |
pdf-filing-v1 | go-1 rebuild | passed 25 json | this version | runner-a |
| kausalflow/lightkone | passed 25 json | this version | runner-a | |
search-query-v1 | kausalflow/lightkone | passed 95 json | this version | runner-c |
| go-1 rebuild | passed 92 json | an unpublished draft | runner-a | |
| AI builder | passed 82 json | an unpublished draft | runner-a | |
tidy-v1 | go-1 rebuild | passed 28 json | this version | runner-a |
| kausalflow/lightkone | passed 28 json | this version | runner-a |
Brief
Lightkone
Lightkone is a research-paper library for people who read a lot of papers and
want to own their data. It is a static web app with no server: the library is
plain files (lightkone/library.json and a generated library.bib) in storage
the owner chooses, such as the browser, an S3 or R2 bucket, or a GitHub
repository.
People use it to collect papers (by DOI, arXiv id, BibTeX import or PDF), find
them again with a small query language, file them in folders, tag them, read
and highlight PDFs, and keep .bib files in sync with their manuscripts on
Overleaf.
What this version of the pack covers
Version 1 covers the pure components everything else leans on, and the library file they all share:
| Component | What it does |
|---|---|
lightkone/search-query@1 |
The search box: filters (tag:, author:, year:, in:, has:, no:, is:), words with accent folding, ranking, sort orders, and the trash rule |
lightkone/bibtex-import@1 |
Reading BibTeX from Paperpile, Zotero, JabRef and Google Scholar into paper drafts, including LaTeX accents, macros, author names and arXiv ids |
lightkone/bibtex-export@1 |
Writing a paper as the exact BibTeX entry lightkone writes, byte for byte |
lightkone/citation-key@1 |
Paperpile-style citation keys for new papers, unique within the library |
lightkone/pdf-filing@1 |
Where a paper's PDF goes, named the way Paperpile names it |
lightkone/library-merge@1 |
Merging two devices' edits to library.json without losing either |
lightkone/tidy@1 |
Finding duplicate papers and how to merge each group |
lightkone/library-file@1 |
Reading library.json and writing it back, with library.bib, byte for byte |
Later versions add the MCP server's tools and UI acceptance scenarios.
Who it is for
Builders who want lightkone's behavior in their own stack: a different language, a server instead of a static app, or a fork with their own features. A build that passes these contracts reads the same queries and BibTeX the same way lightkone does, so its users' libraries and habits carry over.
Non-goals
- Full-text search inside PDFs. Search covers metadata and notes only.
- Collaboration. A library belongs to one person; there are no shared libraries, roles or permissions.
- Being a general BibTeX validator. Import reads what the common managers export and skips the rest.
- Pixel-level UI. Later versions specify user flows, not layout or styling.
Priorities
When goals conflict, earlier ones win:
- The owner's data stays theirs and stays readable. Never lose or corrupt a library, and keep it in plain files.
- Compatibility with lightkone's files. A build reads and writes the same
library.jsonandlibrary.bib, byte for byte where the contracts say so. - Same answers to the same queries, so habits carry over between builds.
- Speed and features beyond the pack.
Who resolves gaps
kausalflow, as the namespace holder. If the pack doesn't settle something, record the
question and the reading you chose in NOTES.md next to your build; obligations marked
open in obligations.toml are yours to choose. Unsettled questions are answered in the
next version of the pack.
Assumptions
| Id | Statement | Monitor |
|---|---|---|
K-bibtex-sources | Imported BibTeX comes from Paperpile, Zotero, JabRef or Google Scholar exports, or from a library.bib lightkone wrote. | Count entries dropped per import; report when more than 5% of an import is dropped. |
K-iso-timestamps | Every added and opened timestamp was written by JavaScript's toISOString (UTC, milliseconds), so string order is time order. | On load, report papers whose timestamps do not match the schema pattern. |
K-personal-library | A library belongs to one person, holds at most tens of thousands of papers, and is searched in memory on their device. | Report the library size when it is loaded. |
K-single-writer | Two clients (devices, or Claude through MCP) do not write the same library within the moment between lightkone re-reading library.json and writing it. | After each save, re-read library.json and report when it differs from what was written. |
Obligations
| Id | Status | Statement |
|---|---|---|
search.results-from-library | decided | Results are distinct ids of papers in the input library. |
search.trash | decided | Trashed papers appear only with is:trash, and then only trashed papers appear; an empty trashed field is not trashed. |
search.query-tokens | decided | Terms split on whitespace; quotes make one word; an unknown field:value or unmatched quote is an ordinary word. |
search.field-edge-cases | decided | A field with nothing after the colon is an ordinary word; an unknown field with a quoted value is one word, quotes included. |
search.terms-conjunctive | decided | Every filter and every word must hold (except year:, see search.year-last-wins). |
search.folding | decided | Text and query compare after NFKD, removing combining marks, and lower-casing. |
search.word-scoring | decided | A word scores 5 in the title, 4 in authors joined by ' ; ', 1 in the rest text; a paper with any zero-scoring word is left out. |
search.filter-tag | decided | tag:T matches a whole tag after folding, never a substring. |
search.filter-author | decided | author:A is a substring of the folded authors joined with ' ; '. |
search.filter-year | decided | year:Y is an exact year and year:A-B an inclusive range; a missing year never matches. |
search.year-last-wins | decided | Only the last year: term that counts applies; a term whose value is not a number does not count. |
search.year-malformed | decided | year:A-B parts: A not a number or 0 means the term does not count; B missing, empty, 0 or not a number means A; later parts are ignored. A typo such as year:20l7 filters nothing, by kausalflow's decision (2026-10-24): forgiving, low stakes. |
search.filter-folder | decided | in:F matches folder F and folders inside it, after folding, ignoring trailing slashes, never by name prefix. |
search.filter-has-no | decided | has:/no: test presence of pdf, abstract, arxiv and notes; empty counts as absent; has:pdf also accepts file; unknown values are ignored. |
search.filter-is | decided | is:starred and is:recent select starred and opened papers; unknown is: values are ignored. |
search.default-order | decided | The default sort is newest added first, relevance when there are words, opened with is:recent; ties keep input order, relevance ties fall back to newest added. |
search.explicit-sorts | decided | year: later first, missing as 0; opened: latest first, never-opened last; an explicit sort overrides the default. |
search.title-sort-ascii | decided | title: sort is ascending and stable for titles of lowercase ASCII letters, digits and spaces. |
search.title-sort-other | decided | title: sort uses Unicode collation (CLDR root, tertiary), as JavaScript's localeCompare does: accents and case only break ties. Kept by kausalflow's decision (2026-10-24); builds need an ICU-grade collator. |
import.never-fails | decided | Import never fails: malformed parts are skipped and the rest is read. |
import.entries | decided | @type{key, fields} and @type(key, fields) become drafts; @comment, @preamble and untitled entries do not. |
import.malformed-field-skip | decided | A field without a name or '=' is skipped to the next comma or the end of the entry, skipping its braced and quoted values whole; the fields after it are read. |
import.values | decided | Values are braced, quoted, numeric or macro parts joined by #; @string and month macros expand; later fields replace earlier ones; whitespace collapses. |
import.non-ascii-whitespace | decided | Whitespace is JavaScript's \s set (incl. no-break space, U+2028, U+3000, BOM), everywhere in import. Kept by kausalflow's decision (2026-10-24). |
import.latex-to-plain | decided | Title, venue, abstract, names and tags go through plain(): LaTeX accents and specials become Unicode, formatting commands unwrap, braces go. |
import.authors | decided | Authors split on 'and'; First Last flips to Last, First; comma names, 'others' and {braced} names are kept; editor stands in only without an author field. |
import.identifiers | decided | doi loses its resolver prefix; arxiv ids come from arxivid, arXiv eprints, arxiv.org URLs or new-style bare eprints, without version. |
import.draft-fields | decided | year is the first four digits; venue is journal, else booktitle, else publisher; notes is annote, else note; empty abstract, doi and venue are absent. |
import.venue-empty-journal | decided | The venue is the first of journal, booktitle, publisher that is not empty, so an empty journal does not hide a booktitle. Fixed in lightkone#53; until then an empty journal left the paper without a venue. |
import.tags | decided | keywords split on ; or , into tags, dropping identifiers and URLs. |
export.byte-exact | decided | Export writes the exact entry lightkone writes: frame, aligned names, field order. |
export.values | decided | Values are written as given, except that & % # are escaped once in title, venue and abstract. |
export.venue | decided | The venue is written as journal, or booktitle for inproceedings and incollection, unless a non-empty journal field is already there. |
export.authors-and-keywords | decided | Authors join with ' and ', whole names braced; tags join with ';' as keywords. |
export.empty-values | decided | Empty values are left out, except title and year 0; an empty type becomes misc. |
export.no-private-fields | decided | Fields whose names start with lightkone- are private and never written to BibTeX. |
keys.format | decided | A new key is surname (NFKD, ASCII letters, Anon if none) + year + '-' + first letters of the first two title words longer than 3 letters. |
keys.unique | decided | Keys are unique in the library: a taken key counts up from 2, and keys given earlier in a batch are taken. |
keys.stable | decided | A key a paper arrives with is kept when it is free, so keys from an imported .bib never change. |
filing.paperpile-layout | decided | PDFs are filed as papers/All Papers/<SURNAME>/<Who Year - Title>.pdf, Paperpile's own layout, so a Paperpile folder can be copied in unchanged. |
filing.clean-names | decided | File names hold no characters a file system or Drive rejects; they become ' - ', and -- / --- become dashes. |
filing.length | decided | Long author lists and names are shortened the way Paperpile shortens them. |
filing.no-overwrite | decided | A new PDF never takes a name another paper uses; it gets (2), (3), ... |
merge.no-lost-edits | decided | A save never silently drops a change made elsewhere: one-sided changes are taken, different fields of one paper both survive. |
merge.conflict-ours | decided | When both sides change the same field differently, this device's edit wins. |
merge.sets | decided | tags and folders merge as sets: additions from either side are kept, removals from either side stick. |
merge.deletes | decided | A paper deleted on one side and unchanged on the other is gone; changed on the other, it survives. |
merge.order-and-count | decided | Their order first, then papers only here; fromThem counts papers that took a change made elsewhere. |
merge.bibsync | decided | bibSync takes theirs unless changed here, and is written only when non-empty. |
tidy.match | decided | Duplicates share a DOI, an arXiv id, a long title, or a short title and first author, after normalising; chains join. |
tidy.trash-ignored | decided | Papers in the trash are never grouped. |
tidy.keeper | decided | The kept paper is the one with a PDF, then notes, then the oldest. |
tidy.nothing-lost | decided | Merging keeps every tag, folder, note and star, and fills the keeper's missing fields from the others. |
tidy.blank-ids-ignored | decided | A DOI or arXiv id that is empty once trimmed and stripped of its prefix gives no match key, so blank ones never make papers duplicates. Fixed in lightkone#52; until then whitespace-only DOIs matched each other. |
file.refuse-foreign | decided | A file that is not a lightkone library (not JSON, wrong format, no papers list, a paper that is not an object) is refused with an error and nothing is written. |
file.keeps-papers | decided | Opening and saving keeps every paper and every key of every paper, in order, plus bibSync when it has entries. |
file.keeps-unknown-keys | decided | Top-level keys other than format, version, papers and bibSync are kept on save, after lightkone's own, in the file's order, so another tool's data at the top level survives. Fixed in lightkone#51; until then they were dropped. |
file.newer-version-refused | decided | A library.json whose version is a number above 1 was saved by a newer lightkone and is refused, writing nothing; any other version, or none, is read and saved as 1. Fixed in lightkone#53; until then a newer file was read as version 1 and could lose data on save. |
file.folders | decided | Papers without folders take them once from Paperpile's Drive paths, skipping All Papers, author folders and Other. |
file.folders-author-only | decided | A single directory is skipped as an author folder only when it is Other or one of the paper's authors' surnames in capitals, so a user's own ML or 2019 folder is kept. With no authors to compare, a directory in capitals with a letter is still skipped. Fixed in lightkone#52; until then every single directory in capitals was skipped. |
file.exact-text | decided | library.json is written byte for byte as lightkone writes it: JSON with a one-space indent, JavaScript's key order, escaping and number printing. |
file.no-needless-write | decided | Nothing is written when the file would not change, so opening a library on several devices does not cause sync churn. |
file.bib-alongside | decided | library.bib is written with every save: the BibTeX entry of each paper not in the trash, in library order. |
file.bib-leaves-out-trash | decided | Papers in the trash are left out of library.bib, so Overleaf does not see deleted papers. A paper is in the trash when its trashed value is true in JavaScript. Fixed in lightkone#51; until then they were written. |