77968683

Virtual assertions

Implements `VIRTUAL-ASSERTIONS-PLAN.md` from this branch, which is deleted by
the last commit now that the work exists.

## The problem

`+ assert` cannot see virtual postings. That is deliberate and consistent —
`balanceTransaction` feeds only real postings into `balancesForAssertions`,
`checkEntryAssertions` reads the without-virtual map, and
`balance/balanced/virtual-assertion.cent` pins it — but it leaves an account
declared `virtual-only` with **no assertable balance at all**.

The failure mode was worse than a refusal. Asserting such an account reported

```
The actual balance has no amount in the asserted currency.
Maybe there is a mistake with the currency?
```

which reads as a typo in the ledger rather than as a form that can never work
there.

## What this adds

```centjes
2025-01-27
  | I bought a coffee.
  * assets:cash -5.00 USD
  * expenses:coffee 5.00 USD
  ! expenses:vat 0.50 USD
  + assert expenses:coffee = 5.00 USD
  + assert virtual expenses:vat = 0.50 USD
```

Plain `+ assert` keeps meaning exactly what it meant. `--virtual` still changes
what you see, not what an assertion means.

## Worth a look

**The scope is lexed as one compound token** in the `<extra>` start code. That
leaves the `assertion` production untouched, so this composes with #5 for free:

```centjes
+ assert virtual assets:broker = +1 SWDA lot @ 500 EUR
```

The scope sits on the extra, the lot sits on the commodity, and neither grammar
production knows about the other.

**An account named exactly `virtual` cannot use the plain form.**
`@assert_virtual` is `"assert virtual "`, fifteen characters against seven for
`"assert "`, so on `+ assert virtual = 5 CHF` longest-match takes the compound
token and swallows the account name, giving a parse error. This was a
regression, caught in review: that ledger reports `Valid` on `master`.

It is accepted rather than fixed, the way a currency cannot be called `lot`.
The collision cannot be avoided by lexing `virtual ` as a keyword inside
`<assertion>` either -- that fails on the same input for the same reason -- and
the alternatives are a grammar production that reconstitutes the swallowed name,
or a hyphenated keyword in a language whose keywords are space separated. The
restriction is narrow: `assets:virtual`, `virtual:sub` and `virtualx` all work,
as does `+ assert virtual virtual = 5 CHF`.

**`AssertionScope` is a two-constructor type, not a `Bool`**, and it sits on
`Assertion` in both `Module` and `Ledger` rather than on `ExtraAssertion` where
the keyword syntactically lives, so `Compile` copies a field instead of moving
one between types.

**The behavioural change is one map selection** in `checkEntryAssertions`.
`checkAssertion` already took the balances as an argument.

**It is legal on any account**, whatever its virtual posting policy. The keyword
names the view, not the kind of account. On an account that forbids virtual
postings the two forms are equivalent, which makes `assert virtual` redundant
there, not wrong.

## Two silent holes closed

- **`CE_REAL_ASSERTION_NOT_ALLOWED`** — a plain assertion on a `virtual-only`
  account. Its real balance is empty by construction, so the assertion is either
  trivially true at zero or always false, and carries no information either way.
  This is what used to produce the misleading message above.
- **`CE_UNDECLARED_ACCOUNT` now covers assertions.** `compileTransactionAssertion`
  took only `currencies`, so it never looked at the account name, and
  `checkAssertion` defaulted a missing account to zero. An assertion naming an
  account that does not exist **passed silently**: a ledger declaring only
  `assets:cash` and asserting `assets:typoo = +0.00 CHF` reported `Valid`.
  Threading the accounts map in for the first error made the second a one-line
  fix, which is why they are in one change.

Both have a fixture and a rendered `.err` golden. Both fixtures start life
*passing*, which is their red state, since `CompileSpec` requires failure.

## Deliberately unchanged

- Account-type assertions stay real-only. A `virtual-only` liability holding a
  positive virtual balance stays legal, as today.
- `balance/balanced/virtual-assertion.cent` is untouched. It belongs to the old
  behaviour and is the regression test for the decision that plain `assert`
  keeps meaning what it means — which is why the new fixture is
  `assert-virtual.cent` and not the other way round.
- Out of scope: virtual-aware account-type assertions, virtual-aware
  `assert currency`, and `net-worth` counting virtual postings.

## Two goldens moved

`BE_ASSERTION.err` and `BE_ASSERTION-wrong-currency.err` change by one line
each: `Calculated:` becomes `Calculated (real postings only):`. That label was
unambiguous only while there was one balance an assertion could mean. Nothing
else in either golden moves, and no other pre-existing golden is touched.

## Checks

`nix flake check` passes. All five suites pass: 777 in `centjes-gen` (up from
775 on `lot-prices`), 28 in `centjes-switzerland`, 3 + 13 + 3 elsewhere.

Suite timing

Time to Start Worker time Duration Time to finish Idle
Config 0s 2s 2s 2s 0s
Eval 2s 17s 17s 20s 0s
Build 19s 3m49s 1m58s 2m18s 0s
Suite 0s 4m10s 2m18s 2m18s 0s

Timeline

0s0s10s20s30s40s50s1m1m10s1m20s1m30s1m40s1m50s2m2m10s