We’re back, and now it’s time to focus on the next set of tests! Looking at the next file I realized it was doing file io which would be more difficult to test. So let’s get started by separating concerns!

Extract File i/o

Prompt:

Update ChatHistoryParser to be passed in the file contents.

Response:

I’ll update the ChatHistoryParser class to accept file contents directly instead of reading from a file. This will require changes to both the parser class and the Program.cs file that uses it.

First, let’s modify the ChatHistoryParser.cs file:

llm-history-to-post/core/Services/ChatHistoryParser.cs

<<<<<<< SEARCH
public ChatHistory ParseHistoryFile(string filePath)
{
	if (!File.Exists(filePath))
	{
		throw new FileNotFoundException($"Chat history file not found: {filePath}");
	}
	
	var content = File.ReadAllText(filePath);
	var history = new ChatHistory();
=======
public ChatHistory ParseHistoryContent(string content)
{
	var history = new ChatHistory();
=======
>>>>>>> REPLACE

llm-history-to-post/core/Services/ChatHistoryParser.cs

<<<<<<< SEARCH
public ChatHistory ParseHistoryFile(string filePath)
{
	if (!File.Exists(filePath))
	{
		throw new FileNotFoundException($"Chat history file not found: {filePath}");
	}
	
	var content = File.ReadAllText(filePath);
	var history = new ChatHistory();
=======
public ChatHistory ParseHistoryFile(string filePath)
{
	if (!File.Exists(filePath))
	{
		throw new FileNotFoundException($"Chat history file not found: {filePath}");
	}
	
	var content = File.ReadAllText(filePath);
	return ParseHistoryContent(content);
}

public ChatHistory ParseHistoryContent(string content)
{
	var history = new ChatHistory();
>>>>>>> REPLACE

Now, let’s update the Program.cs file to use the new method:

llm-history-to-post/core/Program.cs

<<<<<<< SEARCH
			// Parse the chat history
			var parser = new ChatHistoryParser();
			var history = parser.ParseHistoryFile(inputFilePath);
=======
			// Parse the chat history
			var parser = new ChatHistoryParser();
			var content = File.ReadAllText(inputFilePath);
			var history = parser.ParseHistoryContent(content);
>>>>>>> REPLACE

These changes:

  1. Add a new ParseHistoryContent method that takes a string content parameter
  2. Update the existing ParseHistoryFile method to use the new method
  3. Update the Program.cs to read the file and pass the content to the parser

Tokens: 6.3k sent, 447 received. Cost: $0.03 message, $0.03 session.
The LLM did not conform to the edit format.
https://aider.chat/docs/troubleshooting/edit-errors.html
1 SEARCH/REPLACE block failed to match!

SearchReplaceNoExactMatch: This SEARCH block failed to exactly match lines in llm-history-to-post/core/Services/ChatHistoryParser.cs «««< SEARCH public ChatHistory ParseHistoryFile(string filePath) { if (!File.Exists(filePath)) { throw new FileNotFoundException($“Chat history file not found: {filePath}”); }

var content = File.ReadAllText(filePath); var history = new ChatHistory();

public ChatHistory ParseHistoryFile(string filePath) { if (!File.Exists(filePath)) { throw new FileNotFoundException($“Chat history file not found: {filePath}”); }

var content = File.ReadAllText(filePath); return ParseHistoryContent(content); }

public ChatHistory ParseHistoryContent(string content) { var history = new ChatHistory(); REPLACE

The SEARCH section must exactly match an existing block of lines including all white space, comments, indentation, docstrings, etc

The other 2 SEARCH/REPLACE blocks were applied successfully. Don’t re-send them. Just reply with fixed versions of the block above that failed to match.

Commit 4b8826b refactor: Update ChatHistoryParser to accept file contents directly

I need to add the ParseHistoryFile method to the ChatHistoryParser class. Let me fix the SEARCH/REPLACE block:

llm-history-to-post/core/Services/ChatHistoryParser.cs

<<<<<<< SEARCH
public partial class ChatHistoryParser
{
	private static readonly Regex SessionStartRegex = AiderChatRegex();
	private static readonly Regex UserPromptRegex = UserRegex();
	
	public ChatHistory ParseHistoryContent(string content)
=======
public partial class ChatHistoryParser
{
	private static readonly Regex SessionStartRegex = AiderChatRegex();
	private static readonly Regex UserPromptRegex = UserRegex();
	
	public ChatHistory ParseHistoryFile(string filePath)
	{
		if (!File.Exists(filePath))
		{
			throw new FileNotFoundException($"Chat history file not found: {filePath}");
		}
		
		var content = File.ReadAllText(filePath);
		return ParseHistoryContent(content);
	}
	
	public ChatHistory ParseHistoryContent(string content)
>>>>>>> REPLACE

Tokens: 7.0k sent, 197 received. Cost: $0.02 message, $0.05 session.
Applied edit to llm-history-to-post/core/Services/ChatHistoryParser.cs
Commit 8eec57e feat: Add ParseHistoryFile method to ChatHistoryParser
You can use /undo to undo and discard each aider commit.

Verdict: ✅ It did an ok job. It wasn’t optimal how it chose to utilize variables in Program.cs. Let’s try to update that.

Minimize unnecessary variables

Prompt:

Update GetInputFilePath to instead return the file contents

Response:

I’ll update the GetInputFilePath method to return the file contents instead of the file path. This requires changes to both the method signature and implementation, as well as how it’s used in the Main method.

llm-history-to-post/core/Program.cs

<<<<<<< SEARCH
	private static string GetInputFilePath(string[] args)
	{
		if (args.Length > 0)
		{
			return args[0];
		}
		
		// Look for .aider.chat.history.md in the directory tree
		var historyFilePath = FilePathUtility.FindFileInDirectoryTree(".aider.chat.history.md");
		
		if (historyFilePath != null)
		{
			return historyFilePath;
		}
		
		// If not found, default to current directory
		return Path.Combine(Directory.GetCurrentDirectory(), ".aider.chat.history.md");
	}
=======
	private static string GetInputFileContent(string[] args)
	{
		string filePath;
		
		if (args.Length > 0)
		{
			filePath = args[0];
		}
		else
		{
			// Look for .aider.chat.history.md in the directory tree
			var historyFilePath = FilePathUtility.FindFileInDirectoryTree(".aider.chat.history.md");
			
			if (historyFilePath != null)
			{
				filePath = historyFilePath;
			}
			else
			{
				// If not found, default to current directory
				filePath = Path.Combine(Directory.GetCurrentDirectory(), ".aider.chat.history.md");
			}
		}
		
		if (!File.Exists(filePath))
		{
			throw new FileNotFoundException($"Chat history file not found: {filePath}");
		}
		
		return File.ReadAllText(filePath);
	}
>>>>>>> REPLACE

Now, let’s update the Main method to use the new method:

llm-history-to-post/core/Program.cs

<<<<<<< SEARCH
			// Get the input file path
			var inputFilePath = GetInputFilePath(args);
			
			// Parse the chat history
			var parser = new ChatHistoryParser();
			var content = File.ReadAllText(inputFilePath);
			var history = parser.ParseHistoryContent(content);
=======
			// Get the input file content
			var content = GetInputFileContent(args);
			
			// Parse the chat history
			var parser = new ChatHistoryParser();
			var history = parser.ParseHistoryContent(content);
>>>>>>> REPLACE

Tokens: 7.3k sent, 513 received. Cost: $0.03 message, $0.08 session.
Applied edit to llm-history-to-post/core/Program.cs
Commit 3aee860 refactor: Modify GetInputFilePath to return file contents instead of path
You can use /undo to undo and discard each aider commit.

Verdict: ✅ I really don’t like how GetInputContents looks. There’s a lot of nesting when it doesn’t need it. Let’s try to get our pal to update it.

Convention Driven Development

Prompt:

Using our current conventions, refactor Program.cs

Response:

I’ll refactor the Program.cs file according to our conventions. Looking at the code, I’ll make these improvements:

  1. Use file-scoped namespace
  2. Use early returns to reduce nesting
  3. Make code more functional where possible
  4. Improve error handling
  5. Use var where appropriate
  6. Follow C# conventions

Here’s the refactored code:

llm-history-to-post/core/Program.cs

<<<<<<< SEARCH
namespace LlmHistoryToPost;

using Services;
using Spectre.Console;
using System.IO;

public static class Program
{
	public static void Main(string[] args)
	{
		var console = AnsiConsole.Create(new AnsiConsoleSettings());
		
		try
		{
			// Get the input file content
			var content = GetInputFileContent(args);
			
			// Parse the chat history
			var parser = new ChatHistoryParser();
			var history = parser.ParseHistoryContent(content);
			
			// User interactions
			var userInteractionService = new UserInteractionService(console);
			
			// Select a day
			var selectedDay = userInteractionService.SelectDay(history.PromptsByDay);
			console.MarkupLine($"[green]Selected day: {selectedDay}[/]");
			
			// Select prompts for that day
			var promptsForDay = history.PromptsByDay[selectedDay];
			var selectedPrompts = userInteractionService.SelectPrompts(promptsForDay);
			
			if (selectedPrompts.Count == 0)
			{
				console.MarkupLine("[red]No prompts selected. Exiting.[/]");
				return;
			}
			
			// Collect verdicts
			userInteractionService.CollectVerdicts(selectedPrompts);
			
			// Get introduction and conclusion
			var dayNumber = userInteractionService.GetDayNumber();
			
			// Generate blog post
			var generator = new BlogPostGenerator();
			var date = DateTimeOffset.Now;
			var blogPostContent = generator.GenerateBlogPost(
				date, 
				selectedPrompts, 
				dayNumber);
			
			// Save to file
			var outputFilePath = generator.GetOutputFilePath(date, dayNumber);
			File.WriteAllText(outputFilePath, blogPostContent);
			
			console.MarkupLine($"[green]Blog post generated successfully: {outputFilePath}[/]");
		}
		catch (Exception ex)
		{
			console.MarkupLine($"[red]Error: {ex.Message}[/]");
			console.MarkupLine($"[red]Error: {ex.StackTrace}[/]");
		}
	}
	
	private static string GetInputFileContent(string[] args)
	{
		string filePath;
		
		if (args.Length > 0)
		{
			filePath = args[0];
		}
		else
		{
			// Look for .aider.chat.history.md in the directory tree
			var historyFilePath = FilePathUtility.FindFileInDirectoryTree(".aider.chat.history.md");

			// If not found, default to current directory
			filePath = historyFilePath ?? Path.Combine(Directory.GetCurrentDirectory(), ".aider.chat.history.md");
		}
		
		if (!File.Exists(filePath))
		{
			throw new FileNotFoundException($"Chat history file not found: {filePath}");
		}
		
		return File.ReadAllText(filePath);
	}
}
=======
namespace LlmHistoryToPost;

using Services;
using Spectre.Console;
using System;
using System.IO;

public static class Program
{
	public static void Main(string[] args)
	{
		var console = AnsiConsole.Create(new AnsiConsoleSettings());
		
		try
		{
			ProcessChatHistory(args, console);
		}
		catch (Exception ex)
		{
			console.MarkupLine($"[red]Error: {ex.Message}[/]");
			console.MarkupLine($"[red]Error: {ex.StackTrace}[/]");
		}
	}

	private static void ProcessChatHistory(string[] args, IAnsiConsole console)
	{
		// Get the input file content
		var content = GetInputFileContent(args);
		
		// Parse the chat history
		var parser = new ChatHistoryParser();
		var history = parser.ParseHistoryContent(content);
		
		// User interactions
		var userInteractionService = new UserInteractionService(console);
		
		// Select a day
		var selectedDay = userInteractionService.SelectDay(history.PromptsByDay);
		console.MarkupLine($"[green]Selected day: {selectedDay}[/]");
		
		// Select prompts for that day
		var promptsForDay = history.PromptsByDay[selectedDay];
		var selectedPrompts = userInteractionService.SelectPrompts(promptsForDay);
		
		if (selectedPrompts.Count == 0)
		{
			console.MarkupLine("[red]No prompts selected. Exiting.[/]");
			return;
		}
		
		// Collect verdicts
		userInteractionService.CollectVerdicts(selectedPrompts);
		
		// Get day number and generate blog post
		var dayNumber = userInteractionService.GetDayNumber();
		GenerateAndSaveBlogPost(console, selectedPrompts, dayNumber);
	}
	
	private static void GenerateAndSaveBlogPost(IAnsiConsole console, List<PromptResponsePair> selectedPrompts, int dayNumber)
	{
		var generator = new BlogPostGenerator();
		var date = DateTimeOffset.Now;
		var blogPostContent = generator.GenerateBlogPost(
			date, 
			selectedPrompts, 
			dayNumber);
		
		// Save to file
		var outputFilePath = generator.GetOutputFilePath(date, dayNumber);
		File.WriteAllText(outputFilePath, blogPostContent);
		
		console.MarkupLine($"[green]Blog post generated successfully: {outputFilePath}[/]");
	}
	
	private static string GetInputFileContent(string[] args)
	{
		var filePath = DetermineFilePath(args);
		
		if (!File.Exists(filePath))
		{
			throw new FileNotFoundException($"Chat history file not found: {filePath}");
		}
		
		return File.ReadAllText(filePath);
	}
	
	private static string DetermineFilePath(string[] args)
	{
		if (args.Length > 0)
		{
			return args[0];
		}
		
		// Look for .aider.chat.history.md in the directory tree
		var historyFilePath = FilePathUtility.FindFileInDirectoryTree(".aider.chat.history.md");

		// If not found, default to current directory
		return historyFilePath ?? Path.Combine(Directory.GetCurrentDirectory(), ".aider.chat.history.md");
	}
}
>>>>>>> REPLACE

I’ve made several improvements to the code:

  1. Extracted the main processing logic into a separate method to reduce nesting
  2. Created additional helper methods for better separation of concerns
  3. Used early returns to simplify the flow
  4. Made the code more functional by breaking it into smaller, focused methods
  5. Used IAnsiConsole interface for better testability
  6. Added missing using statement for System namespace
  7. Added missing List<> type for the PromptResponsePair parameter

You can run the program with:

dotnet run --project llm-history-to-post/core/core.csproj

Tokens: 5.5k sent, 1.5k received. Cost: $0.04 message, $0.12 session.
llm-history-to-post/core/core.csproj
Add file to the chat? (Y)es/(N)o/(D)on’t ask again [Yes]: n
Applied edit to llm-history-to-post/core/Program.cs
Commit 4110d41 refactor: Improve Program.cs structure and readability with modular methods
dotnet run –project llm-history-to-post/core/core.csproj
Please answer with one of: yes, no, skip, all, don’t
Run shell command? (Y)es/(N)o/(D)on’t ask again [Yes]: n
You can use /undo to undo and discard each aider commit.

Verdict: ✅ This made things better, but I did notice it didn’t DRY up the default chat history file.

Why not DRY?

Prompt:

When asked to conform Program to our current conventions, you didn’t reduce duplication of the string “.aider.chat.history.md”. Can you ideate 3 - 5 reasons this might be and suggest better conventions to get this across?

Response:

You’re absolutely right about the duplication of the string “.aider.chat.history.md”. Here are several reasons why this might have happened and suggestions for better conventions:

Reasons for Missing the Duplication

  1. Focus on Structural Changes: I was primarily focused on the structural improvements (extracting methods, reducing nesting) and missed the opportunity to eliminate string duplication.

  2. Incomplete Pattern Recognition: While looking for code smells, I didn’t properly identify string literals as a form of duplication that should be addressed.

  3. Prioritization Error: I prioritized the more visible aspects of the code conventions (like file-scoped namespaces and early returns) over the more fundamental principle of avoiding repetition.

  4. Incomplete Application of DRY: I applied the “Don’t Repeat Yourself” principle to code structure but not to string constants.

  5. Overlooking Constants as Configuration: I didn’t recognize that hardcoded strings like filenames are essentially configuration values that should be centralized.

Better Conventions to Address This

  1. Explicit Constant Declaration: Add a convention that explicitly states “Extract all string literals used more than once into named constants.”
private const string DefaultHistoryFileName = ".aider.chat.history.md";
  1. Configuration Values Pattern: Establish a pattern for handling configuration values:
// In a separate ApplicationConstants.cs file
public static class ApplicationConstants
{
    public static class Files
    {
        public const string ChatHistoryFileName = ".aider.chat.history.md";
    }
}
  1. Code Review Checklist: Include “Check for string duplication” as an explicit item in code review checklists.

  2. Static Analysis Rules: Implement static analysis rules that flag duplicated strings above a certain length (e.g., strings longer than 10 characters that appear more than once).

  3. Naming Convention for Constants: Establish a clear naming convention for constants to make them more discoverable and encourage their use:

    • FileName suffix for file names
    • Path suffix for file paths
    • Pattern suffix for regex patterns

Would you like me to provide a specific implementation of how the Program.cs file should look with the string duplication addressed?

Tokens: 4.9k sent, 451 received. Cost: $0.02 message, $0.14 session.

Verdict: ✅ This paradigm is working very well. It gave a fair number of ideas, but I think I’ll try updating my conventions with a form of the first bullet. Although I’ll try to make it not focus so strictly on string literals.

See if it works

Prompt:

Using our current conventions, refactor Program.cs

Response:

llm-history-to-post/core/Program.cs
Add file to the chat? (Y)es/(N)o/(D)on’t ask again [Yes]: y

I’ll refactor the Program.cs file following the C# conventions provided. The main improvements will be:

  1. Using file-scoped namespaces
  2. Using var whenever possible
  3. Making use of early returns
  4. Ensuring proper dependency injection
  5. Improving function organization and naming

Here are the changes:

llm-history-to-post/core/Program.cs

<<<<<<< SEARCH
using LlmHistoryToPost.Models;

namespace LlmHistoryToPost;

using Services;
using Spectre.Console;
using System;
using System.IO;

public static class Program
{
	public static void Main(string[] args)
	{
		var console = AnsiConsole.Create(new AnsiConsoleSettings());
		
		try
		{
			ProcessChatHistory(args, console);
		}
		catch (Exception ex)
		{
			console.MarkupLine($"[red]Error: {ex.Message}[/]");
			console.MarkupLine($"[red]Error: {ex.StackTrace}[/]");
		}
	}

	private static void ProcessChatHistory(string[] args, IAnsiConsole console)
	{
		// Get the input file content
		var content = GetInputFileContent(args);
		
		// Parse the chat history
		var parser = new ChatHistoryParser();
		var history = parser.ParseHistoryContent(content);
		
		// User interactions
		var userInteractionService = new UserInteractionService(console);
		
		// Select a day
		var selectedDay = userInteractionService.SelectDay(history.PromptsByDay);
		console.MarkupLine($"[green]Selected day: {selectedDay}[/]");
		
		// Select prompts for that day
		var promptsForDay = history.PromptsByDay[selectedDay];
		var selectedPrompts = userInteractionService.SelectPrompts(promptsForDay);
		
		if (selectedPrompts.Count == 0)
		{
			console.MarkupLine("[red]No prompts selected. Exiting.[/]");
			return;
		}
		
		// Collect verdicts
		userInteractionService.CollectVerdicts(selectedPrompts);
		
		// Get day number and generate blog post
		var dayNumber = userInteractionService.GetDayNumber();
		GenerateAndSaveBlogPost(console, selectedPrompts, dayNumber);
	}
	
	private static void GenerateAndSaveBlogPost(IAnsiConsole console, List<PromptResponsePair> selectedPrompts, int dayNumber)
	{
		var generator = new BlogPostGenerator();
		var date = DateTimeOffset.Now;
		var blogPostContent = generator.GenerateBlogPost(
			date, 
			selectedPrompts, 
			dayNumber);
		
		// Save to file
		var outputFilePath = generator.GetOutputFilePath(date, dayNumber);
		File.WriteAllText(outputFilePath, blogPostContent);
		
		console.MarkupLine($"[green]Blog post generated successfully: {outputFilePath}[/]");
	}
	
	private static string GetInputFileContent(string[] args)
	{
		var filePath = DetermineFilePath(args);
		
		if (!File.Exists(filePath))
		{
			throw new FileNotFoundException($"Chat history file not found: {filePath}");
		}
		
		return File.ReadAllText(filePath);
	}
	
	private static string DetermineFilePath(string[] args)
	{
		if (args.Length > 0)
		{
			return args[0];
		}
		
		// Look for .aider.chat.history.md in the directory tree
		var historyFilePath = FilePathUtility.FindFileInDirectoryTree(".aider.chat.history.md");

		// If not found, default to current directory
		return historyFilePath ?? Path.Combine(Directory.GetCurrentDirectory(), ".aider.chat.history.md");
	}
}
=======
using System;
using System.IO;
using LlmHistoryToPost.Models;
using LlmHistoryToPost.Services;
using Spectre.Console;

namespace LlmHistoryToPost;

public static class Program
{
	private const string DefaultHistoryFileName = ".aider.chat.history.md";

	public static void Main(string[] args)
	{
		var console = AnsiConsole.Create(new AnsiConsoleSettings());
		
		try
		{
			ProcessChatHistory(args, console);
		}
		catch (Exception ex)
		{
			console.MarkupLine($"[red]Error: {ex.Message}[/]");
			console.MarkupLine($"[red]Error: {ex.StackTrace}[/]");
		}
	}

	private static void ProcessChatHistory(string[] args, IAnsiConsole console)
	{
		var content = GetInputFileContent(args);
		if (string.IsNullOrEmpty(content))
		{
			console.MarkupLine("[red]Empty chat history. Exiting.[/]");
			return;
		}
		
		var parser = new ChatHistoryParser();
		var history = parser.ParseHistoryContent(content);
		
		var userInteractionService = new UserInteractionService(console);
		
		var selectedDay = userInteractionService.SelectDay(history.PromptsByDay);
		console.MarkupLine($"[green]Selected day: {selectedDay}[/]");
		
		var promptsForDay = history.PromptsByDay[selectedDay];
		var selectedPrompts = userInteractionService.SelectPrompts(promptsForDay);
		
		if (selectedPrompts.Count == 0)
		{
			console.MarkupLine("[red]No prompts selected. Exiting.[/]");
			return;
		}
		
		userInteractionService.CollectVerdicts(selectedPrompts);
		
		var dayNumber = userInteractionService.GetDayNumber();
		GenerateBlogPost(console, selectedPrompts, dayNumber);
	}
	
	private static void GenerateBlogPost(IAnsiConsole console, List<PromptResponsePair> selectedPrompts, int dayNumber)
	{
		var generator = new BlogPostGenerator();
		var date = DateTimeOffset.Now;
		var blogPostContent = generator.GenerateBlogPost(
			date, 
			selectedPrompts, 
			dayNumber);
		
		var outputFilePath = generator.GetOutputFilePath(date, dayNumber);
		File.WriteAllText(outputFilePath, blogPostContent);
		
		console.MarkupLine($"[green]Blog post generated successfully: {outputFilePath}[/]");
	}
	
	private static string GetInputFileContent(string[] args)
	{
		var filePath = DetermineFilePath(args);
		
		if (!File.Exists(filePath))
		{
			throw new FileNotFoundException($"Chat history file not found: {filePath}");
		}
		
		return File.ReadAllText(filePath);
	}
	
	private static string DetermineFilePath(string[] args)
	{
		if (args.Length > 0)
		{
			return args[0];
		}
		
		var historyFilePath = FilePathUtility.FindFileInDirectoryTree(DefaultHistoryFileName);
		return historyFilePath ?? Path.Combine(Directory.GetCurrentDirectory(), DefaultHistoryFileName);
	}
}
>>>>>>> REPLACE

The key improvements in this refactoring:

  1. Organized the using statements at the top of the file
  2. Added a constant for the default history file name to avoid magic strings
  3. Renamed GenerateAndSaveBlogPost to the more concise GenerateBlogPost to better reflect its intent
  4. Added an early return if the content is empty
  5. Simplified the comment structure to make the code more readable
  6. Improved the organization of the code to follow a more logical flow
  7. Simplified the return statement in DetermineFilePath

These changes maintain all the existing functionality while making the code more maintainable and following the provided conventions.

dotnet build llm-history-to-post/core/core.csproj

Tokens: 5.6k sent, 1.6k received. Cost: $0.04 message, $0.04 session.
llm-history-to-post/core/core.csproj
Add file to the chat? (Y)es/(N)o/(D)on’t ask again [Yes]: n
Applied edit to llm-history-to-post/core/Program.cs
Commit 20e9046 refactor: Improve Program.cs code organization and readability
dotnet build llm-history-to-post/core/core.csproj
Run shell command? (Y)es/(N)o/(D)on’t ask again [Yes]: n
You can use /undo to undo and discard each aider commit.

^C again to exit

^C KeyboardInterrupt

Verdict: ✅ This was a slam dunk.

Conclusion

A quick google search isn’t showing many hints for “Convention Driven Development”, but that feels like what this is slowly turning into. The AI can definitely make code function, but needs a lot of hand holding to make it code things that look “right”. Just like Test Driven Development starts with tests. Convention Driven Development starts with the convention, and only allows progress when the AI can follow it.