---
title: "What Counts as Done? Verifying AI Changes in a TypeScript App"
description: "Verify AI-written TypeScript with a worked search example and a reusable autonomous engineering brief: clear acceptance criteria, short repair loops, independent review, and evidence before delivery."
canonical: "https://zainahmed.net/blog/essential-ai-coding-feedback-loops-for-typescript"
published: "2026-09-13"
---

# What Counts as Done? Verifying AI Changes in a TypeScript App

> Author: Zain Ahmed
> Published: 2026-09-13
> Read Time: 12 min read
> Category: AI & Tech
> Tags: TypeScript, AI Agents, Vitest, Testing, Developer Experience
> Canonical URL: https://zainahmed.net/blog/essential-ai-coding-feedback-loops-for-typescript

## Excerpt
Verify AI-written TypeScript with a worked search example and a reusable autonomous engineering brief: clear acceptance criteria, short repair loops, independent review, and evidence before delivery.

---

An AI agent can finish a search feature, pass TypeScript, and still leave readers unable to find an article by its tags. The function accepts a string and returns an array. Those types say nothing about which articles belong in the results.

That gap is the useful starting point for a coding feedback loop. Before asking the agent to fix its work, decide what would demonstrate that the work is wrong. Then give it a way to run that check and inspect the result.

This site's blog makes a concrete example. Its articles live in MDX files, its search works with titles, excerpts, and tags, and its filters are reflected in the URL. A change can be correct inside a helper function yet fail when someone clears the search box or reloads a filtered page.

The walkthrough below uses a simplified version of that search behavior. It assumes an existing TypeScript project with Vitest installed and a working development server. The fixtures are illustrative; they are not results from a production incident or benchmark.

## Write down what the reader should be able to do

“Improve blog search” leaves the agent to choose the meaning of improvement. Faster filtering, fuzzy matching, and a different layout could all satisfy that prompt while missing the intended change.

For this example, the request is narrower: readers should find an article when their query appears in its title, excerpt, or tags. Capitalization and surrounding whitespace should not affect the result. An empty query should restore the list, and filtering should preserve the incoming order.

Those decisions make a small acceptance table possible:

| Input or action | Expected result | Where to check it |
| --- | --- | --- |
| A word found only in an excerpt | The matching article is included | Unit test |
| A query such as `  TYPESCRIPT  ` | The same matches as `typescript` | Unit test |
| A tag with no matching title text | The tagged article is included | Unit test |
| A query absent from every field | No articles are returned | Unit test and browser |
| Clear the search field | Articles return and the URL query clears | Browser |
| Reload a URL containing a query | The input and results reflect that query | Browser |

The table also exposes decisions that still need an owner. Should clearing search preserve a selected topic? Should an accent-insensitive query match accented text? Resolve the behavior relevant to the task before making it a test. Otherwise, an agent can faithfully implement an assumption nobody agreed to.

Before looking at the helper, try the small fixture below. `typescript` appears only as a tag. Switch between the title-only implementation and the full contract; then try `regressions`, a title word, an unmatched query, and an empty query. This is a simulation of the example, not a live search of this site.

<searchlab></searchlab>

## Build an example that can catch the wrong implementation

A useful fixture separates the search fields. If the same word appears in an article's title, excerpt, and tags, a test can pass even when the implementation searches only one of them.

Create `lib/article-search.ts` with this small, self-contained helper:

```typescript
export type SearchableArticle = {
  slug: string
  title: string
  excerpt: string
  tags: readonly string[]
}

export function filterArticles(
  articles: readonly SearchableArticle[],
  query: string,
): SearchableArticle[] {
  const normalizedQuery = query.trim().toLowerCase()

  return articles.filter((article) => {
    if (normalizedQuery === "") return true

    const fields = [article.title, article.excerpt, ...article.tags]
    return fields.some((field) =>
      field.toLowerCase().includes(normalizedQuery),
    )
  })
}
```

This function treats the query as one substring. It does not split words, rank results, or correct spelling. Naming that limit keeps a simple example from quietly becoming a promise of full-text search.

In `__tests__/article-search.test.ts`, use articles whose searchable fields differ:

```typescript
import { describe, expect, it } from "vitest"
import {
  filterArticles,
  type SearchableArticle,
} from "../lib/article-search"

const articles: SearchableArticle[] = [
  {
    slug: "release-checks",
    title: "Before the next release",
    excerpt: "Catch regressions while a change is still small.",
    tags: ["TypeScript"],
  },
  {
    slug: "queue-backlogs",
    title: "When a queue falls behind",
    excerpt: "Investigate worker saturation and retry volume.",
    tags: ["Operations"],
  },
]

describe("filterArticles", () => {
  it.each([
    ["release", ["release-checks"]],
    ["regressions", ["release-checks"]],
    ["  TYPESCRIPT  ", ["release-checks"]],
    ["no-such-topic", []],
    ["   ", ["release-checks", "queue-backlogs"]],
  ])("matches %j against the search contract", (query, expected) => {
    const slugs = filterArticles(articles, query).map(
      (article) => article.slug,
    )
    expect(slugs).toEqual(expected)
  })

  it("handles an empty collection", () => {
    expect(filterArticles([], "typescript")).toEqual([])
  })

  it("preserves input order when both articles match", () => {
    const reversed = [...articles].reverse()
    expect(filterArticles(reversed, "a")).toEqual(reversed)
  })
})
```

The tag case contains a useful negative check: the unrelated article must be absent. An implementation that returns every article would otherwise look successful if the test merely asked whether one expected result was present.

Before accepting these tests, temporarily remove `...article.tags` from the helper and run the focused test file. The tag case should fail. Restore the code and rerun it. This small deliberate break checks whether the test can detect the mistake it claims to cover.

<checkpoint id="search-contract"></checkpoint>

## Give each check a specific job

Type checking still belongs in this workflow. It catches incompatible values, missing required properties, and other errors expressed by the project's types. It cannot determine whether the acceptance table describes the right product behavior.

In this repository, `npm run typecheck` runs `tsc --noEmit`, which checks the project without writing compiled JavaScript. Use the project's existing configuration rather than replacing its framework settings with a generic configuration block. The [TypeScript documentation explains `noEmit`](https://www.typescriptlang.org/tsconfig/noEmit.html).

For code that reads arrays or dictionaries by index, consider `noUncheckedIndexedAccess` alongside strict checking. It adds the possibility of `undefined` to unchecked indexed reads. That can expose a missing guard; it does not validate an incoming API response or guarantee that a lookup will succeed. See the [compiler option reference](https://www.typescriptlang.org/tsconfig/noUncheckedIndexedAccess.html) before adopting it across an existing project.

For the search helper, run the narrow test first, then check the surrounding project:

```bash
# Assumes package.json defines "test": "vitest run".
npm test -- __tests__/article-search.test.ts
npm run typecheck
npm run lint
npm test
```

The [Vitest CLI](https://vitest.dev/guide/cli.html) supports filtering by test filename, and `vitest run` exits after a single run. A focused run gives the agent a short path from failure to repair. The full suite checks whether that repair affected behavior elsewhere.

These commands are useful only if the relevant files are included by the project configuration. A successful test command that did not discover the new test is not evidence about the helper. Read the reported file and test counts as well as the exit status.

<checkpoint id="repair-order"></checkpoint>

## Take the same requirement into the browser

The helper tests end at an array of results. The application still has to read the input, apply any topic filter, update the URL, and render the list. Test that path in the running app.

For this site's blog, a concrete browser check is:

1. Open `/blog` and enter a tag known to exist on an article.
2. Confirm that the expected article appears and an unrelated one disappears.
3. Check that the URL contains the query, then reload it. Confirm that the input and results agree.
4. Enter an unmatched query and inspect the empty state.
5. Clear the query. Confirm that the URL and results reset according to the agreed topic-filter behavior.
6. Repeat the interaction at a narrow viewport and use the keyboard to reach the input and clear control.

A screenshot helps inspect wrapping, clipped controls, and the empty-state layout. It cannot establish that reloading preserves state. Keep the observed behavior with the image so a reviewer knows what was actually exercised.

If you use [agent-browser](https://agent-browser.dev/), start with an isolated session and inspect the page before interacting:

```bash
agent-browser --session blog-search-check open http://localhost:3000/blog
agent-browser --session blog-search-check snapshot -i
```

Use the current snapshot's element references for subsequent actions, and refresh the snapshot after the page changes. After exercising the feature, inspect runtime errors and the browser console. Treat those logs as evidence to read: printing an error list is not, by itself, an assertion that the list is empty.

<checkpoint id="browser-proof"></checkpoint>

## When a repair fails, preserve the question

Suppose the tag test fails. A useful follow-up to the agent includes the command, the failing expectation, and the requirement that remains in force:

```text
Command: npm test -- __tests__/article-search.test.ts
Failure: query "  TYPESCRIPT  " returned []
Expected: ["release-checks"]

The query appears only in that article's tags.
Keep matching case-insensitive and trim surrounding whitespace.
Inspect the search implementation and explain the cause before editing.
Rerun this test after the repair, then run the project checks.
```

“Make the tests green” leaves too much room to change the test's meaning. A test may need correction, but changing its expectation requires a reason grounded in the agreed behavior. A passing result after removing the tag requirement would conceal the unfinished work.

Also distinguish a product failure from an unavailable check. If the browser cannot reach the development server, first establish whether the server is running. Editing the search function cannot repair a stopped server.

Repeated attempts should earn their continuation. If the agent proposes the same repair after seeing the same failure, ask for a new hypothesis and a check that could distinguish it from the previous one. Set a task budget in advance. When that budget is reached, preserve the failing command, current diff, and unresolved question for review; do not label the task complete.

## Turn the loop into an autonomous engineering brief

The search example is small, but it establishes an operating sequence that can carry through a larger task:

```text
Understand -> investigate -> plan -> implement -> verify
                                                 |
                                                 v
Deliver <- document <- re-verify <- fix <- adversarial review
                         |                       ^
                         +-- review fixes -------+
```

The agent owns the routine work between those steps. It should inspect the repository, run available checks, investigate failures, and repair its implementation without asking someone to relay every diagnostic. Its authority still comes from the task: passing a local build does not authorize a deployment, infrastructure apply, or destructive migration.

The brief below puts that distinction into reusable instructions. Add it to the repository's existing agent guidance, reconcile any conflicting rules, and substitute the project's real commands. A prompt describes the process; CI, permissions, and review requirements enforce the parts that must not be skipped.

```text
# Autonomous production engineering mode

Act as a senior engineer responsible for completing the requested change
end to end. Optimize for correctness, maintainability, security, simplicity,
and verifiable evidence. Do not treat edited files as proof of completion.

ESTABLISH THE TASK

Read repository instructions and the relevant implementation, tests, docs,
and history. Discover the tools and skills actually installed. Load
specialized guidance as it becomes relevant; never claim an unavailable
skill or command ran.

State the desired behavior, acceptance criteria, compatibility constraints,
failure cases, and verification plan. Resolve routine choices from the
codebase. Escalate missing decisions that materially affect safety or scope.

Inspect Git and dependency state. Record relevant baseline failures.
Use the existing package manager and lockfile. For substantial or parallel
work, use an isolated worktree; reuse an appropriate isolated checkout
when one already exists. Give concurrent writers separate checkouts.

IMPLEMENT IN SMALL, VERIFIABLE STEPS

For each step, identify the goal, implementation, check, and evidence.
Use the smallest maintainable design that fits the repository. Preserve
public contracts and avoid unrelated refactors or speculative abstractions.

Run the cheapest useful check after each meaningful change, then expand
verification to the affected package and full relevant suite. Use the
repository's compiler, lint, test, and production build commands.

Do not weaken compiler settings, assertions, or test coverage to obtain
a pass. Investigate changed snapshots. Document any unavoidable unsafe
type assertion at an interoperability boundary.

For browser changes, exercise the actual interaction, navigation, reload,
loading, empty, and error states. Check keyboard and mobile behavior where
relevant. Inspect console and runtime errors; preserve useful screenshots.
When practical, smoke-test the built app using its production start command.

APPLY CHECKS APPROPRIATE TO THE RISK

For APIs, check validation, error contracts, authorization, retry safety,
and partial failure. Exercise real local dependencies when supported.
For database changes, review migrations, data preservation, rollout
compatibility, and rollback where supported.

For infrastructure, inspect a preview for replacement, deletion, IAM,
exposure, environment, and cost changes. Do not apply without authorization.
Review security-sensitive changes for abuse cases and secret exposure.
Measure performance when it is an acceptance criterion.

SEPARATE BUILDING FROM CHECKING

The Maker implements and repairs. An independent Checker receives the task,
acceptance criteria, final diff, and verification evidence. Prefer fresh
context and read-only source access. Ask for reproducible failures,
unsupported assumptions, missing boundary cases, and simpler alternatives.

Fix actionable findings and rerun affected checks. Have material fixes
reviewed again. If independent review is unavailable, report the limitation;
do not present self-review as an independent pass.

BOUND THE REPAIR LOOP

Track the unresolved failure, hypothesis, experiment, result, and next step.
After about three materially similar failures, change strategy. Around
eight attempts on one issue, reassess scope, evidence, and the task budget.
These are review triggers, not proof that another attempt cannot succeed.

Stop repetitive edits when no new evidence supports them. Preserve the diff
and failing command, and report the blocker honestly. Never manufacture
completion by removing a requirement or weakening a check.

REVIEW AND DELIVER

Inspect the complete diff for unrelated changes, secrets, temporary files,
and debug output. Update relevant documentation. Report checks actually run,
results, unverified behavior, and remaining risks.

For an authorized PR workflow: verify locally, commit locally, review before
pushing, fix findings, retest, and review material fixes again. Use the
configured PR reviewer; record findings and final evidence in the team's
review log. Push and create or update the PR only within the task's authority.

The objective is maximum verified useful change with minimum unnecessary
complexity. Do not increase autonomy beyond what you can reliably verify.
```

This keeps the common workflow in one place while leaving specialist checks conditional. A blog-search change needs no database migration rehearsal. A change to authorization needs more than the search helper's test suite. Applying the same long checklist indiscriminately can obscure the check that matters most.

Tool names also need that distinction. If a team has GSD or Superpowers skills, the agent can use the relevant installed guidance. If it exposes `/pr-reviewer`, that command can perform the PR review step. If reviews belong in Obsidian, use its configured integration; when that integration is unavailable, preserve the note in the team's established repository location and report the sync limitation. None of those names should appear in a completion report as work performed merely because the prompt mentioned them.

## Give the Checker something specific to challenge

For the search example, the Checker should get the acceptance table and ask whether each row has evidence. Can it remove tag matching without causing a failure? Does clearing the query preserve the selected topic as specified? Does the reloaded URL reproduce the same results? Those questions are more useful than asking a second model whether the code looks good.

A fresh reviewer can still share the Maker's mistaken interpretation. Independent review adds another opportunity to find a defect; it does not guarantee correctness. Give the Checker the requirements and observed outputs, and ask it to explain findings with an input, expected behavior, and actual result.

After a repair, refresh the evidence. A build from before the final code change does not verify the final code. Rerun the affected checks and any final gates required by the repository, then record the result against the version being handed off.

## Make the handoff inspectable

The final message should let someone assess the change without reconstructing the conversation. This compact structure is enough for the search example:

```text
Implemented: behavior changed and files affected
Verified: commands run, outcomes, and browser scenarios exercised
Review: findings, fixes, reviewer used, and any unavailable review step
Delivery: local changes, commit, or PR, as authorized
Remaining uncertainty: failed checks, untested behavior, or known limits
```

Use actual results in those fields. Distinguish “the tag test passed” from “the search feature was verified”; the latter also needs the browser observations. A justified completion claim names the acceptance criteria and the evidence that supports them. It cannot establish that every possible defect has been ruled out.

### Acknowledgment

Matt Pocock's [AIHero guide to TypeScript feedback loops](https://www.aihero.dev/essential-ai-coding-feedback-loops-for-type-script-projects) covers a baseline of type checking, tests, commit hooks, and formatting. This walkthrough develops a separate acceptance-testing example around this site's blog search. The behavior table, fixtures, and repair scenario above are written for that example.

When reviewing the next AI-written change, pick one requirement and try to break it. If the checks stay green after that requirement is deliberately removed, the verification still has work to do.

## Interactive article data

```json
{
  "checkpoints": {
    "search-contract": {
      "section": "Testing the search contract",
      "prompt": "An agent's search helper passes TypeScript and matches article titles. The request also requires tag matches. What evidence is still missing?",
      "options": [
        {
          "text": "A test where the query appears only in a tag, with an unrelated article excluded.",
          "correct": true,
          "why": "This isolates the missing requirement. A title match cannot establish that tag matching works, and the unrelated article catches an implementation that returns everything."
        },
        {
          "text": "A second successful typecheck.",
          "correct": false,
          "why": "The compiler checks type compatibility. It does not know which fields the product requires search to inspect."
        },
        {
          "text": "A screenshot showing at least one article in the results.",
          "correct": false,
          "why": "Without a known query and expected result, the screenshot cannot show whether the tag requirement is satisfied."
        }
      ],
      "reveal": "Choose evidence that could expose the specific mistake. Each requirement needs a check capable of failing when that requirement is broken."
    },
    "repair-order": {
      "section": "Choosing the next check",
      "prompt": "The tag-only case fails after an agent changes the helper. What is the most useful next move?",
      "options": [
        {
          "text": "Inspect that focused failure, repair the helper, rerun the focused test, then expand to project checks.",
          "correct": true,
          "why": "The narrow failure points to the missing behavior. A focused rerun confirms the repair before broader checks look for regressions."
        },
        {
          "text": "Run the full suite repeatedly without reading the failing assertion.",
          "correct": false,
          "why": "A larger log does not explain the specific failure if the first useful assertion has not been inspected."
        },
        {
          "text": "Remove the tag-only case because the typecheck passes.",
          "correct": false,
          "why": "That changes the evidence to fit the implementation and leaves the agreed tag requirement untested."
        }
      ],
      "reveal": "Keep the failing requirement fixed. Use the cheapest check that can distinguish a repair from another guess, then widen the feedback loop."
    },
    "browser-proof": {
      "section": "Checking the whole path",
      "prompt": "The helper tests pass and a screenshot shows the right search result. Which browser action still checks behavior neither proves?",
      "options": [
        {
          "text": "Reload a URL with a query and confirm the input and results still agree.",
          "correct": true,
          "why": "A reload tests whether URL state is read back into the interface. Neither an array test nor a static image exercises that path."
        },
        {
          "text": "Take another screenshot of the same unchanged result.",
          "correct": false,
          "why": "The same image cannot show how the page reconstructs state after navigation."
        },
        {
          "text": "Run TypeScript again and assume the URL behavior follows.",
          "correct": false,
          "why": "Type checking cannot establish a browser state transition or persisted query behavior."
        }
      ],
      "reveal": "Test the transition the evidence has not covered. For URL-backed search, loading the URL afresh is a separate contract."
    }
  }
}
```

---

## Related Endpoints & Navigation
- **All Articles**: [https://zainahmed.net/blog](https://zainahmed.net/blog)
- **Author Profile**: [https://zainahmed.net/about](https://zainahmed.net/about)
- **Machine-Readable LLM Manifest**: [https://zainahmed.net/llms.txt](https://zainahmed.net/llms.txt)
- **RSS Feed**: [https://zainahmed.net/feed.xml](https://zainahmed.net/feed.xml)
