Document Data Panel — Phase 1a: Renderer Engine — 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: Build a pure-function Hiccup renderer for the document data panel's content model (:sections:fields with value kinds + callouts, the :payment-schedule component, the panel-level :coverage view, and per-fact source chips), fully TDD'd against fixtures — the renderer-first core that the spine/integration and source-anchoring plans build on.

Architecture: A new namespace com.getorcha.app.http.documents.view.panel holds the generic renderer. It is pure (no DB, no IO): it takes a content-model map and returns Hiccup data, reusing existing helpers (collapsible-section, format-currency, format-date) from com.getorcha.app.ui.components. Source chips render markup only — clicking-to-navigate is wired in the separate source-anchoring spike. This plan does not touch contract.clj, shared.clj, dispatch, the tab strip, or the existing contract-risk-signals-box (those are Phase 1b integration).

Tech Stack: Clojure, hiccup2.core (data → HTML at the view layer), clojure.test. Tests render with (str (hiccup/html …)) and assert with re-find / clojure.string/includes?, mirroring test/com/getorcha/app/http/documents/view/shared_test.clj.

Conventions (from orcha-clojure-style): kebab-case; ^:private for internal fns (tested via #'panel/fn); requires alphabetical; two blank lines between top-level forms; alias com.getorcha.app.ui.components :as app.ui.components.

Commit trailer: end every commit message with Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Test command: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' (run from /Users/maximilianbrandstaetter/Orcha/orcha). Lint command: clj-kondo --lint src test (run from orcha/).

Content-model reference (from the spec):

{:type "subscription"
 :currency "EUR"
 :sections
 [{:id "billing" :title "Subscription & Billing"
   :fields [{:label "Recurring fee" :value 2400 :kind :currency :source 2}
            {:label "Billing cycle" :value "Annual" :source 2}
            {:kind :callout :severity :warning :source 5
             :value "Auto-renews for 12 months unless cancelled 90 days prior."}]}
  {:id "payment-plan" :title "Payment Schedule"
   :component :payment-schedule
   :rows [["2026-01-01" "€2,400"] ["2027-01-01" "€2,520"]]
   :source 6}]
 :coverage
 [{:term "Liability cap"   :found? false}
  {:term "Confidentiality" :found? true :source 7}]}

Field :kind:text (default) · :number · :currency · :date · :percent · :boolean · :table · :callout.


File Structure

Public API this namespace exposes (used later by Phase 1b integration): render-panel-middle, coverage-view, source-chip. Everything else is ^:private.


Task 1: Namespace skeleton + format-value

Files:

Create the test file:

(ns com.getorcha.app.http.documents.view.panel-test
  (:require [clojure.string :as string]
            [clojure.test :refer [deftest is testing]]
            [com.getorcha.app.http.documents.view.panel :as panel]
            [com.getorcha.app.ui.components :as app.ui.components]
            [hiccup2.core :as hiccup]))


(deftest format-value-test
  (testing "boolean renders Yes/No"
    (is (= "Yes" (#'panel/format-value true :boolean nil)))
    (is (= "No"  (#'panel/format-value false :boolean nil))))
  (testing "percent appends %"
    (is (= "5%" (#'panel/format-value 5 :percent nil))))
  (testing "number stringifies"
    (is (= "42" (#'panel/format-value 42 :number nil))))
  (testing "text (default / nil kind) stringifies"
    (is (= "Annual" (#'panel/format-value "Annual" :text nil)))
    (is (= "Annual" (#'panel/format-value "Annual" nil nil))))
  (testing "currency delegates to format-currency"
    (is (= (app.ui.components/format-currency 2400 "EUR")
           (#'panel/format-value 2400 :currency "EUR"))))
  (testing "date delegates to format-date"
    (is (= (app.ui.components/format-date "2026-01-01")
           (#'panel/format-value "2026-01-01" :date nil)))))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: FAIL — namespace com.getorcha.app.http.documents.view.panel cannot be loaded / format-value unresolved.

Create src/com/getorcha/app/http/documents/view/panel.clj:

(ns com.getorcha.app.http.documents.view.panel
  "Generic content-model renderer for the document data panel.

   Renders the type-adaptive middle (`:sections` → `:fields`) plus the
   panel-level `:coverage` checklist to Hiccup. Section *buckets* are decided by
   config/the LLM; this namespace only renders whatever it is handed. Pure
   functions — no DB, no IO — so it builds and tests against fixtures before the
   extraction pipeline produces real data."
  (:require [com.getorcha.app.ui.components :as app.ui.components]))


(defn ^:private format-value
  "Formats a scalar field value for display by its kind.
   kinds: :text (default) :number :currency :date :percent :boolean."
  [value kind currency]
  (case kind
    :currency (app.ui.components/format-currency value currency)
    :date     (app.ui.components/format-date value)
    :percent  (str value "%")
    :boolean  (if value "Yes" "No")
    :number   (str value)
    (str value)))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: PASS (1 test, 7 assertions).

git add src/com/getorcha/app/http/documents/view/panel.clj test/com/getorcha/app/http/documents/view/panel_test.clj
git commit -m "feat(panel): value-kind formatting for the data-panel renderer"

Task 2: source-chip

Files:

(deftest source-chip-test
  (testing "renders a page chip carrying data-page"
    (let [html (str (hiccup/html (panel/source-chip 3)))]
      (is (string/includes? html "source-chip"))
      (is (string/includes? html "data-page=\"3\""))
      (is (string/includes? html "p.3"))))
  (testing "nil page renders nothing"
    (is (nil? (panel/source-chip nil)))))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: FAIL — source-chip unresolved.

(defn source-chip
  "A page-source chip for a fact. Clicking it (wired separately — see the
   source-anchoring spike) navigates the PDF to `page`. Returns nil when `page`
   is nil so it can be dropped inline."
  [page]
  (when page
    [:button.source-chip {:type "button" :data-page page :title (str "Source: page " page)}
     (str "p." page)]))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: PASS.

git add src/com/getorcha/app/http/documents/view/panel.clj test/com/getorcha/app/http/documents/view/panel_test.clj
git commit -m "feat(panel): page-source chip markup"

Task 3: render-field (scalar, callout, table)

Files:

(deftest render-field-test
  (testing "labeled scalar field formats value and shows a source chip"
    (let [html (str (hiccup/html (#'panel/render-field
                                  {:label "Billing cycle" :value "Annual" :source 2} nil)))]
      (is (string/includes? html "field-label"))
      (is (string/includes? html "Billing cycle"))
      (is (string/includes? html "Annual"))
      (is (string/includes? html "data-page=\"2\""))))
  (testing "field without a source omits the chip"
    (let [html (str (hiccup/html (#'panel/render-field
                                  {:label "Billing cycle" :value "Annual"} nil)))]
      (is (not (string/includes? html "source-chip")))))
  (testing "callout kind renders a severity-styled callout"
    (let [html (str (hiccup/html (#'panel/render-field
                                  {:kind :callout :severity :warning
                                   :value "Auto-renews unless cancelled." :source 5} nil)))]
      (is (string/includes? html "panel-callout"))
      (is (string/includes? html "callout-warning"))
      (is (string/includes? html "Auto-renews unless cancelled."))
      (is (string/includes? html "data-page=\"5\""))))
  (testing "table kind renders columns + rows"
    (let [html (str (hiccup/html (#'panel/render-field
                                  {:label "Tiers" :kind :table
                                   :columns ["Seats" "Price"]
                                   :rows [["50" "€48"] ["100" "€44"]]} nil)))]
      (is (string/includes? html "<th>Seats</th>"))
      (is (string/includes? html "<td>100</td>")))))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: FAIL — render-field unresolved.

(defn ^:private render-field
  "Renders one field. A field is an interpretation callout (`:kind :callout`), an
   inline table (`:kind :table` with `:columns`/`:rows`), or a labeled scalar
   key/value. `currency` is the document currency, used for :currency values.
   Each carries an optional :source page chip."
  [{:keys [label value kind severity source columns rows] :as _field} currency]
  (case kind
    :callout
    [:div {:class (str "panel-callout" (when severity (str " callout-" (name severity))))}
     [:span.callout-text value]
     (source-chip source)]

    :table
    [:div.field-group
     [:div.field-label label]
     [:table.table
      [:thead [:tr (for [c columns] [:th c])]]
      [:tbody (for [row rows] [:tr (for [cell row] [:td cell])])]]
     (source-chip source)]

    [:div.field-group
     [:div.field-label label]
     [:div.field-value (format-value value kind currency) (source-chip source)]]))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: PASS.

git add src/com/getorcha/app/http/documents/view/panel.clj test/com/getorcha/app/http/documents/view/panel_test.clj
git commit -m "feat(panel): generic field renderer (scalar, callout, table kinds)"

Task 4: payment-schedule-component

Files:

(deftest payment-schedule-component-test
  (testing "renders [date amount] rows as a table with a source chip"
    (let [html (str (hiccup/html (#'panel/payment-schedule-component
                                  {:rows [["2026-01-01" "€2,400"] ["2027-01-01" "€2,520"]]
                                   :source 6})))]
      (is (string/includes? html "payment-schedule"))
      (is (string/includes? html "2026-01-01"))
      (is (string/includes? html "€2,520"))
      (is (string/includes? html "data-page=\"6\"")))))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: FAIL — payment-schedule-component unresolved.

(defn ^:private payment-schedule-component
  "Renders a payment / repayment schedule section's `:rows` ([date amount] pairs)
   as a table. (Timeline styling is CSS; this is the per-type component, not
   generic KV — it earns a component because the data wants bespoke treatment.)"
  [{:keys [rows source] :as _section}]
  [:div.subsection.payment-schedule
   [:table.table
    [:thead [:tr [:th "Date"] [:th "Amount"]]]
    [:tbody (for [[date amount] rows]
              [:tr [:td date] [:td amount]])]]
   (source-chip source)])

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: PASS.

git add src/com/getorcha/app/http/documents/view/panel.clj test/com/getorcha/app/http/documents/view/panel_test.clj
git commit -m "feat(panel): payment-schedule per-type component"

Task 5: render-section (component dispatch + collapsible wrapper)

Files:

(deftest render-section-test
  (testing "a fields section wraps its fields in a collapsible block keyed by :id/:title"
    (let [html (str (hiccup/html (#'panel/render-section
                                  {:id "billing" :title "Subscription & Billing"
                                   :fields [{:label "Recurring fee" :value 2400 :kind :currency :source 2}
                                            {:label "Billing cycle" :value "Annual"}]}
                                  "EUR")))]
      (is (string/includes? html "id=\"billing\""))
      (is (string/includes? html "Subscription &amp; Billing"))
      (is (string/includes? html "Recurring fee"))
      (is (string/includes? html "Billing cycle"))))
  (testing "a :component section dispatches to the per-type component"
    (let [html (str (hiccup/html (#'panel/render-section
                                  {:id "payment-plan" :title "Payment Schedule"
                                   :component :payment-schedule
                                   :rows [["2026-01-01" "€2,400"]]}
                                  "EUR")))]
      (is (string/includes? html "payment-schedule"))
      (is (string/includes? html "2026-01-01")))))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: FAIL — render-section unresolved.

(defn ^:private render-section
  "Renders one populated section as a collapsible block. When `:component` is set,
   dispatch to the per-type component; otherwise render `:fields` generically.
   `collapsible-section` takes [title id content]."
  [{:keys [id title component fields] :as section} currency]
  (let [body (case component
               :payment-schedule (payment-schedule-component section)
               [:div.detail-fields (map #(render-field % currency) fields)])]
    (app.ui.components/collapsible-section title id body)))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: PASS.

git add src/com/getorcha/app/http/documents/view/panel.clj test/com/getorcha/app/http/documents/view/panel_test.clj
git commit -m "feat(panel): section renderer with component dispatch"

Task 6: coverage-view

Files:

(deftest coverage-view-test
  (testing "renders found and not-found items color-independently (icon + text)"
    (let [html (str (hiccup/html (panel/coverage-view
                                  [{:term "Liability cap" :found? false}
                                   {:term "Confidentiality" :found? true :source 7}])))]
      (is (string/includes? html "coverage"))
      (is (string/includes? html "Liability cap"))
      (is (string/includes? html "Not found"))
      (is (string/includes? html "Confidentiality"))
      (is (string/includes? html "Found"))
      ;; found item carries a source chip; not-found does not
      (is (string/includes? html "data-page=\"7\""))))
  (testing "empty coverage renders nothing"
    (is (nil? (panel/coverage-view [])))))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: FAIL — coverage-view unresolved.

(defn coverage-view
  "Renders the per-type expected-terms checklist. Each item shows found/not-found
   with an icon AND text (color-independent), plus a source chip when found.
   Returns nil for empty coverage."
  [coverage]
  (when (seq coverage)
    [:div.coverage-view {:id "section-coverage"}
     [:div.coverage-title "Coverage"]
     [:ul.coverage-list
      (for [{:keys [term found? source]} coverage]
        [:li {:class (str "coverage-item " (if found? "found" "not-found"))}
         [:iconify-icon {:icon (if found? "lucide:check" "lucide:minus")}]
         [:span.coverage-term term]
         [:span.coverage-status (if found? "Found" "Not found")]
         (when found? (source-chip source))])]]))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: PASS.

git add src/com/getorcha/app/http/documents/view/panel.clj test/com/getorcha/app/http/documents/view/panel_test.clj
git commit -m "feat(panel): coverage found/not-found view"

Task 7: render-panel-middle (assemble sections + coverage; omit empty)

Files:

(def ^:private subscription-fixture
  {:type "subscription"
   :currency "EUR"
   :sections
   [{:id "billing" :title "Subscription & Billing"
     :fields [{:label "Recurring fee" :value 2400 :kind :currency :source 2}
              {:label "Billing cycle" :value "Annual" :source 2}
              {:kind :callout :severity :warning :source 5
               :value "Auto-renews for 12 months unless cancelled 90 days prior."}]}
    {:id "service-levels" :title "Service Levels" :fields []}   ;; empty → omitted
    {:id "payment-plan" :title "Payment Schedule"
     :component :payment-schedule
     :rows [["2026-01-01" "€2,400"] ["2027-01-01" "€2,520"]]
     :source 6}]
   :coverage
   [{:term "Liability cap" :found? false}
    {:term "Confidentiality" :found? true :source 7}]})


(deftest render-panel-middle-test
  (let [html (str (hiccup/html (panel/render-panel-middle subscription-fixture {:currency "EUR"})))]
    (testing "populated sections render in order"
      (is (string/includes? html "Subscription &amp; Billing"))
      (is (string/includes? html "Payment Schedule")))
    (testing "empty sections are omitted"
      (is (not (string/includes? html "Service Levels"))))
    (testing "callout, payment schedule, coverage, and source chips all appear"
      (is (string/includes? html "panel-callout"))
      (is (string/includes? html "payment-schedule"))
      (is (string/includes? html "Not found"))
      (is (string/includes? html "data-page=\"6\"")))))

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: FAIL — render-panel-middle unresolved.

(defn render-panel-middle
  "Renders the type-adaptive middle (`:sections`) and the panel-level `:coverage`
   for a document content-model map. `opts` may carry `:currency`. Sections render
   in the order given (the section config orders them upstream); a section with no
   `:component` and no `:fields` is omitted."
  [{:keys [sections coverage] :as _content-model} {:keys [currency] :as _opts}]
  [:div.panel-middle
   (for [section sections
         :when  (or (:component section) (seq (:fields section)))]
     (render-section section currency))
   (coverage-view coverage)])

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: PASS — 7 deftests, 47 assertions, 0 failures, 0 errors. (Verified by transcribing the plan and running it during plan review.)

git add src/com/getorcha/app/http/documents/view/panel.clj test/com/getorcha/app/http/documents/view/panel_test.clj
git commit -m "feat(panel): assemble panel middle (sections + coverage)"

Task 8: Lint clean + full-namespace green

Files:

Run (from orcha/): clj-kondo --lint src/com/getorcha/app/http/documents/view/panel.clj test/com/getorcha/app/http/documents/view/panel_test.clj Expected: linting took Nms, errors: 0, warnings: 0.

Fix every reported item (including info-level), per orcha-clojure-style. Note: the project's .clj-kondo/config.edn is permissive and underscore-prefixed bindings (:as _section, :as _field, :as _content-model, :as _opts) do not warn — so this step is most likely a clean no-op and Step 4's commit won't fire. Keep those :as placeholders as documentation.

Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.view.panel-test]' Expected: PASS, 0 failures, 0 errors.

git add src/com/getorcha/app/http/documents/view/panel.clj test/com/getorcha/app/http/documents/view/panel_test.clj
git commit -m "chore(panel): lint clean"

What this plan deliberately leaves to follow-on plans

These are not in this plan (they depend on the engine above existing) and each gets its own plan:


Self-Review

Spec coverage (Phase 1 renderer-first core):

Placeholder scan: no TBD/TODO; every code step shows complete code; every test step shows the assertions. ✓

Type/name consistency: format-value (Task 1) used by render-field (Task 3); source-chip (Task 2) used by render-field/payment-schedule-component/coverage-view; render-field + payment-schedule-component used by render-section (Task 5); render-section + coverage-view used by render-panel-middle (Task 7). Declaration order matches use (no forward declares). Reused helper names match verbatim source: app.ui.components/collapsible-section [title id content], format-currency [amount currency], format-date [date]. ✓