Language-Agnostic Code Generation: The Driver Plugin Model

TestSmith generates test scaffolds for five languages: Go, Python, TypeScript, Java, and C#. Each language has its own project structure conventions, test frameworks, import styles, and code patterns. The naive implementation would be a big switch statement throughout the codebase. We chose a plugin model instead.

The Problem with Hardcoded Branches

When a codebase switches on language in multiple places, every new language requires touching every branch point. Miss one and you get a silent bug — the new language falls through to some default behavior that doesn’t apply to it. This is the classic Open-Closed violation: you have to modify existing code to extend it.

The LanguageDriver Interface

Every language in TestSmith implements a single interface:

type LanguageDriver interface {
    // Detection
    DetectProject(dir string) (*ProjectContext, error)
    FileExtensions() []string

    // Analysis
    AnalyzeFile(path string, ctx *ProjectContext) (*SourceAnalysis, error)
    ClassifyDependency(dep ImportInfo, ctx *ProjectContext) DependencyCategory
    DeriveTestPath(sourcePath string, ctx *ProjectContext) (string, error)
    DeriveModulePath(sourcePath string, ctx *ProjectContext) (string, error)

    // Generation
    GenerateTestFile(analysis *SourceAnalysis, opts GenerateOpts) (*GeneratedFile, error)
    GenerateFixture(dep string, analysis *SourceAnalysis, opts GenerateOpts) (*GeneratedFile, error)
    GenerateBootstrap(plan *GenerationPlan, ctx *ProjectContext) (*GeneratedFile, error)

    // Framework config
    GetTestFrameworkConfig() TestFrameworkConfig
    SelectAdapter(ctx *ProjectContext) TestAdapter

    // LLM integration
    LLMContext(ctx *ProjectContext) map[string]string
    LLMVocabulary() map[string]string

    // Migration and validation
    ListMigrators() []Migrator
    ValidateFile(path string, ctx *ProjectContext) ([]ValidationIssue, error)
}

The generation pipeline, the CLI commands, and the watch mode all work against this interface. They never import a specific driver package.

How Detection Works

When you run testsmith generate, the first step is figuring out what language you’re in. The registry tries each registered driver in turn:

func Detect(dir string) (domain.LanguageDriver, error) {
    for _, d := range drivers {
        ctx, err := d.DetectProject(dir)
        if err == nil && ctx != nil {
            return d, nil
        }
    }
    return nil, domain.ErrProjectNotFound
}

Each driver’s DetectProject walks upward from the starting directory looking for its own project markers — go.mod for Go, pyproject.toml or setup.py for Python, package.json for TypeScript, pom.xml or build.gradle for Java, .csproj or .sln for C#.

One subtle requirement: a driver must not claim an ancestor project that belongs to a different language. If you run TestSmith from inside an example project that lives inside a Go repo, the Python driver shouldn’t walk up past the Go project’s .git boundary and claim the repo root. We solve this by checking VCS stop markers (.git, .hg, .svn) at ancestor directories only — not at the starting directory itself, since a legitimate project root can have both a project marker and a .git directory.

func findRoot(startDir string) (string, error) {
    dir := startDir
    for {
        // At ancestor dirs, stop at VCS boundaries first.
        if dir != startDir {
            for _, stop := range stopMarkers {
                if _, err := os.Stat(filepath.Join(dir, stop)); err == nil {
                    return "", domain.ErrProjectNotFound
                }
            }
        }
        // Then check for project markers.
        for _, marker := range rootMarkers {
            if _, err := os.Stat(filepath.Join(dir, marker)); err == nil {
                return dir, nil
            }
        }
        parent := filepath.Dir(dir)
        if parent == dir {
            break
        }
        dir = parent
    }
    return "", domain.ErrProjectNotFound
}

The Adapter Layer

Within a language, there can be multiple test frameworks. TypeScript has Jest, Vitest, and Mocha. Java has JUnit 4, JUnit 5, TestNG, and Spring Boot Test. Each framework has its own import style, mock library, assertion syntax, and file naming conventions.

We model this with a TestAdapter interface:

type TestAdapter interface {
    Name() string
    FileNamingConvention() FileNaming
    ImportStyle() ImportStyle
    MockLibrary() string
    AssertionStyle() string
    LLMVocabulary() map[string]string
}

Each driver has a registry of adapters and a SelectAdapter method that reads the project config (or sniffs package.json devDependencies, pom.xml dependencies, etc.) to pick the right one. The LLM prompt gets the vocabulary from the selected adapter — so the model knows to generate expect(x).toBe(y) for Jest but assert.Equal(t, x, y) for Go’s testify.

Adding a New Language

Because everything flows through the interface, adding a new language driver is isolated:

  1. Create a new package under internal/drivers/<lang>/
  2. Implement domain.LanguageDriver — the compiler tells you exactly what’s missing
  3. Register it in internal/registry/registry.go
  4. Optionally add a Verifier in internal/generation/verify.go for post-write compile checking

No other files change. The existing drivers are untouched. The pipeline, CLI, and watch mode pick it up automatically.

The Dependency Direction

The plugin model enforces a strict dependency direction:

cmd → generation → domain ← drivers
                           ← llm

domain defines the interfaces. drivers implement them. generation uses them via the interface. Neither generation nor drivers imports the other. This is the Dependency Inversion Principle applied at the package level — and it’s enforced by Go’s import cycle detector.

When you add a new driver, it’s impossible to accidentally reach into the generation pipeline or the LLM layer — Go won’t compile it. The architecture is self-enforcing.

Next in this series: making LLM calls reliable when you’re hitting them for every public member of every source file.

Why We Rewrote Our Python CLI in Go (and What We Gained)

TestSmith v1 was a Python CLI. It worked. Users could pip install testsmith, point it at a source file, and get a test scaffold back. But every team that tried to wire it into CI hit the same wall: Python environments.

The problem wasn’t Python itself — it was distribution. A static analysis tool that requires a matching Python version, a virtual environment, and a pinned dependency tree is a hard sell for a step that runs on every push. We were shipping a tool, not a library. Tools should be frictionless.

The Decision

We rewrote TestSmith v2 in Go. The goal was a single static binary with no runtime dependencies — something you could drop into any CI runner, any Docker image, any developer’s PATH, and it would just work.

Go was the right choice for three reasons:

Single binary. go build produces one self-contained executable. No pip, no venv, no requirements.txt. Users download a binary or brew install it and they’re done.

Cross-platform with one build. The v1 CI matrix was a headache — different Python versions across Ubuntu, macOS, and Windows, with slightly different behavior on each. Go’s cross-compilation gave us linux/amd64, darwin/amd64, darwin/arm64, and windows/amd64 from a single build step.

Native concurrency. Test generation is embarrassingly parallel — each file is independent. Go’s goroutines and channels made the fan-out generation and the debounced file watcher straightforward to implement without pulling in async libraries.

What Changed

The command surface was cleaned up in the same pass. v1 used flags for everything:

testsmith <file>         # generate
testsmith --all          # generate all
testsmith --graph        # dependency graph
testsmith --prune        # prune stale fixtures
testsmith --watch        # watch mode

v2 uses proper subcommands:

testsmith generate <file>
testsmith generate --all
testsmith graph
testsmith prune
testsmith watch

This made shell completion, help text, and per-command flags much cleaner. Cobra’s built-in completion generator gives us bash, zsh, fish, and PowerShell completion for free.

Architecture Change

v1 was monolithic Python — one codebase with hardcoded branches for each language it supported. Adding a new language meant editing multiple core files.

v2 uses a LanguageDriver interface. Each language (Go, Python, TypeScript, Java, C#) is a separate package that implements the interface. The core generation pipeline never knows which language it’s dealing with — it just calls through the interface:

type LanguageDriver interface {
    DetectProject(dir string) (*ProjectContext, error)
    AnalyzeFile(path string, ctx *ProjectContext) (*SourceAnalysis, error)
    DeriveTestPath(sourcePath string, ctx *ProjectContext) (string, error)
    GenerateTestFile(analysis *SourceAnalysis, opts GenerateOpts) (*GeneratedFile, error)
    // ... and more
}

Adding a new language is now a matter of creating a new package and registering it — no changes to the pipeline.

What We Kept

The v1 Python package didn’t disappear. It lives in archive/v1/ and continues to receive bug fixes during the transition period. Teams already using v1 in production don’t need to migrate immediately. The v2 binary is a clean break for new users; v1 stays stable for existing ones.

Was It Worth It?

Yes, unambiguously. The CI story went from “install Python, set up venv, pin deps” to “download one binary.” The Windows test matrix went from flaky (Python path issues) to clean. And the plugin architecture means we can add a Ruby or Rust driver without touching the core generation logic.

The rewrite took longer than a feature would have. But distribution friction is a silent project killer — nobody files a bug for “this was annoying to set up,” they just stop using the tool.

TestSmith is an open-source CLI for generating test scaffolds across Go, Python, TypeScript, Java, and C#. The source is at github.com/orieken/testsmith.

Why We Built TestSmith: The Test Coverage Problem Nobody Talks About

Every team I’ve worked on has had the same conversation at some point. Someone opens the coverage report, sees a sea of red, and asks: “How do we get this up?” The answer is always some version of “we need to write more tests,” followed by a long silence, because everyone knows what that actually means — hours of boilerplate, test file setup, mock wiring, and fixture scaffolding before you’ve written a single meaningful assertion.

That’s the problem TestSmith was built to solve.

The Real Bottleneck Isn’t Willingness

Developers generally want to write tests. The resistance isn’t laziness — it’s the setup cost. For every new module you want to test, you have to:

  • Create the test file in the right location with the right naming convention
  • Import the module under test
  • Import the test framework and any mock libraries
  • Set up fixtures for external dependencies
  • Write the boilerplate class or function structure that the framework expects
  • Then, finally, write the actual test logic

For a well-understood module with clear inputs and outputs, steps 1 through 5 can easily take longer than step 6. You’re doing janitorial work before you can do the meaningful work. And if you’re adding coverage to a large existing codebase — the kind of coverage catch-up project every team eventually faces — you’re doing that setup dozens or hundreds of times.

Why Python First

We wrote the first version of TestSmith in Python for the most straightforward of reasons: our immediate problem was a Python codebase.

But Python also happened to be a good fit for the tool itself. Python’s AST module is excellent — ast.parse() gives you a full parse tree in a few lines, and walking it to extract class names, function signatures, and import statements is straightforward. For a tool that needs to understand source code structure without actually running it, static AST analysis is exactly right, and Python’s standard library makes it easy.

import ast

tree = ast.parse(source_code)
for node in ast.walk(tree):
    if isinstance(node, ast.FunctionDef):
        if not node.name.startswith('_'):  # skip private
            public_functions.append(node.name)

The other reason was speed of iteration. We were solving our own problem — we needed the tool to work on Python projects, and we were Python developers. Building it in Python meant we could use it on itself from day one, which is a useful forcing function for catching rough edges.

What the Tool Actually Does

The core idea is simple: given a source file, generate the test scaffold that you’d write by hand.

For a Python service like this:

# src/services/payment.py

class PaymentService:
    def __init__(self, stripe_client, db):
        self.stripe = stripe_client
        self.db = db

    def process_payment(self, order_id: str, amount: int) -> dict:
        ...

    def refund(self, payment_id: str) -> bool:
        ...

TestSmith generates:

# tests/services/test_payment.py

import pytest
from unittest.mock import MagicMock, patch
from src.services.payment import PaymentService


@pytest.fixture
def stripe_client():
    return MagicMock()


@pytest.fixture
def db():
    return MagicMock()


@pytest.fixture
def payment_service(stripe_client, db):
    return PaymentService(stripe_client=stripe_client, db=db)


class TestPaymentService:
    def test_process_payment(self, payment_service):
        # TODO: implement
        pass

    def test_refund(self, payment_service):
        # TODO: implement
        pass

It’s not a complete test. It’s the scaffold — the file is in the right place, the imports are correct, the fixtures for the constructor dependencies are wired up, and the test methods exist. The developer fills in the assertion logic. The janitorial work is already done.

The tool also handles things that are easy to get wrong: where test files should live relative to source files (which varies by framework and project convention), how to name fixtures based on constructor parameters, which mock library to use, and how to structure the test class if the source is class-based vs. the test functions if it’s function-based.

The Gap Analysis Problem

Coverage reports tell you what’s untested, but they don’t prioritise it. A file with three simple utility functions and a file with a complex payment processing pipeline both show up as “uncovered.” Knowing which one to tackle first requires reading the code.

TestSmith added a coverage gap command that went a step further: it computed a coupling score for each untested module based on how many other modules imported it. A module imported by ten others is higher priority than one imported by none — because a bug in the heavily-imported module has a wider blast radius.

$ testsmith gaps

Coverage gaps (by coupling score):

  src/services/payment.py        coupling: 8   functions: 5   ← fix this first
  src/utils/currency.py          coupling: 6   functions: 3
  src/models/order.py            coupling: 4   functions: 7
  src/scripts/backfill.py        coupling: 0   functions: 12

This gave teams a principled answer to “where do we start?” rather than requiring someone to manually audit the codebase.

What v1 Didn’t Do Well

The tool worked. Teams used it and got value from it. But two things became clear over time.

Distribution was painful. pip install testsmith sounds simple, but in practice it meant managing Python versions, virtual environments, and dependency conflicts — especially in CI. A testing tool that requires its own setup to work in CI is fighting against itself.

One language wasn’t enough. Once word got around that the tool existed, the first question from every team was “does it work for TypeScript?” or “can it do Java?” The Python-only design wasn’t a deliberate choice — it was an artifact of solving our own immediate problem. But the architecture didn’t make adding languages easy. Every language-specific piece of logic was woven through the core code rather than isolated.

Those two problems drove the v2 rewrite in Go: a single static binary that drops into any environment, and a plugin architecture where each language is an isolated driver.

But that’s the next post.

TestSmith is open source at github.com/orieken/testsmith. The v1 Python package is archived at archive/v1/ for reference.

Building a local-first AI tutor for my daughter (and 10–14 year-olds in Austrian schools) with Gemma 4

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

My daughter is 13. Like most students her age in Austria, she has an iPad.
Like most parents, I’m uncomfortable about her typing homework into
ChatGPT — not because the answers are wrong, but because everything she
types disappears into a cloud I don’t control. Names, schools, half-formed
thoughts, the topics where she’s struggling. Stuff I want to stay on her
device.

So I built Lernbuddy. It’s a study companion for ages 10–14 that runs
entirely on-device with Gemma 4 E4B. No network calls. No telemetry.
The model lives next to the kid’s flashcards and her review history, on
the same disk, and that’s it.

What it does

Three things, all driven by Gemma 4 E4B:

  • Socratic chat. She asks a question, the model gives a hint — never
    the answer. The system prompt is built around concrete praise
    (“nice, you spotted that fractions need a common denominator”) and
    refusing to solve problems directly.
  • Flashcards. Paste a school text → cards. Or just type a topic
    (irregular verbs, European capitals) → the model pulls from general
    knowledge and creates a deck. Every card lands in a preview where you
    can edit, delete, or add more before they go in the database.
  • Quiz with model-validated answers. Typed answers, not multiple-
    choice. The model reads the correct answer and the kid’s answer and
    decides correct / almost / incorrect — spelling errors and
    paraphrasing are tolerated. It even writes a short personalized summary
    at the end: “You handled past participles well. ‘choose’ and ‘speak’
    got mixed up — they’ll come back tomorrow.”

Behind the quiz sits a small SM-2 spaced-repetition scheduler. Cards she
gets wrong come back the next day; cards she gets right keep slipping
further out. A streak counter and a per-topic “✓ 12 solid · … 4 to
practise” badge make progress visible without dashboards.

Architecture

The whole thing is a .NET 9 MAUI app — one codebase, four targets
(Windows, Android, iOS, macCatalyst), bilingual from day one (German is
the primary audience, English the default for international demos).

Inference goes through Microsoft.Extensions.AI‘s IChatClient
interface. The concrete implementation wraps LLamaSharp running
unsloth/gemma-4-E4B-it-GGUF (Q4_K_M quant, ~4.6 GB). Gemma 4’s chat
template — <|turn>rolen...<turn|> — gets built by hand; the embedded
jinja in the GGUF carries tool-calling logic the kid will never need.

A singleton InferenceStatus service publishes “Loading model… 4.6 GB”,
“Generating… 23 tokens (4.1/s)” to a small banner that’s visible on every
page. When prompt-processing takes 30 seconds before the first token (CPU
is slow on long prompts), the banner still ticks. No dead spinners —
that matters more than raw throughput when you’re 11 and waiting.

Long source texts get split by paragraph and sentence into
~500-character chunks. Cards appear rolling instead of waiting two
minutes for one mega-prompt. Total throughput is slightly worse;
perceived time is much better.

The privacy story

The project’s privacy contract starts with “local first”: no PII in
prompts, parent-PIN gate on any future cloud mode, model download only
over WiFi with explicit consent. The current app makes zero outbound
network calls after install. The only thing that ever leaves the device
is what you type into a Gemini API form — and we don’t have that form.
By design.

That’s the differentiator. There are dozens of “ChatGPT for kids” apps;
almost none of them can credibly say “your child’s homework never leaves
their device.”

What I learned

I went down three runtime rabbit-holes before things stuck. ONNX Runtime
GenAI doesn’t support the Gemma 4 multi-file ONNX split yet.
transformers.js + WebGPU inside a MAUI HybridWebView is theoretically
beautiful and practically a stack of four bleeding-edge components
silently failing in ways nothing surfaces. LLamaSharp + GGUF works —
except LLamaSharp doesn’t ship iOS-arm64 natives. So I built llama.cpp
myself for iOS on a Mac, statically linked against the resulting .a
files via MAUI’s NativeReference mechanism, and pointed
[DllImport("llama")] at __Internal via a runtime resolver.

The lesson: for a multi-platform local-LLM .NET project in 2026, pick the
runtime that has the deepest community, not the one with the prettiest
abstraction. llama.cpp has a community measured in thousands; the .NET
wrappers around it inherit that depth almost for free.

Try it

Code: https://github.com/gpiwonka/Lernbuddy

more: https://piwonka.cc/Lernbuddy

License: MIT. Built solo, on the side, between a day job and a tired
father’s evenings. PRs welcome — especially from teachers who can point
out what’s missing from a learning-science perspective.

The submission is for the Gemma 4 Challenge. The motivation is more
personal.

EC2 SSH Not Connecting? Here Are the 5 Things That Were Wrong (And How I Fixed Them)

I spent 3 hours staring at this error my first week with AWS:

“Permission denied (publickey)”

My EC2 instance was running. My terminal was open. I’d followed a tutorial step by step. And still — nothing.

Here are the 5 things that were wrong (and how I fixed each one):

  1. Wrong .pem file

I had downloaded the key pair but was pointing to the wrong file path in my command. Sounds obvious. When you’re new, it’s not.

Fix: always use the full absolute path.
ssh -i ~/Downloads/my-key.pem ec2-user@

  1. Wrong username

Different AMIs use different default usernames. I kept typing “ubuntu” on an Amazon Linux instance.

Amazon Linux → ec2-user
Ubuntu → ubuntu
RHEL → ec2-user or root
Debian → admin

This one catches almost everyone.

  1. .pem file permissions too open

SSH refuses to use a key file that other users can read.

Fix: chmod 400 my-key.pem

Run that once and you’ll never see “WARNING: UNPROTECTED PRIVATE KEY FILE!” again.

  1. Security group blocking port 22

I’d launched the instance but never added an inbound rule for SSH. The connection wasn’t being refused — it was being silently dropped at the firewall level.

Fix: go to EC2 → Security Groups → Inbound Rules → add SSH (port 22) from your IP. Not 0.0.0.0/0 — just your IP. Keep it tight.

  1. Using the wrong IP

I was copying the Private IP from the console instead of the Public IPv4 address. Private IPs only work if you’re inside the same VPC.

Fix: use the Public IPv4 DNS or Public IP shown in the instance details.

The honest truth: none of these errors showed up in the tutorial I was following. Tutorials show the happy path. Real learning happens when something breaks and you have to figure out why.

If you’ve hit any of these — you’re not alone. Every person working in cloud has been here.

Which one got you the longest? Drop it in the comments.

Best AI Tools for HVAC Contractors 2026

A 10-truck HVAC shop misses 3 calls per day. Average ticket runs $1,205. Industry close rate on inbound sits around 60 percent. Run the math across 30 days and you land at $65,070 of lost revenue every month. Call it $65K for clean accounting. Not because the work was bad. Not because the price was wrong. The phone just rang and nobody picked up.

That number does not show up on your P&L. It does not get tracked in the AR aging report. It does not get tracked anywhere. The leads call the next company on Google and you never know they existed.

Every AI tool getting marketed to HVAC right now claims to solve some version of this problem. Most of them are generic SMB software with an HVAC sticker on the box. A few are built for the trade. A smaller number actually move money. Here is the breakdown.

What we filtered out and why

We tested every AI tool targeting HVAC contractors over the last 12 months. Some on production calls. Some by demo. Some by talking to operators who paid the bill and have receipts.

This is not a sponsored list. NeverMiss is mine. I will tell you what it does well and where it falls down against the alternatives. The goal is not to sell you. The goal is to save you 40 hours of demo calls and 6 months of bad procurement decisions.

Tools are organized by what they actually do, not alphabetically.

Category 1. AI Receptionists

Answering inbound calls is the single highest-leverage place to put AI in an HVAC shop. Every other system downstream feeds off captured calls. If you only do one thing on this list, it lives in this category.

NeverMiss

Built specifically for HVAC, plumbing, roofing, and electrical contractors. Over 10,000 inbound calls handled in production across home service shops. Pricing is flat-rate per location starting at $500 per month, no per-minute charges, no overage surprises. Native integrations with ServiceTitan, Housecall Pro, Jobber, and GoHighLevel through Make and Zapier. The trial loop is unusual. Visit nevermisshq.com/demo, enter your number, and the AI calls your phone in under 60 seconds for a live conversation. You hear it work before you book a sales call with anyone.

Pros

  • Trade-specific scripts that handle the questions that actually come in for HVAC. Capacitor versus contactor versus refrigerant leak language baked in.

  • After-hours emergency triage with rule-based escalation to your on-call tech.

  • Books appointments directly into your CRM dispatch board with caller details, address, and system type attached.

  • Real customer support staffed by operators who have run service businesses. Not a chatbot, not an offshore desk.

  • Flat-rate billing. No per-minute surprises when call volume spikes during a heat wave.

Cons

  • No native scheduling for jobs longer than 4 hours yet. Long installs still route to a human callback.

  • The voice catalog is solid but not unlimited. If you want a specific voice clone we will need a custom path.

  • Two-day build cycle from signed agreement to live. Faster than the 1 to 2 weeks the rest of the category quotes, but you do need to block the calendar for the calibration calls.

For most HVAC shops under 25 trucks, this is the most aligned option in the market. The deeper write-up sits at our AI receptionist page, and there is a side-by-side pricing comparison at ai-receptionist-pricing-comparison-2026.

Smith.ai

A general-purpose receptionist that started in legal and expanded into trades. Per-minute pricing model.

Pros. Long-established. Polished call handling. Decent transcription quality on summary reports.

Cons. Not trade-built. Scripts read generic. Per-minute pricing punishes shops with longer triage calls or volume spikes. No real CRM dispatch integration for HVAC platforms. Expensive at scale.

Avoca

AI receptionist focused on home service.

Pros. Solid product for larger operations. Strong back-end reporting and call review.

Cons. Enterprise pricing. Longer sales cycle. Less flexible if your workflows are not vanilla. Probably overkill for shops under $3M.

Goodcall

SMB AI receptionist.

Pros. Cheap entry tier. Easy to spin up.

Cons. Not trade-specific. Weak CRM integration. Closer to a smart voicemail than a real receptionist. Customers who hit it during after-hours often hang up.

Hear NeverMiss before you buy

Visit the demo page, enter your phone number, and the AI calls you in under 60 seconds. Same agent that handles production calls for HVAC shops in 4 states.
Try the demo

Category 2. AI Consulting and Custom Builds

Vaught AI

Custom operational software and private AI infrastructure for HVAC contractors doing $1M to $10M in revenue. Founded by Joey Vaught, who has spent years building bespoke automation for trade and industrial operators.

This is the tier above off-the-shelf. You pick this when your workflows are weird, when you have integrations between 5 or more tools, or when you want to own the AI stack rather than rent it from a vendor.

Pros. Bespoke builds, you own the IP, fast iteration cycles, private infrastructure means your data and prompts stay yours.

Cons. Real investment. Custom pricing, engagements typically $15K to $50K. You need internal ops capacity to feed the build process. Not the answer if you have not maxed out off-the-shelf tools first.

Worth reading the trade-specific breakdown at Vaught AI for HVAC and electrical contractors if you are above $1M and feel boxed in by your current stack.

Category 3. FSM Platform AI Features

ServiceTitan Atlas

The AI suite layered onto ServiceTitan. Call summarization, AI-assisted dispatching, and a few other features that ride on top of the core ST account.

Pros. Deep integration with your existing data. No extra system to manage. If you already pay ST, marginal cost is low.

Cons. Only useful if you are on ServiceTitan. Locked behind ST pricing and release cycles. Slower to ship new AI features than focused vendors.

Housecall Pro CSR AI

Voice agent inside the HCP platform.

Pros. Free or low cost if you already run HCP. Zero setup friction.

Cons. Limited customization. Generic call handling. No flexibility for shops with unusual call types like commercial maintenance contracts or new construction.

Jobber Receptionist

Jobber-native receptionist offering.

Pros. Zero setup if you live in Jobber. Fine for small operations.

Cons. Basic feature set. Hard to extend or customize.

When FSM-native AI makes sense versus a custom layer on top. If you run on one platform exclusively and your call volume is under 200 per month, the FSM-native option is usually fine. Once you push above that, run multiple platforms, or have customers who need long emergency triage conversations, you need something built outside the FSM. See the full integration landscape at our integrations page.

Category 4. Quoting and Estimate Tools

The quoting space is younger than the receptionist space. Most of what gets marketed as AI quoting today is templated estimate generation with a chatbot bolted on the front. The genuinely useful tools work at the photo-and-spec stage in the field.

Mobile estimate platforms with AI assist

Most of the FSM platforms now include some flavor of AI-assisted quote generation in their mobile app. ServiceTitan, Housecall Pro, and Jobber all have a version. They speed up techs on close but they still depend on the inputs the tech provides.

Pros. Faster quotes in the truck. Fewer arithmetic mistakes.

Cons. Garbage in, garbage out. The AI does not improve a tech who skips system inspection. Field training matters more than the tool.

Category 5. Follow-up and Marketing AI

GoHighLevel

All-in-one marketing and follow-up automation platform.

Pros. Huge feature set. Decent AI automation built in. Agency-friendly white label.

Cons. Bloated. Steep learning curve. If you buy through a reseller you pay an agency tax. Setting it up properly is a project, not a weekend job.

Hatch

Lead nurture for home services.

Pros. Built for trades. Easier setup than GoHighLevel.

Cons. Another seat license to manage. Limited compared to a fully integrated stack.

The thing most shops get wrong with follow-up AI is plugging it in before the call capture problem is solved. If 35 percent of your inbound never gets answered, automated follow-up to nothing is still nothing. See the breakdowns at lead follow-up and missed call text-back.

Comparison table

The five tools most worth considering for HVAC contractors, side by side.

Tool Best for Pricing model HVAC-specific FSM integrations
NeverMiss Trade shops under 25 trucks Flat-rate from $500/mo per location Yes, built for the trade ServiceTitan, HCP, Jobber, GoHighLevel
Smith.ai Mixed legal and SMB use cases Per-minute No, generic Limited
Avoca Larger home service operations Enterprise tier Yes, home service focus ServiceTitan
Vaught AI $1M to $10M custom builds Custom, $15K to $50K engagement Yes, HVAC and electrical Bespoke per project
ServiceTitan Atlas Existing ST customers Included in ST plan Indirectly ServiceTitan only

What to measure once you have a tool installed

The mistake operators make after installing an AI receptionist is treating it as set-and-forget. The numbers move week to week and you need to know which ones to watch.

Answer rate. Percent of inbound that gets answered. Pre-AI this is often 60 to 70 percent for HVAC shops. With a proper receptionist this should sit at 98 percent or better. If you are below 95, something is misconfigured.

Booking rate on AI-handled calls. The AI does not just answer. It books. Track what percent of AI-handled calls end with an appointment on the calendar. 40 to 55 percent is a healthy band depending on call type mix. Lower than 35 means your scripts need work.

Average handle time. AI calls should run between 90 seconds and 4 minutes. If they are dragging past 5 minutes regularly, the agent is fumbling intent recognition and needs tuning.

Escalation accuracy. When the AI routes a call to your on-call tech, did the call actually need a human. Aim for 90 percent precision. Too many escalations and your tech burns out. Too few and emergencies get missed.

How to choose, by shop size

Frame the decision by revenue, not feature count.

Under $1M revenue. Start with an AI receptionist. Your bottleneck is missed calls, not capacity. Solve the call leak first and the rest follows. NeverMiss at $500 per month pays for itself the first week you book one extra job.

$1M to $5M. Receptionist plus quoting plus follow-up. Your office staff becomes the bottleneck around this stage. Layer the AI to handle the volume so your people focus on the high-value conversations. This is where the lead follow-up automation starts to compound.

$5M to $10M. Add the FSM-native AI features if you are on ServiceTitan or HCP. You are paying for the platform anyway. Stack the trade-built receptionist on top for the layer the FSM cannot do well. This is also Vaught AI tier, custom infrastructure that fits your specific workflows.

$10M and up. Custom build territory. You have unique workflows, multi-location complexity, and probably your own internal systems. Own your stack instead of renting it.

The most common mistake. Buying tools out of order. Receptionist first. Always. The leak in front of every other system is the unanswered call. Plug that. Then move to the next.

More on the trade-built receptionist setup at https://nevermisshq.com/contractors/

Scrumpan is offlically LIVE!!

Today, Scrumpan officially went live 🚀

Scrumpan is an AI-powered Agile collaboration platform built for modern software teams.

The idea behind Scrumpan was simple:

Agile tools have become overloaded, fragmented, and exhausting to use. Teams spend too much time managing process instead of building great products.

We wanted to create something cleaner, faster, and more collaborative.
Scrumpan brings together:
• Planning Poker
• Retrospectives
• Daily Pulse async standups
• AI-powered Smart Summaries
• Sprint analytics
• Jira integration
• Real-time collaboration

And this is only the beginning.

We’re already working on:
• Smart Planner (AI-assisted planning)
• Azure DevOps integration
• Advanced collaboration workflows
• AI-powered productivity insights
• Enhanced planning canvas experience

Building Scrumpan has been an incredible journey of architecture decisions, realtime collaboration engineering, AI integrations, and countless iterations on UX.

Today marks the first public step.

Would love to hear feedback from engineering teams, Scrum Masters, Agile coaches, and product leaders.

Website: https://scrumpan.com

Building a BMI Calculator CLI with TypeScript — Types, Functions, and Vitest

Introduction

This is my first article as a Java engineer learning TypeScript from scratch.

I’ve been writing Java professionally, but I got interested in modern tech stacks and started learning TypeScript and Python. My approach is simple: build small projects one by one, and write honestly about everything — what I struggled with, what I figured out, and what I asked AI to help with.

This article is for people on a similar learning journey. It’s not a showcase of perfect code — it’s an honest record of the process.

My Learning Style (AI Transparency)

I use Claude Pro (for design discussions and Q&A) and Cursor Pro (for coding support) as learning companions.

However, I follow these rules for myself:

  • I write all the code myself — I never ask AI to write code for me
  • AI helps with hints, spec clarification, and bug spotting
  • I make sure I understand why something works before moving on

In this article, I clearly separate “what I implemented myself” from “what I asked AI for.”

What I Built

A CLI tool that calculates BMI from height and weight input.

$ npm start

Enter your height (cm): 170
Enter your weight (kg): 70
BMI result: 24.22 (Normal)

BMI Classification

BMI Label
< 18.5 Underweight
18.5 – 24.9 Normal
≥ 25.0 Obese

📦 Repository: https://github.com/uya0526-design/bmi-calculator

Project Structure

bmi-calculator/
├── src/
│   ├── index.ts              # Entry point / CLI I/O
│   ├── calculator.ts         # BMI calculation and classification logic
│   ├── types.ts              # Type definitions
│   └── __tests__/
│       ├── calculator.test.ts # Unit tests
│       └── types.test.ts      # Type tests (skipped with describe.skip)
├── package.json
├── tsconfig.json
└── LEARNING_LOG.md

Separating responsibilities by file made it much clearer where everything lived.

Tech Stack

  • TypeScript
  • Node.js (readline module)
  • Vitest (unit testing)

What I Implemented Myself

types.ts — Type Definitions

// Type aliases for height, weight, and BMI value
type Height = number;
type Weight = number;
type BmiValue = number;

// Union type for classification labels
type BmiLabel = "Underweight" | "Normal" | "Obese";

// Object type to hold the calculation result
export type BmiOutput = {
  bmi: BmiValue;
  label: BmiLabel;
};

The key decision here was using a union type for BmiLabel. Any string outside of "Underweight" | "Normal" | "Obese" causes a type error at compile time.

calculator.ts — Calculation Logic

import type { BmiOutput } from "./types";

function getBmiLabel(bmi: number): string {
  if (bmi < 18.5) return "Underweight";
  if (bmi < 25) return "Normal";
  return "Obese";
}

export function calculateBmi(height: number, weight: number): BmiOutput {
  const heightInM = height / 100;
  const bmi = weight / heightInM ** 2;
  const label = getBmiLabel(bmi);
  return { bmi, label };  // shorthand property notation
}

I decided not to export getBmiLabel since it’s only used internally — and I made that call myself.

index.ts — CLI Input/Output

import * as readline from "readline";
import { calculateBmi } from "./calculator";

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

function main() {
  rl.question("Enter your height (cm): ", (heightInput) => {
    if (isNaN(Number(heightInput))) {
      console.log("Please enter a number.");
      rl.close();
      return;
    }
    rl.question("Enter your weight (kg): ", (weightInput) => {
      if (isNaN(Number(weightInput))) {
        console.log("Please enter a number.");
        rl.close();
        return;
      }
      const result = calculateBmi(Number(heightInput), Number(weightInput));
      console.log(`BMI result: ${result.bmi.toFixed(2)} (${result.label})`);
      rl.close();  // ← position matters (see "Where I Got Stuck")
    });
  });
}

main();

calculator.test.ts — Unit Tests with Vitest

import { describe, it, expect } from "vitest";
import { calculateBmi } from "../calculator";

describe("calculateBmi", () => {
  it("BMI 18.49 → Underweight", () => {
    const result = calculateBmi(170, 53.5);
    expect(result.label).toBe("Underweight");
  });

  it("BMI 18.5 → Normal", () => {
    const result = calculateBmi(170, 53.52);
    expect(result.label).toBe("Normal");
  });

  it("BMI 25 or above → Obese", () => {
    const result = calculateBmi(170, 72.25);
    expect(result.label).toBe("Obese");
  });

  it("BMI calculation accuracy", () => {
    const result = calculateBmi(170, 70);
    expect(result.bmi).toBeCloseTo(24.22, 1);
  });
});

I deliberately chose boundary values (18.49 / 18.5 / 25) to verify the branching logic.

What I Asked AI For

Topic Details
Type design thinking Asked about the difference between tuple types and object types
tsconfig.json options Learned what module, target, and strict each do
Vitest setup Confirmed the setup steps and package.json config
Test design principles Learned the difference between toBe and toBeOneOf, and the one-case-per-test rule

Where I Got Stuck

1. npm wouldn’t run in PowerShell

Cause: The default execution policy was Restricted (no scripts allowed).

Fix:

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process

Using -Scope Process applies the change only to the current session — no permanent system changes.

2. Mismatch between package.json "type" and tsconfig.json "module"

Situation: package.json had "type": "module" but tsconfig.json had "module": "commonjs" — they were out of sync.

Fix: Changed package.json to "type": "commonjs". These two settings always need to match.

3. rl.close() in the wrong place

Situation: I put rl.close() at the end of the outer callback, but it was closing readline before the inner rl.question could finish.

Insight: rl.question is asynchronous. Writing code after it doesn’t mean it runs after the callback completes.

Fix: Moved rl.close() inside the inner callback, after all processing is done.

What I Learned

TypeScript

Topic Key Takeaway
Union types Union types restrict strings to allowed values, catching mistakes at compile time
Limits of type aliases Height = number and Weight = number are both just number at runtime — swapping arguments doesn’t cause a type error (Branded Types can fix this)
import type Explicitly imports types only — useful with verbatimModuleSyntax
Shorthand properties { bmi, label } works when variable names match property names

Testing (Vitest)

Topic Key Takeaway
TypeScript types don’t exist at runtime Testing types with Vitest is unnecessary — the compiler already guarantees them
toBe vs toBeOneOf toBeOneOf passes if any value matches — it can’t verify correctness. Use toBe with a specific expected value
Boundary value testing Testing around thresholds (18.5, 25) verifies that branching logic is correct
describe.skip Keeps the file in place while skipping tests — useful for preserving learning notes

Wrapping Up

This was my first TypeScript project — a simple BMI calculator CLI.

Two things stood out from this experience:

  1. Designing types first made implementation smoother
  2. Async callbacks are easy to misplace — position matters

Next up: a Rock-Paper-Scissors game (union types, conditionals, enums).

The full learning log is in LEARNING_LOG.md.

This article is part of my public learning journey using AI tools (Claude Pro / Cursor Pro). All code is written by me — AI is used for design discussions, bug hints, and spec clarification only.

From Building WordPress Websites to Node.js APIs: My Honest Full Stack Journey

Five years ago I was building WordPress websites for clients. The kind of work where you install a theme, build a child theme on top of it, write custom PHP for features the theme never imagined, register post types for every content model the client needs, and wire everything together with hooks and filters until it actually does what the client asked for.

That was the job. Not dabbling. Full time. Real projects. Real deadlines.

Today I write Node.js APIs, React dashboards, Next.js applications, and Vue.js interfaces. WordPress is still in the stack on most projects. But the stack is bigger now.

This is the honest story of how that happened. Not a tutorial. Not a career advice post. Just what actually occurred.

What building WordPress websites actually taught me

Before anything else I want to be clear about what WordPress website development actually involves because the internet has a habit of dismissing it.

When I was building recruitment and staffing platforms on WordPress I was writing custom PHP classes. Registering custom post types and taxonomies. Building complex meta box systems for managing job listings and candidate profiles. Writing hooks and filters to modify WordPress core behavior without touching core files. Building custom REST API endpoints for frontend JavaScript to consume. Handling user roles and capabilities so recruiters saw different interfaces than candidates.

That is real software development. It taught me how a large PHP application is structured. It taught me to write code that other developers could extend without breaking. It taught me to think about security because WordPress has strict standards and gets you in the habit of sanitizing everything that comes in and escaping everything that goes out.

It also taught me something most tutorials skip: how to read someone else’s code and understand a system you did not build. WordPress core is millions of lines of PHP written over twenty years by thousands of contributors. Learning to navigate that codebase, understand its patterns, and work with it rather than against it is a genuinely transferable skill.

I did not know at the time how much all of this would matter later.

The first project that pushed beyond WordPress

I was working on a staffing platform. WordPress was handling the content, the job listings, the user profiles, the admin dashboard. But the client needed automation. Resume parsing. Third party CRM sync. Automated email sequences triggered by candidate status changes. Workflow automation that ran on schedules.

Forcing this into WordPress was technically possible. WordPress has cron. WordPress has hooks you can fire on status changes. But the more I thought about it the more it felt like the wrong tool for the job. WordPress is designed around content management and web pages. What the client needed was a background processing system.

So we built a separate Node.js service alongside WordPress. WordPress remained the content layer. Node.js handled the automation layer. They communicated over REST API calls.

I had to learn Node.js properly for this. Not just reading tutorials. Actually building something in production that real people depended on.

The Node.js learning curve was not what I expected

I assumed the hard part would be JavaScript. I had been writing JavaScript inside WordPress for years. jQuery interactions. Block editor components. Admin page scripts. JavaScript was not new to me.

The hard part was the runtime model.

In WordPress PHP handles one request at a time in isolation. You write code that runs top to bottom. When it finishes the process ends. The next request starts fresh.

Node.js runs continuously. It handles multiple requests concurrently through an event loop. If you write blocking synchronous code you freeze the entire server not just one request. Every file read, every database query, every external API call needs to be async or you are doing it wrong.

My first week of Node.js code was full of synchronous operations that worked fine in isolation and would have been a disaster under any real load. I was writing PHP that happened to use JavaScript syntax.

It took about three weeks of real project work before the async model genuinely clicked. Before I stopped fighting it and started working with it. The moment it clicked it felt obvious in retrospect the way most things do once you understand them.

Building WordPress websites had given me something I did not realize

When I started building Express.js APIs I naturally wrote validation on every input. I checked that the authenticated user had permission to perform each action before performing it. I treated every piece of incoming data as potentially malicious until proven otherwise.

My colleagues on the project who came from other backgrounds did not have this habit by default. They would write an API endpoint that accepted whatever was sent and trusted it.

WordPress had trained me so thoroughly in defensive coding that I did it automatically without thinking about it. Sanitize inputs. Escape outputs. Check capabilities. Never trust user supplied data.

In the WordPress plugin ecosystem these practices are enforced. The Plugin Review Team will reject your submission if you violate them. You internalize them because you have to.

In Node.js nothing enforces them. You can ship an insecure API and it will run fine. Until it does not.

The discipline I brought from WordPress website development into Node.js made my APIs meaningfully more secure than they would have been otherwise. That was the first time I understood that WordPress development was not a limitation I was moving beyond. It was a foundation I was building on.

Then a dashboard needed React

The staffing platform automation was running on Node.js. WordPress was handling the content. But the client needed a proper admin dashboard. Not WordPress admin. A custom interface showing real time data from the automation system, candidate pipeline stages, recruiter performance metrics, integration status.

WordPress admin was not the right tool for this. We needed something more interactive and more tailored.

React was the decision.

I had written React inside the Gutenberg block editor. Enough to understand components and props and the basic rendering model. But building a full React application with its own routing, state management, and API layer was different.

The first thing that confused me was that React has strong opinions and punishes you for working against them. Coming from WordPress where you can hook into almost anything and modify almost any behavior, React initially felt rigid.

State flows one direction. Data goes down through props. Events go up through callbacks. You do not reach into a component from outside and change it. You change the state that drives it and React re-renders it automatically.

Once I stopped fighting this and accepted it as a design decision rather than a limitation something shifted. The rigidity is the point. It makes large applications easier to reason about because you always know where data comes from and how it changes.

WordPress is maximally extensible. Any plugin can modify any behavior. This is incredibly powerful and also the source of extraordinary debugging complexity when things go wrong.

React trades extensibility for predictability. Different tradeoff. Different use case. Neither is universally better.

Vue.js through a different project

Not every client chose React. A different project had an existing frontend codebase in Vue.js. New features needed to be built on top of it. I learned Vue.js on that project while it was running in production.

Vue.js after React is genuinely pleasant. The template syntax looks like HTML with some extra attributes. The component structure has a clear organization. The reactivity system works more automatically than React’s manual dependency tracking.

Vue.js also felt conceptually closer to WordPress templating than React did. In WordPress PHP templates you write HTML and embed PHP expressions inside it. In Vue templates you write HTML and embed Vue directives inside it. The mental distance is smaller than React’s JSX which feels more like writing JavaScript that outputs HTML.

For teams coming from HTML-first or PHP-first backgrounds Vue.js has a lower entry barrier. For larger teams with complex state requirements React’s ecosystem is deeper and more established.

Knowing both made me more useful across more types of projects. That was the practical outcome.

Next.js felt like something I had already understood

Next.js came into the picture on a project that needed both great SEO and a rich interactive frontend. Pure client side React would hurt search performance. Pure server side WordPress would not give the interactivity we needed.

Next.js was the answer.

What surprised me was how much my WordPress background helped with Next.js specifically.

WordPress developers understand server side rendering instinctively because WordPress has always rendered pages on the server. The server generates HTML. The browser receives it and displays it. Search engines can crawl it because the content is in the HTML not injected by client side JavaScript.

Many React developers who came from client side rendering backgrounds struggle with Next.js because thinking about what renders on the server versus the client requires a mental model they never needed before.

For me it was familiar territory in a new form. The server renders something meaningful. The client receives it and makes it interactive. This is a more sophisticated version of what WordPress has been doing since the beginning.

Next.js also forced me to make explicit decisions about data fetching that WordPress makes for you automatically. Does this data come at build time. Does it come at request time. Does it come from the client after page load. WordPress decides this based on its own architecture. Next.js requires you to decide deliberately.

Making those decisions explicitly made me think more carefully about performance than I ever had when WordPress was handling it automatically. Discomfort that became a useful skill.

What the full journey changed

The biggest shift was not learning new syntax or new frameworks. It was learning to think about systems rather than just applications.

WordPress is a monolith. The database, the business logic, the templating, the admin interface all live together. This is a strength for many use cases. It is also a constraint on the kinds of architectures you can build.

Working with Node.js as a separate service alongside WordPress, feeding data to React and Vue.js frontends, building Next.js applications that treat WordPress as a headless CMS exposed me to thinking about systems as separate concerns that communicate over APIs.

That thinking changed my WordPress work too. I started designing WordPress plugins to expose clean REST API endpoints rather than assuming WordPress templates would always consume the data. I started thinking about WordPress as a data layer rather than a complete application. That is a more modern and more flexible way to build with WordPress.

I would not have gotten there without the broader journey.

Where things stand now

I still build WordPress systems. Custom plugins with clean architecture. REST API extensions. Block editor components. Headless WordPress feeding modern frontends.

I also build Node.js APIs. React dashboards and applications. Next.js server rendered sites. Vue.js interfaces.

These are not competing skills pulling in different directions. They are complementary skills that inform each other.

WordPress taught me to think about security, extensibility, and writing code that survives contact with the real world.

Node.js taught me to think about async systems and API design.

React and Vue.js taught me to think about state as the source of truth for everything a user sees.

Next.js taught me to think carefully about rendering and performance as deliberate architectural decisions.

Building WordPress websites for clients taught me to think about the person using the thing I am building and what they actually need. That is the skill that travels furthest across every stack.

The journey was not planned. It was a series of projects that each needed something slightly beyond what I already knew. I said yes each time. I figured it out each time.

That pattern has not changed. The stack has just gotten bigger.

XiHan Snore Coach: Privacy-First On-Device MedTech Guardian powered by Gemma 4 Mobile

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

What I Built

XiHan Snore Coach is an Android-based MedTech application designed to help users track, analyze, and alleviate snoring and sleep apnea symptoms.

In the medical health sector, privacy is paramount. Users are hesitant to upload highly sensitive physiological data—such as SpO2 (blood oxygen) levels, snoring audio metrics, and sleep disruption patterns—to cloud servers.

To solve this, I built an entirely offline, on-device AI Sleep Coach. It acts as a pocket medical analyst, allowing users to ask natural language questions about their sleep trends and receive actionable respiratory muscle training plans without a single byte of their personal data leaving their phone.

Demo

*https://youtube.com/shorts/ECAjo9r7Yew *

Code

https://github.com/yueliao11/sleepcoach.git

How I Used Gemma 4

To make this offline MedTech guardian a reality, I chose the Gemma 4 2B/4B Ultra-Mobile Model.

Here is why it was the absolute right tool for the job and how it powers the core experience:

  1. Perfect Edge-Deployment Architecture: Medical apps demand high reliability. The 2B/4B effective parameter sizes allowed me to run the model natively on an Android device. It delivers server-grade conversational UI fluidity directly from the user’s pocket, even in Airplane mode during sleep.
  2. Native Tool Calling / Function Calling: Gemma 4 is doing real work at the heart of the app. Instead of just chatting, the AI utilizes Gemma 4’s Function Calling capabilities. When a user asks “How was my sleep this week?”, the Gemma 4 model intelligently triggers a local function (get_local_sleep_history). This function queries the local Android Room/SQLite database for the last 7 days of SpO2 and Snore data, feeding it back into the model’s context.
  3. Advanced Reasoning: By feeding the extracted local physiological data back into the 128K context window, Gemma 4 correlates drops in SpO2 with snoring durations, ultimately generating highly personalized and safe oropharyngeal muscle training routines for the user.

Gemma 4 proved that we no longer have to compromise between advanced AI reasoning capabilities and strict patient data privacy.

This submission was published on behalf of the team by @your-dev-username.

Contributors:

  • This submission was published on behalf of the team by @your-dev-username.

Contributors: