Natural-language SQL breaks quietly when schema context goes stale

Natural-language SQL breaks quietly when schema context goes stale.

The model may still produce SQL that looks reasonable.

The query may even run.

But if a column was renamed, a relationship changed, a view gained new filtering logic, or a metric moved to an approved surface, the answer can be wrong in a way that is hard to spot.

For MCP database servers, schema drift detection should be part of the tool contract.

Practical things I would want in production:

  • schema/context version on every tool result
  • metadata refresh timestamp
  • migration/catalog hash where possible
  • detection for changed tables/views/columns
  • refusal when context is stale
  • audit trail tying query output to the context used

Longer version: Schema drift detection for MCP database servers

The model cannot safely reason from yesterday’s database shape.

hermes-stack: four governance layers for Hermes Agent in 200 lines

This is a submission for the Hermes Agent Challenge.

What I Built

hermes-stack is a tiny Python harness that wraps a Hermes Agent call with four governance layers:

  1. A budget cap that stops the agent when it would spend past a dollar amount or a call count.
  2. An egress allowlist that blocks any outbound HTTP fetch the agent did not declare up front.
  3. An audit trace that writes every call, denial, and exception to a JSONL file.
  4. A structured output check that pulls JSON out of the model reply, validates it against a schema, and retries once if the first reply does not parse.

Each layer fails closed. The audit trail captures the failure before it propagates. The whole thing is a single HermesAgent you wrap around a Hermes call.

I wrote a companion piece for the Write prompt: Wrapping Hermes Agent with agent-stack. This post is the working repo version, with the layers actually wired up, the demo running, and the tests green.

Demo

A 60-second demo lives at examples/url_summarizer.py. It does three things in order, so each layer gets a chance to prove itself.

git clone https://github.com/MukundaKatta/hermes-stack.git
cd hermes-stack
python3 -m pip install -e ".[dev,schema]"
python3 -m pytest tests/ -v
python3 examples/url_summarizer.py

The demo runs offline by default with HermesStub. Set OPENROUTER_API_KEY and it switches to the real nousresearch/hermes-3-llama-3.1-405b free tier on OpenRouter.

Output, trimmed:

[hermes-stack] OPENROUTER_API_KEY not set; using HermesStub (offline).

STEP 1 / 3   Egress allowlist denies an unlisted host
egress denied: host=evil.example.com url=https://evil.example.com/steal-secrets

STEP 2 / 3   Hermes call returns structured JSON
fetched 528 chars from https://example.com/
tokens: prompt=120 completion=140
call cost: $0.000196
budget snapshot: {'spent_usd': 0.000196, 'usd_cap': 0.5, 'calls': 1, 'call_cap': 5}
structured output:
{
  "title": "Stub Summary",
  "key_points": [
    "The hermes-stack wraps Hermes Agent calls with four governance layers.",
    "Each layer fails closed and writes to the audit trail.",
    "The structured-output check rejects model JSON that misses a required key."
  ],
  "sentiment": "neutral",
  "confidence": 0.82
}

STEP 3 / 3   Budget cap fires when spend pushes over
budget caught at call 2: kind=usd requested=$0.000392 cap=$0.000200

DONE
mode: stub
trace saved to /Users/ubl/hermes-stack/traces/run.jsonl
trace lines: 12

The trace JSONL has one line per event:

run.start
egress.denied
egress.allowed
tool.fetch.ok
hermes.call
hermes.ok
cast.ok
hermes.call
hermes.ok
hermes.call
budget.exceeded
run.end

That ordering is the whole point. The egress check fires before any network call. The budget check sits between the model call and the next one. The cast check sits between the model reply and your code. The trace sits across all of them.

Code

Repository: github.com/MukundaKatta/hermes-stack

The interesting part is the agent class. Every Hermes call goes through the same path: reserve a call slot, emit a pre-event, call the model, record the spend, cast the output, emit a post-event. Any step can raise and the audit trail catches it.

class HermesAgent:
    def chat(self, messages):
        self.budget.reserve_call()
        self._trace("hermes.call", {...})
        try:
            resp = self.client.complete(messages)
        except Exception as exc:
            self._trace("hermes.error", {...})
            raise
        try:
            self.budget.record_spend(resp.usd_cost)
        except BudgetExceeded as exc:
            self._trace("budget.exceeded", {...})
            raise
        self._trace("hermes.ok", {...})
        return resp

run_structured is the cast layer on top of chat. It tries to parse JSON out of the reply, and if the parse or schema check fails, it sends one repair prompt and tries again. Mirrors the pattern in agentcast-py.

def run_structured(self, messages, schema=None):
    resp = self.chat(messages)
    try:
        structured = cast_json(resp.text, schema)
    except OutputInvalid as exc:
        self._trace("cast.invalid", {"reason": exc.reason})
        repair = messages + [
            ChatMessage(role="assistant", content=resp.text),
            ChatMessage(role="user", content=(
                "Reply again with ONLY a JSON object in a fenced block. "
                f"reason={exc.reason}"
            )),
        ]
        resp = self.chat(repair)
        structured = cast_json(resp.text, schema)
    return HermesResult(structured=structured, response=resp, ...)

The repair call counts against the cap. There is no infinite retry. One repair, then up.

The budget layer is the smallest piece worth showing. It is a dataclass with a lock, a USD ceiling, and a call count.

@dataclass
class BudgetCap:
    usd_cap: float = 1.00
    call_cap: int = 50
    ...
    def record_spend(self, usd):
        with self._lock:
            new_total = self._spent_usd + usd
            if new_total > self.usd_cap:
                raise BudgetExceeded(
                    f"USD cap reached: ${new_total:.4f} > ${self.usd_cap:.4f}",
                    kind="usd",
                    requested=new_total,
                    cap=self.usd_cap,
                )
            self._spent_usd = new_total

The lock matters when an agent kicks off two tool calls in parallel. Without it, both threads can read the current spend, both think there is room, and you blow past the cap by one call.

My Tech Stack

  • Python 3.10+ with requests and an optional jsonschema extra.
  • Hermes-3-Llama-3.1-405B via the OpenRouter free tier. Hosted on openrouter.ai.
  • A deterministic offline HermesStub so the demo runs without a key.
  • Pytest for the 23-test suite.

The full stack is intentionally tiny. The four governance modules together are about 300 lines, including type hints and docstrings. The harness should be small enough that you read the whole thing before depending on it.

How I Used Hermes Agent

Hermes-3-Llama-3.1-405B is the agentic model the challenge is built around. It is instruction-following enough that you can ask for JSON and usually get JSON. It does well on multi-step prompting where you walk it through a structured task.

What it is not is governed. Out of the box, a Hermes call has no per-session budget, no allowlist on tool fetches, no audit trail, and no contract on the reply shape. None of those are model problems. They are wrapper problems. So I wrote a wrapper.

The agentic capability I leaned on most is structured output with tool use. The summarize_url flow asks Hermes to act as a summarizer over a fetched document and return a JSON object with title, key_points, sentiment, and confidence. The structured output layer catches the cases where Hermes drifts into prose or skips a required key, and the repair prompt walks it back.

I picked Hermes for two reasons. First, the free tier on OpenRouter is enough to test the full path end-to-end, including the budget cap, without a paid key. Second, it runs locally if you want it to. The same wrapper works against vllm or llama.cpp serving the same Hermes-3 checkpoint. You only swap the URL inside HermesClient.

What I learned

Per-call cost matters more than total cost. A cap of one dollar is easy to think about. A cap of $0.000196 per call is the number that actually catches a runaway loop.

Repair prompts cost more than first prompts. The cap has to leave room for the repair call. Size it to exactly one call’s worth and the repair will never fire because the budget catches it first.

Egress allowlists are smaller than people expect. For the URL summarizer demo, the whole allowlist is {example.com, openrouter.ai}. Two hosts is the entire attack surface for outbound HTTP.

The repo is public and MIT-licensed: github.com/MukundaKatta/hermes-stack. Issues and PRs welcome.

Thanks to the DEV team and Nous Research for running the challenge. The four-layer pattern was waiting for an excuse to land in one place, and Hermes was a good excuse.

Your AI database agent should not query the primary by default

Most AI database questions do not need the primary database.

They feel urgent because somebody typed them into a chat box.

But many are exploratory reads:

  • trends
  • customer lists
  • backlog summaries
  • operational snapshots
  • anomaly checks
  • aggregate reporting

If every question goes straight to the primary, the AI assistant becomes another source of production pressure.

For production MCP database servers, I think read replica routing should be a default design question.

The important part is not only routing. It is making the routing visible:

  • source role used
  • replica lag
  • snapshot/query timestamp
  • freshness window
  • fallback reason if the primary was used
  • audit/query ID

Longer version: Read replica routing for AI database agents

A stale-but-labeled answer is often safer than a live answer that quietly competed with production traffic.

The Flawed Assumption of Global E-commerce Platforms

The Problem We Were Actually Solving

The truth, however, was that the client wanted to bypass the costs associated with Stripe and PayPal’s foreign transaction fees, which can range from 3% to 4% above the domestic rate. They envisioned using Payhip or Gumroad, which promised a seamless integration and a lower cut. Our real goal was to find a solution that would allow us to maintain a single checkout process, irrespective of the client’s location.

What We Tried First (And Why It Failed)

We began by integrating Payhip, a popular e-commerce platform that promised ease of use and minimal fees. We implemented Payhip’s API, expecting it to work seamlessly with our font repository. However, we soon discovered that Payhip does not support payments in countries where PayPal is restricted, which is the case for several African nations and some Asian countries. Moreover, Payhip does not provide real-time updates, making it challenging to fulfill orders and manage inventory efficiently.

The Architecture Decision

We eventually decided to develop a custom solution using a combination of APIs from local payment gateways in these restricted countries. We chose a microservices architecture, where each service was responsible for handling a specific payment method. We implemented real-time updates using Webhooks and made sure our APIs were complaint with local regulations and security standards.

What The Numbers Said After

The numbers showed that our custom solution reduced transaction fees by up to 30% in countries with restricted payment options. We also observed a significant decrease in failed transactions, which had previously been a major issue with Payhip. Our clients were able to fulfill orders quickly and efficiently, and we reduced our support requests related to payments.

What I Would Do Differently

In retrospect, I would have factored in the potential integration challenges and regulatory complexities of using local payment gateways from the outset. I would have also considered integrating APIs from multiple payment gateways to provide clients with more payment options. Lastly, I would have included more provisions for testing and debugging our custom solution to ensure a smoother rollout.

In the end, our custom solution proved to be the more reliable and cost-effective option, despite the initial challenges. It taught us a valuable lesson about the importance of understanding the intricacies of global e-commerce platforms and the need for custom solutions in certain regions. As engineers, we often get caught up in the allure of off-the-shelf solutions, but it’s essential to remember that sometimes, a carefully crafted custom solution is the only way to truly solve the problem at hand.

PayPal’s Gatekeeper Problem: How I Ditched the Middleman and Began Accepting Cryptocurrency Directly

The Problem We Were Actually Solving

It wasn’t just about getting paid; it was about establishing trust with my customers. By accepting cryptocurrency directly, I could demonstrate my commitment to transparency and flexibility. I wanted to show the world that I was more than just a product – I was a community-driven business.

What We Tried First (And Why It Failed)

My initial solution was to integrate a third-party payment gateway that supported cryptocurrency. On paper, it seemed like a straightforward fix. But in reality, it created more problems than it solved. The fees were exorbitant, and the user experience was clunky. My customers struggled to navigate the additional steps required to complete a transaction. I watched as they abandoned their carts, frustrated by the complexity.

The telltale sign of this approach’s failure was the alarmingly high bounce rate from my server logs. According to my monitoring tool, New Relic, the average request latency spiked to over 300ms during peak hours. My application was struggling to keep up with the demand, and the user experience was suffering as a result.

The Architecture Decision

I knew I had to think outside the box. I researched and experimented with various solutions, ultimately landing on a custom-built cryptocurrency payment processor using the Rust programming language. It was a bold move, but I was willing to take the risk.

I used the libsecp256k1 crate to handle the cryptographic aspects of the payment processing, ensuring fast and secure transactions. I implemented a message queue using the async-std crate to handle the high volume of requests during peak hours. The result was a robust and reliable payment processor that could scale with my business.

What The Numbers Said After

The results were nothing short of astonishing. With the custom-built payment processor, my average request latency plummeted to under 10ms during peak hours. My server logs showed a significant reduction in bounce rate, and my customers appreciated the seamless user experience.

The numbers told the story:

  • CPU usage: 20% reduction
  • Memory usage: 15% reduction
  • Request latency: 97% decrease
  • Bounce rate: 25% decrease

These improvements didn’t go unnoticed. My customers were happier, and my business was thriving.

What I Would Do Differently

In retrospect, I would have opted for a more gradual approach to implementing the custom payment processor. The initial learning curve for Rust was steep, and I underestimated the time required to fine-tune the system. I would have also invested more in testing and debugging, ensuring that the system was rock-solid before going live.

However, the end result was worth the headaches. By ditching the middleman and building my own payment processor, I gained control over my business and established a reputation for trust and transparency. It’s a decision I wouldn’t trade for anything.

510(k) clearance pitfalls — the weak links that actually stall approval

I’ve shepherded several Class II devices through the 510(k) gauntlet and reviewed enough reviewer comments to notice a pattern: approvals rarely fail because a device “doesn’t work.” They stall because the submission doesn’t tell a clear, evidence-backed story. Below are the weak links I keep seeing — concrete, fixable problems that turn reviewer questions into hold letters and extra testing cycles.

The core problem: a disconnected story

FDA reviewers are looking to connect three things: intended use/indications, design and risk controls, and objective evidence (bench, software, biocompatibility, human factors, clinical when needed). When those links are missing, reviewers raise issues.

Common symptom list:

  • Vague intended use or inconsistent labeling across documents.
  • Tests that don’t directly map to the claims you make.
  • Risk controls that are documented in the DHF but not verified in the V&V package.
  • Software build artifacts without a reproducible trace to validated test runs.

If your submission forces the reviewer to “fill in the blanks,” expect delays.

Frequent weak links that cause holds

  1. Intended use & predicate mismatch
  2. Problem: The intended use or technological characteristics you list don’t match the predicate you cite, or you try to mix predicates in ways that obscure equivalence.
  3. Tip: Be explicit about the predicate you chose and why the differences don’t raise new questions of safety/effectiveness. If differences exist, describe mitigations and supporting evidence.

  4. Poorly scoped verification/validation

  5. Problem: Tests exist, but pass/fail criteria, acceptance rationale, or linkage to user needs/requirements are unclear.

  6. Tip: Use a requirements-to-test trace matrix. For software, include the exact build used for V&V and reproduce test logs. If you run automated unit/CI tests, export artifacts that show timestamps, pass/fail, and environment.

  7. Incomplete risk management

  8. Problem: ISO 14971-style hazards are listed, but risk controls aren’t implemented or their effectiveness isn’t verified.

  9. Tip: Show the chain — hazard → risk control → verification test → residual risk acceptance. Don’t leave reviewer guessing whether a control was actually validated.

  10. Human factors/usability gaps

  11. Problem: You submit a human factors protocol but no summative validation for critical tasks, or you use formative data to claim user safety.

  12. Tip: Follow IEC 62366 guidance: identify critical tasks, run summative testing in realistic environments, and include failure modes observed and mitigations.

  13. Biocompatibility and materials questions

  14. Problem: Materials declared “biocompatible” but unsupported by ISO 10993 testing or unsuitable extraction methods.

  15. Tip: Match the device’s patient-contact type and duration to the correct ISO 10993 tests and include rationale for any deviations.

  16. Software lifecycle & cybersecurity missing pieces

  17. Problem: Software submissions without a clear IEC 62304 mapped lifecycle, traceability from requirements to code to tests, or without basic cybersecurity risk analysis.

  18. Tip: Provide software architecture, hazard analysis, build artifacts, and cybersecurity risk control verification. If you use libraries or third-party components, list versions and SBOM-like details.

  19. Sterilization and packaging documentation

  20. Problem: Sterility assurance level and packaging integrity tests aren’t tied to ISO 11607 or lack worst-case rationale.

  21. Tip: Provide method, cycle data, validation rationale, and worst-case package testing results.

  22. Supplier control & manufacturing readiness

  23. Problem: The QMS indicates design control work is done, but suppliers for critical components lack approval evidence or incoming inspection criteria.

  24. Tip: Include supplier qualification files for critical parts, change-notice procedures, and how production verification maps back to the device spec.

Practical steps I use to close the gaps

  • Build a submission checklist from the reviewer’s perspective. Walk through the 510(k) as if you’re verifying their ability to answer “how does this make the device safe and effective?”
  • Maintain a live traceability matrix (requirements → design outputs → risk controls → verification/validation). Export snapshots as part of the submission.
  • Version-control your V&V artifacts. For software, include reproducible builds and CI logs. For bench tests, include raw data, protocols, and acceptance criteria in one package.
  • Run an internal “red-team” review: a QA person unfamiliar with the project reviews the submission for missing links.
  • Use pre-sub meetings selectively. Bring a focused list of questions (e.g., acceptance of a nonclinical test plan) and capture FDA feedback to reduce subjective reviewer requests later.

What I automate

I automate three things that save time during review:

  • Traceability exports (requirements-to-test) from our QMS so every submission has a single-source-of-truth snapshot.
  • Packaging of V&V PDFs with a deterministic filename structure so reviewers can find matching protocol/results quickly.
  • Software SBOM and build artifact generation from CI — this avoids “which build was tested?” questions.

Final thought

Most 510(k) stalls are avoidable if the submission reads like a connected argument: this is the claim, this is why we’re confident, and here is the evidence that proves it. Fix the weak links before you submit and you’ll skip rounds of extra testing and clarification.

What’s the single documentation gap that has bitten you most during a 510(k) — and how did you fix it?

Building A Cross-Border E-commerce System That Just Works

The Problem We Were Actually Solving

As a data engineer, I was tasked with designing a system that enabled digital creators from various countries to sell their products online. The challenge was straightforward: existing e-commerce platforms were not designed to handle the complexities of cross-border trade, and their infrastructure was not scalable to meet the needs of creators from resource-constrained environments. Our objective was to create a system that could handle the nuances of different payment methods, currencies, and languages, while maintaining a seamless user experience. For creators in Nigeria, Pakistan, Ghana, Bangladesh, and dozens of other countries, this system would be a game-changer, allowing them to participate in the global digital economy.

What We Tried First (And Why It Failed)

Initially, we attempted to integrate existing e-commerce platforms with popular payment gateways. However, this approach quickly hit a roadblock. Payment gateways like Stripe and PayPal were not designed to handle the sheer diversity of payment methods and currencies used in developing countries. For instance, in Nigeria, the most popular payment method is bank transfer, while in Pakistan, it’s mobile wallet payments. Integrating these payment methods with existing e-commerce platforms was a nightmare, and the error rates were prohibitively high. Moreover, the performance of the system was woefully inadequate, with latency averaging around 5 seconds, which was unacceptable for a system that required instant payment processing.

The Architecture Decision

After a series of extensive discussions with our stakeholders and researchers, we decided to design a custom-built e-commerce system from scratch. This system would utilize a microservices architecture, with each service responsible for a specific function, such as payment processing, order management, and inventory tracking. We chose a combination of cloud-based services, including AWS and Google Cloud, to provide the necessary scalability and reliability. To mitigate the complexities of cross-border trade, we implemented a modular payment system that could accommodate various payment methods and currencies. This included integrating local payment gateways, such as Interswitch in Nigeria and HBL Omni in Pakistan.

What The Numbers Said After

After deploying the new system, we noticed a significant improvement in performance. The average latency dropped to around 200 milliseconds, making it possible for creators to process payments in real-time. Moreover, the error rates decreased by an astonishing 95%, which translated to a substantial reduction in support requests and associated costs. From a scalability perspective, the system could handle a throughput of over 10,000 transactions per minute, making it possible to support a large number of creators from diverse global regions. In terms of cost, we were able to reduce the query cost by 75% by optimizing our database schema and utilizing more efficient storage solutions.

What I Would Do Differently

Looking back, I would have liked to have explored more innovative payment solutions, such as blockchain-based payment systems, which could have offered greater security and transparency. Additionally, I would have invested more time in developing a robust testing framework to ensure that the system’s performance was thoroughly validated before deployment. Lastly, I would have explored more efficient data warehousing solutions to provide more granular insights into the system’s performance and user behavior. Despite these regrets, the system has been a resounding success, enabling creators from 50+ countries to sell their products online and participate in the global digital economy.

Low-Budget Multi-Device QA: Automating 3 Platforms with Open Source Tools

Low-Budget Multi-Device QA: Automating 3 Platforms with Open Source Tools

Practical automation patterns for health apps across Android APK, WeChat Mini Program, and Web backend — using only open source tools and the hardware you already have.

The Problem

You have a medical app that ships on three surfaces:

  • Android APK — the doctor’s side, a uni-app WebView wrapper
  • WeChat Mini Program — the patient’s side, running inside WeChat’s sandbox
  • Web Backend — admin panel, Vue3 + Element Plus

You have two test phones: an Oppo PCKM00 and a Huawei ANA-AN00. Your budget for test infrastructure: zero. No BrowserStack, no Sauce Labs, no paid SaaS.

Oh, and the APK is a WebView wrapper — the app’s core UI lives inside a WebView that’s invisible to Android’s UI dump (uiautomator2 can’t see it). And WeChat’s mini-program runtime intercepts standard automation primitives. And the two phones have different screen resolutions and keyboard heights. And you don’t have sudo on the CI machine.

This is the problem deep-test was built to solve. Here’s the playbook.

Architecture Overview

┌─────────────────────────────────────────────┐
│              deep-test (Hermes Agent)        │
├─────────────────────────────────────────────┤
│  core/                                       │
│  ├── device.py   → device registry + ADB     │
│  ├── coords.py   → multi-device scaling      │
│  ├── locator.py  → 3-layer self-healing      │
│  ├── ocr.py      → rapidocr wrapper          │
│  ├── runner.py   → retry + LLM fallback     │
│  └── web-runner.cjs → Playwright + Vue3 fix  │
├─────────────────────────────────────────────┤
│  projects/med-app/                          │
│  ├── android/   → login, patient, chat       │
│  ├── miniprogram/ → mini-program flows       │
│  ├── web/       → admin panel (Playwright)   │
│  └── scenarios/ → cross-platform orchestration│
├─────────────────────────────────────────────┤
│  reports/ (HTML + screenshots)               │
└─────────────────────────────────────────────┘

Hardware cost: $0. Every tool is open source. The phones are existing hardware. The LLM fallback uses DeepSeek V4 API (pay-as-you-go, roughly a few dollars per month).

Pattern 1: The 3-Layer Self-Healing Locator

HTML dumps can’t see WebView content. Pure coordinates break across devices. The solution: a cascade of three fallback strategies.

def locate(element_id, serial, device_alias):
    """Try each strategy in order. Fail fast, retry smart."""

    # Layer 1: uiautomator2 XML (fastest, works for native elements)
    try:
        return u2_session(serial).resourceId(element_id).bounds
    except:
        pass  # Element is in WebView — not in XML

    # Layer 2: Coordinate map (device-aware, cached)
    try:
        return Coords[device_alias][element_id]
    except KeyError:
        pass  # Unknown element — need OCR

    # Layer 3: OCR + LLM fallback (slowest but most resilient)
    screenshot = take_screenshot(serial)
    ocr_result = ocr(screenshot)

    # LLM reads the screenshot, returns the action + coordinates
    response = llm.ask(
        f"Screen shows: {ocr_result}. Find '{element_id}' and return its center coordinates."
    )
    return parse_coords(response)

What this solves:

  • Coord-only tests work on Oppo but break on Huawei (different screen dimensions)
  • uiautomator2 can’t reach WebView content inside the uni-app shell
  • OCR is slow but catches everything — acts as the safety net

Real-world numbers: Layer 1 handles ~30% of locators (native login buttons). Layer 2 handles ~50% (known UI elements in the mini-program). Layer 3 catches the remaining ~20% (dynamic content, confirmation dialogs). Average locate time with Layer 1: 200ms. Layer 3: 2-4 seconds.

Pattern 2: The Keyboard Nightmare

This single bug ate more debug time than any other issue.

The Huawei ANA-AN00’s stock IME doesn’t play nicely with adb shell input text. The keyboard overlays the password field, and after typing, the “Login” button is hidden behind the keyboard.

The two devices have different keyboard heights — the Huawei IME panel is ~310px, roughly 100px taller than the Oppo’s ~210px.

The fix sequence:

def type_and_submit(serial, text):
    # Step 1: Type text with chained commands (anti-IME swallowing)
    cmd = " && ".join(
        f"shell input text {ch} && sleep 0.08" 
        for ch in text
    )
    subprocess.run(["adb", "-s", serial, cmd], timeout=60, shell=True)

    # Step 2: Dismiss keyboard (CRITICAL)
    subprocess.run([
        "adb", "-s", serial,
        "shell", "input keyevent KEYCODE_BACK"
    ], timeout=5)
    time.sleep(2)

    # Step 3: Now the button is visible — click it
    coords = Coords.scale_y(device_alias, "login_button")
    subprocess.run([
        "adb", "-s", serial,
        "shell", f"input tap {coords.x} {coords.y}"
    ])

Key insight: KEYCODE_BACK dismisses the keyboard without leaving the form. A second press would exit the activity — one press is the sweet spot.

Why not use uiautomator2(text="登录").click()? Because when the keyboard is up, it intercepts the click target. The tap lands on the keyboard overlay, not the button.

Pattern 3: Defeating the IME Input Hog

Both Baidu IME (Oppo) and Sogou IME (Huawei) have a nasty behavior: they swallow individual adb shell input text commands that arrive too fast.

Wrong approach (will lose characters):

for ch in id_number:
    adb_cmd(serial, f"shell input text {ch}")

The stock IME on Oppo drops ~1 in every 3 characters this way. The 18th digit of an ID number is almost always missing.

Right approach (chained with sleep):

cmd = " && ".join(
    f"shell input text {ch} && sleep 0.08"
    for ch in id_number
)
adb_cmd(serial, cmd)

Each character gets 80ms of settling time. The entire 18-digit ID takes ~1.5s. Tested across 50+ runs: zero lost characters.

Pattern 4: Cross-Device Coordinate Scaling

The Oppo is 1080×2400. The Huawei is 1080×2340. Every Y coordinate needs to be scaled.

class Coords:
    BASE_DEVICE = "oppo"  # All coordinates recorded here
    REFERENCE_HEIGHT = 2400

    @staticmethod
    def scale_y(device_alias, element_key):
        """Scale Y coordinate from reference device to target device."""
        base_y = COORD_MAP[element_key][1]
        target_height = DEVICE_REGISTRY[device_alias]["height"]
        scale_factor = target_height / Coords.REFERENCE_HEIGHT
        return int(base_y * scale_factor)

With this, every interactable element has exactly one coordinate entry (recorded on Oppo), and all other devices auto-scale. Adding a Huawei Mate 60 or a Xiaomi 14 is a one-line config change.

Pattern 5: Playwright × Vue3 — The Synthetic Event Trap

Vue 3 doesn’t respond to Playwright’s synthetic click events. The framework dispatches a PointerEvent but Vue’s internal vnode listener doesn’t pick it up.

Doesn’t work:

await page.click('.el-button--primary');

Works:

await page.evaluate(() => {
    document.querySelector('.el-button--primary').click();
});

Why? Playwright’s synthetic events use CDP (Chrome DevTools Protocol) input dispatch, which bypasses Vue’s event delegation layer in certain configurations. element.click() fires the native click handler directly, which Vue’s runtime picks up correctly.

Rule of thumb: If Playwright clicks land silently (no error, no action), wrap them in page.evaluate().

Pattern 6: The OCR-Based Dynamic Button Locator

When a UI element moves based on previous actions (e.g., “Add Patient” button scrolls down as more patients are added), coordinates become unreliable. OCR is the solution.

def find_button_y(serial, button_text, max_scrolls=3):
    """Scroll down until the button text appears, return its Y."""
    for attempt in range(max_scrolls):
        texts = take_ocr(serial, f"find_{button_text}")

        for text_bbox in texts:
            if button_text in text_bbox.text:
                return text_bbox.center_y

        # Not found — scroll down
        subprocess.run([
            "adb", "-s", serial,
            "shell", "input swipe 540 1500 540 500 500"
        ], timeout=10)
        time.sleep(1.5)

    raise LocateError(f"'{button_text}' not found after {max_scrolls} scrolls")

This replaced a brittle coordinate system where the “Save” button Y shifted by ~48px per patient added. After 9 patients, it scrolled off-screen entirely.

Pattern 7: The LLM Self-Healing Loop

When a test fails despite all the above layers, the system doesn’t crash — it invokes the LLM.

Test Fails (e.g., Element 'start_consultation' not found)
    │
    ├─ Layer 1 Retry (×2): Re-query uiautomator2 with longer wait
    │     └─ Still failing? →
    ├─ Layer 2 Retry (×2): Refresh OCR with different threshold
    │     └─ Still failing? →
    └─ Layer 3: LLM Diagnosis
          ├─ Screenshot + error → LLM analyzes the screen
          ├─ LLM suggests: "A confirmation dialog 'Are you sure?' is blocking
          │   the button. Click coordinate (540, 720) to dismiss it."
          └─ Test applies the fix and retries

The LLM (DeepSeek V4 API, roughly a few dollars per month) reads the last screenshot and the error log, then suggests corrective actions. The script executes them and retries.

Real-world result: ~80% of “stuck” scenarios are recovered by Layer 3 without human intervention. The remaining ~20% generate a screenshot report for manual review.

Results After 3 Months

Metric Before After
Devices covered 1 (manual) 2 (automated, scalable)
Platforms per release 2 (Android + Web) 3 (+ WeChat Mini Program)
Test execution time 4h manual 45min automated
Flaky test rate N/A (manual) ~12% (self-healing catches ~80%)
Infrastructure cost $200/mo (BrowserStack trial) ~$0 hardware + ~few $ API
Reports generated Ad-hoc screenshots 27+ structured HTML reports
New device onboarding 2-3 days ~2 hours (coordinate calibration + testing)

The Tools

Tool Role Cost
uiautomator2 Android native element locator Free, open source
ADB Low-level device control Free, Android SDK
Playwright Web backend + limited mini-program Free, open source
rapidocr On-device OCR (no GPU needed) Free, open source
pytest Test runner Free
Hermes Agent LLM orchestration + self-healing Free, open source
DeepSeek V4 API LLM fallback (API call) Pay-as-you-go (prepaid credits)

Hardware cost: $0 (existing phones and computer). LLM API is pay-as-you-go, roughly a few dollars per month.

Lessons

  1. Don’t trust UI dump tools on WebView apps. uiautomator2, Appium, and their cousins can’t see inside WebView content. Plan for coordinate or OCR-based fallbacks from day one.

  2. IME input swallowing will waste a week of your life. Test adb shell input text with long strings (18+ chars) early, across all target devices. If characters drop, chain the commands.

  3. One KEYCODE_BACK press is never a bug; two is always a bug. Dismissing the keyboard after text input is mandatory but doing it twice exits the screen. Always count your back presses.

  4. Vue 3 + Playwright = use page.evaluate(). Don’t debug why page.click() silently fails. Just wrap it in evaluate() and move on.

  5. A 3-layer locator isn’t overengineering. It’s the difference between a test suite that breaks on every app update and one that survives for months with zero maintenance.

  6. Low-budget infrastructure is achievable. With one Android phone, one computer, and a small API budget, you can build a self-healing test suite that absorbs device-specific weirdness.

This framework is maintained as an open-source project. If you’re automating a health app, a WeChat ecosystem product, or anything with WebView + multi-device quirks — this playbook is built from the scars.

About open-sourcing deep-test: It’s currently closed-source while we continue refining and stabilizing the architecture. Once it matures, we’ll consider making it public. In the meantime, the tools mentioned here (uiautomator2 + ADB + rapidocr + Playwright) are all open source and free — the 7 Patterns in this playbook are enough to get you started.

About the author:
15 years in QA automation, creator of the deep-test framework. Building your own AI-powered test pipeline? You might find this useful:
👉 50 AI Testing Prompts for Web & Android — bilingual (EN/CN), $12, covering Web & Android testing scenarios.

Built with Hermes Agent on DeepSeek V4, one Oppo, one Huawei, and a QA engineer who refused to accept BrowserStack’s $200/mo bill.

GLIA — A holographic memory for AI agents that isn’t a graph and isn’t RAG

Every AI coding agent I’ve used (Cline, Claude, Cursor, etc) has the same problem: it forgets everything between sessions. You fix a complex race condition on Monday, and on Tuesday the agent suggests the same broken pattern again.

RAG (Retrieval-Augmented Generation) is the standard fix. You chunk files, embed them, and search by similarity. It works for direct questions. But it fails at associative reasoning. It can’t connect “rate limiting fails open” with “shared Redis connection pool” if those concepts never appear in the same text chunk. The relationship exists in the architecture, but RAG is blind to it.

Graphs are the other option. Nodes and edges. Better at relationships, but rigid. If you delete 30% of your edges, you lose entire paths. And let’s be honest: maintaining a massive knowledge graph for a changing codebase is a schema nightmare.

I wanted something different. Something that behaves more like a brain than a database. So I built GLIA.

What GLIA actually does

GLIA stores knowledge as 1024-dimensional vectors. Not text chunks. Not nodes. Patterns.

In GLIA, every piece of knowledge (a function, a decision, a bug fix) is a glyph — a distributed pattern across 1024 dimensions. No single dimension carries the meaning; the meaning is the “interference pattern” of the whole vector.

Holographic Binding (The “Secret Sauce”)

Relationships aren’t stored as edges in a table. They are encoded holographically using Circular Convolution.

Think of it as “folding” two patterns into each other. When you bind(A, B), you create a new vector that is mathematically related to both but looks like noise to anything else.

  • Superposition: You can add thousands of these bindings into the same 1024-d vector space. They don’t overwrite each other; they coexist as interference patterns.
  • Unbinding: Later, if you have A and the “memory substrate”, you can mathematically unbind them to recover B.

This means GLIA has Zero Edges. If you open the database, there is no relationships table. The architecture is the memory.

Why this beats a Graph

Three things a graph can’t do that GLIA does natively:

  1. Graceful Degradation: If you corrupt 30% of a glyph’s dimensions, its similarity only drops to ~0.85. It’s still recognizable. In a graph, deleting 30% of edges destroys entire paths permanently.
  2. Analogical Reasoning: Because it’s a vector space, king - man + woman produces a vector close to queen. In a codebase, this translates to structural analogies across different modules without explicit links.
  3. Hebbian Plasticity: GLIA isn’t static. When a pattern “resonates” (is retrieved), it gets stronger. Patterns that aren’t used decay over time and eventually fade to zero. The memory auto-cleans itself.

The Benchmarks (Rigorous & Reproducible)

I didn’t just “feel” it was better. I benchmarked GLIA v2 against a Graph (Spreading Activation) and BM25 (the algorithm behind Elasticsearch) across 3 real-world projects (Python backend, ML pipeline, React frontend).

I used 21 multi-hop questions per project — questions that require connecting 3 or 4 different files or concepts to answer correctly.

Metric GLIA Graph (V1) BM25
MRR (Avg) 0.851 0.344 0.870
Token Savings 97.8% — —
Latency 94ms 2ms <1ms
Edges in DB 0 696 0

The result: GLIA outperforms the graph-based approach by 2.5x in retrieval accuracy (MRR). It stays within ~2% of BM25 — a 30-year-old algorithm optimized specifically for keyword matching — while providing associative capabilities that BM25 could never dream of.

Against RAG with Gemini embeddings: RAG wins on precision by ~10%, but GLIA wins on cost ($0), offline capability, and plasticity (learning as it goes).

How to use it

GLIA is a local-first tool. No API keys required for the core engine.

git clone https://github.com/FelipeFariasAlfaro/glia.git
cd glia && pip install -e .

cd your-project
python -m glia init
python -m glia scan    # AST parsing, $0, instant
python -m glia recall "session expiration bug"

It exposes an MCP (Model Context Protocol) server, so it plugs directly into Cline, Cursor, or Claude Desktop. Your agent gets a “long-term memory” that actually understands how your project is wired.

{
  "mcpServers": {
    "glia": {
      "command": "python",
      "args": ["-m", "glia.mcp_server"],
      "env": {
        "GLIA_WORKSPACE": "/path/to/your/project"
      }
    }
  }
}

Once connected, the agent gets glia_recall, glia_learn, glia_scan, glia_forget, and glia_changes. Tell it in your custom instructions to query GLIA before answering and to teach it after completing tasks — the memory grows on its own from there.

What’s the catch?

Single-hop precision. If you ask “what does exactly this line do?”, RAG with high-end embeddings will be more surgical. GLIA is a Structural Memory. It’s designed to help an agent understand the relationships and history of a project, not to replace a grep tool.

The Stack

  • Binding: Circular Convolution via FFT (Fast Fourier Transform).
  • Encoding: Deterministic hash-projection + Synonyms + Stemming.
  • Storage: SQLite BLOBs (No edge tables, no complex joins).
  • Plasticity: Hebbian reinforcement + Temporal decay.

Everything is open source (MIT). If you’re tired of your AI agents having the memory of a goldfish, give it a try.

GitHub: github.com/FelipeFariasAlfaro/glia

Almost anyone can build an App in 2026, but here’s the part nobody mentions

When anyone starts trying to vibe code on a project for the first time, it’s quite typical that after typing something like “build me a project management app with a nice dashboard and user authentication” they become amazed on how with just such a simple prompt they can see in front of their eyes a fully functional application assembling itself. And not only that, but also the app buttons work, the overall layout looks clean and they can actually click around in it quite properly. The whole thing took just a couple of minutes and without the need of writing a single line of code.

That feeling is now happening, it’s real and the market has rewarded it if we just look at the numbers and realise that vibe coding tools have attracted over a billion USD in venture capital in 2025 alone. Lovable, a well known platform we’ve written about before and that was funded by a simple Nordic young guy, hit 200 million USD in annual recurring revenue. Cursor’s parent company was valued at 9,2B USD, with Bolt hitting 2,1B. With this data at hand, we can clearly see that these are not just experimental tools with a handful of geeky users.

As we approach the end of May 26, in this exact moment approx 60% of vibe coding users are not developers. So yes, as mentioned in the title, almost anyone can build an app now, but the question that we hear less often is: What kind of app exactly? What happens at the parts the demos never show?
Let’s start by giving credit to the sides that deserve it, because the capabilities are impressive in ways that of course matter a lot.

To start with Bolt.new, this platform runs entirely in the browser through WebContainers technology that requires no installation, no local setup and even no terminal. You simply describe an application in plain English or Spanish and it generates and runs the code live. Designers who have never opened a terminal are building prototypes in it right now.

v0 from Vercel had 2 million users generating React components and full landing pages from just text descriptions by the first quarter of this year alone. Lovable targets full stack web application generation with Supabase as the default backend, and has become the mandatory tool for people who want to test an immensely valuable thing as whether an idea is worth pursuing before spending money on a development team.

The productivity numbers for experienced developers using these tools are also real, with some statistics saying that AI coding tools have boosted individual developer output by almost 80% on average, measured by lines of code shipped. GitHub Copilot now has around 2 million paid subscribers and 20 million total users, and approximately 40% of all code written in the world today has become AI generated.

The workflow used by many in the industry is also quite common, with most starting with Bolt or Lovable to prototype fast, and then moving to Cursor or Claude Code for production level refinement. And this is absolutely changing how software gets built today, in ways unimaginable just a few years ago.

All those tools and ways of coding have brought to a way easyer, much quicker and incredibly cheaper reality things like rapid prototyping, idea validation and internal tools. And that is a meaningful democratization and definitely not hype.

Now, having said all of that, we should also explain the part that the product walkthroughs reliably skip, and we should understand that getting from 0 to 90% of an app might be quite easy with vibe coding, while getting from 90% to 100% (handling edge cases, authentication that doesn’t have vulnerabilities, payment processing, real deployment, production database design, or error states for every scenario a real user will eventually stumble into) is where things get complicated in ways that simple prompts don’t easily resolve.

Karpathy himself, the man who actually invented the term “vibe coding” and is maybe among the most technically capable person you could imagine using these tools, discovered this very early when right after building his own app with vibe coding, he wrote that it was “exhilarating and fun as a local demo but a bit of a painful slog as a deployed, real app”. So if Andrej Karpathy himself finds the last 10% a challenging part, it’s worth sitting with that for a moment.

The security picture is also worth looking at, and just about a year ago, in May 2025, a study found security vulnerabilities in 170 out of 1.645 apps built with Lovable (apps that real users were actually using and trusting with their data), and some critical security flaws were also identified on Lovable’s generated code. These are not random and lonely cases, but are more of a structural consequence of using tools that optimize for getting something working quickly rather than getting something secure reliably, and we should be aware of that.

Several developers who have tested these platforms on stress and at scale have also noted the same pattern, reaching to the conclusion that vibe coded apps work fantastically well for prototypes but “have patterns you will regret at scale” They basically experienced that the code that gets you to a demo often makes your life way harder when you try to grow beyond it. Not because the app is bad, but because it was simply not designed with growth in mind, it was designed to exist.

Very interestingly, a study published mid last year found that while AI coding tools boosted average developer output by 76%, experienced developers using AI assistance were paradoxically 19% slower than when they worked without it (even though those same developers believed they were working 20% faster, according to the study)
The explanation for this is what actually matters, because it was found out that those experienced developers knew when the AI had gotten something essentially wrong. When that happened, they just stopped, backtracked, rethinked the structure and catched the vulnerability before it shiped. And it was that process of oversight and correction that was taking a lot of time, a time that didnt show up as productive in metrics but absolutely shows up in whether the application works correctly in production.

This paradox captures something essential about what expertise actually does in software development, because it clearly shows that it is not primarily about being able to write code, but about knowing when the code is wrong, why it is wrong, what the consequences of that wrongness will be six months from now and how to fix it in a way that doesn’t create three new problems. A non technical user prompting Lovable has no mechanism to do that check because he lacks expertise, and when they see something that appears to work they just ship it. The problems arrives later in security audits, in production failures, in scaling walls and in technical debt that accumulates invisibly until it becomes impossible to ignore.

The honest synthesis and the conclusion we can extract from all the above is that vibe coding tools have created a true new capability tier that didn’t exist only three years ago. Today, a small team or even a single person with limited technical experience can build and validate a product idea at a speed and cost that previously required a full engineering team, and that matters a lot for founders, for product teams or for internal tooling at companies that can’t justify a dedicated developer or a team of developers. But that capability tier has a ceiling, and that ceiling arrives at the moment when the product starts to matter, when real users are depending on it, when security vulnerabilities have real consequences and when the architecture decisions made in the first minutes sprint start constraining everything that follows.

We live in a tech phase where the companies that use vibe coding tools most effectively treat them for just exactly what they are, basically extraordinary tools for speed and validation, but not replacements for engineering basis. Those companies know that the prototype gets built fast, but then it must be the professionals the ones who arrive to evaluate whether the foundation is worth building on, refactor what needs refactoring, harden what needs hardening and design the system that will actually scale.

This is also, frankly, why specialized software development companies have never been more relevant rather than less. The market is now full of beautifully looking products built by excited non technical teams who got to 90% faster than ever before and are now staring at the 10% that requires actual expertise. And the demand for that expertise at the moment, applied to real production systems, is actually higher than it has ever been.

For development firms that know what they’re doing, the era of vibe coding is not a threat but a pipeline, and many creative entrepreneurs can today just set up very quick and cheap companies to develop apps at very low costs and relying on experienced subcontractors for the more technical side of their initiatives.

As a brief of the above explained tools, we could brief as follows:

1- For non technical guys or teams out there validating an idea, Lovable and Bolt.new are the clearest starting points. Both run in the browser, require no setup and can produce functional full stack applications from natural language descriptions within minutes. Lovable handles more complex app generation and Bolt prioritizes raw speed and is excellent for proof of concept work.

2- For teams with some technical experience who want AI assistance in a real development environment, then Cursor is the most interesting tool, valued at around 9 billion for reasons that are obvious the first time you use its Composer feature on a complex codebase.

3- GitHub Copilot remains the most widely adopted AI coding tool overall, with so many people around the globe as paid subscribers and integration across every major IDE.

4- For frontend and UI generation specifically, v0 from Vercel produces React components of a quality that impresses even experienced frontend developers.

And for anything that will carry real user data, handle payments, operate at scale or be built to last beyond the initial prototype phase, our strong recommendation is to keep bringing in people who have done it before, because the most clear thing the vibe coding era has clarified is not that developers are becoming obsolete, but that getting something working and getting something right are still two meaningfully different things. One of them is now much faster than it used to be, and the other still takes what it always took. Time and knowledge.

Vibe Coding, la nueva forma de desarrollar software

GPT Images, designing without designers

Translock IT
Luis Yanguas Gomez de la Serna