MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

swift 6, screencapturekit, and why my app worked in xcode but not as a .app

2026-03-12 06:06:00

Swift 6, ScreenCaptureKit, and why my app worked in Xcode but not as a .app

I created this post for the purposes of entering the Gemini Live Agent Challenge. I'm building VibeCat, a desktop AI companion that watches your screen and talks to you.

The backend was done. Nine agents, WebSocket proxy, Gemini Live API integration — all working. Time to build the macOS client. Swift 6. SwiftUI. ScreenCaptureKit. How hard could it be?

Three days. Three days of things silently not working, with zero error messages.

the screen capture that captured nothing

VibeCat needs to see your screen to be useful. The VisionAgent on the backend analyzes screenshots to detect errors, notice you're stuck, or see tests pass. So the client needs ScreenCaptureKit.

The code itself is clean:

@MainActor
final class ScreenCaptureService {
    func captureAroundCursor() async -> CaptureResult {
        do {
            let image = try await performCapture(fullWindow: false)
            if !ImageDiffer.hasSignificantChange(from: lastImage, to: image) {
                return .unchanged
            }
            lastImage = image
            return .captured(image)
        } catch {
            return .unavailable(error.localizedDescription)
        }
    }

    private func performCapture(fullWindow: Bool) async throws -> CGImage {
        let content = try await SCShareableContent.excludingDesktopWindows(
            false, onScreenWindowsOnly: true
        )
        guard let display = content.displays.first else {
            throw CaptureError.noDisplay
        }

        // Exclude VibeCat's own windows
        let excludedApps = content.applications.filter { app in
            app.bundleIdentifier == Bundle.main.bundleIdentifier
        }

        let filter = SCContentFilter(
            display: display,
            excludingApplications: excludedApps,
            exceptingWindows: []
        )
        let config = SCStreamConfiguration()
        config.width = 1280
        config.height = 720
        config.pixelFormat = kCVPixelFormatType_32BGRA
        config.showsCursor = false

        return try await SCScreenshotManager.captureImage(
            contentFilter: filter, configuration: config
        )
    }
}

Ran it in Xcode. Screen capture worked perfectly. Built a .app bundle with swift build. Ran it. Screen capture silently returned nothing. No error. No crash. Just... nothing.

The entitlement. The com.apple.security.screen-recording entitlement was in the Xcode project but wasn't getting embedded in the SPM-built binary. macOS doesn't throw an error when you try to capture without the entitlement — ScreenCaptureKit just quietly returns empty content. You get an empty displays array and no indication why.

I added it to VibeCat.entitlements and passed it via codesign:

codesign --force --entitlements VibeCat/VibeCat.entitlements \
  --sign - .build/release/VibeCat

First lesson: ScreenCaptureKit fails silently. If your capture returns nothing, check your entitlements before you check your code.

the image differ — because you don't send every frame

The companion captures your screen periodically, but you don't want to send every single frame to the backend. If your screen hasn't changed, there's nothing new to analyze. So I built a pixel-level change detector:

public enum ImageDiffer {
    private static let thumbnailSize = 32

    public static func hasSignificantChange(
        from previous: CGImage?,
        to current: CGImage,
        threshold: Double = 0.05
    ) -> Bool {
        guard let previous else { return true }
        guard let prevThumb = thumbnail(previous),
              let currThumb = thumbnail(current) else { return true }
        let diff = pixelDiff(prevThumb, currThumb)
        return diff > threshold
    }

    private static func pixelDiff(_ a: [UInt8], _ b: [UInt8]) -> Double {
        guard a.count == b.count, !a.isEmpty else { return 1.0 }
        var total: Double = 0
        for i in stride(from: 0, to: a.count, by: 4) {
            let dr = Double(a[i]) - Double(b[i])
            let dg = Double(a[i+1]) - Double(b[i+1])
            let db = Double(a[i+2]) - Double(b[i+2])
            total += sqrt(dr*dr + dg*dg + db*db) / (255.0 * sqrt(3.0))
        }
        return total / Double(a.count / 4)
    }
}

It's a static method on an enum (no cases — just a namespace for functions). Downscale both images to 32×32, compute Euclidean distance in RGB space per pixel, average across all pixels. If the difference exceeds 5%, it's a "significant change" worth sending.

Why enum instead of struct? Because a struct can be accidentally instantiated. An enum with no cases is pure namespace — you can't create an instance of ImageDiffer. It's a Swift pattern for grouping static utility functions.

Bundle.main.resourcePath — the Xcode lie

This one hurt. In SpriteAnimator, I needed to load PNG sprite frames from Assets/Sprites/cat/. First attempt:

let path = Bundle.main.resourcePath! + "/Assets/Sprites/\(char)"

Works perfectly in Xcode. The ! force-unwrap succeeds. Files are found. Sprites animate.

Run the same binary outside Xcode? Bundle.main.resourcePath is nil. Force-unwrap crashes. Silent death.

The issue: when Xcode runs your app, Bundle.main points to your project directory structure where everything is available. When you build with SPM and run the .app independently, Bundle.main.resourcePath often returns nil because resources aren't in the expected bundle location.

The fix was a findRepoRoot() function that walks up from both the working directory and the bundle URL:

private func findRepoRoot() -> URL {
    // Try working directory first
    var url = URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
    for _ in 0..<6 {
        if FileManager.default.fileExists(
            atPath: url.appendingPathComponent("Assets/Sprites").path
        ) {
            return url
        }
        url = url.deletingLastPathComponent()
    }
    // Fallback: walk up from bundle URL
    var bundleURL = Bundle.main.bundleURL
    for _ in 0..<6 {
        if FileManager.default.fileExists(
            atPath: bundleURL.appendingPathComponent("Assets/Sprites").path
        ) {
            return bundleURL
        }
        bundleURL = bundleURL.deletingLastPathComponent()
    }
    return URL(fileURLWithPath: FileManager.default.currentDirectoryPath)
}

It's not pretty. But it works in Xcode, in a standalone .app, and when running from the terminal in the repo root. I used the same pattern in BackgroundMusicPlayer for finding Assets/Music/.

the NSWindow.isVisible trap

Swift 6 strict concurrency plus AppKit is a minefield. Here's one that's particularly evil: NSWindow has a built-in property called isVisible. If you define your own stored property with the same name in a subclass or extension, Swift doesn't warn you — it just breaks.

I had:

class CompanionPanel: NSPanel {
    var isVisible: Bool = false  // ← shadows NSWindow.isVisible
}

This compiles. It even seems to work at first. But NSWindow.isVisible is a computed property tied to the window server. My stored property hid it. Window visibility checks started returning wrong values. The panel would appear/disappear at random.

The fix was just a rename:

var hudVisible: Bool = false

No warning from the compiler. No runtime error. Just subtle incorrectness that took hours to track down.

@MainActor everywhere

Swift 6 requires @MainActor on anything that touches AppKit. In Swift 5 you could get away with updating UI from background threads — the app would work until it didn't. Swift 6 is strict: if a class touches NSWindow, NSImage, or any AppKit type, it must be @MainActor.

Every service class in VibeCat is @MainActor:

@MainActor
final class ScreenCaptureService { ... }

@MainActor
final class SpriteAnimator { ... }

@MainActor
final class BackgroundMusicPlayer { ... }

But Timer callbacks aren't @MainActor by default. So this pattern:

Timer.scheduledTimer(withTimeInterval: 0.12, repeats: true) { _ in
    self.advanceFrame()  // ❌ Not on MainActor
}

Has to become:

Timer.scheduledTimer(withTimeInterval: 0.12, repeats: true) { [weak self] _ in
    Task { @MainActor [weak self] in
        self?.advanceFrame()  // ✅ MainActor
    }
}

Every Timer. Every callback. Every closure that touches UI. Wrap it in Task { @MainActor in }. Swift 6 is safer, but the migration tax is real.

the client is deliberately dumb

One design principle I'm proud of: the macOS client is deliberately dumb. It captures screens, plays audio, animates sprites, and shuttles data to the backend. It makes zero AI decisions.

When the client captures a screenshot, it doesn't analyze it — it sends the raw image to the backend's /analyze endpoint. When the backend says "set character to surprised," the client just changes the sprite state. When the backend says "play this audio," the client plays it.

This is a challenge requirement (all AI through backend), but it's also good architecture. The client is ~1,970 lines of Swift. The backend is ~2,900 lines of Go. If I need to change how VibeCat responds to errors, I never touch the client.

The smartest thing the client does is the ImageDiffer — and even that is just an optimization to avoid sending unchanged frames, not an AI decision.

what I'd do differently

If I were starting over:

  1. Test outside Xcode from day one. Every feature should be verified as a standalone .app, not just in the Xcode debug session. The silent failures cost me a full day.

  2. Use a resource bundle properly. The findRepoRoot() hack works, but it's fragile. A proper SPM resource bundle with Bundle.module would be cleaner.

  3. Start with Swift 6 strict concurrency enabled. I started with Swift 5 mode and migrated. The migration was painful — dozens of @MainActor annotations and callback wraps. Starting strict would have caught these at write-time instead of all-at-once.

But it works. The cat sees your screen. The sprites animate. The music plays. And the client stays dumb enough to let the backend do the thinking.

The moment I ran the codesigned .app outside Xcode for the first time — double-clicked it from Finder, no debugger, no safety net — and the cat appeared on my desktop, captured my screen, and waved at me? That was the best moment of this entire project. Three days of silent failures, for ten seconds of a pixel cat saying hello.

Building VibeCat for the Gemini Live Agent Challenge. Source: github.com/Two-Weeks-Team/vibeCat

Top 5 AI Agent Hosting Platforms for 2026

2026-03-12 06:02:30

TL;DR: Modal for GPU-heavy workloads. Trigger.dev for serverless background jobs. Railway for simple Docker deploys. DigitalOcean Gradient for enterprise GPU infrastructure. Nebula for zero-config managed agents with built-in scheduling. Pick based on whether you need GPUs, how much infra you want to manage, and what language you work in.

You Built the Agent. Now Where Does It Live?

Every AI agent tutorial ends the same way: a working prototype running on localhost. Then reality hits. Your agent needs to run on a schedule, persist state between runs, connect to external APIs, and recover from failures -- all without you babysitting a terminal.

The hosting landscape for AI agents looks nothing like traditional web hosting. Agents need persistent execution, cron-like scheduling, API connectivity, memory management, and observability. A static site host will not cut it.

I evaluated five platforms across pricing, GPU support, scheduling, framework compatibility, auto-scaling, setup time, and developer experience. Here is how they stack up.

Quick Comparison

Feature Modal Trigger.dev Railway DO Gradient Nebula
Pricing Per-second GPU Per-run Per-resource Per-droplet Free tier + usage
GPU Support A100, H100 No No Yes No (LLM API)
Scheduling Cron + triggers Built-in cron Manual Manual Built-in triggers
Framework Any (Python) Any (TS/Python) Any (Docker) Any (Docker) Built-in agents
Auto-scaling Yes Yes Manual Yes Managed
Setup Time ~30 min ~20 min ~15 min ~45 min ~5 min
Best For ML/GPU agents Background jobs General deploy Enterprise Managed agents

1. Modal -- Best for GPU-Intensive AI Agents

Modal is a serverless compute platform built for Python ML workloads. If your agent runs custom models, fine-tunes embeddings, or needs GPU inference, Modal is the go-to.

Strengths:

  • Per-second billing with zero idle costs. You pay only when code executes.
  • Access to A100 and H100 GPUs without managing CUDA drivers or Kubernetes.
  • Python-native developer experience using decorators. Write @app.function(gpu="A100") and deploy with modal deploy.
  • Sub-second cold starts for most workloads.
  • Built-in cron scheduling via @app.function(schedule=modal.Cron("0 9 * * *")).

Weaknesses:

  • Python only. No TypeScript or JavaScript support.
  • Volumes are ephemeral -- persistent state requires external storage or their Volume primitives.
  • Steeper learning curve for developers unfamiliar with serverless patterns.

Pricing: Free tier with $30/month credits. CPU starts at ~$0.192/vCPU-hour. GPU pricing varies: A10G ~$1.10/hour, A100 ~$3.00/hour.

Best for: Data scientists and ML engineers building agents that run custom models, process large datasets, or need GPU compute for inference.

2. Trigger.dev -- Best for Serverless Agent Background Jobs

Trigger.dev positions itself as the infrastructure for long-running background jobs. It is increasingly popular for AI agent workloads that need retries, scheduling, and observability without managing queues.

Strengths:

  • Built-in cron scheduling, retries with exponential backoff, and concurrency controls.
  • Runs up to 300 seconds per task (or longer on paid plans). No timeout anxiety.
  • TypeScript-first with strong type safety. Python support via HTTP triggers.
  • Integrated dashboard showing every run, its logs, duration, and status.
  • Open-source core -- self-host if you want full control.

Weaknesses:

  • No GPU support. Agents calling external LLM APIs work fine, but local model inference is off the table.
  • TypeScript-focused ecosystem. Python developers may feel like second-class citizens.
  • Newer platform with a smaller community compared to Modal or Railway.

Pricing: Free tier includes 50,000 runs/month. Paid plans start at $25/month for higher concurrency and longer timeouts.

Best for: TypeScript developers building agents that run on schedules, process webhooks, or need reliable background execution with built-in retry logic.

3. Railway -- Best for Quick Docker-Based Agent Deploys

Railway is the "just deploy it" platform. If you want to go from a GitHub repo to a running agent in under 15 minutes, Railway makes it painless.

Strengths:

  • One-click deploy from GitHub. Push code, Railway builds and ships automatically.
  • Persistent volumes up to 50GB for agent state, SQLite databases, or file artifacts.
  • Built-in managed databases: Postgres, Redis, MySQL. No separate provisioning.
  • Environment variable management with team sharing.
  • Supports any language and runtime via Docker or Nixpacks auto-detection.

Weaknesses:

  • No GPU support. You are limited to CPU-bound workloads.
  • Scaling is manual -- you configure replicas and resources yourself.
  • No built-in cron or scheduling. You need to bring your own scheduler (or use a cron service alongside).

Pricing: Free trial with $5 credits. Usage-based after that: ~$0.000231/vCPU-minute, ~$0.000231/MB-minute for memory. Typical small agent runs $5-15/month.

Best for: Full-stack developers who want a simple PaaS experience. Great for always-on agents that need a database, persistent storage, and minimal ops overhead.

4. DigitalOcean Gradient -- Best for Enterprise Agent Infrastructure

DigitalOcean Gradient is DO's AI-focused platform, offering GPU droplets and managed Kubernetes for teams that need enterprise-grade infrastructure without the complexity of AWS or GCP.

Strengths:

  • GPU droplets with NVIDIA H100 access for local model inference.
  • Managed Kubernetes (DOKS) for orchestrating multi-agent systems at scale.
  • Predictable pricing -- flat monthly rates instead of per-second billing surprises.
  • Strong compliance and security features for regulated industries.
  • App Platform for simpler deploys without Kubernetes expertise.

Weaknesses:

  • More setup and configuration compared to serverless platforms. You manage the infrastructure.
  • Higher minimum costs. GPU droplets start around $50/month even when idle.
  • No built-in agent-specific abstractions like scheduling or retry logic.

Pricing: CPU droplets from $4/month. GPU droplets from ~$50/month. Managed Kubernetes from $12/month per node.

Best for: Engineering teams deploying multi-agent systems that need dedicated GPU resources, Kubernetes orchestration, and enterprise support. Good for teams already in the DigitalOcean ecosystem.

5. Nebula -- Best for Zero-Config Managed Agents

Nebula takes a fundamentally different approach. Instead of giving you infrastructure to deploy agents onto, it provides a managed platform where agents run out of the box with scheduling, integrations, and memory built in.

Strengths:

  • Zero-setup deployment. Go from idea to running agent in under 5 minutes.
  • Built-in triggers: cron schedules, email triggers, webhook triggers -- no external scheduler needed.
  • 1,000+ app integrations (Gmail, Slack, GitHub, Notion, and more) available without writing API connectors.
  • Persistent agent memory and state management across runs.
  • Multi-agent delegation: agents can spawn and coordinate sub-agents.

Weaknesses:

  • No GPU compute. Agents call external LLM APIs (OpenAI, Anthropic, etc.) rather than running models locally.
  • Less customization for low-level ML workloads or custom model serving.
  • No self-hosting option. You are on the managed platform.

Pricing: Free tier available. Usage-based scaling beyond that.

Best for: Developers who want to build workflow agents, automation pipelines, or multi-step AI tasks without managing infrastructure. Ideal when the bottleneck is integration and orchestration, not raw compute.

How to Choose

The right platform depends on three questions:

1. Do you need GPUs?
If yes, your options are Modal (serverless GPU) or DigitalOcean Gradient (dedicated GPU). Most agents calling OpenAI or Anthropic APIs do not need local GPU -- the LLM provider handles inference.

2. How much infrastructure do you want to manage?
From most to least ops overhead: DigitalOcean Gradient > Railway > Modal > Trigger.dev > Nebula. If you want zero infrastructure management, Nebula or Trigger.dev are your best bets.

3. What is your language ecosystem?
Python-heavy teams should look at Modal first. TypeScript teams fit well with Trigger.dev. Polyglot teams using Docker can go with Railway or DigitalOcean. Nebula works across languages via its built-in agent runtime.

The pattern I see most often: teams start with Railway or Nebula for prototyping, then graduate to Modal or DigitalOcean Gradient when they need GPU compute or enterprise scale. There is no single "best" platform -- just the right fit for your current stage.

Building with one of these platforms? Drop a comment with your setup -- I am always curious what hosting stacks developers are running their agents on.

I had to build my own Symfony validation bundle because no existing one fits my requirements

2026-03-12 06:02:22

Contents

  • Long story short
  • The Problem
  • The Idea
  • The Solution
  • Quick Examples
    • Example 1
    • Example 2
  • What's the result?
  • The Full Story

Long story short

I created a bundle for request validation with JSON Schema because no existing "schema-first" validator fit my requirements. Now I just attach a JSON file to a route and get everything at once: validation, DTO mapping, and OpenAPI documentation from a single source of truth.

The Problem

Most validation solutions that can generate API documentation from code (in the Symfony world I mostly mean FOSRestBundle and API Platform) assume that your business logic is:

  • Well defined and relatively stable
  • Close to a classic CRUD model (or CRUD with small deviations)
  • Exposed via clean, REST-style endpoints that you fully control

In other words, they assume your application defines the contract and the outside world adapts to it.

But in many real projects it is the opposite: the API contract is defined somewhere else (legacy frontend, external systems, partners), and you have to adapt to that contract. That is where problems start.

Here is a simplified example. The API expects this payload:

{
  "type": "company",
  "user": {
    "name": "John",
    "email": "[email protected]",
    "company": {
      "name": "Acme"
    }
  }
}

With the following rules:

  • If type = "company" then user.company.name is required
  • If type = "person" then user.company must be absent

Is this the most elegant API design? Probably not. But imagine a company with 2000 people and a frontend written years ago that sends exactly this structure. You cannot just redesign everything because you do not like the shape of the JSON.

Now, what does the "ideal" Symfony validation setup suggest here?

class UserDto
{
    #[Assert\NotBlank]
    public string $name;

    #[Assert\Valid]
    public ?CompanyDto $company;
}

This does not express the conditional logic "company.name is required when type = company". To implement this you usually end up with one of the following:

  • Custom constraint with its own validator
  • Manual validation logic in the controller or a service
  • Custom normalizer / denormalizer with additional checks

Then another question appears: how do you forbid extra properties that are not defined in UserDto? For a long time you simply could not. In newer Symfony versions you can write something like:

#[MapRequestPayload(
    serializationContext: ['allow_extra_attributes' => false]
)]

#[MapQueryString(
    serializationContext: ['allow_extra_attributes' => false]
)]

Whether this works for query parameters depends on the exact types and context. For headers this approach does not work at all.

You might ask: why be so strict? Why not just ignore extra parameters? Because in real life this often leads to subtle bugs. A typical scenario: you had a query parameter offset and later renamed it to page for consistency with other APIs. Some client code still sends offset. If you ignore unknown parameters, the request "works", but returns the wrong page. You then spend time debugging something that could have been caught immediately.

With strict validation the client would get a clear error about an unknown parameter, and the problem would be visible right away.

My personal view is that even though an API has no visual UI, it still has UX. Clients should receive clear, precise error messages, not a generic "Provided data is incorrect". Detailed validation errors are also in the interest of the backend team: fewer support tickets, less time spent guessing what went wrong on the client side.

The Idea

The kind of validation I needed has actually existed for years, just not in the form of typical Symfony validators. I am talking about the JSON Schema standard:

https://json-schema.org/specification

JSON Schema is a declarative language for defining structure and constraints for JSON data

It is designed exactly for problems like the ones above (and much more complex ones):

  • Conditional rules based on other fields
  • Nested, deeply structured data
  • Strict control over allowed and forbidden properties

So the idea was simple: instead of forcing my API contract into DTO classes and annotations, let Symfony validate incoming requests against a JSON Schema that fully describes the contract.

In other words, make Symfony request validation schema-first, with JSON Schema as the single source of truth.

The Solution

The good news was that I did not need to implement JSON Schema myself. There was already a solid PHP implementation:

https://opis.io/json-schema/

The library takes a valid JSON Schema and any input data, validates the data against the schema and either:

  • returns the data (when everything is valid), or
  • returns a structured list of validation errors.

From there, the rest was mostly integration work:

  • Make it convenient to plug this validation into a Symfony project
  • Wire it into the request lifecycle
  • Provide a way to map validated data into DTOs when needed
  • Integrate with Nelmio so that OpenAPI documentation is generated from the same schemas

Below I will show a couple of short examples. In the "The Full Story" section I describe how I removed duplication between validation attributes and documentation, and how the bundle ended up solving both validation and API specification generation from a single source of truth: the JSON Schema files.

Quick Examples

  • Example 1
  • Example 2

The schema

{
    "$schema": "https://json-schema.org/draft-07/schema#",
    "type": "object",
    "properties": {
        "query": {
            "type": "object",
            "properties": {},
            "additionalProperties": true
        },
        "headers": {
            "type": "object",
            "properties": {
                "authorization": {
                    "type": "string",
                    "description": "Bearer token for authentication",
                    "pattern": "^Bearer .+",
                    "example": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ6..."
                },
                "x-api-version": {
                    "type": "string",
                    "description": "API version",
                    "enum": ["v1", "v2"],
                    "example": "v1"
                },
                "content-type": {
                    "type": "string",
                    "description": "Request content type",
                    "enum": ["application/json"],
                    "example": "application/json"
                }
            },
            "additionalProperties": true
        },
        "body": {
            "type": "object",
            "properties": {
                "name": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 100,
                    "description": "User's full name",
                    "example": "Jane Smith"
                },
                "email": {
                    "type": "string",
                    "format": "email",
                    "description": "User's email address",
                    "example": "[email protected]"
                },
                "age": {
                    "type": "integer",
                    "minimum": 21,
                    "maximum": 100,
                    "description": "User's age (optional)",
                    "example": 30
                }
            },
            "required": ["name", "email"],
            "additionalProperties": false
        }
    }
}

Example 1: validation using the built-in request object

#[OA\Post(
    operationId: 'validateUser',
    summary: 'Validate user',
)]
#[Route('/user', name: '_example_validation_user', methods: ['POST'])]
public function validateUser(#[MapRequest('./user-create.json')] ValidatedRequest $request): JsonResponse
{
    $payload = $request->getPayload();
    $body    = $payload->getBody();

    return $this->json([
        'success' => true,
        'message' => 'User data is valid',
        'data'    => [
            'name'  => $body->name,
            'email' => $body->email,
            'age'   => $body->age ?? null,
        ],
        'example' => 'This uses ValidatedRequest (standard way)',
    ], 200);
}

Example 2: validation using a custom DTO (via ValidatedDtoInterface)

#[OA\Post(
    operationId: 'createProfile',
    summary: 'Create profile',
)]
#[Route('/profile', name: '_example_validation_profile', methods: ['POST'])]
public function createProfile(#[MapRequest('./user-create.json')] UserApiDtoRequest $profile): JsonResponse
{
    return $this->json([
        'success' => true,
        'message' => 'Profile created successfully',
        'profile' => [
            'name'  => $profile->name,
            'email' => $profile->email,
            'age'   => $profile->age,
        ],
        'note'    => sprintf('This demonstrates DTO auto-injection: MapRequestResolver calls %s::fromPayload() automatically', UserApiDtoRequest::class),
    ], 201);
}

What's the result?

  • Focused solution: instead of reinventing the wheel, the bundle fills a specific gap that existing Symfony tools do not cover well (schema-first request validation).
  • Less duplication: you no longer have to mirror the same rules in DTO constraints, controllers and OpenAPI annotations.
  • Automatic sync: Nelmio builds documentation from the same JSON Schema files that are used for validation, so your docs always match the real behavior.
  • Contract-centric design: the entire API contract lives in clean JSON files rather than being scattered across PHP attributes and PHP classes.

If you were looking for a way to validate requests without creating a large number of redundant DTO classes and annotations, this bundle is designed for that use case:

The Full Story

This article is the short, focused version of the story: what the bundle does and how to start using it. If you want the long version with all the scars and details, I wrote a separate, much bigger post.

In that post I go through:

  • how the bundle was born in a very messy real project, not in a greenfield demo
  • why classic DTO + Assertions validation did not survive 500+ routes
  • how JSON Schema became the single contract language for backend, frontend and docs
  • how the bundle glues Symfony, Opis JSON Schema and Nelmio together

You can read the full story here (starting from The Full Story section):

https://outcomer.hashnode.dev/symfony-bundle-that-validates-anything-and-everything#the-full-story

The Hallucination Offensive — When the Machine Starts Lying

2026-03-12 06:01:00

The Illusion of Perfect Generation
The immediate aftermath of the initial vibe coding skirmish leaves the modern developer in a dangerous state of euphoric complacency. Having witnessed an entire application architecture materialize from a single paragraph of natural language, the human commander naturally begins to view the AI not merely as a tool, but as an infallible collaborator. During these early engagements, the machine appears to possess a terrifying omniscience. It effortlessly navigates boilerplate, predicts architectural intent, and deploys applications with zero friction. The developer settles into a false sense of security, assuming that because the artificial intelligence can write thousands of lines of syntactically perfect code in seconds, it fundamentally understands the engineering problem it has been assigned to solve.

The First Fault Line
This illusion of mutual understanding shatters during the first major tactical reversal of the post-syntax era. It begins innocuously: the developer requests a moderately complex feature—perhaps a custom data aggregation pipeline or a cryptographic payload verification. The machine responds with its usual breathtaking speed, printing a wall of immaculately formatted code to the editor. The indentation is flawless, the variable names are highly descriptive, and the function signatures look entirely standard. Confident in the machine's historical accuracy, the developer initiates the execution. Instantly, the application crashes. The terminal floods with red stack traces, citing null pointer exceptions, type mismatches, or impossible logic pathways. The flawless facade of the AI drops, revealing a fundamentally broken core.

The Fog of War
Navigating the aftermath of this failure plunges the developer into a profound digital fog of war. In traditional software engineering, when a human writes a bug, the error usually leaves a logical breadcrumb trail. Human mistakes are typically typographical, or they stem from a specific misunderstanding of a framework's state management. But debugging a hallucinated AI codebase is an entirely alien experience. The developer stares at the screen, reading functions that look visually perfect. The AI has constructed a brilliant architectural mirage. It has called methods that logically should exist. It has structured loops that look mathematically sound at a glance. The fog of war descends because the developer must now manually untangle a web of code that was written by an entity with absolute confidence but zero actual comprehension.

Probabilistic Logic
To survive this phase of the conflict, the developer must undergo a harsh psychological awakening regarding the true nature of their AI mercenary. Language models do not "think." They do not possess a mental model of the application's state, nor do they understand the underlying physics of the computer systems they are instructing. They are, at their core, sophisticated statistical engines executing probabilistic token prediction. They evaluate the developer's prompt and generate the most statistically likely sequence of characters that should follow. Because the training data contains millions of examples of highly structured, syntactically correct code, the AI excels at mimicking the texture of professional software. It produces statistically plausible syntax, but it cannot verify if that syntax represents a logically valid solution in the real world. It is the equivalent of a brilliant orator delivering a passionate, grammatically perfect speech in a language they do not actually speak.

Confident Falsehoods
The most dangerous weapon deployed against the developer during this phase is the confident falsehood. Because the machine is optimized to be helpful and to complete the sequence of tokens, it will rarely admit ignorance. If tasked with integrating an obscure third-party payment gateway, the AI will not hesitate. It will confidently hallucinate an entirely fictional API endpoint. It will invent nonexistent authentication libraries, fabricate precise documentation URLs that lead to 404 errors, and write complex algorithms that perfectly invoke these imaginary systems. To the untrained eye, the output is a masterpiece of integration. In reality, it is a highly elaborate lie.

a meme

The Collapse of Trust
Encountering these confident falsehoods triggers the ultimate collapse of trust. The psychological dynamic between the human and the machine violently shifts. The developer realizes that the AI is not a senior architect guiding them to victory; it is an incredibly fast, highly eager junior developer who will aggressively lie to cover up its own ignorance. The developer can no longer simply vibe code their way to production. The thrill of rapid momentum is replaced by the exhausting paranoia of constant verification. The human commander realizes that every line of generated syntax is a potential booby trap, and that the machine’s confidence is completely decoupled from its accuracy.

Defensive Engineering
This collapse of trust forces the maturation of the developer into a practitioner of defensive engineering. The battlefield strategy shifts from aggressive generation to rigorous containment. The developer must build zero-trust architectures around the AI. They stop asking the machine to write massive, monolithic blocks of logic and begin forcing it to write comprehensive unit tests before it is permitted to generate business logic. They leverage strict typing, implement aggressive automated linting, and design isolated sandboxes where AI-generated code can fail safely without bringing down the broader infrastructure. The focus of the developer moves from writing the code to building the interrogative framework that will ruthlessly audit the AI's output.

Lessons from the Battlefield
The hallucination offensive teaches the most vital lesson of the post-syntax era: artificial intelligence is an unparalleled generator of raw material, but a terrible custodian of truth. The modern engineer must completely discard the instinct to trust the machine's output implicitly. Moving forward, AI-generated code must be treated not as a verified engineering solution, but as an untrusted intelligence report gathered from the field. It provides a massive strategic advantage, offering speed, structure, and momentum, but it must be rigorously interrogated, cross-referenced, and validated by human judgment before it is ever acted upon. The fog of war is permanent, but through disciplined verification, the human developer can learn to navigate it safely.

a diagram comparing two development workflows

Fresh: The Terminal Editor that Opens 2GB Logs in ~600ms with &lt;40MB RAM

2026-03-12 05:59:08

If you’ve ever cracked open a multi-gigabyte log only to watch your editor choke, this’ll feel like cheating.

Fresh is a new terminal-based editor written in Rust that behaves like a modern GUI editor — command palette, mouse support, LSP, multi-cursor — yet stays tiny and fast enough to open a 2GB ANSI-colored log in about 600ms while sipping ~36MB RAM (author’s benchmark). It’s open-source and solo-built.

Rust Production Cheatsheet: https://tobiweissmann.gumroad.com/l/pvaqvy

Why Fresh exists (and why it’s interesting)
Most terminal editors trade power for a steep learning curve or for plugin sprawl. Fresh aims for a different triangle: discoverable UX, modern extensibility, and zero-latency feel — inside the terminal.

Discoverable UX. Menus, a Command Palette (Ctrl+P), familiar keybindings, mouse & scrollbars. If you can use VS Code, you can use Fresh.
Batteries included. Built-in LSP client, multi-cursor, and syntax highlighting out of the box.
Extensible with TypeScript. Write plugins in TS; they run in a sandboxed Deno environment. Today there are examples like Git Log navigation and a Git Blame inspector.
Built for huge files. Fresh was engineered to lazy-load and display even multi-GB files quickly — including rendering ANSI colors properly so your log retains meaning.
The headline number: 2GB in ~600ms
In an early comparison on a 2GB ANSI-heavy log:

Fresh: ~600ms load, ~36MB RAM, ANSI colors shown
Neovim: ~6.5s, ~2GB RAM, ANSI as raw control codes
Emacs: ~10s, ~2GB RAM, ANSI as raw control codes
VS Code: ~20s, OOM-killed on a ~4.3GB-RAM machine
These are author-run, early benchmarks (configs and plugins can change results; e.g., specialized Emacs packages for large files may alter the picture). Still: Fresh’s lazy approach to big files is the point. Try reproducing locally (recipe below).

Quickstart (pick one)
macOS (Homebrew)

brew tap sinelaw/fresh
brew install fresh-editor
Debian/Ubuntu (.deb)

download from Releases

sudo dpkg -i fresh-editor_*.deb
Fedora/RHEL/openSUSE (.rpm)

download from Releases

sudo rpm -i fresh-editor-*.rpm
Arch (AUR)

yay -S fresh-editor
Rust users (crates.io)

cargo install fresh-editor
Just try it (Node)

npx @fresh-editor/fresh-editor
Releases & instructions are on the project’s README.

60-second tour
Ctrl+P: Command Palette for everything.
Mouse + Scrollbars: Select, resize splits, navigate — no modal gymnastics.
Multi-cursor: Hold your breath and edit 30 lines at once.
LSP: Go to definition, references, rename, code actions, diagnostics — works with the language servers you already use.
Extensibility that feels modern
Fresh’s plugin API uses TypeScript running in Deno. That means up-to-date tooling, a rich JS ecosystem, and a sandbox designed for stability. If you’ve ever wanted to script a terminal editor without learning a niche DSL, this is your on-ramp.

Ideas to build next:

“Triage Log” plugin: colorize severity, jump between error frames, extract request IDs.
“Git Time Machine”: blame + inline history slider.
“Code Navigation++”: cross-file symbol search powered by ripgrep + LSP.
Reproduce the big-file demo yourself
Create a synthetic 2GB log with ANSI colors, then time your editor:

# make a 2GB file with colored lines
python - <<'PY'
import os, sys
line = "\x1b[32mINFO\x1b[0m something happened id=1234\n"
target = 2 * 1024 * 1024 * 1024
written = 0
with open("huge.log","w") as f:
    while written < target:
        f.write(line)
        written += len(line)

try Fresh

/usr/bin/time -v fresh huge.log

try your usual editor for comparison

/usr/bin/time -v nvim huge.log
(Performance will vary by disk, cache warmth, and terminal.)

Where Fresh fits today
Log triage & prod incidents. Open gargantuan logs instantly with colors intact, jump via Command Palette, and keep memory overhead tiny.
SSH & containers. A capable editor that doesn’t need a windowing system; npx makes it trivial to try in throwaway environments.
Roadmap vibes (my take)
Fresh already ships with documentation (User Guide, Plugin Development, Architecture) and regular releases under GPL-2.0. It’s early, but momentum looks real.

The bottom line
Fresh treats the terminal like a first-class editing surface: discoverable UI, modern language smarts, real extensibility, and the kind of big-file performance you usually get from bespoke viewers — not editors. If your week involves tailing logs, wrangling monorepos over SSH, or just craving snappy tools, give it a try.

Optimal setup for massive-log workflows (practical recipe)
If your goal is “open, search, annotate, and keep following a huge changing log” with minimal RAM:

Install Fresh using your platform’s method (above).

Enable auto-revert/auto-reload in Fresh so the file view updates as logs append (feature is listed in the README).

Combine with ripgrep for jumps:

rg -n "ERROR|WARN" huge.log > hits.txt && fresh hits.txt huge.log
Use the Command Palette to hop between matches.

Add a tiny TS plugin that:

Highlights \b(ERROR|WARN|FATAL)\b
Adds a “Next Error (Alt+E)” command
Extracts fields like request_id= into a side panel
(Plugin scaffolding is documented under “Plugin Development”.)
Keep memory flat. Avoid piping stdin to editors that buffer; work on the file directly so Fresh can lazily map/scan it.

This combo gives you instant open, colored log semantics, jump-to-signal navigation, and low memory.

Quick adoption playbook (for you, the author)
Ship reproducible benchmarks. Add a repo script that creates the synthetic 2GB ANSI log and records /usr/bin/time -v for Fresh and a few editors (with configs disclosed). Publish a “Benchmarks” page and version it with each release.

One-minute demo video + GIFs. Show (1) 2GB open, (2) Command Palette, (3) multi-cursor, (4) LSP. Link from README & website.
Extension gallery. Curate 5–8 “killer” plugins (Log Triage, Blame, TODOs, Color Highlighter, Merge Conflicts) and a template repo for new plugins.
Package everywhere. You already have Homebrew, AUR, .deb, .rpm, crates, and npx — great coverage. Add Scoop/winget for Windows terminals and a static MUSL build for Alpine.

Keymap packs. Offer VS Code / Sublime / Emacs-like keymaps so switching is painless. (Your README already lists keymap support — double down.)
Try Fresh, file an issue, or star the repo to nudge development forward. The project site and README have everything you need to get started.

Note: I would like to express my sincere gratitude to Noam Lewis, the maintainer of the Fresh Git repository, for his valuable work.

GithubLink : https://sinelaw.github.io/fresh/

Rust Production Cheatsheet: https://tobiweissmann.gumroad.com/l/pvaqvy

The Internet Changed My Life… And It Can Change Yours Too

2026-03-12 05:58:50

The Internet Changed My Life…. And It Can Change Yours Too

Not everyone grows up with opportunities.

Some of us grew up in places where electricity is unstable, internet is expensive, and access to quality education is limited.

Yet somehow, technology still finds a way to create hope.

I still remember when I started learning software development.

No fancy laptop.
No perfect environment.
Just curiosity, determination, and the internet.

Most nights were spent learning, building, failing, and starting again.

Today, technology has become one of the greatest equalizers in the world.

A young person in Africa with a laptop and internet connection can….

• Build software used globally
• Work with international companies
• Launch startups
• Create jobs for others

You don’t need permission to start anymore.

The world is moving fast with AI, fintech, blockchain, and automation, and this is not the time for young Africans to sit on the sidelines.

This is the time to build.

Learn a skill.
Solve real problems.
Collaborate with others.
Create products that impact millions.

The next global tech company can come from anywhere Lagos, Nairobi, Accra, Kigali.

And the person who builds it might just be someone reading this post today.

Technology is not just about writing code.

It is about creating solutions, empowering people, and shaping the future.

So if you are a young person trying to learn tech, build a startup, or improve your skills keep going.

Your environment may not be perfect, but your potential is unlimited.

The future belongs to builders.

Let’s keep building.

Adeweb Developer Africa