# From prose to assertions: making 'done' checkable when an AI agent writes the code

> Source: <https://dev.to/dogwind221/from-prose-to-assertions-making-done-checkable-when-an-ai-agent-writes-the-code-1645>
> Published: 2026-11-03 08:15:38+00:00

The hardest part of an AI coding loop is not getting code written. It is the moment after the agent says "done".

When one agent writes the code and another agent reviews it, you have not removed the trust problem — you have just moved it. The reviewer reads the diff, reads the tests, reads the ticket, and produces a verdict. If the ticket says "spacing should be 12vp" and the screenshot looks about right, the verdict is "pass". Two auditors can look at the same delivery and disagree, and nothing in the tooling forces that disagreement to resolve.

That is the gap I tried to close in [DwGuard-Dsh-Hmos](https://github.com/Dogwind221/dwguard-dsh-hmos), a plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) that orchestrates HarmonyOS app delivery: DSH freezes a four-dimension acceptance pack (function / aesthetic / performance / accessibility), runs an adversarial pre-review before dispatch, hands the work to DevEco Code in Goal mode, then audits the delivery with independently gathered evidence.

The change I want to write about is small in surface area and large in consequence: **acceptance criteria can carry a machine-checkable assertion, and the audit gate recomputes the verdict instead of trusting prose.**

My criteria used to be a sentence plus a verification method:

```
statement: 冷启动到首帧 ≤ 800ms          (cold start to first frame ≤ 800ms)
verify:    Profiler 启动分析毫秒数        (milliseconds from the profiler)
```

And an audit check was a result plus free-text evidence:

```
result:   pass
evidence: 3 runs, 700/720/760ms, screenshot artifacts/perf.png
```

Three problems showed up immediately:

`evidence` is free text. No tool can tell whether "1200ms" satisfies "≤ 800ms" without a human eyeballing it.
So the audit was still a person voting, with extra steps.

The fix is an optional `assertion` on each criterion:

```
{
  "id": "P-01",
  "dimension": "performance",
  "statement": "Cold start to first frame ≤ 800ms",
  "verify": "ms reported by the startup profiler",
  "weight": "must",
  "ruleId": "DW-PERF-01",
  "assertion": {
    "matcher": "lte",
    "expected": 800,
    "unit": "ms",
    "source": "startup profiler"
  }
}
```

Eight matchers cover the criteria I actually write:

| matcher | meaning | 
|---|---|
| `equals` | numeric comparison when both sides are numbers, exact string otherwise | 
| `contains` | substring, case-insensitive | 
| `regex` | pattern (unanchored — add `^$` yourself) | 
| `range` | within `[min, max]` , inclusive | 
| `gte` /`lte` | numeric greater/less or equal | 
| `oneOf` | equals one of a list | 
| `present` | non-empty | 

The audit side then must report an `actual` value next to the verdict:

```
{
  "criterionId": "P-01",
  "result": "pass",
  "evidence": "three cold starts: 700/720/760ms; screenshot artifacts/perf.png",
  "actual": "720ms"
}
```

One detail matters more than it looks: **numeric matchers take the first number inside `actual`**. That means measured values can keep their units — `"12vp"` parses as 12, `"720ms"` as 720, `"4.9:1"` as 4.9. The assertion compares numbers; the report stays human-readable.

An optional field that nobody enforces is decoration. So the gate has opinions:

| situation | result | 
|---|---|
| criterion has an assertion, check says `pass` , but no`actual` | **error** — you cannot pass an assertion you never ran | 
| criterion has an assertion, check says `pass` , assertion recomputes to`fail` | **error** — audit refused; this is the self-contradiction case | 
| criterion has an assertion, check says `fail` , assertion recomputes to`pass` | **warning** — something else may have failed it; ask for review | 
| criterion has an assertion, check says `unverified` without`actual` | warning (the existing must-gate already errors) | 
| criterion has no assertion | unchanged behaviour: human verdict + evidence | 
| the assertion itself is invalid (unknown matcher, `range` with no bounds, bad regex) | **the spec refuses to freeze** with`E_CRITERION_ASSERTION_INVALID` | 

Plus a nudge at authoring time: a `must` criterion whose statement contains numbers but carries no assertion gets `W_CRITERION_NO_ASSERTION`, because that is exactly where two auditors will disagree later.

The subtle rule is the third one. Saying "fail" when the number passes is not always wrong — a screenshot can show a layout bug that a timing assertion cannot see. But the gate should make that mismatch visible instead of silently accepting whatever the reviewer wrote.

Because the verdict is now recomputed, the reports gain a column each:

`spec.md` and the Goal brief gain an `audit-<n>.md` gains And `audit-<n>.json` now contains `checks[].assertionVerdict`, which means CI can fail a build on `verdict: fail` without parsing markdown. That was the whole point of making the report consumable.

I want to be careful here, because "machine-checkable acceptance" is easy to oversell.

`dumpsys` reading, a layout tree. Evidence files get copied, hashed, and recorded, but the assertion only decides pass/fail from the number.`actual` is the same process being audited, an agent can satisfy the letter. That is why the audit playbook insists on externally observable sources — `hidumper` render stats and `hitrace` for performance, layout bounds for spacing — and treats in-app timing instrumentation as auxiliary evidence, never the sole basis.
The change shipped as `0.5.0` and the suite went from 49 to **55 tests, all green** (six new ones specifically about assertions: accepting valid, rejecting invalid, recomputing, refusing contradictions, refusing a pass without `actual`, and keeping old packs unchanged).

The real payoff is not "the AI is now trustworthy". It is that **disagreement became resolvable**. When two auditors disagree now, there is a file with a number and a rule that says whether it passes. Either the number is wrong, or the threshold is wrong, or the measurement is wrong — and all three are fixable. Before, the disagreement was just two paragraphs.

If you are building an agent loop where one model checks another, the question to ask is not "is the reviewer good". It is: **can the reviewer's verdict be recomputed by something that did not write it?**

Plugin: [https://github.com/Dogwind221/dwguard-dsh-hmos](https://github.com/Dogwind221/dwguard-dsh-hmos) · npm: [https://www.npmjs.com/package/dwguard-dsh-hmos](https://www.npmjs.com/package/dwguard-dsh-hmos) · listed in [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin).
