Latest updates

Company and industry news, featured projects, open source code, tech tips, and more.

How to reliably build software with an AI agent

Michael Argentini Avatar
Michael ArgentiniTuesday, November 3, 2026

More software developers each day are using AI software development agents (e.g. Open AI Codex, Claude Code, etc.) to build, maintain, and debug apps. Anecdotally the process is called vibe coding, and there are a couple ways people tend to work:

  1. The informal chat workflow: Simply start chatting with the AI, describing what you want, correcting or confirming responses, reviewing the built application, and repeat until happy with the result. This is the easiest process to use, and for smaller less complex apps it can work. But it takes much more time in the end, and the resulting app likely has security and/or performance holes, among other issues.

  2. The spec-driven workflow: This process requires more planning and is usually where serious vibe coders end up. This process involves writing a specification for your app (as one or more markdown files), using an AGENTS or README file to document where and what the spec is, more or less, and then proceed with workflow #1.

The technical flaw(s)

Although AI agents can be amazing at inference and accuracy, the gremlin in the machine is the fact that AI models don't think; they simply return probabilistic token predictions. That's why the same prompt can result in different responses, some of which may conflict with prior responses. For example, ask an AI what the most popular oil is, and you may get "olive oil" as the result most of the time, but then out of the blue you may get "Mobile 1". Specifying "cooking oil" would certainly help. But you may still get something other than "olive oil" every so often.

Besides AI models being probabilistic I would argue that perhaps the most important flaw is the fact that human language is too imprecise to be used for any meaningful software development. This is why the same series of chat messages will produce a different result, as if two different people were asked to create the same app independently.

This can also yield interesting security flaws. For example, I have seen vibe coded apps that appeared feature complete, passed the AI generated unit tests, and yet had "hidden" features woven into the code that were never requested. This can allow unauthorized access, crash opportunities for incomplete feature code, and a myriad of other exploitation vectors. This behavior is by design, but inherently insidious for the human at the wheel.

The imprecision of human language is also why markdown specs don't guarantee reliable, performant software development. It's just too difficult to write a spec with enough detail and nuance to ensure the AI agent builds exactly what you want. You'll never answer every important question, and you'll always forget important details. This is also why the waterfall development process was largely abandoned.

The code review myth

Software development shops and enterprise dev teams have been relying to some degree on code reviews to identify and remediate bad AI generated code. Some of these workflows even require a sign off review in order to merge the code into a production branch for deployment. The thought is that having experienced developers checking things will fill that gap.

There are two issues with this approach.

  1. If your AI generates 10,000 lines of code in a single day, it could take a week for a human developer to meaningfully review and understand that code. At that pace AI becomes the bottleneck.

  2. Software developers don't want to review code all day. They want to understand and solve problems by building things.

This results in an industry that thinks it filled the gap with code review even though not much actual code is being reviewed.

A reliable workflow

One workflow that may become the de facto standard for software development is a variation of test-driven development (TDD) called acceptance test-driven development (ATDD). How does it work?

Acceptance test-driven development is a way of building software where you first define what the finished software should do and how you will know it works before asking an AI agent to write the code. These expectations are written as clear, real-world examples of how someone should be able to use the software and what should happen in response. But instead of writing these scenarios as prose, they are written as tests. As you add tests for specific features, the AI agent sees that they're failing, and implements the code to make the test pass. You end up with a complete software application that is also testable! In fact, you could give another AI agent the same tests and it will also build a fully functioning app, even if the code varies a bit.

Instead of simply trusting that the AI has produced something that looks right, you have an objective way to verify that it does what was actually requested. This creates a shared understanding between you and the AI, keeps development focused on the desired outcome, and makes it easier to catch mistakes before they become bigger problems. The tests ARE the spec!

Simple ATDD workflow

The workflow is straightforward, albeit deep in flexibility. Here's one way to use it:

First, set up an empty project, and two test projects, one for ATDD tests, the other for AI-generated unit tests (yes, unit tests are also used to ensure reliability).

You can add an AGENTS file to help orient the AI agent regarding what you're building and how you want to work. Here's an example you can add to your AGENTS.md file:

# Pet Paws API Development Rules

## Method

Agents MUST USE Acceptance Test-Driven Development with fine-grained TDD. Acceptance test scenarios are the authoritative specification of business behavior.

## Read access

Agents may read all requirements, source code, tests, and test infrastructure.

## Permitted changes

Agents may change these paths during an authorized implementation task:

- `src/**`
- `tests/PetPaws.Api.AcceptanceTests/Drivers/**`
- `tests/PetPaws.Api.AcceptanceTests/Infrastructure/**`
- `tests/PetPaws.Api.AcceptanceTests/Scenarios/**/*Dsl.cs`
- `tests/PetPaws.Api.UnitTests/**`

Identify DSL, driver, and test-infrastructure changes for human review.

## Protected files

Do not change these paths without explicit authorization:

- `docs/**`
- `tests/PetPaws.Api.AcceptanceTests/Scenarios/**/*Tests.cs`
- `AGENTS.md`

Do not change solution or project configuration unless the authorized task requires it. Report those changes for review.

## Test integrity

Never change expectations merely to make tests pass. Never delete, skip, or disable failing acceptance tests. Never replace assertions with no-ops. Never fabricate application results inside a DSL or driver.

## Implementation

Keep business rules outside HTTP handlers. Add only behavior required by an approved scenario. Demonstrate RED before implementation, then use small GREEN and REFACTOR steps.

Put DSL code alongside its acceptance test file. Replace the `Tests` suffix with `Dsl`. Group related test and DSL pairs by feature. Keep folder nesting at three layers maximum.

Tests use real `WebApplicationFactory` hosts where appropriate.

Each test gets unique temporary directories, database files, etc.

Test cleanup removes only test-owned directories.

Next, in the ATDD test project start writing tests against a domain-specific language (e.g. pet store) for a specific scenario.

[Fact]
public async Task CustomerCanPurchaseAvailablePetFood()
{
    await using var store = new PetStoreDsl();

    await store.GivenProductInStockAsync("DOG-001", price: 24.99m, quantity: 10);
    await store.WhenCustomerPurchasesAsync("DOG-001", quantity: 2);
    await store.ThenOrderIsAcceptedAsync(expectedTotal: 49.98m);
    await store.ThenRemainingStockIsAsync("DOG-001", quantity: 8);
}

This code will not even compile. You made it all up.

Lastly, you then tell the AI agent to get it passing by using ATDD and implementing two things:

  1. A domain-specific language (DSL) which will use a protocol driver to perform actual work against the application.

  2. A protocol driver which is interface code that allows the DSL to call the application to get work done.

Fancy names, but basically the DSL houses the made up test methods, like GivenProductInStockAsync() which call the driver code to perform the work. The AI agent will understand this workflow. It will write the code for both of those layers, and also write the code in the application that ultimately gets tested. This pattern uses a Given/When/Then (GWT) naming convention/structure that makes it easier to read scenarios as domain-specific english language.

Here's a sample prompt template for the scenario test cycle:

# Implement One Approved Scenario

Target scenario: `<fully-qualified test method>`.

Follow `AGENTS.md` and all repository rules.

First, inspect the target scenario.
Confirm the target has `[Approved]`.
Treat only that scenario as scope.
Report missing DSL steps and conflicts.
Define precise success criteria.
Report the expected blast radius.
Then await explicit implementation approval.

After approval, show meaningful acceptance RED.
Extend DSL and drivers only when required.
Never change approved scenario expectations.
Use fine-grained unit RED/GREEN/REFACTOR cycles.
Implement only target-required production behavior.
Keep business rules outside HTTP handlers.
Preserve unrelated working-tree changes.

Run relevant unit tests.
Run the target acceptance scenario.
Then run the full solution suite.
Report commands and actual results.
Report DSL, driver, and infrastructure changes.
Report unresolved failures and risks.
Clean only touched implementation code.

Do not use git history as specification.
Git status and diffs remain allowed.

You then remediate and repeat until the test scenario works the way you want at the current stage of development.

With enough tests and good coverage, an app will be built; a testable app. Early in development features that pass won't perform all the correct actions. For example, adding a login may work, but it may not use an API endpoint. Later tests may implicitly make that a requirement in which case the earlier login code will get modified by the agent in order to keep the newer tests passing.

And the best part of ATDD for a software developer is that the act of writing and testing these scenarios still satisfies their need to solve problems and build things; secure, performant, testable things.

Want to know more?

There's usually more to the story so if you have questions or comments about this post let us know!

Do you need a new software development partner for an upcoming project? We would love to work with you! From websites and mobile apps to cloud services and custom software, we can help!

Beyond technology: Fynydd's commitment to our communities

Michael Argentini Avatar
Chris BywatersFriday, October 9, 2026
Making a difference isn't just about the work we do. It's about the communities we help build. Making a difference isn't just about the work we do. It's about the communities we help build.

Great companies help build great communities

At Fynydd, we believe that being a successful business means more than delivering exceptional technology solutions. It also means being an active, positive presence in the communities where our team members live, work, and raise their families.

Our business is built on solving problems, building relationships, and helping organizations succeed. We believe those same principles extend naturally into community service.

Whether it's supporting local nonprofits, volunteering our time, participating in civic organizations, or helping advance initiatives that make our neighborhoods better places to live, we recognize that even modest contributions can create a lasting impact.

A recent initiative in Lower Salford Township, Pennsylvania, offers a meaningful example of this philosophy in action.

Helping make Lower Salford Township a safer place

In October 2026, Lower Salford Township announced the completion of an initiative to expand public access to automated external defibrillators (AEDs) throughout its parks and community spaces.

Earlier in the year, the Township purchased 16 new AEDs to improve access to emergency equipment in locations where residents and visitors gather, exercise, and enjoy recreational activities.

These devices can play a critical role during sudden cardiac emergencies, providing potentially lifesaving assistance while emergency responders are on their way.

The AEDs are designed to be accessible to the general public, with voice-guided instructions that help users respond in an emergency. Installations have been completed at six locations throughout the township:

  • Heckler Plains Farmstead

  • Charles Reed Park

  • Dan Roth Park

  • Jacob Reiff Park

  • The Community Center

  • Alderfer Park

The devices are housed in publicly accessible, climate-controlled cabinets, making this important safety resource available in the places where our neighbors spend time together.

AED installation. AED installation.

Local organizations working together for a common purpose

This important initiative demonstrates the power of collaboration between local government and community service organizations.

The project received financial support from three organizations with a strong tradition of serving the Harleysville community:

  • Harleysville Jaycees

  • Harleysville Lions Club

  • Rotary Club of Harleysville

Working alongside Lower Salford Township, these organizations helped make lifesaving equipment more accessible to residents and visitors.

Each organization brings its own history of service and commitment to the community. Together, their contributions supported an initiative with the potential to make a difference when every second matters.

At Fynydd, we appreciate and recognize the work of all three organizations, the Township, and the many individuals who helped make this project a reality.

https://www.lowersalfordtownship.org/resources/news/article/?id=7330

Want to know more?

There's usually more to the story so if you have questions or comments about this post let us know!

Do you need a new software development partner for an upcoming project? We would love to work with you! From websites and mobile apps to cloud services and custom software, we can help!

Umbraco CMS 19 is focused on reusable content

Michael Argentini Avatar
Michael ArgentiniMonday, September 28, 2026

For more complex websites, Umbraco CMS developers have had to use the content tree in creative ways to implement reusable and non-routable content features. So what does that mean in practice?

Reusable content includes website content that should appear on more than one page, like a call to action for a newsletter signup, an alert or advertisement, a header or other visual element, etc. It can also include content used outside of web pages, like product specs used in a similar series of products.

Non-routable content is any content that is not a web page. This can include things like custom website settings, libraries of content like product data, complex picker options, et al. Many of these kinds of content are shown on web pages in one way or another, but are not themselves web pages in the technical sense.

Umbraco CMS 19 is a LTS (long-term support) release with the usual bug fixes and performance improvements, set to be released in December of 2026. One notable new addition addresses the issues with reusable and non-routable content I just described. It's a new content editor section named "Library".

The Library

The new Library area of the back office first appeared in version 18 supporting reusable content elements using a content picker. This allowed content editors to choose 1 or more reusable items as a property of another content item. For example, choosing a color scheme for a page header.

Demo of the initial Library release in Umbraco CMS 18.

Version 19 adds the use of Library items in the block editor, which is used for stacking content to build web pages, among other things. In fact, it also adds the ability to both detach Library content so changes don't affect all consumers of the item, and save a block to the Library so others can access it. This can even be used as a sort of templating system to bootstrap page content with a starter block.

One of the patterns we've used for a long time is creating a Settings node under the home page node, and under that creating a tree structure of configuration items like short codes, forms, shared blocks, organization profile, menus, 301/302 redirect mappings, domain redirects, and more. Much of this structure can be moved to the Library, and some of it (like shared blocks) can go away entirely, which is a big win!

Settings and Global Units are now in the Library, leaving more content tree space for web pages. Settings and Global Units are now in the Library, leaving more content tree space for web pages.

Another benefit of moving these items outside the content tree is that all tree items are inherently routable, meaning, even without a template appliedm, Umbraco will still process requests to these items and try to serve them as pages, which wastes server resources and allows curious or malicious people to explore the structure of your content tree by looking at the request responses. You can code around this, as we typically do, to serve 404 (not found) pages. But it's another chore to remember which the Library solves by not routing its content.

Want to know more?

There's usually more to the story so if you have questions or comments about this post let us know!

Do you need a new software development partner for an upcoming project? We would love to work with you! From websites and mobile apps to cloud services and custom software, we can help!

SQL Server 2025 on Apple Silicon

Michael Argentini Avatar
Michael ArgentiniThursday, July 16, 2026

Software developers using Apple Silicon Macs who work with Microsoft SQL Server have been wrestling with how to build on their local computers for some time. The problem is that SQL Server only supports x64 processors, even in the latest editions. And that's just the start.

The problem

In the past, when Macs used Intel x64 chips, developers could run a SQL Server instance in several ways:

  1. Use an x64 Windows machine on their local network (yuck)

  2. Run a Docker container with the free version of Azure SQL Edge

  3. Run SQL Server on an x64 Windows virtual machine using Parallels Desktop

  4. Boot into x64 Windows using Boot Camp, and just work in Windows

When Apple Silicon was released, things got complicated. We were left with these options:

  1. Use an x64 Windows machine on their local network (yuck)

  2. Run a Docker container with the free version of Azure SQL Edge; uses Rosetta 2 to emulate x64

  3. SQL Server would not run in a Parallels virtual machine because only Arm64 Windows 11 was supported, and no SQL Server editions would run through the Windows Prism emulation.

  4. Boot Camp no longer exists

Apple recently added another wrinkle by deprecating Rosetta 2 in 2028. And Microsoft added a wrinkle by deprecating Azure SQL Edge in 2025. So we are left with one option:

  1. Use an x64 Windows machine on their local network (yuck)

The solution

We now have a viable solution. It's not the perfect option. A perfect option would be a containerized version of SQL Server that was Arm64 native. But we do have the next best thing...

When Microsoft recently released SQL Server 2025, one of the things they added was support for running it on Arm-based Windows 11 using the Prism emulation layer. This meant that, although not officially supported, we could use an Arm-based Windows 11 virtual machine (using Parallels Desktop) to host the latest version of SQL Server, even Enterprise Edition!

The caveat

There is an issue with the installation that causes SQL Server to only bind to a named pipe. This allows you to use it on the virtual machine using SSMS but nowhere else. But after much trial and error I discovered that TCP binding support is there, it's just not available in the SQL configuration tool where you would enable it. Some registry additions restore and enable the binding. Below is a simple PowerShell script that does this for you. Run this in an elevated PowerShell instance.

$registry = [Microsoft.Win32.RegistryKey]::OpenBaseKey(
    [Microsoft.Win32.RegistryHive]::LocalMachine,
    [Microsoft.Win32.RegistryView]::Registry64
)

$instances = $registry.OpenSubKey('SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL')

$instanceNames = $instances.GetValueNames()

if ($instanceNames.Count -ne 1)
{
    $instanceNames
    throw 'Multiple instances were found. Set $instanceName explicitly.'
}

$instanceName = $instanceNames[0]
$instanceId = [string]$instances.GetValue($instanceName)

Write-Host "Configuring $instanceName ($instanceId)"

$tcpPath = "SOFTWARE\Microsoft\Microsoft SQL Server\$instanceId\MSSQLServer\SuperSocketNetLib\Tcp"
$tcp = $registry.OpenSubKey($tcpPath, $true)

if ($null -eq $tcp)
{
    throw "TCP configuration was not found at HKLM:\$tcpPath"
}

$ipAll = $tcp.OpenSubKey('IPAll', $true)

if ($null -eq $ipAll)
{
    throw "IPAll configuration was not found under HKLM:\$tcpPath"
}

$tcp.SetValue('Enabled', 1, [Microsoft.Win32.RegistryValueKind]::DWord)
$tcp.SetValue('ListenOnAllIPs', 1, [Microsoft.Win32.RegistryValueKind]::DWord)
$ipAll.SetValue('TcpDynamicPorts', '', [Microsoft.Win32.RegistryValueKind]::String)
$ipAll.SetValue('TcpPort', '1433', [Microsoft.Win32.RegistryValueKind]::String)

$ipAll.Dispose()
$tcp.Dispose()
$instances.Dispose()
$registry.Dispose()

$serviceName = 'MSSQL$' + $instanceName

if ($instanceName -eq 'MSSQLSERVER')
{
    $serviceName = 'MSSQLSERVER'
}

Restart-Service -Name $serviceName -Force

To make the server instance available to the host Mac, and devices on your local network, there are a couple additional steps.

First, set your Parallels virtual machine to use Shared Networking

Next, add 2 port forwards to the Shared networking model within the Parallels application preferences.

  • TCP port 1433 to 1433

  • UDP port 1434 to 1434

parallels-tcp-forwarding.png

Lastly, open those two ports in the virtual machine Windows Firewall.

Once these remaining steps are complete you should be able to connect to the SQL Server instance from your host Mac, or over a network from any other device, using the host Mac's IP address. This method uses more memory than a container, but it's also better than a container because it's a native edition of SQL Server 2025 running on actual Windows. This means you get all the additional features, including the new AI and vector embeddings support, C# code compatibility, and more.

#YOUREWELCOME

Want to know more?

There's usually more to the story so if you have questions or comments about this post let us know!

Do you need a new software development partner for an upcoming project? We would love to work with you! From websites and mobile apps to cloud services and custom software, we can help!

Open Source: Umbraco.Search.Qdrant

Michael Argentini Avatar
Michael ArgentiniSunday, June 14, 2026

The latest versions of Umbraco CMS offer a set of free AI enhancements as Nuget packages, one of which is a semantic search feature. The platform already comes bundled with Lucene/Examine for a powerful keyword search using word similarity matching, so the semantic search add-on fills a contextual search gap.

When implemented, this kind of search analyzes your content using an AI embeddings model so that your searches return results based on intent, not keyword matching or word similarity. The only requirement is that you have a vector database, which is used to store and query the embeddings. This project allows you to use the Umbraco semantic search feature with Qdrant; a free and open source vector database. It's also one of the fastest.

You can see this in action right on this blog! Our blog search can be used to ask questions and will return relevant results even if the words you use aren't in the content. It does this by analyzing the content and question using AI language analysis, much like a chatbot would, to return results based on what the post text means, not simply the words it contains.

Try this question:

How to know if data has been falsified?

This should return AI data poisoning and  Benford's Law as a top results, unless we added something more relevant since they were posted. Most of the question is noise words. And the remaining words are either not in the posts at all, or exist in many other posts as well. And yet it understood what you meant.

Give it a go

If you'd like to use the provider in your own Umbraco CMS 17+ project, add it by searching nuget for "Fynydd.Umbraco.Search.Qdrant", or find it using the button below.

If you want to check out the source code, fork and customize it, or make a pull request with new features or bug fixes, you can also find the project on Github.

Want to know more?

There's usually more to the story so if you have questions or comments about this post let us know!

Do you need a new software development partner for an upcoming project? We would love to work with you! From websites and mobile apps to cloud services and custom software, we can help!

The Umbraco AI Evolution

Michael Argentini Avatar
Michael ArgentiniWednesday, May 6, 2026

Umbraco CMS is a .NET content management platform used by individuals and large enterprise for everything from personal blogs to global SaaS products. It's powerful, extensible, easy to use, and open source. And it's evolving to embrace AI in ways that genuinely improve developer and content author experiences.

The new features and capabilities are foundational, based on real world needs, taking advantage of the technical opportunities large language models (LLMs) provide, targeting discreet areas in the platform. There are official Umbraco blog posts that talk about philosophy, areas of focus, primers on the individual AI technologies, and more.

The goal of this post is to provide a high-level summary of the changes and who they affect.

Make your AI agent an Umbraco CMS expert

Umbraco is constantly evolving, most notably releasing a new major version each year just after Microsoft likewise releases a new major version of .NET. This is a blessing and a curse. On one hand, Umbraco developers get to take advantage of new .NET performance, security, and other features. But supporting a sprawling ecosystem of Umbraco versions is a challenge, especially when the breaking changes and development patterns change so frequently and vary so much between major releases.

To address this, the Umbraco CMS team has added integrations to your AI agent platform of choice. These integrations provide additional technical information about Umbraco CMS, including best practices, as well as provide controlled access to your schema and data through the official Umbraco CMS APIs.

1. Model Context Protocol (MCP)

The MCP standard allows the hosting of very specific tools that can be used by AI agents to get things done in a more simple, direct way. Umbraco supports this by providing a separate MCP host application that developers can run to give their agents deep platform and data access through the Umbraco Management API.

How can MCP help?

There are a myriad of actions that your agent can perform using these MCP extensions, including categories from site structure and user permissions, to monitoring, analytics, localization, and advanced workflows.

They allow the agent to do things with the API that would normally be done using the back office. These are actions you perform during development, like creating document types and setting permissions.

2. Agent skills

Originally a Claude Code-only feature, but now available in other AI agent platforms like OpenAI Codex, agent skills are a way to tell the AI coding environment about a given technology, which speeds development and ensures that the correct development patterns (best practices) are followed.

Umbraco is providing agent skills that will allow you to better use your AI agent of choice to build on the latest versions of Umbraco (version 17+). It's important to note that as magical as they can appear to be, there are caveats:

  • Not a replacement for documentation. Skills point to the documentation and teach the AI how to apply it. You should still read the Umbraco documentation.

  • Not scaffolding tools. They don't generate boilerplate from templates. They teach the AI to write code that fits your specific requirements.

  • Not perfect. Skills improve results but cannot guarantee correctness. Always review generated code.

How can agent skills help?

They allow you to work with your agent to write code for things like a back office property editor for a YouTube video picker, or an notification that adds metadata to uploaded media, etc.

Use AI in Umbraco CMS

You can also leverage AI tools in the Umbraco platform itself, through the platform agnostic AI integration in the back office. You simply add the official Umbraco AI package, and configure the appropriate AI provider. Supported providers include:

  • Anthropic

  • AWS Bedrock

  • Google

  • Microsoft AI Foundry

  • OpenAI

You finish by adding a new profile (chat or embedding), which controls the type, usage, and behavior of the AI model instance. For example, you can set the model temperature to help with determinism. Note: the profile is the vehicle through which you use the AI instance in your own code.

How can the integration help?

You can use your configured AI tool in the back office through a chat interface, to help manage content and structure, clean up data, add media metadata, and more. You can also use the configured AI instance in your application code to provide AI-driven features with pipelines and guardrails, all using the Umbraco APIs (which use the Microsoft Extensions AI package under the hood).

Official documentation

The official Umbraco CMS documentation on AI features can be found at the links below. Though not exhaustive, these are key resources you should check out.

Umbraco in AI — Enhancements to make building with AI smarter.

AI in Umbraco — Add AI tools and capabilities to Umbraco CMS

Want to know more?

There's usually more to the story so if you have questions or comments about this post let us know!

Do you need a new software development partner for an upcoming project? We would love to work with you! From websites and mobile apps to cloud services and custom software, we can help!

Mythos: The forbidden AI model

Michael Argentini Avatar
Michael ArgentiniThursday, April 23, 2026

In April of 2026 Anthropic released a new AI model named Claude Mythos. This isn't the typical mixture of experts (MoE) model you'd use to find a recipe or answer a health question. It's trained specifically to find software security vulnerabilities at scale.

Even when working with the largest enterprise codebases, Mythos can...

  • Identify vulnerabilities at scale across major operating systems, browsers, and critical infrastructure

  • Generate working exploits for the vulnerabilities it finds

  • Operate at a speed and volume that has no human equivalent in offensive security research

  • Find vulnerabilities that have been missed for decades

  • Operate largely autonomously, without human steering

In one documented example, the model identified a 27-year-old vulnerability in OpenBSD, which Anthropic describes as “one of the most security-hardened operating systems in the world.” OpenBSD is not a consumer operating system. It runs firewalls, VPN gateways, and critical network infrastructure across financial institutions and government networks globally. Even the MacBook Pro I'm using to write this post is running an operating system (macOS) that is based on BSD Unix.

Project Glasswing: controlled access for gatekeepers

The conventional approach to managing vulnerabilities has been a race between patch cycles and attacker awareness. Mythos-class AI accelerates the creation of exploits in ways that strain the ability to code against them.

Needless to say, exploiting a single vulnerability like the one found in OpenBSD could wreak havoc globally across industries overnight. So Anthropic decided to create Project Glasswing, a controlled-access program providing Mythos Preview to twelve major technology organizations, including AWS, Microsoft, Google, Cisco, CrowdStrike, Nvidia, and Palo Alto Networks, backed by $100 million in usage credits. Usage credits are key because the current cost to scan these kinds of codebases can be in the tens of thousands of dollars.

This controlled access gives the good guys a head start on plugging the most critical security holes before this technology gets into the wrong hands. Attackers, according to Anthropic's offensive cyber research lead Logan Graham, will get equivalent capability from other labs within six to eighteen months. OpenAI is reportedly finalizing a model with comparable offensive cybersecurity capability. And while Anthropic has committed to investing in improved guardrails for future models, guardrails can be broken.

What's next?

We're in the early days of this new paradigm. The industry is handling it well, for the moment, albeit the head start won't last forever. Like the fabled tortoise in The Tortoise and the Hare, eventually the threat actors will catch up.

We're on the verge of seeing an increase in software security exploits. So it's important to focus on the basics, like upgrading your web platforms to their latest versions, and updating the OS and firmware of your Internet-connected computers and devices.

Start the process now. Your window of opportunity will eventually close.

Want to know more?

There's usually more to the story so if you have questions or comments about this post let us know!

Do you need a new software development partner for an upcoming project? We would love to work with you! From websites and mobile apps to cloud services and custom software, we can help!

AI hallucination is a feature not a bug

Michael Argentini Avatar
Michael ArgentiniThursday, April 2, 2026

While AI "hallucination" is typically viewed as a flaw (often providing comic relief), a recent whitepaper by OpenAI suggests it's actually a natural, emergent property of the way language models are currently trained and benchmarked.

Hallucinations are a logical consequence of optimization strategies that prioritize performance on standard benchmarks. These benchmarks are used to compare AI models, so the providers have every incentive to game the system so their models perform better than their competitors on these benchmarks.

Though that strategy does improve performance, it doesn't hide the inherent flaws in how models are trained.

For example, according to the whitepaper, on "Humanity's Last Exam" (HLE), a benchmark designed to be "google-proof" across dozens of fields, all reported state-of-the-art model scores were below 30% accuracy. This indicates a failure rate of over 70% on expert-level questions.

AI models are often inaccurate in both their answers and their self-assessment of correctness. Most current models on the HLE benchmark show calibration error rates above 70%. Though pretrained base models can be well-calibrated (with errors as low as 0.7%), post-training processes like reinforcement learning (PPO) can increase this flawed calibration to 7.4% or higher, making models more overconfident in their incorrect guesses

Also according to the report, if a certain percentage of facts (like birthdays) appear only once in a training data set, the model is expected to hallucinate on at least that same percentage of those facts. For example, if 20% of birthday facts are singletons, the model’s hallucination rate will be at least 20% for those prompts.

Best guess

Because models are trained to recognize patterns in large data sets, statistical pressures encourage them to calibrate their performance to the training data. This is because they are trained to minimize cross-entropy loss. For a model to be well-calibrated, it must assign probabilities that reflect the true likelihood of a statement being correct. In these cases, the statistical objective of the model forces it to produce a "best guess" rather than admitting it doesn't know.

The test-taking incentive

Benchmarks typically penalize honesty. Most influential benchmarks (like GPQA, MMLU-Pro, and HLE) use binary (0-1) scoring. In a binary scoring system, a correct answer earns 1 point, while an incorrect answer and an "I don't know" (IDK) response both earn 0 points.

Because there is no penalty for being wrong compared to being silent, the mathematically optimal strategy for a model is to always guess when in doubt. A model that always guesses will statistically outperform a "more honest" model that admits uncertainty on current leaderboards because guesses can be right some of the time.

Jack of all trades

Users expect modern AI models to "know everything". Even though they are trained on incredibly diverse data sets, this expectation is mostly unreasonable. Given this, any model that attempts to generalize beyond its training data must inherently risk hallucination. Otherwise, it would suffer from mode collapse, failing to produce the full range of valid human responses.

Even advanced techniques like Retrieval-Augmented Generation (RAG) or chain-of-thought reasoning do not eliminate this pressure because the underlying grading system still rewards guessing when these tools fail to find a definitive answer.

It's a consequence

Hallucinations are a rewarded behavior. AI models are optimized to be "good test-takers," and in the current world of binary evaluations, hallucination-like guessing is the most successful survival strategy for a model aiming for the top of a leaderboard.

Service confidence is also a concern. If a model responds "I don't know" or "I'm not confident in this answer", how many users would abandon the service? Since an informed guess is correct some of the time, this is seen as a good compromise to ensure service confidence.

The obvious answer

There are two obvious answers to this problem. One is to push the industry away from benchmarks and toward confidence scores. In this way the number of incorrect responses matters less than how often a model definitively provides a helpful response.

The second answer is to be dubious about the responses we get, knowing that a significant percentage of them are partially or completely incorrect. Be skeptical! As software developers our focus is to both use AI tools to provide faster, more varied services, and also to know when to check the recommendations and/or work performed by an AI model using the decades of experience we have in our field to do so. This mix of skepticism and "trust but verify" is what we bring to every client project and it has served us well.

Want to know more?

There's usually more to the story so if you have questions or comments about this post let us know!

Do you need a new software development partner for an upcoming project? We would love to work with you! From websites and mobile apps to cloud services and custom software, we can help!

AI data poisoning

Michael Argentini Avatar
Michael ArgentiniMonday, March 2, 2026

AI data poisoning is a process where an attacker deliberately alters an AI model’s training data to influence its behavior, causing it to generate biased, misleading, or harmful output. This threat is now recognized as a major vulnerability by security organizations like OWASP.

According to Carnegie Mellon University Security and Privacy Institute: "Modern AI systems that are trained to understand language are trained on giant crawls of the internet," said Daphne Ippolito, assistant professor at the Language Technologies Institute. "If an adversary can modify 0.1 percent of the Internet, and then the Internet is used to train the next generation of AI, what sort of bad behaviors could the adversary introduce into the new generation?"

Example: Propaganda

The Pravda network is a large group of fake news websites created by Russia in 2014. These sites target audiences in more than 80 countries and are designed to spread stories that support Kremlin disinformation. They work by repeating and amplifying messages from Russian media and pro-government Telegram channels. In 2024, the network expanded its efforts by launching sites focused on NATO and prominent political leaders such as Donald Trump and France’s President Emmanuel Macron.

To get around international restrictions on Russian state media, this network has shifted its tactics. Instead of relying only on traditional propaganda channels, it now tries to appear as a trustworthy source so that some of its content is used in resources like Wikipedia.

As a result, AI tools may unknowingly absorb and repeat these biased or false narratives. This can expose users to messaging that favors the Kremlin and criticizes Ukraine or Western governments when they interact with AI chatbots. It can influence elections. And it can drive people to make decisions that go against their self interests.

What can be done?

But fear not! There are ways to combat this problem. Some target the training process, and others put users in control. Here are a couple examples:

Blockchains

A blockchain is a shared digital ledger for logging transactions and tracking assets. You've undoubtedly heard this term when discussed in the context of cryptocurrency. Blockchains provide secure and transparent records of how updates to data are shared and verified due to the fact that existing information cannot be changed; only new items can be added.

In the context of AI training, if you need to change a fact, the original is never touched. So any new items that claim to revise the original stand out like a sore thumb. So by using consensus mechanisms, AI systems with blockchain-protected training can validate additions more reliably and help identify the kinds of anomalies that can indicate data poisoning before it spreads.

Skepticism

It's imperative that users corroborate information they find on the Internet with reputable sources, and this includes AI output. In a past article I used the example of Judge Julien Xavier Neals of the District of New Jersey, who had to withdraw his entire opinion after a lawyer politely pointed out that it was riddled with fabricated quotes, nonexistent case citations, and completely backwards case outcomes.

The old adage "don't believe everything you read" is never more true than when it refers to Internet-derived content. Be skeptical and check your sources!

Want to know more?

There's usually more to the story so if you have questions or comments about this post let us know!

Do you need a new software development partner for an upcoming project? We would love to work with you! From websites and mobile apps to cloud services and custom software, we can help!

Better search results with character n-grams

Michael Argentini Avatar
Michael ArgentiniWednesday, February 11, 2026

Vector search has been around for a long time. For example, Google has been using it since the late 1990s. This powerful, almost magical technology serves as a core component of most web and app services today, including modern AI-powered search using retrieval augmented generation (RAG). It provides a low-power way to leverage AI's natural language processing to find data with semantic context.

What is a vector database?

A vector database stores data as numerical embeddings that capture the semantic meaning of text, images, or other content, and it retrieves results by finding the closest vectors using similarity search rather than exact matches. Sounds like an AI large language model (LLM) right? This makes it great for web and app content because users can search by meaning and intent, not just keywords. So synonyms and loosely related concepts still match. It also scales efficiently which is one reason large organizations like Google have used it.

A happy side effect of how these platforms work is that they're also good at handling misspellings, to a point. To really get robust handling of spelling variations, however, two strategies tend to be common:

  1. Spell correct the actual search text before using it

  2. Include character n-grams in your vector database entries

What are character n-grams?

Character n-grams are vector embeddings (like commonly used semantic embeddings) that break text into overlapping sequences of characters, allowing vector search systems to better match terms despite typos, inflections, or spelling variations. Without these n-grams, a misspelled query like "saracha sauce" would likely return a higher score for "hot sauce" entries. But including character n-grams, a combined (fused) search would more consistently return a higher score for items with the correct spelling "sriracha sauce".

Using these n-grams can better handle searches with:

  • typos

  • missing letters

  • swapped letters

  • phonetic-ish variants

  • common misspellings

How does this work? At a high level, it adds a character match capability to the standard semantic search used by most vector database implementations. Here's a quick example of what happens under the hood. Take the first word in our previous example:

sriracha

  • 3-grams: sri, rir, ira, rac, ach, cha

  • 4-grams: srir, rira, irac, rach, acha

saracha

  • 3-grams: sar, ara, rac, ach, cha

  • 4-grams: sara, arac, rach, acha

Shared grams:

  • shared 3-grams: rac, ach, cha

  • shared 4-grams: rach, acha

So even though the beginning is wrong (sri vs sa), the ending chunks that carry a lot of the distinctive shape of "sriracha" survive (racha, acha, cha). And since the second word is the same, they have even more matching grams.

When these matches are fused with semantic matches, it adds weight to the correctly spelled "sriracha sauce" entry, yielding a better match set.

How to use character n-grams

When it comes to including character n-grams, there are only a couple changes you need to make to a standard semantic vector database implementation:

  1. When you generate embeddings, you also need to generate character n-gram embeddings; this is true both when you store data in the database, and when you search.

  2. When searching, you need to execute a search both on the semantic vectors and the n-gram vectors, then fuse the results using Reciprocal Rank Fusion (RRF), which is a great way to merge disparate result sets and combine the scores.

The following samples will fill those gaps. They are written with C# for .NET, which is part of a common stack we use to build cross-platform, secure, high-performance web and mobile apps and services for our clients. We also tend to prefer the vector database Qdrant for its performance, maintainability, and open source model. So that is also referenced in the samples.

References to AiService.GenerateEmbeddingsAsync() are not covered here. Essentially it's a method to generate standard semantic embeddings. Replace that with your own (likely existing) method. And references to QdrantService.Client are merely references to a standard Qdrant client provided by the Qdrant Nuget package.

Note: Some of the code was generated by AI, but was reviewed and refactored by an actual human developer (me!).

Character n-gram helper

First, you need a way to create n-grams. The CharNGramEmbedding class below will fill that gap. It allows you to generate character n-grams for a given string, and it also provides a method for fusing the semantic and n-gram search results into a single, weighted result set.

using System.Globalization;

namespace MyApp.Extensions;

/// <summary>
/// Generates a typo-robust, fixed-length dense vector representation of text
/// using hashed character n-grams.
/// </summary>
public static class CharNGramEmbedding
{
    /// <summary>
    /// Generates a normalized dense embedding vector for the specified text
    /// using hashed character n-grams.
    /// </summary>
    /// <param name="text">
    /// The input text to embed.
    /// </param>
    /// <param name="dims">
    /// The dimensionality of the output vector. Higher values reduce hash
    /// collisions at the cost of additional memory and storage.
    /// A value of 256 is a good default for typo-robust search.
    /// </param>
    /// <param name="minGram">
    /// The minimum character n-gram size to generate.
    /// Smaller values increase recall but may introduce noise.
    /// </param>
    /// <param name="maxGram">
    /// The maximum character n-gram size to generate.
    /// Larger values emphasize longer, more specific substrings.
    /// </param>
    public static float[] Embed(string text, int dims = 256, int minGram = 3, int maxGram = 4)
    {
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(dims);

        var v = new float[dims];
        var normalized = Normalize(text);

        if (normalized.Length == 0)
            return v;

        // Add boundary markers so "sriracha" and "sriracha sauce"
        // still share useful grams
        var s = $"^{normalized}$";

        for (var n = minGram; n <= maxGram; n++)
        {
            if (s.Length < n)
                continue;

            for (var i = 0; i <= s.Length - n; i++)
            {
                var gram = s.AsSpan(i, n);

                // Hash n-gram → index
                var h = Fnv1A32(gram);
                var idx = (int)(h % (uint)dims);

                // Optional sign-hash reduces collisions bias
                var sign = ((h & 1u) == 0u) ? 1f : -1f;

                v[idx] += sign;
            }
        }

        // L2 normalize for cosine similarity
        // (or dot product on normalized vectors)
        L2NormalizeInPlace(v);

        return v;

        static string Normalize(string input)
        {
            if (string.IsNullOrWhiteSpace(input))
                return string.Empty;

            // lowercase + strip accents + keep letters/digits/spaces
            var lower = input.ToLowerInvariant().Normalize(NormalizationForm.FormD);
            var sb = new StringBuilder(lower.Length);

            foreach (var ch in lower)
            {
                var uc = CharUnicodeInfo.GetUnicodeCategory(ch);
            
                if (uc == UnicodeCategory.NonSpacingMark)
                    continue;

                // ignore punctuation
                if (char.IsLetterOrDigit(ch))
                    sb.Append(ch);
                else if (char.IsWhiteSpace(ch) || ch == '-' || ch == '_')
                    sb.Append(' ');
            }

            // collapse spaces
            return string.Join(' ', sb.ToString().Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
        }

        static uint Fnv1A32(ReadOnlySpan<char> s)
        {
            const uint offset = 2166136261;
            const uint prime = 16777619;

            var hash = offset;
            
            for (var i = 0; i < s.Length; i++)
            {
                // hash UTF-16 chars (fine for this purpose)
                hash ^= s[i];
                hash *= prime;
            }

            return hash;
        }

        static void L2NormalizeInPlace(float[] v)
        {
            double sumSq = 0;

            for (var i = 0; i < v.Length; i++)
                sumSq += (double)v[i] * v[i];
            
            if (sumSq <= 0)
                return;

            var inv = (float)(1.0 / Math.Sqrt(sumSq));
            
            for (var i = 0; i < v.Length; i++)
                v[i] *= inv;
        }
    }
    
    /// <summary>
    /// Fuses multiple ranked result lists using <b>Reciprocal Rank Fusion (RRF)</b>.
    /// RRF is robust when combining heterogeneous retrieval signals (e.g. semantic
    /// embeddings and character n-gram embeddings) whose raw scores are not directly
    /// comparable.
    /// </summary>
    /// <param name="a">
    /// The first ranked result list (e.g. results from a semantic embedding search),
    /// ordered from best to worst. The list should already be truncated to a reasonable
    /// top-K size.
    /// </param>
    /// <param name="b">
    /// The second ranked result list (e.g. results from a character n-gram or typo-robust
    /// search), ordered from best to worst. The list should already be truncated to a
    /// reasonable top-K size.
    /// </param>
    /// <param name="getId">
    /// A function that extracts a stable, unique identifier from a result item.
    /// This identifier is used to merge and score items that appear in multiple lists.
    /// </param>
    /// <param name="take">
    /// The maximum number of fused results to return after applying Reciprocal Rank Fusion.
    /// </param>
    /// <param name="k">
    /// The RRF rank constant. Higher values reduce the impact of rank position differences
    /// between lists. Typical values range from 50 to 100; a default of 60 is commonly used
    /// in practice.
    /// </param>
    /// <returns>
    /// A list of fused results ordered by descending RRF score, containing at most
    /// <paramref name="take"/> items.
    /// </returns>
    public static IReadOnlyList<TPoint> FuseScoredPoints<TPoint>(
        IReadOnlyList<TPoint> a,
        IReadOnlyList<TPoint> b,
        Func<TPoint, string> getId,
        int take,
        int k = 60)
    {
        var scores = new Dictionary<string, double>(StringComparer.Ordinal);
        var best = new Dictionary<string, TPoint>(StringComparer.Ordinal);

        Add(a);
        Add(b);

        return scores
            .OrderByDescending(kvp => kvp.Value)
            .Take(take)
            .Select(kvp => best[kvp.Key])
            .ToList();

        void Add(IReadOnlyList<TPoint> list)
        {
            for (var i = 0; i < list.Count; i++)
            {
                var p = list[i];
                var id = getId(p);
                
                if (scores.TryGetValue(id, out var s) == false)
                    s = 0;

                // rank is i+1 (1-based)
                s += 1.0 / (k + (i + 1));
                scores[id] = s;

                // keep a representative point object
                best.TryAdd(id, p);
            }
        }
    }
}

Example upsert to Qdrant

Now that you have the character n-gram generation and fusion handled, following is an example of performing a Qdrant upsert of a sample food object, including both sets of vectors.

/// <summary>
/// Generates embeddings (semantic and character n-grams), and upserts data to Qdrant.
/// </summary>
/// <param name="food"></param>
/// <param name="json"></param>
/// <returns></returns>
public async Task<bool> UpsertFoodItemAsync(SampleFoodItem? food)
{
    if (food?.Description is null)
        return false;
    
    var semantic = await AiService.GenerateEmbeddingsAsync(food.Description) ?? [];
    var chargram = CharNGramEmbedding.Embed(food.Description);
    
    if (semantic.Length != AiService.SemanticEmbeddingSize || chargram.Length != AiService.CharGramEmbeddingSize)
        return false;

    var point = new PointStruct
    {
        Id = food.Id,
        Vectors = new Dictionary<string, float[]>
        {
            ["semantic"] = semantic,
            ["chargram"] = chargram,
        },
        Payload = 
        {
            ["description"] = food.Description
        }                
    };

    var result = await QdrantService.Client.UpsertAsync("food-collection", [point]);

    return result.Status == UpdateStatus.Completed;
}

Example Qdrant search

Lastly, the following example shows how you can search the Qdrant data using both sets of vectors. Embeddings (semantic and character n-grams) for the prompt are generated and used in the search.

For the best fused results each search (semantic, n-grams) needs to return 3-5 times the number of the final result set. This is because you're trying to recover a good final top-K from two imperfect retrievers. If each retriever only returns exactly K (or close to it), you often don't have enough overlap + near misses to let fusion do its job, especially when the two methods return different items, and rank positions aren't directly comparable.

/// <summary>
/// Search food data items.
/// </summary>
/// <param name="prompt">
/// Search text prompt can be a question or just search text (e.g. keywords)
/// </param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public async Task<List<ScoredPoint>> SearchFoodItemsAsync(string prompt, CancellationToken cancellationToken = default)
{
    const int MaxSearchResults = 5;

    var semantic = await AiService.GenerateEmbeddingsAsync(prompt);
    var chargram = CharNGramEmbedding.Embed(prompt);

    var semanticHits = await QdrantService.Client.SearchAsync(
        "food-collection",
        semantic,
        limit: MaxSearchResults * 5, // extra results padding for fusing
        vectorName: "semantic",
        cancellationToken: cancellationToken
    );

    var chargramHits = await QdrantService.Client.SearchAsync(
        "food-collection",
        chargram,
        limit: MaxSearchResults * 5, // extra results padding for fusing
        vectorName: "chargram",
        cancellationToken: cancellationToken
    );
    
    return CharNGramEmbedding.FuseScoredPoints(
        semanticHits,
        chargramHits,
        getId: p => p.Id.ToString(),
        take: MaxSearchResults
    ).OrderByDescending(o => o.Score).ToList();
}

Want to know more?

There's usually more to the story so if you have questions or comments about this post let us know!

Do you need a new software development partner for an upcoming project? We would love to work with you! From websites and mobile apps to cloud services and custom software, we can help!

© 2026, Fynydd LLC / King of Prussia, Pennsylvania; United States / +1 855-439-6933

By using this website you accept our privacy policy. Choose the browser data you consent to allow:

Only Required
Accept and Close