Some time ago I wrote A Kind of Blue to explain why the Blue theme did not survive the Fluent refresh in Visual Studio. The short version was that Blue did not meet our accessibility standards, and the new tinted themes were designed around three pillars: cohesiveness, productivity, and accessibility.
The fuller answer arrived in the June update to Visual Studio 2026. The team shipped a new Theme colors options page that lets you override any Fluent color token directly in the IDE; no extension required, no marketplace detour. Customizations are saved per theme, and individual tokens can be reset without wiping everything else. The release notes describe it, and the underlying surface area is documented in the theme color token reference.
This is the right answer, and a better one than re-shipping any particular theme would have been. Blue was only the loudest variant of the request. Other people wanted darker than Dark, lighter than Light, no borders at all, or simply hated every design we offered. Re-shipping Blue would have settled exactly one argument, and not even the largest one. Opening the tokens settles all of them, including the ones we have not heard yet. The persistent thread across community tickets like Once more we ask for the Blue Theme back was never a request for one palette. It was a request for agency.
To make that concrete, I published three small JSON files:
Classic Blue, recreated — periwinkle background, slate body, warm tan tabs, navy status bar.
Dark, quieted — accents pushed to near-black so buttons and borders stop drawing the eye.
Light, flattened — accents pushed toward white so the chrome reads as faint outlines on a uniform canvas.
For more comprehensive changes, a full theme pack like Mads Kristensen's Blue Steel is still the right tool. For small overrides, this is the way. I suspect communities of taste will form around particular sets of tokens the way they form around color schemes in Vim or Emacs.
A Kind of Blue ended with a recommendation for Cool Breeze. I still recommend it. But for anyone who read that post and quietly wished the answer were a little more in their hands, the answer now is.
When I was a kid, most people did not know how to type. We took typing class. The final exam was a speed test: words per minute. Today, you will not impress anyone by saying you can type. In fact, cursive writing is fading. Kids increasingly cannot read or write it. We type constantly. We forget how many skills are learned, and how often some of these skills have faded.
But not everything fades. Socrates would be immensely popular today as a teacher. I still buy and recommend paper books.
Is reading and writing code more like Socrates, or more like cursive writing? There are clear signs that code could become like cursive writing. This year, I have met more than one student who could use AI to build an application but could not read or write code. It is not new. Software has long had non-technical people who describe what they built or designed. In fact, in much of the industry, the standard view was that once you had a university degree, you no longer coded. Coding was for monkeys or low-status employees. Top engineers paid a million dollars a year at Google or Meta know how to write code. They often read and write assembly and TypeScript. They know it all.
Why the discrepancy?
We pay an engineer a million dollars because he understands concepts few others grasp. He outruns others because he sees the problems more deeply. Reading and writing large amounts of code is part of how you gain those insights. Chatting with an AI will not make you a top 1% programmer. In the future, top engineers might read more code than anyone could in the past. These engineers will not be everywhere, but they will pack a punch. “But Daniel, people say programming is solved. Why read or write code?” Be careful with your models. When television arrived, some predicted it would replace the university lecturer. In some respects the model was correct, yet it did not happen. The lecturer’s job was never to deliver a TV show. The Google engineer paid a million dollars was never a machine that produces code. Nobody actually wants code, any more than they want raw text.
In fact, I predict a bifurcation in the tooling. The best engineers will work with tools that maximize their understanding of the code. I believe that reading and writing code, at a high level, is more like studying Socrates than like cursive writing. It is a necessary mental labor that does not become obsolete just because we have better tools for generating output.
We’ve invested in better frameworks, continuous integration, mocking libraries, code coverage tools, and more recently, AI coding assistants capable of generating hundreds of tests in minutes.
For years, software teams have focused on one goal: write more automated tests.
The results have been impressive.
Projects that once had almost no automated tests now boast thousands or even hundreds of thousands of them. Build pipelines run on every commit. Dashboards proudly display 85%, 90%, or even 100% code coverage.
Yet many development teams share the same uncomfortable feeling:
Can we actually trust these tests?
Passing tests tell us that nothing failed.
They don’t tell us whether the tests themselves are providing meaningful confidence.
As automated testing continues to grow, especially with AI generating tests at unprecedented speed, the challenge is no longer creating tests.
It’s understanding whether those tests are actually worth keeping.
We’ve Become Good at Measuring Quantity
Modern development teams have no shortage of testing metrics.
We measure:
Number of tests
Code coverage
Build success rates
Test execution time
Pass/fail percentages
These metrics are useful.
But they all answer essentially the same question:
Did the tests execute successfully?
They do not answer a far more important question:
Are these good tests?
A test can pass every day while still being:
Fragile
Duplicated
Dependent on external resources
Using ineffective mocks
Expensive to maintain
Providing almost no additional confidence
Traditional testing tools simply weren’t designed to answer these questions.
Why Code Coverage Isn’t Enough
Code coverage has become one of the most common quality metrics in software development.
Coverage is valuable.
It helps identify untested code.
But coverage was never intended to measure test quality.
Imagine two projects.
Both report 90% coverage.
Project A contains carefully designed tests that isolate behavior, validate meaningful outcomes, and fail only when production behavior changes.
Project B contains dozens of duplicated tests, fragile assertions, unnecessary mocks, and tests that quietly depend on the developer’s machine.
Both projects report exactly the same coverage.
Yet one provides significantly more confidence than the other.
Coverage measures what your tests execute.
It does not measure whether your tests deserve to exist.
AI Changes the Equation
Artificial intelligence is dramatically changing software development.
Today, developers can ask an AI assistant to generate dozens—or hundreds—of unit tests in seconds.
That’s an incredible productivity boost.
But it also introduces a new challenge.
If generating tests becomes nearly free, the bottleneck shifts.
Someone still needs to determine:
Which tests add value?
Which tests duplicate existing behavior?
Which tests are fragile?
Which tests rely on implementation details?
Which tests should be deleted?
The industry has spent years solving test generation.
Now we need to solve test validation.
Looking Beyond Static Analysis
Many tools analyze test code statically.
Static analysis is excellent for finding formatting issues, naming problems, or code smells.
But some of the most important testing issues only become visible while tests are actually running.
For example:
Does a test access the file system?
Does it communicate over the network?
Does it read the Windows Registry?
Does it depend on the current system time?
Does it rely on environment-specific configuration?
Is a mock configured but never actually used?
Are multiple tests exercising exactly the same behavior?
These aren’t simply code style issues.
They’re runtime behaviors.
Finding them requires observing tests as they execute.
Introducing TypeMock Test Review
TypeMock Test Review is designed to answer a simple question:
How good are your automated tests?
Rather than looking only at source code, Test Review executes your tests and analyzes what actually happens during execution.
Instead of simply reporting pass or fail, it reviews each test and highlights issues that may reduce confidence or increase maintenance costs.
Examples include:
Tests accessing external resources
Duplicate tests
Ineffective or unnecessary fake usage
Runtime behaviors that make tests fragile
The goal isn’t to criticize tests.
It’s to help teams continuously improve them. Learn more about TypeMock Test Review
One of the biggest causes of flaky tests is hidden dependencies.
A unit test should ideally execute in complete isolation.
Unfortunately, many tests quietly depend on resources outside the application.
Examples include:
Files
Network communication
Registry access
Current time
Environment variables
Random GUID generation
These dependencies often go unnoticed because they work perfectly on the original developer’s machine.
Months later, they begin failing in CI.
Or on another developer’s workstation.
Or after an infrastructure change.
By identifying these dependencies early, teams can build more reliable and predictable test suites.
Duplicate Tests Cost More Than You Think
Developers are quick to remove duplicated production code.
But duplicated tests often remain for years.
Duplicate tests create several problems:
Longer build times
Slower CI pipelines
More maintenance
Confusing failures
Reduced signal-to-noise ratio
Removing unnecessary duplicate tests makes the entire suite easier to understand and maintain.
Sometimes writing fewer tests actually increases confidence.
Better Tests Mean Faster Development
The goal isn’t perfection.
It’s confidence.
A smaller collection of reliable, meaningful tests often delivers more value than thousands of tests that developers no longer trust.
When developers believe the test suite, they refactor more confidently.
They release faster.
They spend less time investigating failures.
They stop asking whether a failing build represents a real problem or simply another flaky test.
The Next Evolution of Automated Testing
Software testing has evolved significantly over the last two decades.
First, teams focused on writing tests.
Then they focused on increasing coverage.
Today, AI can generate tests faster than ever before.
The next challenge isn’t generating even more tests.
It’s understanding which tests provide real value.
That requires looking beyond traditional metrics.
Passing tests and code coverage remain important.
But they are no longer enough.
Development teams need visibility into the quality of the tests themselves.
Try the Preview
TypeMock Test Review is available as a preview in TypeMock Isolator 9.5.
It helps development teams identify fragile, duplicate, and ineffective automated tests through runtime analysis—providing insights that traditional testing metrics often miss.
If your team already has thousands of automated tests—or AI is helping generate more every day—it’s worth asking a new question.
Not “Did the tests pass?”
But “Are these tests actually helping us build better software?”
The T-SQL Analyzer extensions for Visual Studio and SQL Server Management Studio have been significantly updated. Here's what's new.
Background
I maintain a collection of over 140 open source static code analysis rules based on the DacFX API, covering T-SQL best practices for design, naming, and performance.
I previously blogged about the launch of the T-SQL Analyzer extension for Visual Studio, which provided live analysis of SQL scripts as you author them.
This post covers a significant update to both the Visual Studio and the new SQL Server Management Studio extension.
What's new
Now available in SQL Server Management Studio 22
The live analysis experience is no longer limited to Visual Studio — it is now also available in SQL Server Management Studio 22 (SSMS). You get the same real-time feedback on your SQL scripts as you type, directly in the SSMS query editor.
Analysis of any SQL script — not just CREATE statements
Previously, live analysis only triggered on SQL scripts that contained CREATE statements — the kind typically found inside SQL database projects. The extension has now been revamped to analyze any SQL script you have open in the editor, whether it is a stored procedure, a query, a migration script, a one-off data fix, or anything else. If you're writing SQL, you now get feedback.
If you encounter any issues with this new feature, please create an issue in the GitHub repo
Configure via Tools / Options
Both extensions now expose settings under Tools → Options → *T-SQL Analyzer in Visual Studio and SSMS, so you can customize behavior without editing project files.
Setting
Description
Run static T-SQL analysis
Enable or disable live analysis entirely
SQL engine version
Set the SQL Server dialect used for analysis (SQL Server 2005 through SQL Server 2025, Azure SQL Database, Synapse, Fabric, and more)
Rule exceptions
Set a rules expression to suppress or enforce specific rules when no SQL project configuration is present (e.g. +!SqlServer.Rules.SRD0006;-SqlServer.Rules.SRN*)
These options apply globally when a script is not part of a SQL database project. When a project is involved, the extension defers to the project configuration (see below).
Project settings are still respected
If your SQL script is part of a SQL database project (based on MSBuild.Sdk.SqlProj, Microsoft.Build.Sql, or a classic .sqlproj), the extension continues to respect the project's own configuration:
<ProjectSdk="MSBuild.Sdk.SqlProj/3.2.0"><PropertyGroup><TargetFramework>net8.0</TargetFramework><SqlServerVersion>Sql170</SqlServerVersion><RunSqlCodeAnalysis>True</RunSqlCodeAnalysis><CodeAnalysisRules>-SqlServer.Rules.SRD0006</CodeAnalysisRules><!-- This property supports wildcard rule filters --><!-- and overrides 'CodeAnalysisRules' above if present --><AnalyzerCodeAnalysisRules>-SqlServer.Rules.SRD0006;-Microsoft.*</AnalyzerCodeAnalysisRules></PropertyGroup></Project>
This means if analysis is disabled in the project, or if you have suppressed specific rules via CodeAnalysisRules or AnalyzerCodeAnalysisRules, the live extension honours those settings. The global Tools/Options settings only apply when no project configuration is found.
Visual Studio 2026 and later: No separate installation required — the extension uses the dnx command to run the T-SQL Analyzer CLI as a NuGet package automatically.
Visual Studio 2022: Install the latest version of the CLI tool first:
The extension automatically uses the dnx command to run the T-SQL Analyzer CLI tool. No separate installation is needed, but the .NET 10 SDK is required (automatically installed with the Database DevOps workload in SSMS).
Summary
Feature
Before
Now
Visual Studio support
✓
✓
SSMS support
✗
✓
Any SQL script analyzed
✗ (CREATE only)
✓
Tools/Options settings
✗
✓
Respect SQL project settings
✓
✓
Feedback and contributions
Should you encounter bugs or have feature requests, head over to the GitHub repo to open an issue if one doesn't already exist.
If you enjoy using the extensions, please give them a ★★★★★ rating on the Visual Studio Marketplace.
In the previous blog post (Part 2) we began the migration by setting up the configuration. In this post, we’ll tackle the Blogger, which acts as an orchestrator for the agents.
In the python version of our program the blogger_prompt_template is in its own cell and fairly short. In the C# version we create a file Prompts.cs. The class is static and has a const string for each prompt. Let’s start with the Blogger prompt:
namespace BlogMigration;
public static class Prompts
{
public const string BloggerPromptTemplate = """
You are a blogger managing a blog post creation workflow.
Current Task: {main_task}
Current State:
- Research Findings: {research_findings}
- Blog Draft: {draft}
- Reviewer Feedback: {review_notes}
- Revision Number: {revision_number}
Your goal is to ensure a clear, engaging, and valuable blog post targeted at software developers.
Decide the next step and respond only with a JSON object (no extra text):
{
"next_step": "researcher" or "author" or "END",
"task_description": "Brief description of what needs to be done next"
}
Decision Rules:
- If no research exists, choose "researcher"
- If research exists but no draft, choose "author"
- If draft exists and reviewer says "APPROVED", choose "END"
- If draft needs revision, choose "author"
- If revision_number >= 4, choose "END"
""";
The triple quotes in a C# string create a raw string literal, introduced in C# 11. With this no escaping is needed and the string can be multi-line. There’s more to it, and I’ll refer you to the C# documentation.
With that, we’re ready to create the BloggerChain. The structure matches the Python code closely, of course using C# syntax.
using System.Text.Json;
using Microsoft.Extensions.AI;
namespace BlogMigration;
/// <summary>Creates the blogger decision chain.</summary>
public class BloggerChain(IChatClient llm, ChatOptions chatOptions) : IBloggerChain
{
public async Task<BloggerDecision> InvokeAsync(ResearchState state)
{
List<string> research = state.ResearchFindings;
string researchText = research.Count > 0 ? string.Join("\n", research) : "No research yet.";
int revision = state.RevisionNumber;
bool hasResearch = research.Count > 0;
bool hasDraft = !string.IsNullOrWhiteSpace(state.Draft);
string review = state.ReviewNotes;
if (review.ToUpperInvariant().Contains("APPROVED") && hasDraft)
{
Console.WriteLine("Blogger: Draft approved, ending workflow");
return new BloggerDecision("END", "Report approved and complete");
}
if (!hasResearch)
{
Console.WriteLine("Blogger: No research yet, directing to researcher");
return new BloggerDecision("researcher", $"Research the topic: {state.MainTask}");
}
if (hasResearch && !hasDraft)
{
Console.WriteLine("Blogger: Have research, creating first draft");
return new BloggerDecision("author", "Write the first draft based on research findings");
}
if (hasDraft && string.IsNullOrEmpty(review))
{
Console.WriteLine("Blogger: Have draft, sending to reviewer");
return new BloggerDecision("reviewer", "Prepare draft for review");
}
if (!string.IsNullOrEmpty(review) && !review.ToUpperInvariant().Contains("APPROVED") && revision <= 4)
{
Console.WriteLine($"Blogger: Revision {revision}, sending back to author");
return new BloggerDecision("author", "Revise the draft based on review feedback");
}
// Max revisions reached
if (revision >= 4)
{
Console.WriteLine("Blogger: Max revisions reached! Ending");
return new BloggerDecision("END", "Maximum revisions reached! Finalizing report");
}
// LLM decision as fallback
string prompt = Prompts.BloggerPromptTemplate
.Replace("{main_task}", state.MainTask)
.Replace("{research_findings}", researchText)
.Replace("{draft}", string.IsNullOrEmpty(state.Draft) ? "No draft yet." : state.Draft)
.Replace("{review_notes}", string.IsNullOrEmpty(review) ? "No review yet." : review)
.Replace("{revision_number}", revision.ToString());
try
{
ChatResponse response = await llm.GetResponseAsync(prompt, chatOptions);
string content = response.Text;
// Try to parse JSON
string text = content.Trim();
if (text.StartsWith("```"))
{
IEnumerable<string> lines = text.Split('\n').Where(l => !l.TrimStart().StartsWith("```"));
text = string.Join("\n", lines);
}
text = text.Trim();
BloggerDecision? decision = JsonSerializer.Deserialize<BloggerDecision>(text);
if (decision is not null && !string.IsNullOrEmpty(decision.NextStep))
{
return decision;
}
}
catch (Exception e)
{
Console.WriteLine($"LLM parsing error: {e.Message}");
}
// Final fallback - continue with author
Console.WriteLine("Blogger: Using final fallback - continuing with author");
return new BloggerDecision("author", "Continue with draft creation");
}
It is in this class that we can create the BloggerNode, which will be used in the same way as it is in the Python example:
Notice the use of a BloggerDecision object. Let’s create that in a file BloggerDecision.cs
using System.Text.Json.Serialization;
namespace BlogMigration;
public record BloggerDecision(
[property: JsonPropertyName("next_step")] string NextStep,
[property: JsonPropertyName("task_description")] string TaskDescription);
Program.cs has more to do than we’ve seen so far. Among other things, it will instantiate a BloggerChain object, which we’ll see after we create the Author, Researcher, and Reviewer related classes.
We’ll tackle the Author files in the next installment.