VAT Rate Statement Check (§14 Abs. 4 Nr. 8) Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Flag German invoices that omit the explicit VAT rate (§14 Abs. 4 Satz 1 Nr. 8 UStG) as a warning, escalated through the Uncertain Validations Resolver (UVR) for a final vision-LLM decision.

Architecture: Extraction emits a new optional boolean :tax-rate-stated?. A new deterministic check-tax-rate-stated (validation.clj) never terminally warns — it only escalates to "uncertain". UVR owns it as a 4th sub-path: deterministic check → vision-LLM resolver (final say) → pass/not-applicable/warning. The warning surfaces through the existing findings path, which requires touching five duplicated check-ordering lists.

Tech Stack: Clojure, Malli (schema), HoneySQL/next-jdbc, Integrant, clojure.test, the IProcessor v2 / UVR resolver pattern.

Spec: docs/superpowers/specs/2026-05-18-vat-rate-statement-check-design.html


File Structure

File Responsibility Change
src/com/getorcha/schema/invoice/structured_data.clj InvoiceData schema Add optional nilable boolean
src/com/getorcha/workers/ap/ingestion/extraction.clj Extraction prompt Add field + instruction
src/com/getorcha/workers/ap/ingestion/validation.clj Deterministic checks New check-tax-rate-stated
src/com/getorcha/workers/ap/processors/uncertain_validations.clj UVR 4th sub-path + resolver
src/com/getorcha/diagnostics/findings.clj Shared findings Label + ordering
src/com/getorcha/app/http/documents/view/invoice.clj Issue-count badge Hardcoded list entry
src/com/getorcha/app/ui/components.clj UI ordering/text 3 hardcoded maps

Tests mirror source: test/com/getorcha/....

Conventions for every task:


Task 1: Add :tax-rate-stated? to InvoiceData schema

Files:

Add this deftest after test-valid-invoice-baseline (after line 72):

(deftest test-tax-rate-stated-field
  (testing "key absent — still valid (optional)"
    (is (m/validate invoice.schema/InvoiceData valid-invoice)))
  (testing "explicit true / false / nil all valid"
    (is (m/validate invoice.schema/InvoiceData (assoc valid-invoice :tax-rate-stated? true)))
    (is (m/validate invoice.schema/InvoiceData (assoc valid-invoice :tax-rate-stated? false)))
    (is (m/validate invoice.schema/InvoiceData (assoc valid-invoice :tax-rate-stated? nil))))
  (testing "wrong type rejected (key is declared, not just an open-map passthrough)"
    (is (not (m/validate invoice.schema/InvoiceData
                         (assoc valid-invoice :tax-rate-stated? "yes"))))))

Run: clj -X:test:silent :nses '[com.getorcha.schema.invoice.structured-data-test]' 2>&1 | grep -E "FAIL in|Ran .* tests|failures" Expected: FAIL on the "wrong type rejected" assertion (open map currently ignores the unknown key, so "yes" validates and (not ...) is false).

In src/com/getorcha/schema/invoice/structured_data.clj, add the line immediately after :tax-rate-breakdowns (line 163):

     [:tax-rate-breakdowns [:maybe [:vector TaxRateBreakdown]]]  ;; Per-rate breakdown if printed on invoice
     [:tax-rate-stated? {:optional true} [:maybe :boolean]]       ;; true iff the VAT rate is printed verbatim; absent/nil ⇒ unknown (legacy)

Run: clj -X:test:silent :nses '[com.getorcha.schema.invoice.structured-data-test]' 2>&1 | grep -E "FAIL in|Ran .* tests|failures" Expected: PASS, 0 failures, 0 errors.

Run: clj-kondo --lint src/com/getorcha/schema/invoice/structured_data.clj test/com/getorcha/schema/invoice/structured_data_test.clj Expected: errors: 0, warnings: 0.

git add src/com/getorcha/schema/invoice/structured_data.clj test/com/getorcha/schema/invoice/structured_data_test.clj
git commit -m "feat(schema): add optional :tax-rate-stated? to InvoiceData"

Task 2: Extraction prompt emits tax-rate-stated?

Files:

No unit test — prompt wording is verified by the regression run in Task 6. This is a deliberate, documented exception to TDD (LLM prompt text has no deterministic unit assertion); the behavioral check is the ingestion-regression-test in Task 6.

In extraction.clj, the tax-amount bullet ends at line 103. Insert a new bullet immediately after it (before the - tax-rate-breakdowns bullet at line 104):

   - tax-amount = the SINGLE TOTAL tax/VAT amount, ONLY if explicitly shown as one number on the invoice.
     Set to null if the invoice shows multiple separate tax lines without a combined total.
     Do NOT calculate this - extract only if a single total tax figure is printed.
   - tax-rate-stated? = true ONLY if the applicable VAT rate is printed verbatim on the invoice
     (e.g. "19 %", "MwSt 19%", "USt 19 %", "zzgl. 19% USt") OR an explicit exemption /
     reverse-charge note is present. false if the rate is absent and only derivable from the
     amounts. Do NOT infer it from tax-amount / subtotal. Set null only if you genuinely cannot tell.
   - tax-rate-breakdowns = Extract EVERY tax breakdown shown on the invoice, even if they share the

In extraction.clj, after the tax-rate-breakdowns array closes (line 463, the ] | null, line), add:

  ] | null,
  "tax-rate-stated?": boolean | null,  // true iff VAT rate printed verbatim; do NOT infer

Run: clj -M -e "(require 'com.getorcha.workers.ap.ingestion.extraction :reload) (println \"ok\")" Expected: prints ok (no reader/compile error from the edited string).

Run: clj-kondo --lint src/com/getorcha/workers/ap/ingestion/extraction.clj Expected: errors: 0, warnings: 0.

git add src/com/getorcha/workers/ap/ingestion/extraction.clj
git commit -m "feat(extraction): prompt for tax-rate-stated? (§14 Abs. 4 Nr. 8)"

Task 3: Deterministic check-tax-rate-stated

Files:

invoice-tier and resolve-issuer-country are private fns already in validation.clj; the new fn lives in the same ns and uses them directly.

Add after test-line-item-issue-message-format in validation_test.clj:

(deftest test-check-tax-rate-stated
  (let [eu-base {:document-type "invoice"
                 :total 1000.0 :tax-amount 190.0 :tax-rate 19
                 :issuer {:name "Acme GmbH" :country "DE"
                          :tax-id-type "vat" :tax-id "DE136695976"}}]
    (testing "rate printed (stated? true) → pass"
      (is (= "pass" (:status (validation/check-tax-rate-stated
                              (assoc eu-base :tax-rate-stated? true))))))
    (testing "legacy: key absent → pass"
      (is (= "pass" (:status (validation/check-tax-rate-stated eu-base)))))
    (testing "key explicitly nil → pass"
      (is (= "pass" (:status (validation/check-tax-rate-stated
                              (assoc eu-base :tax-rate-stated? nil))))))
    (testing "non-EU issuer → pass even when not stated"
      (is (= "pass" (:status (validation/check-tax-rate-stated
                              (-> eu-base
                                  (assoc :tax-rate-stated? false)
                                  (assoc-in [:issuer :country] "US")
                                  (assoc-in [:issuer :tax-id-type] nil)
                                  (assoc-in [:issuer :tax-id] nil)))))))
    (testing "no positive VAT (zero tax, no breakdowns) → pass"
      (is (= "pass" (:status (validation/check-tax-rate-stated
                              (assoc eu-base :tax-rate-stated? false
                                     :tax-amount 0.0 :tax-rate 0))))))
    (testing "reverse-charge with compliance statement → not-applicable"
      (is (= "not-applicable"
             (:status (validation/check-tax-rate-stated
                       (assoc eu-base :tax-rate-stated? false
                              :tax-amount 0.0 :tax-rate 0
                              :compliance-statements
                              [{:type "reverse-charge"
                                :text "Steuerschuldnerschaft des Leistungsempfängers"
                                :legal-basis "§13b UStG"}]))))))
    (testing "exempt via legal-basis §13b only (type not in set) → not-applicable"
      (is (= "not-applicable"
             (:status (validation/check-tax-rate-stated
                       (assoc eu-base :tax-rate-stated? false
                              :tax-amount 0.0 :tax-rate 0
                              :compliance-statements
                              [{:type "other"
                                :text "Steuerschuldnerschaft des Leistungsempfängers"
                                :legal-basis "§13b UStG"}]))))))
    (testing "EU, positive VAT, rate not stated → uncertain (escalate)"
      (let [r (validation/check-tax-rate-stated
               (assoc eu-base :tax-rate-stated? false))]
        (is (= "uncertain" (:status r)))
        (is (re-find #"§14" (:message r)))))
    (testing "positive VAT via breakdowns only (tax-amount nil) still escalates"
      (is (= "uncertain"
             (:status (validation/check-tax-rate-stated
                       (assoc eu-base :tax-rate-stated? false
                              :tax-amount nil
                              :tax-rate-breakdowns
                              [{:rate 19 :subtotal 1000.0 :tax-amount 190.0}]))))))))

Run: clj -X:test:silent :nses '[com.getorcha.workers.ap.ingestion.validation-test]' 2>&1 | grep -E "FAIL in|ERROR in|Ran .* tests|failures" Expected: ERROR — check-tax-rate-stated is unresolved.

In validation.clj, after check-required-fields (it ends at line 556 with {:status "pass" :details {:tier tier}})))), add:

(defn check-tax-rate-stated
  "§14 Abs. 4 S. 1 Nr. 8 UStG: an invoice must print the applicable VAT rate,
   not only the tax amount. Ordered cond — first match wins. Never terminally
   warns; only escalates to \"uncertain\" for UVR to decide against the PDF.

   `:tax-rate-stated?` is set by extraction (true only if the rate is printed
   verbatim). Absent/nil ⇒ unknown (legacy doc) ⇒ pass, so legacy docs stay
   silent until re-ingested. \"Positive VAT\" is decided from PRINTED fields
   (`tax-amount` / `tax-rate-breakdowns`), never the LLM-inferred `tax-rate`."
  [{:keys [tax-rate-stated? total tax-amount tax-rate tax-rate-breakdowns
           compliance-statements issuer]}]
  (let [tier         (invoice-tier total (resolve-issuer-country issuer))
        positive-vat? (or (and tax-amount (pos? (double tax-amount)))
                          (some #(and (:rate %) (pos? (double (:rate %))))
                                tax-rate-breakdowns))
        exempt?      (and (or (nil? tax-rate) (zero? tax-rate))
                          (some (fn [{:keys [type legal-basis]}]
                                  (or (#{"reverse-charge" "vat-exemption"} type)
                                      (and legal-basis (re-find #"(?i)13b" legal-basis))))
                                compliance-statements))]
    (cond
      (true? tax-rate-stated?)         {:status "pass"}
      (nil? tax-rate-stated?)          {:status "pass"}
      (= :non-eu tier)                 {:status "pass"}
      exempt?                          {:status "not-applicable"}
      (not positive-vat?)              {:status "pass"}
      :else
      {:status  "uncertain"
       :message (str "Invoice does not state the applicable VAT rate "
                     "(§14 Abs. 4 Nr. 8 UStG); only the tax amount is shown.")
       :details {:tier tier}})))

Run: clj -X:test:silent :nses '[com.getorcha.workers.ap.ingestion.validation-test]' 2>&1 | grep -E "FAIL in|ERROR in|Ran .* tests|failures" Expected: PASS, 0 failures, 0 errors.

Run: clj-kondo --lint src/com/getorcha/workers/ap/ingestion/validation.clj test/com/getorcha/workers/ap/ingestion/validation_test.clj Expected: errors: 0, warnings: 0.

git add src/com/getorcha/workers/ap/ingestion/validation.clj test/com/getorcha/workers/ap/ingestion/validation_test.clj
git commit -m "feat(validation): add check-tax-rate-stated (§14 Abs. 4 Nr. 8)"

Task 4: Wire tax-rate-stated into the UVR

Files:

The resolver mirrors resolve-dates (value→status, no corrections — a presentation defect has nothing to fix). UVR only calls a resolver when the deterministic status is "uncertain"; (or (:det-result resolved) det-check) means the resolver overwrites the escalation.

Add to uncertain_validations_test.clj. Verified ns aliases (lines 3-7): [com.getorcha.ai.llm :as llm], [com.getorcha.ai.prompts :as ai.prompts], [com.getorcha.workers.ap.processors.uncertain-validations :as uncertain-validations] — so use the uncertain-validations alias for the resolver (not uv). The only missing alias is cheshire.core.

(deftest test-resolve-tax-rate-stated-mapping
  (let [ctx   {:db-pool nil
               :llm-config {:post-processing :pp :vision :vis}}
        state {:structured-data {:document-type "invoice" :tax-amount 190.0}
               :document {:document/tenant-id (random-uuid)}
               :file nil}
        det   {:status "uncertain"
               :message "Invoice does not state the applicable VAT rate (§14 Abs. 4 Nr. 8 UStG); only the tax amount is shown."}
        run   (fn [llm-value]
                (with-redefs [ai.prompts/tenant-prompt (fn [& _] "PROMPT")
                              llm/generate (fn [& _]
                                             {:text (json/generate-string
                                                     {:tax-rate-stated
                                                      {:value llm-value :confidence 0.9
                                                       :reasoning "r"}})})]
                  (-> (#'uncertain-validations/resolve-tax-rate-stated ctx state det)
                      :det-result :status)))]
    (is (= "pass"           (run "pass")))
    (is (= "not-applicable" (run "not-applicable")))
    (is (= "warning"        (run "warning")))
    (is (= "warning"        (run "unresolved")))))

Add only [cheshire.core :as json] to the test ns :require (llm, ai.prompts, and the uncertain-validations processor alias are already present).

Run: clj -X:test:silent :nses '[com.getorcha.workers.ap.processors.uncertain-validations-test]' 2>&1 | grep -E "FAIL in|ERROR in|Ran .* tests|failures" Expected: ERROR — resolve-tax-rate-stated unresolved.

In uncertain_validations.clj, after the recipient-identity-instruction def block (the private instruction strings section), add:

(def ^:private tax-rate-stated-instruction
  "**VAT rate statement (§14 Abs. 4 S. 1 Nr. 8 UStG)**:
Our extractor reports the applicable VAT rate is NOT printed on the invoice
(only the tax amount / net / gross are shown). German law requires the rate
itself to be stated. Look at the actual invoice and decide:
- \"pass\": the applicable rate (e.g. \"19 %\", \"MwSt 19%\", \"USt-Satz 19 %\")
  IS printed somewhere — the extractor missed it.
- \"not-applicable\": this is a reverse-charge / VAT-exempt / 0% invoice that
  legitimately carries a compliance statement instead of a rate.
- \"warning\": a positive VAT applies but the rate is genuinely not printed.
Return: {\"tax-rate-stated\": {\"value\": \"pass\"|\"not-applicable\"|\"warning\",
\"confidence\": 0.9, \"reasoning\": \"...\"}}")

Add after resolve-dates (it ends at line 358):

(defn ^:private resolve-tax-rate-stated
  "Calls the vision-LLM resolver for the tax-rate-stated uncertain check.
   No corrections — a missing printed rate is a defect in the paper, not our
   data. Mirrors resolve-required-fields' PDF handling."
  [ctx state deterministic]
  (ai-events/with-ai-phase :post-process.uncertain-validations
    (let [{:keys [db-pool]} ctx
          {:keys [structured-data document]} state
          tenant-id    (:document/tenant-id document)
          invoice-data (select-keys structured-data
                                    [:document-type :tax-rate :tax-amount
                                     :tax-rate-breakdowns :compliance-statements
                                     :subtotal :total])
          questions    (format "1. tax-rate-stated: %s" (:message deterministic))
          prompt       (ai.prompts/tenant-prompt db-pool tenant-id
                                                 :resolve-uncertain-validations
                                                 {:invoice-data (json/generate-string invoice-data {:pretty true})
                                                  :questions    questions
                                                  :instructions tax-rate-stated-instruction})
          pdf-bytes    (get-in state [:file :contents])
          relevant-pdf (when pdf-bytes
                         (try
                           (common/extract-relevant-pdf-pages pdf-bytes structured-data)
                           (catch Exception e
                             (log/warn "Could not extract PDF pages for tax-rate-stated resolver"
                                       {:error (ex-message e)}))))
          input        (if relevant-pdf
                         [{:type :text :content prompt}
                          {:type :document :mime-type "application/pdf" :data relevant-pdf}]
                         prompt)
          llm-config   (if relevant-pdf
                         (get-in ctx [:llm-config :vision])
                         (get-in ctx [:llm-config :post-processing]))
          {:keys [text] :as llm-stats} (llm/generate llm-config input)
          parsed       (llm/parse-json-response text)
          {:keys [value confidence reasoning]} (:tax-rate-stated parsed)]
      {:det-result {:status      (case value
                                   "pass"           "pass"
                                   "not-applicable" "not-applicable"
                                   "warning")
                    :resolved-by :uncertain-validations-resolver
                    :confidence  confidence
                    :reasoning   reasoning
                    :message     (:message deterministic)}
       :stats      llm-stats})))

Change the docstring sub-path list (lines 4-8). Replace:

   Owns three diagnostic sub-paths under the `:validations` slice for
   invoice documents:
   - `:validations.required-fields`
   - `:validations.date-reasonableness`
   - `:validations.recipient-identity`

with:

   Owns four diagnostic sub-paths under the `:validations` slice for
   invoice documents:
   - `:validations.required-fields`
   - `:validations.date-reasonableness`
   - `:validations.recipient-identity`
   - `:validations.tax-rate-stated`

In -reads (after line 507's (reads/read-leaf :line-items :* :amount) / before the date-reasonableness comment) add:

     ;; Tax-rate-stated check
     (reads/read-leaf :tax-rate-stated?)
     (reads/read-leaf :tax-rate-breakdowns :* :rate)
     (reads/read-leaf :compliance-statements :* :type)

In -diagnostic, change the :sub-paths vector to add the 4th:

       :sub-paths [[:required-fields]
                   [:date-reasonableness]
                   [:recipient-identity]
                   [:tax-rate-stated]]}))

In -compute, add the deterministic call, the conditional resolve, and the result key. Replace the let bindings + result map:

  (-compute [_ ctx state]
    (let [sd              (:structured-data state)
          le              (:tenant state)
          det-required    (validation/check-required-fields sd)
          det-dates       (validation/check-date-reasonableness sd)
          det-identity    (validation/check-recipient-identity sd le)
          det-tax-rate    (validation/check-tax-rate-stated sd)
          req-resolved    (when (= "uncertain" (:status det-required))
                            (resolve-required-fields ctx state det-required))
          dates-resolved  (when (= "uncertain" (:status det-dates))
                            (resolve-dates ctx state det-dates))
          ident-resolved  (when (#{"uncertain" "warning"} (:status det-identity))
                            (resolve-identity ctx state det-identity))
          tax-resolved    (when (= "uncertain" (:status det-tax-rate))
                            (resolve-tax-rate-stated ctx state det-tax-rate))
          result          {:required-fields     (or (:det-result req-resolved)   det-required)
                           :date-reasonableness (or (:det-result dates-resolved) det-dates)
                           :recipient-identity  (or (:det-result ident-resolved) det-identity)
                           :tax-rate-stated     (or (:det-result tax-resolved)   det-tax-rate)}]
      {:result result
       :stats  (merge-llm-stats (:stats req-resolved)
                                 (:stats dates-resolved)
                                 (:stats ident-resolved)
                                 (:stats tax-resolved))}))

merge-llm-stats is variadic — (defn ^:private merge-llm-stats [& stats-maps] ...) at uncertain_validations.clj:467. The 4-arg call works directly; no nesting needed. Also update its docstring (line 468), which says "up to three resolver calls", to "four".

Run: clj -X:test:silent :nses '[com.getorcha.workers.ap.processors.uncertain-validations-test]' 2>&1 | grep -E "FAIL in|ERROR in|Ran .* tests|failures" Expected: PASS, 0 failures, 0 errors.

Run: clj-kondo --lint src/com/getorcha/workers/ap/processors/uncertain_validations.clj test/com/getorcha/workers/ap/processors/uncertain_validations_test.clj Expected: errors: 0, warnings: 0.

git add src/com/getorcha/workers/ap/processors/uncertain_validations.clj test/com/getorcha/workers/ap/processors/uncertain_validations_test.clj
git commit -m "feat(uvr): resolve tax-rate-stated as 4th sub-path"

Task 5: Surface the warning across all five ordering lists

Files:

Add to findings_test.clj (use the file's existing findings/alias convention):

(deftest test-tax-rate-stated-finding
  (testing "warning status yields a formal finding"
    (let [fs (findings/extract-findings
              {:validations {:tax-rate-stated
                             {:status "warning"
                              :message "Invoice does not state the applicable VAT rate (§14 Abs. 4 Nr. 8 UStG); only the tax amount is shown."}}
               :fraud-flags []}
              nil)
          f  (first (filter #(= :tax-rate-stated (:check %)) fs))]
      (is (some? f))
      (is (= "VAT Rate Statement" (:label f)))
      (is (= :warning (:severity f)))))
  (testing "pass / not-applicable produce no finding"
    (doseq [st ["pass" "not-applicable"]]
      (is (empty? (filter #(= :tax-rate-stated (:check %))
                          (findings/extract-findings
                           {:validations {:tax-rate-stated {:status st}}
                            :fraud-flags []}
                           nil)))))))

Run: clj -X:test:silent :nses '[com.getorcha.diagnostics.findings-test]' 2>&1 | grep -E "FAIL in|ERROR in|Ran .* tests|failures" Expected: FAIL — no finding produced / label nil (key not in formal-requirement-checks).

Add to validation-check-labels (after :recipient-country line 18):

   :recipient-country           "Recipient Country"
   :tax-rate-stated             "VAT Rate Statement"
   :tax                         "Tax"})

Add to formal-requirement-checks (line 37-38) — place before :tax:

  [:large-document-summary-only :required-fields :financial-math :date-reasonableness
   :issuer-country :recipient-country :tax-rate-stated :tax])

view/invoice.clj:201-202. Add :tax-rate-stated to the literal:

        formal-issue-count  (count (filter (fn [k] (#{"error" "warning" "uncertain"}
                                                     (:status (get validation-results' k))))
                                           [:required-fields :financial-math :date-validity
                                            :issuer-country :recipient-country :tax-rate-stated :tax]))

validation-check-order (def at 1939, body 1943-1949) — place after :tax:

  [:large-document-summary-only
   :required-fields
   :financial-math
   :tax
   :tax-rate-stated
   :date-reasonableness
   :issuer-country
   :recipient-country])

validation-check-descriptions (1958-1967) — add entry:

   :recipient-country            "The recipient's country could not be determined from the address or tax ID."
   :tax-rate-stated              "The invoice may not print the applicable VAT rate (§14 Abs. 4 Nr. 8 UStG requires the rate, not only the tax amount)."
   :tax                          "The tax ID format may be incorrect or there may be tax compliance issues."})

formal-status-texts (1971-1978) — add entry:

   :recipient-country            {"pass" "Available"     "uncertain" "No Recipient" "error" "Not Available"}
   :tax-rate-stated              {"pass" "Stated"        "warning" "Rate Not Stated" "not-applicable" "Not Applicable" "uncertain" "Checking"}
   :tax                          {"pass" "Correct"       "not-applicable" "No Tax ID" "warning" "Issue Found" "error" "Issue Found"}})

Run: clj -X:test:silent :nses '[com.getorcha.diagnostics.findings-test]' 2>&1 | grep -E "FAIL in|ERROR in|Ran .* tests|failures" Expected: PASS, 0 failures, 0 errors.

Run: clj -M -e "(doseq [n '[com.getorcha.diagnostics.findings com.getorcha.app.http.documents.view.invoice com.getorcha.app.ui.components]] (require n :reload)) (println \"ok\")" Expected: prints ok.

Run: clj-kondo --lint src/com/getorcha/diagnostics/findings.clj src/com/getorcha/app/http/documents/view/invoice.clj src/com/getorcha/app/ui/components.clj test/com/getorcha/diagnostics/findings_test.clj Expected: errors: 0, warnings: 0.

git add src/com/getorcha/diagnostics/findings.clj src/com/getorcha/app/http/documents/view/invoice.clj src/com/getorcha/app/ui/components.clj test/com/getorcha/diagnostics/findings_test.clj
git commit -m "feat(findings): surface tax-rate-stated warning across ordering lists"

Task 6: Full regression + behavioral verification

Files: none (verification only).

Run: clj-kondo --lint src test Expected: errors: 0, warnings: 0.

Run: clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|Ran .* tests)" Expected: Ran N tests, 0 failures, 0 errors. If anything else fails, it is in scope to fix (likely another hardcoded check list or a fixture lacking the new key) before proceeding.

Re-ingest the bikosigma document locally (system must be running; see clojure-eval skill for the nREPL port). In the REPL:

(require '[com.getorcha.app.ingestion :as app.ingestion] :reload)
(let [sys integrant.repl.state/system
      bytes (with-open [in (java.io.FileInputStream.
                            "/tmp/debug-fetch/documents/019e3a85-4776-7067-81b6-49a6dfde031a.pdf")]
              (.readAllBytes in))]
  (app.ingestion/queue-for-ingestion!
   (:com.getorcha.db/pool sys) (:com.getorcha.aws/state sys)
   {:content bytes :content-type "application/pdf"
    :tenant-id #uuid "00000000-0000-0000-0000-000000000001"
    :uploaded-by #uuid "00000000-0000-0000-0000-000000000001"
    :file-original-name "bikosigma-vat-rate-check.pdf" :source-metadata {}}))

Wait for ingestion to complete, then assert the diagnostics carry the new check:

(get-in (:document/diagnostics
         (com.getorcha.db.sql/execute-one!
          (:com.getorcha.db/pool integrant.repl.state/system)
          {:select [:diagnostics] :from [:document]
           :where [:= :id <new-doc-id>]}))
        [:validations :tax-rate-stated])

Expected: a map with :status of "warning" (rate genuinely absent on this invoice; UVR confirmed) or "pass" (if UVR finds a rate we missed). Either is a correct outcome — confirm it is not absent and not "uncertain".

Invoke the ingestion-regression-test skill comparing structured data before/after for a representative sample including doc 019e3a85-4776-7067-81b6-49a6dfde031a. Expected: no regressions in unrelated fields; :tax-rate-stated? populated on the sample.

git add -A
git commit -m "fix(vat-rate-check): regression follow-ups"

Self-Review

Spec coverage:

Placeholder scan: No TBD/TODO; every code step shows complete code; commands have expected output. Task 2's "no unit test" is explicitly justified (prompt text) with the behavioral check located in Task 6. ✔

Type consistency: check-tax-rate-stated (Task 3) is the exact symbol called in Task 4 -compute; resolve-tax-rate-stated (Task 4) is the exact private symbol exercised by the Task 4 test via #'uncertain-validations/resolve-tax-rate-stated. Diagnostics key :tax-rate-stated is consistent across Task 4 (result map / -diagnostic sub-path) and Task 5 (findings ordering, labels, the three components maps, invoice.clj literal) and Task 5/6 tests. Status vocabulary pass/not-applicable/uncertain/warning consistent with findings/formal-finding ("error":error, else :warning; pass/not-applicable silent). ✔

No open implementation risks. The previously-flagged merge-llm-stats arity is resolved: it is variadic ([& stats-maps], uncertain_validations.clj:467); the 4-arg call works directly, and Task 4 Step 6 also updates its stale "three"→"four" docstring.