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.
src/com/getorcha/app/http/documents/view/panel.clj — the generic renderer (this plan's only source file).test/com/getorcha/app/http/documents/view/panel_test.clj — all tests for it.com.getorcha.app.ui.components — collapsible-section, format-currency, format-date.Public API this namespace exposes (used later by Phase 1b integration): render-panel-middle, coverage-view, source-chip. Everything else is ^:private.
format-valueFiles:
Create: src/com/getorcha/app/http/documents/view/panel.clj
Test: test/com/getorcha/app/http/documents/view/panel_test.clj
Step 1: Write the failing test
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"
source-chipFiles:
Modify: src/com/getorcha/app/http/documents/view/panel.clj
Test: test/com/getorcha/app/http/documents/view/panel_test.clj
Step 1: Write the failing test (append to the test ns)
(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.
panel.clj)(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"
render-field (scalar, callout, table)Files:
Modify: src/com/getorcha/app/http/documents/view/panel.clj
Test: test/com/getorcha/app/http/documents/view/panel_test.clj
Step 1: Write the failing test (append)
(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.
source-chip)(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)"
payment-schedule-componentFiles:
Modify: src/com/getorcha/app/http/documents/view/panel.clj
Test: test/com/getorcha/app/http/documents/view/panel_test.clj
Step 1: Write the failing test (append)
(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.
render-field)(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"
render-section (component dispatch + collapsible wrapper)Files:
Modify: src/com/getorcha/app/http/documents/view/panel.clj
Test: test/com/getorcha/app/http/documents/view/panel_test.clj
Step 1: Write the failing test (append)
(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 & 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.
payment-schedule-component)(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"
coverage-viewFiles:
Modify: src/com/getorcha/app/http/documents/view/panel.clj
Test: test/com/getorcha/app/http/documents/view/panel_test.clj
Step 1: Write the failing test (append)
(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.
render-section)(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"
render-panel-middle (assemble sections + coverage; omit empty)Files:
Modify: src/com/getorcha/app/http/documents/view/panel.clj
Test: test/com/getorcha/app/http/documents/view/panel_test.clj
Step 1: Write the failing test (append; this is the fixture-driven snapshot test)
(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 & 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.
coverage-view)(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)"
Files:
Possibly modify: src/com/getorcha/app/http/documents/view/panel.clj (lint fixes only)
Step 1: Run the linter
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"
These are not in this plan (they depend on the engine above existing) and each gets its own plan:
render-panel-middle into the contract panel; add the per-type section-bucket config for the 11 contract sub-types; reorder the contract spine (promote Parties, add a new Term & Renewal section, replace Financial+Scope with the type-adaptive middle, move Validation down); add auto-hide for Parties / Term & Renewal / Legal / Obligations; rework the coupled section-tabs-for-type tab strip + scroll-spy + section ids; remove the misleading all-green contract-risk-signals-box and surface coverage instead.#invoice-frame/#email-frame; whether mutating the document frame's #page=N&view=FitH fragment + reload actually re-navigates across target browsers, vs. PDF.js). Exit criterion: the page visibly changes in a loaded viewer. Then wire source-chip clicks to it.Spec coverage (Phase 1 renderer-first core):
:kind renderer (text/number/currency/date/percent/boolean/table) → Tasks 1, 3. ✓:callout interpretation field kind → Task 3. ✓:component dispatch + payment-schedule per-type component → Tasks 4, 5. ✓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]. ✓