
The [declarative API and persistence work](/articles/declarative-api-and-persistence-shape/) moves API contracts, table schemas, and read-model queries into the language. One `api` block owns the public surface. One `data` with a `table` block owns the schema. One `query` declaration owns a read model. The compiler builds the manifest, the OpenAPI document, the typed client, the row decoder, and the migration plan from those declarations.

The type system can keep declared code honest. What it cannot do, on its own, is stop application code from working around the declaration and going back to strings and helpers. That is what the new `vary check` rules are for. Each one names a pattern that the declarative path replaces, and each one points the diagnostic at the canonical replacement.

The rule reference is in [check](/docs/check/).

Eight rules ship together. They are the second half of the declarative story.

## VCA006: manual-route-catalog

Flags hand-maintained HTTP route catalogs.

Before:

```vary
let user_route = "/users/:id"
let post_route = "/posts/:id"
let list_route = "/posts"

def get_route_methods() -> Dict[Str, Str] {
    return {"users.show": "GET", "posts.show": "GET", "posts.list": "GET"}
}
```

A list of paths beside a list of methods is the smell. The paths drift from the interface, the OpenAPI document, the generated client, and the tests because each one stores them separately.

After: declare the routes inside an `api` block. The path, method, and handler are one source of truth, and every projection reads it.

## VCG017: manual-json-string

Flags hand-built JSON strings and JSON substring checks.

Before:

```text
let payload = "{\"id\": " + id + ", \"status\": \"ok\"}"
let parsed = Json.parse("{\"user_id\": 42, \"verified\": true}")
assert response.contains("{\"ok\":true}")
```

Each one is an invitation to a quoting bug. The fix is to build `Json` values structurally, return them through typed response payloads, and assert on decoded values rather than on substrings of stringified output.

## VCG026: manual-handler-adapter

Flags wrapper classes that exist only to inject shared dependencies into free handler functions.

Before:

```text
class UserEndpoint(store: UserStore, auth: AuthService) {
    def get_user(self, id: Str) -> UserResponse {
        return user_handler(self.store, self.auth, id)
    }
    def list_users(self) -> UserListResponse {
        return list_handler(self.store, self.auth)
    }
}
```

The class does not express any product behaviour. Every method is `return some_handler(self.store, self.auth, ...)`. The declarative path binds handlers to the contract directly, so the dependency wiring is no longer a class.

## VCG022: manual-row-mapping

Flags constructors built out of `row.get_*` calls.

Before:

```text
let record = UserRecord(
    row.get_str("name"),
    row.get_long("age"),
    row.get_bool("active"),
)
```

The selected row shape is duplicated in the application code. Type the projection with `sql ... into DataType` or declare a `query` and let the compiler write the decoder. Aliases, nullability, and column types stay under the type checker.

## VCG024: manual-response-envelope

Flags hand-built JSON response envelopes.

Before:

```text
let resp = Json.empty_object()
resp.set_bool("ok", true)
resp.set_str("message", "Success")
return resp.stringify()
```

Every endpoint repeating that pattern is duplicating envelope policy. The fix is a declared response type or a shared envelope helper. Serialization is the framework's job, not the handler's.

## VCG025: manual-field-projection

Flags repeated visibility conditionals on JSON setters.

Before:

```text
if user_field_allowed("email") { obj.set_str("email", user.email) }
if user_field_allowed("phone") { obj.set_str("phone", user.phone) }
if user_field_allowed("ssn") { obj.set_str("ssn", user.ssn) }
```

Field-by-field visibility checks bury the privacy policy inside endpoint bodies. The fix is `field_projection` from the projection stdlib: declare the allowed list once and apply it. The policy becomes inspectable instead of distributed.

## VCG027: structured-markup-string

Flags XML, Atom, or HTML assembled by string concatenation.

Before:

```text
let entry = "<entry><title>" + title + "</title><id>" + id + "</id></entry>"
let doc = "<?xml version=\"1.0\"?><root>" + content + "</root>"
```

Concatenated markup makes escaping a caller responsibility. Markup builders escape in one place and stop attribute and tag rules from leaking into application code.

## VCG018: dsl-replaceable-sql

Flags raw SQL strings where the typed SQL DSL would do.

Before:

```text
db.execute("INSERT INTO users (name, email) VALUES (?, ?)", name, email)
db.execute_query("SELECT id, name FROM users WHERE active = true")
```

Raw SQL hides the table name, the column names, and the row shape from the compiler. The DSL blocks (`query`, `query_one`, `count`, `insert`, `update`, `delete`) keep those names typed and still emit parameterised SQL.

## How the rules read

Each diagnostic names the rule, points at the offending span, and prints the canonical replacement.

```text
src/services/users.vary:17:5
VCG022 manual-row-mapping (great-code)
  manual SqlRow getter constructor mapping
  fix: project the query with `sql ... into DataType` or a declared `query`
```

`vary explain VCG022` prints the same fix with the rationale and an example. The rules ship without auto-fixes on purpose. Each replacement is a structural change that benefits from a human reading the surrounding code once.

## Why this set, together

The point of the declarative `api`, `data` plus `table`, and `query` forms is that the compiler owns the parts of the system that have to agree across boundaries. The point of these check rules is that the rest of the application has to ask the compiler instead of working around it.

| Manual pattern | Declarative replacement |
|---|---|
| Route catalog and method tables | `api` block routes |
| Hand-built JSON strings and substring asserts | `Json` builders and typed responses |
| Handler adapter classes | Handlers bound to the contract |
| `row.get_*` constructor mapping | `sql ... into` or a `query` declaration |
| Manual response envelopes | Declared response types |
| Field visibility conditionals | `field_projection` and named field policy |
| Concatenated markup strings | Markup builders |
| Raw SQL strings | The typed SQL DSL |

A single declaration, a single source of truth, and a check rule that says "no, not that way" when application code tries to build the same thing by hand. That is what closes the loop. The type system keeps the declarative path honest. The check engine keeps application code on it.

If you want the declarations these rules push toward, the [declarative API and persistence article](/articles/declarative-api-and-persistence-shape/) is the companion piece. The full rule list and rationale text shows up in `vary explain` and in the next release notes.
