#PascalCase
I love how they PascalCase all the headlines to make you know that it's a real programming magazine.

Truly unreadable, but very dedicated.
October 5, 2026 at 8:02 AM
Your Model Upgrade Is a Breaking Change: Build Contract Tests for LLM Providers in TypeScript
Most code that calls a model has one line that looks harmless. model: "claude-sonnet-5" Changing it feels like a config change. But that string is part of an API contract. And this month, the contracts changed. * On September 17, Google released Antigravity Agent 09-2026. If you run tools locally or parse `function_call` steps, "the built-in tools changed": PascalCase parameters, and `write_file(path, content)` became `write_to_file` or `replace_file_content`. The old `antigravity-preview-05-2026` "shuts down on October 5, 2026." * On September 22, Anthropic launched Claude Opus 5.5. Per the release notes, `thinking: {"type": "disabled"}` and `{"type": "enabled", ...}` "return a 400 error." So do `tool_choice` types `any` and `tool`. * On September 28, Anthropic launched Claude Sonnet 5.5. The release notes say "Code written for Claude Sonnet 5 can break on Claude Sonnet 5.5 in five ways." * On September 29, at DevDay, OpenAI released GPT-6.1 Sol. Its model page says "The `none` and `minimal` reasoning efforts are not supported." Here are Sonnet 5.5's five, in the release notes' words: 1. "To turn off up-front thinking, send `thinking: {"type": "between_tools"}` instead of `"disabled"`, at `high` effort or below." 2. "Forced tool use (`tool_choice` types `any` and `tool`) returns a 400 error." 3. "Thinking blocks are tied to the model and the conversation." 4. "On the Claude API and Google Cloud, the earlier `computer_20251124` computer use tool isn't accepted." 5. "The advisor tool rejects Claude Opus 4.8, Claude Opus 4.7, and Claude Sonnet 5 as advisors." The What's new page adds one that "alters the response shape without failing any request": text between tool calls comes back in `thinking` blocks. A 400 is loud. An empty progress message is quiet. Different companies. Same pattern. **Changing a model string is a dependency upgrade. It deserves a test suite.** So let's build one. No API key. Both providers are mocks: their request rules follow the docs above, and their replies are made up. ## Table of Contents 1. What We Are Building 2. Project Setup 3. Step 1: A Neutral Request and Response 4. Step 2: Mock Two Model Versions 5. Step 3: Write the Contracts 6. Step 4: Diff Two Runs 7. Step 5: Check Known Breaks From the Docs 8. Step 6: Build the Gate and Run It 9. Where It Breaks Down 10. The Bigger Idea ## What We Are Building One check reads the release notes. The other diffs two model versions. ## Project Setup You will need Node.js 18 or newer. mkdir model-upgrade-gate cd model-upgrade-gate npm init -y npm install --save-dev typescript tsx @types/node Save the following blocks, in order, as `upgrade-gate.ts`. ## Step 1: A Neutral Request and Response type Req = { prompt: string; maxTokens: number; thinking?: { type: "adaptive" | "disabled" | "between_tools" }; toolChoice?: { type: "auto" | "none" | "any" | "tool" }; tools?: string[]; }; type Block = | { type: "text"; text: string } | { type: "thinking"; thinking: string } | { type: "tool_use"; name: string; input: Record<string, unknown> }; type Stop = "end_turn" | "tool_use" | "max_tokens" | "refusal"; type Ok = { status: 200; stopReason: Stop; content: Block[] }; type Res = Ok | { status: 400; error: string }; type Provider = { model: string; send: (req: Req) => Res }; Your app's shape, not a vendor SDK. **Your contracts should describe your app, not the provider.** ## Step 2: Mock Two Model Versions // MOCK PROVIDER. No network, no API key. The two Sonnet 5.5 rejections follow // Anthropic's docs (Sep 28), and the tool_choice error is quoted from them. // The other error text and every reply are made up. const reply = (stopReason: Stop, ...content: Block[]): Ok => ({ status: 200, stopReason, content }); function mockClaude(model: "claude-sonnet-5" | "claude-sonnet-5-5"): Provider { const v55 = model === "claude-sonnet-5-5"; const send = (req: Req): Res => { if (v55 && req.thinking?.type === "disabled") { return { status: 400, error: 'invalid_request_error: use "between_tools"' }; } if (v55 && ["any", "tool"].includes(req.toolChoice?.type ?? "auto")) { return { status: 400, error: 'tool_choice: type "tool" and "any" are not supported for this model.' }; } if (req.prompt.startsWith("[refuse]")) return reply("refusal"); if (req.maxTokens < 50) return reply("max_tokens", { type: "text", text: "Q3 revenue grew" }); if (req.tools?.includes("get_weather")) { const note = "Checking the forecast first. Then I'll compare it with yesterday."; const shown = req.thinking?.type === "between_tools" ? note : ""; const progress: Block = v55 ? { type: "thinking", thinking: shown } : { type: "text", text: note }; return reply("tool_use", progress, { type: "tool_use", name: "get_weather", input: { city: "Paris" } }); } if (req.tools?.includes("classify_ticket")) { return reply("tool_use", { type: "tool_use", name: "classify_ticket", input: { label: "billing" } }); } return reply("end_turn", { type: "text", text: '{"total": 42.5, "currency": "USD"}' }); }; return { model, send }; } Sonnet 5.5 rejects `disabled` thinking and forced tool use. Its progress note also moves into a `thinking` block. At the default `display: "omitted"`, the docs say its text is empty. With `between_tools`, it comes back. A mock that agrees with everything is just a very polite liar. ## Step 3: Write the Contracts type Contract = { name: string; req: Req; check: (res: Ok) => string | null }; const weather: Req = { prompt: "Weather in Paris?", maxTokens: 500, tools: ["get_weather"] }; const contracts: Contract[] = [ { name: "output schema", req: { prompt: "Extract the invoice total as JSON", maxTokens: 500 }, check: ({ content: [first] }) => { const data = JSON.parse(first?.type === "text" ? first.text : "null"); return typeof data?.total === "number" && typeof data?.currency === "string" ? null : "bad JSON shape"; }, }, { name: "tool call format", req: weather, check: (res) => { const call = res.content.find((b) => b.type === "tool_use"); return res.stopReason === "tool_use" && typeof call?.input.city === "string" ? null : "bad tool call"; }, }, { name: "progress text between tools", req: weather, check: ({ content: [first] }) => { const shown = first?.type === "text" ? first.text : first?.type === "thinking" ? first.thinking : ""; return shown ? null : `user sees nothing before the tool call (empty ${first?.type} block)`; }, }, { name: "forced tool use", req: { prompt: "Classify this ticket", maxTokens: 200, tools: ["classify_ticket"], toolChoice: { type: "tool" } }, check: (res) => (res.stopReason === "tool_use" ? null : "no tool call"), }, { name: "thinking off (fast path)", req: { prompt: "Summarize in one line", maxTokens: 200, thinking: { type: "disabled" } }, check: () => null, // a 200 is the whole contract }, { name: "token limit stop reason", req: { prompt: "Write the full quarterly report", maxTokens: 20 }, check: (res) => (res.stopReason === "max_tokens" ? null : `got ${res.stopReason}`), }, { name: "refusal behavior", req: { prompt: "[refuse] a request the model declines", maxTokens: 200 }, check: (res) => (res.stopReason === "refusal" && res.content.length === 0 ? null : "refusal not clean"), }, ]; Seven promises. Each check returns `null` or a reason. The refusal contract follows the docs: a declined request returns HTTP 200 with `stop_reason: "refusal"`. ## Step 4: Diff Two Runs type Result = { name: string; pass: boolean; detail: string }; function runSuite(provider: Provider): Result[] { return contracts.map(({ name, req, check }) => { const res = provider.send(req); if (res.status !== 200) return { name, pass: false, detail: `${res.status} ${res.error}` }; try { const failure = check(res); return { name, pass: failure === null, detail: failure ?? "" }; } catch (err) { return { name, pass: false, detail: `threw: ${(err as Error).message}` }; } }); } function contractDiff(current: Provider, candidate: Provider) { const before = runSuite(current); const after = runSuite(candidate); const broke: string[] = []; console.log(`\nContract diff (MOCK ${current.model} -> MOCK ${candidate.model})`); after.forEach((a, i) => { const status = before[i].pass && !a.pass ? "BROKE" : a.pass ? "same" : "FAIL"; if (status === "BROKE") broke.push(a.name); console.log(` ${status.padEnd(6)} ${a.name.padEnd(28)} ${a.detail}`.trimEnd()); }); return broke; } Only one transition matters: **passed before, fails now.** ## Step 5: Check Known Breaks From the Docs type KnownBreak = { model: string; param: string; bad: string[]; docs: string; source: string }; // From the vendors' docs, checked Oct 3, 2026. const knownBreaks: KnownBreak[] = [ { model: "claude-sonnet-5-5", param: "thinking.type", bad: ["disabled"], docs: 'send "between_tools" instead', source: "Claude notes, Sep 28" }, { model: "claude-sonnet-5-5", param: "tool_choice.type", bad: ["any", "tool"], docs: "returns a 400 error", source: "Claude notes, Sep 28" }, { model: "claude-opus-5-5", param: "thinking.type", bad: ["disabled", "enabled"], docs: "returns a 400 error", source: "Claude notes, Sep 22" }, { model: "gpt-6.1-sol", param: "reasoning.effort", bad: ["none", "minimal"], docs: "not supported", source: "OpenAI model page" }, { model: "antigravity-preview-09-2026", param: "tools", bad: ["write_file", "read_file", "list_files"], docs: "built-in tools changed", source: "Gemini changelog, Sep 17" }, ]; const shutdowns: Record<string, string> = { "antigravity-preview-05-2026": "2026-10-05" }; type CallSite = { site: string; from: string; to: string; params: Record<string, string[]> }; function checkKnownBreaks(sites: CallSite[], today: string) { let count = 0; console.log("\nKnown breaks (from release notes)"); for (const s of sites) { for (const rule of knownBreaks.filter((r) => r.model === s.to)) { for (const value of (s.params[rule.param] ?? []).filter((v) => rule.bad.includes(v))) { count++; console.log(` BREAK ${s.site}: ${rule.param}=${value}: ${rule.docs} [${rule.source}]`); } } const end = shutdowns[s.from]; const days = (Date.parse(end) - Date.parse(today)) / 86_400_000; if (end) console.log(` DEADLINE ${s.site}: ${s.from} shuts down ${end} (${days} days)`); } if (count === 0) console.log(" no known breaks"); return count; } This is the deprecated-params check. Every row comes from a vendor's docs, with its date. The `DEADLINE` line isn't a failure. It's a reason to hurry. ## Step 6: Build the Gate and Run It // Illustrative call sites in a made-up app. const S5 = "claude-sonnet-5", S55 = "claude-sonnet-5-5"; const callSites: CallSite[] = [ { site: "invoice-extractor", from: S5, to: S55, params: {} }, { site: "ticket-classifier", from: S5, to: S55, params: { "tool_choice.type": ["tool"] } }, { site: "fast-summary", from: S5, to: S55, params: { "thinking.type": ["disabled"] } }, { site: "code-agent", from: "gpt-6-sol", to: "gpt-6.1-sol", params: { "reasoning.effort": ["none"] } }, { site: "file-agent", from: "antigravity-preview-05-2026", to: "antigravity-preview-09-2026", params: { tools: ["write_file"] } }, ]; const today = "2026-10-03"; console.log(`Upgrade gate, ${today}`); const breaks = checkKnownBreaks(callSites, today); const broke = contractDiff(mockClaude(S5), mockClaude(S55)); const blocked = breaks > 0 || broke.length > 0; console.log(blocked ? `\nGATE: BLOCKED (${breaks} known breaks, ${broke.length} contract regressions)` : "\nGATE: OPEN"); process.exitCode = blocked ? 1 : 0; Run it: npx tsx upgrade-gate.ts Real output: Upgrade gate, 2026-10-03 Known breaks (from release notes) BREAK ticket-classifier: tool_choice.type=tool: returns a 400 error [Claude notes, Sep 28] BREAK fast-summary: thinking.type=disabled: send "between_tools" instead [Claude notes, Sep 28] BREAK code-agent: reasoning.effort=none: not supported [OpenAI model page] BREAK file-agent: tools=write_file: built-in tools changed [Gemini changelog, Sep 17] DEADLINE file-agent: antigravity-preview-05-2026 shuts down 2026-10-05 (2 days) Contract diff (MOCK claude-sonnet-5 -> MOCK claude-sonnet-5-5) same output schema same tool call format BROKE progress text between tools user sees nothing before the tool call (empty thinking block) BROKE forced tool use 400 tool_choice: type "tool" and "any" are not supported for this model. BROKE thinking off (fast path) 400 invalid_request_error: use "between_tools" same token limit stop reason same refusal behavior GATE: BLOCKED (4 known breaks, 3 contract regressions) Exit code 1. CI stops. Look at `progress text between tools`. No 400. The user just stops seeing progress. **The quiet break is the one a status code will never catch.** The docs name the fixes: `between_tools`, `auto` plus strict tool use, `low` instead of `none`, and the new Antigravity tool names. ## Where It Breaks Down ### Mocks Drift I copied the rules by hand. They also differ by platform: `computer_20251124` is rejected on the Claude API and Google Cloud, but Sonnet 5.5 still accepts it on Amazon Bedrock. Run the contracts against the real API before trusting a green gate. ### Behavior Isn't a Contract Anthropic says Sonnet 5.5's "effort levels are recalibrated." A schema check can't see that. Evals can. ### State Needs Real Conversations Thinking blocks are tied to the model, the conversation and the account. On newer accounts, replaying one after editing history can return a 400. Single requests miss that. ## The Bigger Idea We already treat libraries this way. Pin the version. Read the changelog. Run the tests. Then upgrade. Models get a string change and a hopeful deploy. ┌──────────────────────────────────────────────┐ │ Upgrade gate │ │ │ │ Release notes ──→ Known breaks ──┐ │ │ ↓ │ │ Current ──→ Contracts ──→ Diff ──→ Gate │ │ Candidate ──→ Contracts ──┘ │ └──────────────────────────────────────────────┘ The model provides capability. The release notes provide warnings. The contracts provide expectations. The diff provides evidence. The gate provides a decision. Three vendors, four releases, twelve days. I think upgrade gates become as normal as lockfiles. That part is prediction, not history. **A new model is a new dependency. Ship it like one.** ## **Your code has a history. Helix makes it understandable.** I'm building Helix so every change, including a model upgrade, comes with evidence: what changed, why, and what it touched. **Connect your GitHub and see what your code knows.** Explore Helix →
dev.to
October 3, 2026 at 10:04 PM
Novinky v .NET 11 – díl 2/3 [Robert Haken, Vzdělávací okénko, 25.9.2026]

Co přináší .NET 11 kromě C# 15? Runtime async, nová API v BCL, asynchronní validace a novinky v Blazoru a ASP.NET Core. Runtime async – async/await řízený runtimem a čistší call stacky JIT optimalizace a CoreCLR místo Mono…
Novinky v .NET 11 – díl 2/3 [Robert Haken, Vzdělávací okénko, 25.9.2026]
Co přináší .NET 11 kromě C# 15? Runtime async, nová API v BCL, asynchronní validace a novinky v Blazoru a ASP.NET Core. Runtime async – async/await řízený runtimem a čistší call stacky JIT optimalizace a CoreCLR místo Mono pro WebAssembly Process.RunAndCaptureTextAsync, StringStream, Rune, Base64, Zstandard System.Text.Json: PascalCase, naming policy per property, union typy LINQ FullJoin, EqualityComparer.Create, TryParsePartial Asynchronní validace v DataAnnotations, Blazoru i Minimal API&hellip;
knowledge-base.havit.cz
September 29, 2026 at 5:11 AM
setting a calendar event now for the day i can camelcase instead of pascalcase
September 27, 2026 at 10:56 PM
Is it PascalCase (capitalise the first letter of each word), but with an added underscore to separate them?

It's a commonly used naming convention in Oracle PL/SQL, which is derived from Ada.
September 27, 2026 at 11:32 AM
Capitalization affects how people read hashtags or how people hear them on screen readers. Use #camelCase or #PascalCase in hashtags instead of lowercase. You could have #DoctorWhoRewatch ("Doctor Who Rewatch") or #doctorwhorewatch ("doctor whore watch.")
September 20, 2026 at 11:42 PM
Also just now realising that StarCraft is probably the reason I like PascalCase so much, aesthetically.
September 19, 2026 at 6:43 PM
Yeah, team lead, your genAI-generated slop PR really needed those 200ish lines to force PascalCase on the React app data models to match the C# backend DTOs.

*deletes*
September 15, 2026 at 7:18 PM
[Git] 大文字小文字だけのリネームでマージが失敗する原因と対処法
## 背景 コンポーネント名の命名規則(PascalCase)に合わせて、ファイル名の大文字小文字だけを直すリネームをすることがある。中身は一切変えていないので何の問題もないはずだが、このリネームを取り込む`git merge`や`git pull`が原因不明のエラーで止まることがあった。原因と対処法をまとめる。 ## 環境 * macOS(ファイルシステムはAPFS。大文字小文字を区別しない設定がデフォルト) * Git 2.55.0 ## 状況 `userProfile.tsx`というファイル名をコンポーネントの命名規則に合わせて`UserProfile.tsx`にリネームし、別ブランチへマージしようとしたところ、以下のエラーで止まった。 error: The following untracked working tree files would be overwritten by merge: UserProfile.tsx Please move or remove them before you merge. Aborting `UserProfile.tsx`はGitで管理しているファイルのはずなのに、「未追跡ファイル(untracked)」だと言われる。しかも大文字小文字を変えただけであり、`ls`で見ても「ファイルはちゃんとある」ようにしか見えず、何が起きているのか分かりにくい。 ## 原因 macOSの既定のファイルシステム(APFS)は大文字小文字を区別しない(case-insensitive)。Gitはリポジトリの作成時にこれを検出し、`core.ignorecase`を自動で`true`に設定する。公式ドキュメントでは、この設定を次のように説明している。 > Internal variable which enables various workarounds to enable Git to work better on filesystems that are not case sensitive, like APFS, HFS+, FAT, NTFS, etc. つまり`core.ignorecase`は、大文字小文字を区別しないファイルシステムの上でGitをうまく動かすための「回避策」の集合であり、Gitが本来前提とする大文字小文字を区別する世界と、OS側の区別しない世界とのずれを埋め合わせる仕組みである。この埋め合わせは常に完全とは限らない。そのため、大文字小文字だけのリネームをマージで取り込む際に、ワークツリー上の実ファイルとGitの追跡状態にずれが生じる余地を残す。 なお同じエラーメッセージは、大文字小文字のリネームに限らず、ローカルに上流と同名の未追跡ファイルが存在するだけでも発生する(GitHubのDiscussionでも報告されている)。 ### 再現性の確認 対処法をまとめる前に、最小構成で再現するか確認した。 # ブランチを分けて片方だけファイルをリネームし、もう片方には無関係な変更を加えてからマージする git init -b main repo && cd repo echo "export const UserProfile = () => null;" > userProfile.tsx git add userProfile.tsx && git commit -m "add userProfile.tsx" git branch feature git checkout feature git mv userProfile.tsx UserProfile.tsx git commit -m "rename to UserProfile.tsx" git checkout main echo "readme" > README.md git add README.md && git commit -m "add README" git merge feature 手元のGit 2.55.0では、この手順は問題なくマージが成功した。ブランチの往復や、`merge.autostash`を有効にしてコミットしていない変更が残った状態での`merge`も試したが、いずれも再現しなかった。 つまりこのエラーは「別ブランチでリネームしてマージするだけ」では起きず、作業ツリーに何らかの状態(古いcasingの実ファイルが複数ヵ所に残っている、複数回のブランチ切り替えを経ているなど)が重なったときに発生すると考えられる。確実な再現条件は特定できていないため、以降は発生したときの対処法として書く。 ## 対処法 ### 1. 内容差分を確認する 対処の前に、リネーム前後で中身が本当に同じかを必ず確認する。ここを省略すると、リネームだと思っていた変更が実は内容の変更も含んでいた場合にデータを失う。 # マージ先に取り込まれる内容と手元の実ファイルを比較する git show feature:UserProfile.tsx > /tmp/incoming.tsx diff /tmp/incoming.tsx UserProfile.tsx 差分がなければ、純粋なcasingの変更だと確認できる。 ### 2. 取り込む側に独自の変更がない場合 いま手元にある実ファイルを一時退避してから、マージし直す。 mv UserProfile.tsx /tmp/UserProfile.tsx.bak git merge feature ここで`git pull`ではなく`git merge <branch>`を直接使う。`pull.rebase`や`merge.autostash`を有効にしている環境では、`git pull`が追跡中の変更を自動で`stash`してからマージし、完了後に戻す。この`stash pop`は、直前に退避した古いcasingの実ファイルを作業ツリーへ復元してしまうため、同じcasingの衝突を再び招くことがある。 マージがfast-forwardにならず、空のマージコミットだけが余分にできてしまった場合は、以下でブランチを追跡先にそろえ直す。 # ローカルブランチを追跡先のコミットへ付け直す git checkout -B feature origin/feature ### 3. 取り込む側にも独自の変更がある場合 取り込む側(マージする側)のブランチ自身に、そのファイルへの変更がすでにある場合は、退避ではなく「先に同じリネームを済ませる」方法を取る。 # 両ブランチのcasingを先に揃えてからマージする git mv -f userProfile.tsx UserProfile.tsx git commit -m "fix: casingをコンポーネント名に合わせる" git merge feature --no-edit 両ブランチのcasingをそろえてからマージすると、Gitは双方を「同じファイルへの変更」として扱えるようになり、casingそのものの競合は起きなくなる。中身の変更がある側は、通常のマージと同じように採用される。 ## まとめ * 原因は`core.ignorecase`が埋め合わせている大文字小文字の差である。同じエラーメッセージ自体は、上流と同名の未追跡ファイルがあるだけでも発生し、GitHub上でも報告がある * ただし最小構成では再現せず、発生条件は作業ツリーの状態に依存すると考えられる * 発生したら、内容差分の確認 →`git merge`の直接実行 → そろわない場合は両ブランチでcasingをそろえてからマージ、の順で対処する ## 参考 * git-config Documentation - core.ignoreCase * git pull error: The following untracked working tree files would be overwritten by merge · community · Discussion #62958
b.0218.jp
September 16, 2026 at 3:04 AM
Nushell also tries to solve this problem. But I just write my scripts in TypeScript. My roots are there, after all. I'm biased against PowerShell because I don't like too much PascalCase.
github.com/google/zx is nice. Bun has a built-in alternative. github.com/dsherret/dax for Deno
September 8, 2026 at 5:56 PM
Capitalization affects how people read hashtags or how people hear them on screen readers. Use #camelCase or #PascalCase in hashtags instead of lowercase. You could have #DoctorWhoRewatch ("Doctor Who Rewatch") or #doctorwhorewatch ("doctor whore watch.")
September 8, 2026 at 12:17 AM
If you have a hashtag with multiple words, write the hashtag in #PascalCase or #camelCase to help users of screen readers. That helps the screen reader to read out the words out individually, rather than trying to read them in one long word.
August 30, 2026 at 12:09 AM
Blocked for using PascalCase. Justified
August 29, 2026 at 4:20 PM
An easy way to make posts more accessible is to use #PascalCase in multi-word hashtags. The capitalization helps screen readers distinguish between the words.
August 27, 2026 at 3:53 PM
Capitalization affects how people read hashtags or how people hear them on screen readers. Use #camelCase or #PascalCase in hashtags instead of lowercase. You could have #DoctorWhoRewatch ("Doctor Who Rewatch") or #doctorwhorewatch ("doctor whore watch.")
August 22, 2026 at 2:43 AM
brought on by the fact that the worst part about godot is that it won't let me use PascalCase for everything like i wanna without getting very mad at me sometimes
August 20, 2026 at 9:09 PM
Naming conventions are funny

That's PascalCase.

This would be camelCase.
August 19, 2026 at 4:48 AM
自分が関数や変数を命名するとき、最近触れたプログラミング言語や環境に引っ張られる気がする
(特に"今の言語"で命名規則が言語レベルで決まってないとき。Bashとか)

Javaに触れた直後はしばらく関数/メソッド名にlowerCamelCase使ってたし
Win32やC#のあとはUpperCamel/PascalCase使ってたし
Rustのあとはsnake_case使ってたし

…ってな感じで
August 18, 2026 at 3:59 PM
😬

It's conventions for writing variable names

snake_case
camelCase
PascalCase
August 16, 2026 at 8:02 PM
🙏

Evening Bakerhelm . . .

Since I been coding again I get so tempted to write Bakerhelm in PascalCase as BakerHelm . . .
August 16, 2026 at 3:37 PM
criminal use of PascalCase instead of Whatever_this_is for constructors
August 9, 2026 at 7:44 PM
?? GDScript uses PascalCase for class names and snake_case for everything else, like a sensible language
August 8, 2026 at 2:04 PM
my snake_case ass seeing that Godot uses PascalCase
August 8, 2026 at 1:56 PM