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 Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Improve agent UX for FP&A Excel analysis by adding excel/profile and excel/find, removing excel/summary, renaming the context tool, and rewriting tool descriptions to funnel agents toward the Data Map.
Architecture: Three host-side functions added to functions.clj (profile, find-in-workbook, col-index->letter), wired through sandbox.clj into SCI. Tool descriptions updated in fpna.clj. TDD throughout.
Tech Stack: Clojure, FastExcel (streaming reader), SCI (sandbox), clojure.test
col-index->letter HelperThe excel/find function needs to convert 0-based column indices back to Excel letters (e.g., 0→"A", 27→"AB"). This is the inverse of the existing col-letter->index.
Files:
src/com/getorcha/link/mcp/tools/fpna/excel/functions.cljtest/com/getorcha/link/mcp/tools/fpna/excel/functions_test.cljStep 1: Write the failing test
In functions_test.clj, add after the test-col-letter->index test:
(deftest test-col-index->letter
(let [col-index->letter @#'excel.fn/col-index->letter]
(testing "single-letter columns"
(is (= "A" (col-index->letter 0)))
(is (= "B" (col-index->letter 1)))
(is (= "Z" (col-index->letter 25))))
(testing "double-letter columns"
(is (= "AA" (col-index->letter 26)))
(is (= "AB" (col-index->letter 27)))
(is (= "AZ" (col-index->letter 51)))
(is (= "BA" (col-index->letter 52)))
(is (= "ZZ" (col-index->letter 701))))
(testing "triple-letter columns"
(is (= "AAA" (col-index->letter 702))))))
Step 2: Run test to verify it fails
clj -X:test:silent :nses '[com.getorcha.link.mcp.tools.fpna.excel.functions-test]' 2>&1 | grep -A 3 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: ERROR — col-index->letter doesn't exist.
Step 3: Write minimal implementation
In functions.clj, add after the col-letter->index function (after line 77):
(defn ^:private col-index->letter
"Converts a 0-based column index to Excel column letters. 0=A, 1=B, 25=Z, 26=AA."
^String [^long idx]
(loop [n (inc idx)
acc ""]
(if (pos? n)
(let [r (mod (dec n) 26)]
(recur (quot (dec n) 26) (str (char (+ 65 r)) acc)))
acc)))
Step 4: Run test to verify it passes
clj -X:test:silent :nses '[com.getorcha.link.mcp.tools.fpna.excel.functions-test]' 2>&1 | grep -A 3 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: all tests pass, 0 failures, 0 errors.
Step 5: Commit
git add src/com/getorcha/link/mcp/tools/fpna/excel/functions.clj test/com/getorcha/link/mcp/tools/fpna/excel/functions_test.clj
git commit -m "feat: add col-index->letter helper for Excel column conversion"
excel/find to functions.cljImplement the host-side find-in-workbook function that searches all sheets for a case-insensitive substring match.
Files:
src/com/getorcha/link/mcp/tools/fpna/excel/functions.cljtest/com/getorcha/link/mcp/tools/fpna/excel/functions_test.cljStep 1: Write the failing tests
In functions_test.clj, add before the test-named-ranges-filters-hidden test:
(deftest test-find-in-workbook
(let [bs (create-test-workbook {"P&L" [["Category" "Jan" "Feb"]
["Umsatzerlöse" 100 200]
["Materialaufwand" 50 80]]
"Budget" [["Item" "Amount"]
["Umsatzerlöse (Plan)" 300]
["Miete" 120]]})
wb (workbook-from bs)]
(try
(testing "finds substring across sheets, case-insensitive"
(let [results (excel.fn/find-in-workbook wb "umsatz")]
(is (= 2 (count results)))
(is (= #{"P&L" "Budget"} (set (map :sheet results))))
(is (every? #(re-find #"(?i)umsatz" (:value %)) results))))
(testing "returns correct cell addresses"
(let [results (excel.fn/find-in-workbook wb "Materialaufwand")]
(is (= 1 (count results)))
(is (= "A3" (:cell (first results))))
(is (= "P&L" (:sheet (first results))))))
(testing "skips numeric cells"
(let [results (excel.fn/find-in-workbook wb "100")]
(is (= 0 (count results)))))
(testing "returns empty vec for no matches"
(is (= [] (excel.fn/find-in-workbook wb "nonexistent"))))
(finally (.close wb)))))
(deftest test-find-in-workbook-truncation
(testing "caps results at 100 with truncation marker"
(let [;; Create a sheet with 110 rows all containing "match"
rows (vec (cons ["Header"] (mapv (fn [i] [(str "match-" i)]) (range 110))))
bs (create-test-workbook {"Sheet1" rows})
wb (workbook-from bs)]
(try
(let [results (excel.fn/find-in-workbook wb "match")]
(is (= 101 (count results)))
(is (= true (:truncated (last results)))))
(finally (.close wb))))))
Step 2: Run tests to verify they fail
clj -X:test:silent :nses '[com.getorcha.link.mcp.tools.fpna.excel.functions-test]' 2>&1 | grep -A 3 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: ERROR — find-in-workbook doesn't exist.
Step 3: Write minimal implementation
In functions.clj, add after the read-range function (after line 149). This function needs the col-index->letter helper from Task 1:
(def ^:private find-max-results 100)
(defn find-in-workbook
"Searches all sheets for cells containing `search-str` (case-insensitive substring).
Returns a vector of {:sheet s :cell ref :value v} maps. Caps at 100 results;
if truncated, appends {:truncated true} as the last element."
[^ReadableWorkbook workbook ^String search-str]
(let [needle (.toLowerCase search-str)]
(loop [sheets (iterator-seq (.iterator (.getSheets workbook)))
results (transient [])]
(if-let [^Sheet sheet (first sheets)]
(let [sheet-name (.getName sheet)
results (with-open [stream (.openStream sheet)]
(loop [rows (iterator-seq (.iterator stream))
results results]
(if-let [^Row row (first rows)]
(let [results (loop [col-idx 0
results results]
(if (< col-idx (.getCellCount row))
(let [^Cell cell (.getCell row (int col-idx))
text (when cell
(let [v (util.excel/read-cell-value cell)]
(when (string? v) v)))]
(if (and text (.contains (.toLowerCase text) needle))
(let [results (conj! results
{:sheet sheet-name
:cell (str (col-index->letter col-idx)
(inc (- (.getRowNum row) 1)))
:value text})]
(if (>= (count results) find-max-results)
(reduced results)
(recur (inc col-idx) results)))
(recur (inc col-idx) results)))
results))]
(if (and (not (reduced? results))
(>= (count results) find-max-results))
results
(recur (rest rows)
(if (reduced? results) @results results))))
results)))]
(if (>= (count results) find-max-results)
(persistent! (conj! results {:truncated true}))
(recur (rest sheets) results)))
(persistent! results)))))
Note: FastExcel Row.getRowNum() is 1-based, so the cell address uses (.getRowNum row) directly (no need to add 1).
Step 4: Run tests to verify they pass
clj -X:test:silent :nses '[com.getorcha.link.mcp.tools.fpna.excel.functions-test]' 2>&1 | grep -A 3 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: all tests pass.
Step 5: Commit
git add src/com/getorcha/link/mcp/tools/fpna/excel/functions.clj test/com/getorcha/link/mcp/tools/fpna/excel/functions_test.clj
git commit -m "feat: add excel/find cross-sheet substring search"
excel/profile to functions.cljImplement the host-side profile function that returns a complete sheet profile in one call.
Files:
src/com/getorcha/link/mcp/tools/fpna/excel/functions.cljtest/com/getorcha/link/mcp/tools/fpna/excel/functions_test.cljStep 1: Write the failing tests
In functions_test.clj, add before the test-find-in-workbook test:
(deftest test-profile
(let [;; Build a workbook with merged regions and named ranges
wb-poi (XSSFWorkbook.)
out (ByteArrayOutputStream.)
sh (.createSheet wb-poi "P&L")
_ (.createSheet wb-poi "Budget")]
;; Row 0 (header): 2 cells
(let [r (.createRow sh 0)]
(.setCellValue (.createCell r 0) "Category")
(.setCellValue (.createCell r 1) "Jan"))
;; Rows 1-12: 3 cells each (13 total rows, so sample-rows = rows 2-11 = 10 rows)
(doseq [i (range 1 13)]
(let [r (.createRow sh i)]
(.setCellValue (.createCell r 0) (str "Item" i))
(.setCellValue (.createCell r 1) (double (* i 10)))
(.setCellValue (.createCell r 2) (double (* i 20)))))
;; Merged region
(.addMergedRegion sh (org.apache.poi.ss.util.CellRangeAddress. 0 0 0 1))
;; Named ranges: one scoped to P&L, one to Budget, one workbook-level
(let [n1 (.createName wb-poi)]
(.setNameName n1 "Revenue")
(.setRefersToFormula n1 "'P&L'!$A$2:$A$10"))
(let [n2 (.createName wb-poi)]
(.setNameName n2 "BudgetItems")
(.setRefersToFormula n2 "Budget!$A$1:$A$5")
(.setSheetIndex n2 1))
(let [n3 (.createName wb-poi)]
(.setNameName n3 "GlobalRange")
(.setRefersToFormula n3 "'P&L'!$B$1:$B$10"))
(.write wb-poi out)
(.close wb-poi)
(let [bs (.toByteArray out)
wb (workbook-from bs)
sheet-names (excel.fn/sheets wb)]
(try
(let [result (excel.fn/profile wb bs sheet-names "P&L" 0)]
(testing "row-count and column-count"
(is (= 13 (:row-count result)))
(is (= 3 (:column-count result))))
(testing "headers padded to max width"
(is (= ["Category" "Jan" nil] (:headers result))))
(testing "sample-rows are rows 2-11 (max 10)"
(is (= 10 (count (:sample-rows result))))
(is (= "Item1" (ffirst (:sample-rows result)))))
(testing "merged-regions included"
(is (= 1 (count (:merged-regions result))))
(is (= "A1:B1" (:range (first (:merged-regions result))))))
(testing "named-ranges filtered: includes P&L-scoped and workbook-scoped, excludes Budget-scoped"
(let [names (set (map :name (:named-ranges result)))]
(is (contains? names "Revenue"))
(is (contains? names "GlobalRange"))
(is (not (contains? names "BudgetItems"))))))
(finally (.close wb))))))
(deftest test-profile-empty-sheet
(let [bs (create-test-workbook {"Empty" []})
wb (workbook-from bs)
sheet-names (excel.fn/sheets wb)]
(try
(let [result (excel.fn/profile wb bs sheet-names "Empty" 0)]
(is (= 0 (:row-count result)))
(is (= 0 (:column-count result)))
(is (= [] (:headers result)))
(is (= [] (:sample-rows result)))
(is (= [] (:merged-regions result))))
(finally (.close wb)))))
(deftest test-profile-few-rows
(testing "sample-rows returns only available data rows when < 10"
(let [bs (create-test-workbook {"Sheet1" [["H1" "H2"]
["A" "B"]
["C" "D"]]})
wb (workbook-from bs)
sheet-names (excel.fn/sheets wb)]
(try
(let [result (excel.fn/profile wb bs sheet-names "Sheet1" 0)]
(is (= 2 (count (:sample-rows result))))
(is (= [["A" "B"] ["C" "D"]] (:sample-rows result))))
(finally (.close wb))))))
Step 2: Run tests to verify they fail
clj -X:test:silent :nses '[com.getorcha.link.mcp.tools.fpna.excel.functions-test]' 2>&1 | grep -A 3 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: ERROR — profile doesn't exist.
Step 3: Write minimal implementation
In functions.clj, add after the summary function (after line 59):
(def ^:private profile-sample-rows 10)
(defn profile
"Returns a complete profile of a single sheet.
Combines dimensions, headers, sample rows, merged regions, and named ranges
into one map. `sheet-name` is the sheet name, `sheet-index` is its 0-based
index. `xlsx-bytes` are needed for merged-regions (StAX). `all-sheet-names`
is the full workbook sheet list (for named-range scope resolution)."
[^ReadableWorkbook workbook ^bytes xlsx-bytes all-sheet-names
^String sheet-name ^long sheet-index]
(let [^Sheet sheet (.orElse (.findSheet workbook sheet-name) nil)
_ (when-not sheet
(throw (ex-info (str "Sheet not found: " sheet-name)
{:sheet-name sheet-name})))]
(with-open [stream (.openStream sheet)]
(let [iter (.iterator stream)
first-row (when (.hasNext iter) ^Row (.next iter))
;; Collect sample rows (up to profile-sample-rows after header)
sample (loop [n 0
rows (transient [])
max-cols (if first-row (.getCellCount first-row) 0)]
(if (and (.hasNext iter) (< n profile-sample-rows))
(let [^Row r (.next iter)]
(recur (inc n)
(conj! rows r)
(max max-cols (.getCellCount r))))
;; Continue counting remaining rows + max-cols
(loop [total (+ (if first-row 1 0) n)
max-cols max-cols]
(if (.hasNext iter)
(let [^Row r (.next iter)]
(recur (inc total) (max max-cols (.getCellCount r))))
{:rows (persistent! rows)
:row-count total
:max-cols max-cols}))))
{:keys [rows row-count max-cols]} sample
read-row (fn [^Row row width]
(mapv (fn [col-idx]
(util.excel/read-cell-value
(when (< col-idx (.getCellCount row))
(.getCell row (int col-idx)))))
(range width)))
regions (try
(merged-regions xlsx-bytes sheet-name sheet-index)
(catch Exception _ []))
all-named (named-ranges xlsx-bytes all-sheet-names)
sheet-named (filterv (fn [{:keys [refers-to scope]}]
(or (= scope :workbook)
(= scope sheet-name)
(and (string? refers-to)
(.contains ^String refers-to sheet-name))))
all-named)]
{:row-count row-count
:column-count max-cols
:headers (if first-row (read-row first-row max-cols) [])
:sample-rows (mapv #(read-row % max-cols) rows)
:merged-regions regions
:named-ranges sheet-named}))))
Step 4: Run tests to verify they pass
clj -X:test:silent :nses '[com.getorcha.link.mcp.tools.fpna.excel.functions-test]' 2>&1 | grep -A 3 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: all tests pass.
Step 5: Commit
git add src/com/getorcha/link/mcp/tools/fpna/excel/functions.clj test/com/getorcha/link/mcp/tools/fpna/excel/functions_test.clj
git commit -m "feat: add excel/profile for single-call sheet profiling"
profile and find into Sandbox, Remove summaryUpdate the SCI sandbox to expose excel/profile and excel/find, and remove excel/summary.
Files:
src/com/getorcha/link/mcp/tools/fpna/excel/sandbox.cljtest/com/getorcha/link/mcp/tools/fpna/excel/sandbox_test.cljStep 1: Write the failing tests
In sandbox_test.clj, add after the test-evaluate-loop-over-variable-width-rows test:
(def ^:private profile-bytes
(test-xlsx-bytes {"Revenue" [["Month" "Amount"]
["Jan" 100]
["Feb" 200]]}))
(deftest test-evaluate-profile
(testing "excel/profile returns sheet structure"
(let [result (edn/read-string
(sandbox/evaluate-excel
"(excel/profile \"Revenue\")" profile-bytes))]
(is (map? result))
(is (= 3 (:row-count result)))
(is (= 2 (:column-count result)))
(is (= ["Month" "Amount"] (:headers result)))
(is (= 2 (count (:sample-rows result))))
(is (vector? (:merged-regions result)))
(is (vector? (:named-ranges result))))))
(deftest test-evaluate-find
(testing "excel/find searches across sheets"
(let [bytes (test-xlsx-bytes {"Sheet1" [["Revenue" "Cost"]
["Umsatz" 100]]
"Sheet2" [["Umsatzerlöse" 200]]})
result (edn/read-string
(sandbox/evaluate-excel
"(excel/find \"umsatz\")" bytes))]
(is (vector? result))
(is (= 2 (count result)))
(is (every? #(contains? % :sheet) result))
(is (every? #(contains? % :cell) result))
(is (every? #(contains? % :value) result)))))
(deftest test-evaluate-summary-removed
(testing "excel/summary is no longer available"
(let [result (edn/read-string
(sandbox/evaluate-excel "(excel/summary)" sample-bytes))]
(is (:error result)))))
Step 2: Run tests to verify they fail
clj -X:test:silent :nses '[com.getorcha.link.mcp.tools.fpna.excel.sandbox-test]' 2>&1 | grep -A 3 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: test-evaluate-profile and test-evaluate-find fail (functions not wired), test-evaluate-summary-removed fails (summary still works).
Step 3: Write minimal implementation
In sandbox.clj, make these changes:
excel/summary with excel/profile and excel/find in allowed-symbols (line 56): ;; Replace this line:
excel/sheets excel/summary excel/read excel/merged-regions excel/named-ranges
;; With:
excel/sheets excel/profile excel/read excel/find excel/merged-regions excel/named-ranges
build-sci-opts (lines 66-77), replace the summary binding and add profile + find: (let [sheet-names (excel.functions/sheets workbook)
excel-ns {'sheets (fn [] sheet-names)
'profile (fn [sheet-name]
(let [idx (.indexOf ^java.util.List sheet-names sheet-name)]
(when (= -1 idx)
(throw (ex-info (str "Sheet not found: " sheet-name)
{:sheet-name sheet-name})))
(excel.functions/profile workbook file-bytes sheet-names
sheet-name idx)))
'read (fn
([range-str] (excel.functions/read-range workbook range-str {}))
([range-str opts] (excel.functions/read-range workbook range-str opts)))
'find (fn [search-str]
(excel.functions/find-in-workbook workbook search-str))
'merged-regions (fn [sheet-name]
(let [idx (.indexOf ^java.util.List sheet-names sheet-name)]
(when (= -1 idx)
(throw (ex-info (str "Sheet not found: " sheet-name)
{:sheet-name sheet-name})))
(excel.functions/merged-regions file-bytes sheet-name idx)))
'named-ranges (fn [] (excel.functions/named-ranges file-bytes sheet-names))}]
Step 4: Run tests to verify they pass
clj -X:test:silent :nses '[com.getorcha.link.mcp.tools.fpna.excel.sandbox-test]' 2>&1 | grep -A 3 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: all tests pass. Note that test-evaluate-summary (the old test from before) will now fail since summary is removed — update it:
In sandbox_test.clj, find test-evaluate-summary and delete it (it's superseded by test-evaluate-summary-removed and test-evaluate-profile).
Re-run tests to confirm all pass.
Step 5: Commit
git add src/com/getorcha/link/mcp/tools/fpna/excel/sandbox.clj test/com/getorcha/link/mcp/tools/fpna/excel/sandbox_test.clj
git commit -m "feat: wire excel/profile and excel/find into sandbox, remove excel/summary"
Rename orcha-fpna-context → orcha-fpna-data-map, rewrite all tool descriptions.
Files:
src/com/getorcha/link/mcp/tools/fpna.cljStep 1: No test needed — this is a description-only change
Step 2: Update fpna.clj
Replace the ::context defmethod (lines 10-19):
(defmethod tools/-tool ::context [_]
{:name "orcha-fpna-data-map"
:description "Retrieve the financial data map for a legal entity — a structured index of all known financial data sources (budget, payroll, AR, revenues, cash flow, travel expenses) with file locations, sheet names, and cell ranges. Returns one of three statuses: 'boot' (no data map exists — returns the discovery protocol to create one), 'draft' (partial data map from a previous session — returns protocol + draft for continuation), or 'ready' (complete data map for downstream use). You MUST call this before using orcha-fpna-excel or orcha-fpna-list-files."
:inputSchema {:type "object"
:properties {"legal_entity_id" {:type "string"
:description "UUID of the legal entity. Optional if the identity has access to only one legal entity."
:format "uuid"}}
:required []}
:handler fpna.context/handle-fpna-context
:scope "fpna:read"})
Update ::list-files description (lines 22-38) — prepend the MUST line:
:description "IMPORTANT: You MUST call orcha-fpna-data-map first. If a data map already exists, it tells you where all financial data lives — you may not need to list files at all.
List files in a legal entity's FP&A data directory. Returns file names, sizes, modification dates, and types. Use `file_type` to filter (e.g., 'xlsx'). Use `path` to navigate subdirectories. Use `include_summary` to get Excel sheet names, column headers, and row counts for triage."
Replace ::excel description (lines 65-88) with the new function reference. The full description:
:description "IMPORTANT: You MUST call orcha-fpna-data-map first to check if the data you need has already been mapped. The data map tells you which file, sheet, and cell range contains each financial data point. Do not scan files manually if a data map already exists.
Execute Clojure code to analyze an Excel file. The code runs in a sandboxed environment with these functions available:
**excel/sheets** `()` — Returns vector of sheet names.
`(excel/sheets)` => `[\"Sheet1\" \"Sheet2\"]`
**excel/profile** `(sheet-name)` — Returns a complete profile of a sheet: row-count, column-count (true max width), headers (first row), sample-rows (rows 2-11), merged-regions, and named-ranges scoped to that sheet. Use this to understand a sheet's structure in one call.
`(excel/profile \"Sheet1\")` => `{:row-count 50 :column-count 5 :headers [...] :sample-rows [[...] ...] :merged-regions [...] :named-ranges [...]}`
**excel/read** `(range)` or `(range opts)` — Reads cells. Range uses Excel notation: \"A1\", \"A1:D10\", \"Sheet1!A1:D10\". Always returns a 2D vector, padding short rows with nil.
`(excel/read \"A1\")` => `[[42]]`
`(excel/read \"A1:C2\")` => `[[1 2 3] [4 5 6]]`
With `{:metadata? true}`, values become `{:value v :formula \"...\" :format \"...\"}`.
**excel/find** `(search-string)` — Case-insensitive substring search across all sheets. Returns up to 100 matches as [{:sheet s :cell ref :value v} ...].
`(excel/find \"Revenue\")` => `[{:sheet \"P&L\" :cell \"A5\" :value \"Revenue\"}]`
**excel/merged-regions** `(sheet-name)` — Returns merged cell ranges (no values).
`(excel/merged-regions \"Sheet1\")` => `[{:range \"B1:F1\"} {:range \"A3:A8\"}]`
**excel/named-ranges** `()` — Returns named ranges in the workbook.
`(excel/named-ranges)` => `[{:name \"Revenue\" :refers-to \"Sheet1!$B$2:$B$50\" :scope :workbook}]`
**Available Clojure core:** map, filter, remove, reduce, mapv, filterv, into, get, get-in, assoc, dissoc, update, select-keys, keys, vals, merge, zipmap, group-by, sort-by, frequencies, first, second, last, rest, next, nth, ffirst, take, drop, take-while, drop-while, concat, cons, conj, distinct, flatten, reverse, partition, partition-by, interleave, interpose, count, empty?, not-empty, contains?, some, some?, every?, vector, hash-map, hash-set, set, list, vec, seq, range, str, subs, let, fn, do, ->, ->>, as->, cond->, cond->>, some->, some->>, +, -, *, /, inc, dec, mod, rem, quot, max, min, abs, <, >, <=, >=, =, not=, compare, and, or, not, if, when, when-let, if-let, cond, condp, case, apply, partial, comp, complement, every-pred, identity, re-find, re-matches, re-seq, nil?, string?, number?, integer?, double?, keyword?, map?, vector?, set?, seq?, coll?, boolean?, true?, false?, zero?, pos?, neg?, even?, odd?, Math/floor, Math/ceil, Math/round, Math/pow, clojure.string/split, clojure.string/join, clojure.string/replace, clojure.string/trim, clojure.string/lower-case, clojure.string/upper-case, clojure.string/includes?, clojure.string/starts-with?, clojure.string/ends-with?.
**Not available:** No IO, no Java interop, no def/defn, no loop/recur, no repeat/repeatedly, no atoms. 30s timeout. Max file size 50 MB."
Step 3: Commit
git add src/com/getorcha/link/mcp/tools/fpna.clj
git commit -m "feat: rename context to data-map, rewrite tool descriptions for agent funnel"
summary from functions.clj and Clean UpRemove the summary function from functions.clj and remove or update tests that depend on it.
Files:
src/com/getorcha/link/mcp/tools/fpna/excel/functions.cljtest/com/getorcha/link/mcp/tools/fpna/excel/functions_test.cljStep 1: Remove summary function
In functions.clj, delete the entire summary function (lines 32-59).
Step 2: Update tests
In functions_test.clj:
test-summary (lines 92-113)test-summary-column-count-reflects-max-width (lines 265-276) — this is now covered by test-profileStep 3: Run all tests
clj -X:test:silent :nses '[com.getorcha.link.mcp.tools.fpna.excel.functions-test com.getorcha.link.mcp.tools.fpna.excel.sandbox-test]' 2>&1 | grep -A 3 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: all tests pass.
Step 4: Lint
clj-kondo --lint src test dev
Expected: 0 errors, 0 warnings.
Step 5: Commit
git add src/com/getorcha/link/mcp/tools/fpna/excel/functions.clj test/com/getorcha/link/mcp/tools/fpna/excel/functions_test.clj
git commit -m "chore: remove excel/summary, superseded by excel/profile"
Run the full test suite for both namespaces and lint.
Step 1: Run all FP&A tests
clj -X:test:silent :nses '[com.getorcha.link.mcp.tools.fpna.excel.functions-test com.getorcha.link.mcp.tools.fpna.excel.sandbox-test]' 2>&1 | grep -A 3 -E "(FAIL in|ERROR in|Ran .* tests)"
Expected: all tests pass, 0 failures, 0 errors.
Step 2: Lint
clj-kondo --lint src test dev
Expected: 0 errors, 0 warnings.