notes::language::from_content guesses by first match wins, down a hand-ordered chain of ifs. The comment says so — "the order of attempts runs from the most discriminating signal to the vaguest" — and the order is the priority. Nothing scores, nothing compares, and nothing can answer "I do not know".
Adding Rust, Go, Java, C#, PHP and C (#17) fixed the case that exposed this: a Rust snippet carrying a match arm came back js, because is_javascript matches => anywhere in the text. The fix was to insert the five compiled languages ahead of TypeScript and JavaScript in the chain. It works, and it is the right fix for that ticket — but it is a slot in a list, and the next language will need someone to find the right slot again.
What the shape costs
is_javascript is the greediest predicate in the file. content.contains("=>") on the whole text, plus const, let, var, class, function, export and import at the start of any line. C++, Kotlin, Swift, Scala, Dart and Groovy all land there, and so does anything with a lambda. It sits near the end of the chain precisely because it catches everything, which makes it the default answer rather than an answer.
is_typescript claims bare type and enum at line start. Rust writes both. It only loses them today because is_rust was put in front of it and deliberately does not test enum/struct/trait — markers it would otherwise deserve, given up to avoid stealing from a neighbour. A heuristic weakened to protect the one after it is a sign the arbitration is in the wrong place.
- Detection cannot abstain. Every snippet gets an answer. A wrong one is worse than no answer: it colours the body wrongly, puts a language badge on the card, and files the note under a facet in the rail.
txt says nothing, which is honest. There is no confidence anywhere in the chain.
- The guess sticks.
after_patch refuses to detect when the language was chosen, when before.content is non-empty, or when the patch carries no content — so the first paste decides for the life of the note, and there is no "detect again". A wrong guess is permanent unless the user notices the select.
- The tests are one snippet per case. Real, useful cases, but written from memory of what the author was fixing. There is no corpus, so a change to one predicate is only shown to break what somebody thought to write down.
What to consider
Score instead of ordering. Each language returns how many of its markers matched; the highest score wins; a tie or a score below a threshold falls to txt. The ordering knowledge does not disappear — it becomes weights, and stops being invisible.
⚠️ Two facts the current order encodes and a rewrite must not lose:
- PHP is tried before
markup_kind, which reads <?php as an XML processing instruction.
- The compiled languages are tried before TypeScript and JavaScript, for the
=> and const reasons above.
A corpus rather than examples. A folder of small real snippets, one or more per supported language, asserted in bulk. That is where a change to a predicate shows what it broke — and it is the only way to know whether a scoring rewrite is actually better than the chain, rather than differently wrong.
Decide what "not sure" means, and write it down. Falling back to txt on a weak signal is a behaviour change: some notes that are coloured today would stop being. That is probably right, and it is a decision, not a detail.
While in there, the languages still absent: C++, Kotlin, Swift, Ruby, Dockerfile, .env, diff/patch, PowerShell, HCL, Makefile. Each is cheap once #17's four-edit path exists (docs/architecture.md → "Adding a language"), but each one added to the current chain is another slot to find by hand — which is the argument for doing this first.
Done when
A corpus test covers every supported language; a snippet of a language DevBox does not support comes back txt rather than js; the rule that decides "not confident enough" is written down next to the code that applies it; and adding a language no longer means finding the right place in a list.
notes::language::from_contentguesses by first match wins, down a hand-ordered chain ofifs. The comment says so — "the order of attempts runs from the most discriminating signal to the vaguest" — and the order is the priority. Nothing scores, nothing compares, and nothing can answer "I do not know".Adding Rust, Go, Java, C#, PHP and C (#17) fixed the case that exposed this: a Rust snippet carrying a match arm came back
js, becauseis_javascriptmatches=>anywhere in the text. The fix was to insert the five compiled languages ahead of TypeScript and JavaScript in the chain. It works, and it is the right fix for that ticket — but it is a slot in a list, and the next language will need someone to find the right slot again.What the shape costs
is_javascriptis the greediest predicate in the file.content.contains("=>")on the whole text, plusconst,let,var,class,function,exportandimportat the start of any line. C++, Kotlin, Swift, Scala, Dart and Groovy all land there, and so does anything with a lambda. It sits near the end of the chain precisely because it catches everything, which makes it the default answer rather than an answer.is_typescriptclaims baretypeandenumat line start. Rust writes both. It only loses them today becauseis_rustwas put in front of it and deliberately does not testenum/struct/trait— markers it would otherwise deserve, given up to avoid stealing from a neighbour. A heuristic weakened to protect the one after it is a sign the arbitration is in the wrong place.txtsays nothing, which is honest. There is no confidence anywhere in the chain.after_patchrefuses to detect when the language was chosen, whenbefore.contentis non-empty, or when the patch carries no content — so the first paste decides for the life of the note, and there is no "detect again". A wrong guess is permanent unless the user notices the select.What to consider
Score instead of ordering. Each language returns how many of its markers matched; the highest score wins; a tie or a score below a threshold falls to
txt. The ordering knowledge does not disappear — it becomes weights, and stops being invisible.markup_kind, which reads<?phpas an XML processing instruction.=>andconstreasons above.A corpus rather than examples. A folder of small real snippets, one or more per supported language, asserted in bulk. That is where a change to a predicate shows what it broke — and it is the only way to know whether a scoring rewrite is actually better than the chain, rather than differently wrong.
Decide what "not sure" means, and write it down. Falling back to
txton a weak signal is a behaviour change: some notes that are coloured today would stop being. That is probably right, and it is a decision, not a detail.While in there, the languages still absent: C++, Kotlin, Swift, Ruby, Dockerfile,
.env, diff/patch, PowerShell, HCL, Makefile. Each is cheap once #17's four-edit path exists (docs/architecture.md→ "Adding a language"), but each one added to the current chain is another slot to find by hand — which is the argument for doing this first.Done when
A corpus test covers every supported language; a snippet of a language DevBox does not support comes back
txtrather thanjs; the rule that decides "not confident enough" is written down next to the code that applies it; and adding a language no longer means finding the right place in a list.