Note (2026-04-24): After this document was written,
legal_entitywas renamed totenantand the oldtenantwas renamed toorganization. Read references to these terms with the pre-rename meaning.
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: Scope reconciliation findings to the document being viewed and prevent contracts from inflating clusters.
Architecture: Two changes: (1) assign-cluster! skips cluster operations when either document is a contract, (2) UI filters reconciliation issues by current document ID before rendering.
Tech Stack: Clojure, HoneySQL, Hiccup, clojure.test with embedded Postgres
Files:
Modify: src/com/getorcha/workers/ap/matching/core.clj:98-129 — assign-cluster!
Test: test/com/getorcha/workers/ap/matching/core_test.clj
Step 1: Write failing tests for contract cluster skipping
Add these tests after the existing assign-cluster-test block (after line 207 in core_test.clj):
(deftest assign-cluster-skips-contracts-test
(testing "skips cluster creation when source doc is a contract"
(let [le-id (helpers/create-legal-entity!)
contract-id (helpers/create-document-with-type! le-id :contract {:counterparty {:name "A"}})
invoice-id (helpers/create-document-with-type! le-id :invoice {:issuer {:name "A"}})]
(matching/assign-cluster! fixtures/*db* contract-id invoice-id)
(let [contract (get-document contract-id)
invoice (get-document invoice-id)]
(is (nil? (:document/cluster-id contract)))
(is (nil? (:document/cluster-id invoice))))))
(testing "skips cluster creation when candidate doc is a contract"
(let [le-id (helpers/create-legal-entity!)
invoice-id (helpers/create-document-with-type! le-id :invoice {:issuer {:name "A"}})
contract-id (helpers/create-document-with-type! le-id :contract {:counterparty {:name "A"}})]
(matching/assign-cluster! fixtures/*db* invoice-id contract-id)
(let [contract (get-document contract-id)
invoice (get-document invoice-id)]
(is (nil? (:document/cluster-id contract)))
(is (nil? (:document/cluster-id invoice))))))
(testing "skips cluster merge when one doc is a contract with existing cluster"
(let [le-id (helpers/create-legal-entity!)
invoice-id (helpers/create-document-with-type! le-id :invoice {:issuer {:name "A"}})
contract-id (helpers/create-document-with-type! le-id :contract {:counterparty {:name "A"}})
cluster (db.matching/create-cluster! fixtures/*db*)
cluster-id (:ap-document-cluster/id cluster)]
;; Invoice already in a cluster
(db.matching/set-cluster-id! fixtures/*db* [invoice-id] cluster-id)
(matching/assign-cluster! fixtures/*db* invoice-id contract-id)
(let [invoice (get-document invoice-id)
contract (get-document contract-id)]
;; Invoice keeps its cluster, contract stays out
(is (= cluster-id (:document/cluster-id invoice)))
(is (nil? (:document/cluster-id contract)))))))
Run: clj -X:test:silent :nses '[com.getorcha.workers.ap.matching.core-test]'
Expected: 3 new test failures — contracts get assigned to clusters.
assign-cluster!In src/com/getorcha/workers/ap/matching/core.clj, modify assign-cluster! to early-return when either document is a contract. The function already fetches both documents from the DB (line 107), so the types are available:
(defn assign-cluster!
"Assign cluster to matched documents.
Cases:
- Either document is a contract: no-op (contracts don't participate in clusters)
- Neither has a cluster: create a new one for both
- One has a cluster: add the other to it
- Both have different clusters: merge cluster-b into cluster-a
- Both share the same cluster: no-op
TODO: contract-level compliance reconciliation — contracts are excluded from
clusters to keep line-item reconciliation focused, but a second-tier
reconciliation pass comparing all documents under a contract (cumulative
spend, price compliance, coverage) would be valuable. See #332."
[db doc-a-id doc-b-id]
(let [docs (db.matching/get-documents-by-ids db [doc-a-id doc-b-id])
doc-a (first (filter #(= doc-a-id (:document/id %)) docs))
doc-b (first (filter #(= doc-b-id (:document/id %)) docs))
contract? (fn [{:document/keys [type]}] (= type "contract"))]
(when-not (or (contract? doc-a) (contract? doc-b))
(let [cluster-a (:document/cluster-id doc-a)
cluster-b (:document/cluster-id doc-b)]
(cond
(and cluster-a cluster-b (= cluster-a cluster-b))
nil
(and cluster-a cluster-b)
(let [docs-to-move (db.matching/get-cluster-documents db cluster-b)]
(db.matching/set-cluster-id! db (mapv :document/id docs-to-move) cluster-a)
(db.matching/delete-cluster! db cluster-b))
cluster-a
(db.matching/set-cluster-id! db [doc-b-id] cluster-a)
cluster-b
(db.matching/set-cluster-id! db [doc-a-id] cluster-b)
:else
(let [cluster (db.matching/create-cluster! db)]
(db.matching/set-cluster-id! db [doc-a-id doc-b-id] (:ap-document-cluster/id cluster))))))))
Run: clj -X:test:silent :nses '[com.getorcha.workers.ap.matching.core-test]'
Expected: all tests pass, including existing assign-cluster-test cases.
Run: clj-kondo --lint src/com/getorcha/workers/ap/matching/core.clj test/com/getorcha/workers/ap/matching/core_test.clj
Expected: no warnings or errors.
git add src/com/getorcha/workers/ap/matching/core.clj test/com/getorcha/workers/ap/matching/core_test.clj
git commit -m "feat: skip cluster assignment for contract matches (#332)"
Files:
Modify: src/com/getorcha/app/http/documents/view/shared.clj:238-324 — reconciliation-badge, reconciliation-issues, matches-section
Step 1: Add filtering helper and update reconciliation-badge
The filtering logic is needed in two places (badge and issues), so add it inline where used. In shared.clj, replace the existing reconciliation-badge function (lines 238-249):
(defn ^:private reconciliation-badge
"Renders the reconciliation status badge next to the Matches heading.
Filters issues to only those referencing the given document."
[document-id reconciliation]
(let [issues (filterv #(some #{(str document-id)} (:document-ids %))
(:issues reconciliation))]
(if (seq issues)
(let [issue-count (count issues)
has-error? (some #(= "error" (:severity %)) issues)
badge-class (if has-error? "math-badge math-error" "math-badge math-warning")]
[:span {:class badge-class}
(str issue-count " issue" (when (not= 1 issue-count) "s") " found")])
(when (:status reconciliation)
[:span.math-badge.math-pass "Reconciled"]))))
reconciliation-issuesReplace the existing reconciliation-issues function (lines 252-278):
(defn ^:private reconciliation-issues
"Renders the list of reconciliation issues below match cards.
Only shows issues where the given document is a party to the discrepancy."
[document-id reconciliation]
(let [issues (filterv #(some #{(str document-id)} (:document-ids %))
(:issues reconciliation))]
(when (seq issues)
[:div.reconciliation-issues
[:h4 {:style "margin: 16px 0 8px; font-size: 14px; color: #c9d1d9;"} "Reconciliation Issues"]
(for [{:keys [severity summary details]} issues]
[:div.reconciliation-issue {:class (str "severity-" severity)}
[:div.issue-header
[:iconify-icon {:icon (if (= "error" severity)
"lucide:alert-circle"
"lucide:alert-triangle")
:style (str "color: " (if (= "error" severity) "#f85149" "#d29922")
"; flex-shrink: 0; font-size: 16px;")}]
[:div.issue-body
[:span.issue-summary summary]]]
(when (seq details)
[:details.issue-details {:open true}
[:summary "Field comparisons"]
[:table.details-table
[:thead [:tr [:th "Field"] [:th "Expected"] [:th "Actual"]]]
[:tbody
(for [{:keys [field expected actual]} details]
[:tr
[:td field]
[:td (or expected "\u2014")]
[:td (or actual "\u2014")]])]]])])])))
matches-section to pass document-idIn matches-section (line 295 and 324), update the two callsites:
Line 295 — change:
title-with-badge [:span "Matches" (reconciliation-badge reconciliation)]]
to:
title-with-badge [:span "Matches" (reconciliation-badge document-id reconciliation)]]
Line 324 — change:
(reconciliation-issues reconciliation)))))
to:
(reconciliation-issues document-id reconciliation)))))
Run: clj-kondo --lint src/com/getorcha/app/http/documents/view/shared.clj
Expected: no warnings or errors.
git add src/com/getorcha/app/http/documents/view/shared.clj
git commit -m "feat: filter reconciliation issues by current document (#332)"
No new code — verify the changes work together by running the full test suite.
Run: clj -X:test:silent :nses '[com.getorcha.workers.ap.matching.core-test]'
Expected: all pass.
Run: clj -X:test:silent 2>&1 | grep -A 5 -E "(FAIL in|ERROR in|Execution error|failed because|Ran .* tests)"
Expected: no new failures.