Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,21 @@ trueup.audit(List.of(Table.file("inv-1050.txt")), result.details.weights);

`auditStored(fileIds, model)` audits stored files. One analysis per call.

## Estimate

Price a new job from your past estimates. Send a domain file for the trade (a `.tu` file naming the facts to read, what costs scale with, and the cost categories), at least 3 past estimates in any format (CSV, TSV, Markdown, JSON, or text proposals), and one request describing the new job in plain words:

```java
EstimateResult result = trueup.estimate(List.of(Table.file("barndo.tu"), Table.file("01_anderson.csv"), /* … */ Table.file("job_a.txt")));
System.out.println(result.headline);
// job_a.txt: $292,267 (80% range $248,742 – $335,792) from 10 past estimates.

// The next job, with what was learned (no need to send the history again):
trueup.estimate(List.of(Table.file("job_b.txt")), result.details.weights);
```

`estimateStored(fileIds, model)` prices from stored files. One analysis per call.

## Stored files, runs and saved models

Files uploaded to your team stay there (you'll also see them in the dashboard). Runs on stored files are kept, and what a run learned can be saved as a model:
Expand Down Expand Up @@ -179,7 +194,7 @@ TrueUp.builder()
The tests run in Docker against the live API:

```bash
export TRUEUP_API_KEY=tu_live_... # a key for a test team (each run uses 8 analyses)
export TRUEUP_API_KEY=tu_live_... # a key for a test team (each run uses 10 analyses)
just test # or: docker compose run --rm test
```

Expand Down
23 changes: 23 additions & 0 deletions src/main/java/io/github/merchantprotocol/trueup/Models.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,29 @@ public static final class AuditDetails {
public static final class Law { public String scope; public String law; public String held; }
}

/** The answer to an estimate call: the new job priced part by part (findings of kind priced_line), with an 80% range. */
public static final class EstimateResult {
public String analysis;
public String title;
public String headline;
/** total, low, high (the 80% range), categories priced, past estimates, ... */
public Map<String, Double> stats;
public List<Finding> findings;
public EstimateDetails details;
public List<String> inputs;
public String engine;
public String run_id;
}

/** The job's scope and total, and what was learned. */
public static final class EstimateDetails {
public List<JsonObject> scope;
public JsonObject total;
public JsonObject model;
/** The trade and its labeled past estimates: pass back to {@link TrueUp#estimate(List, JsonObject)} with just a request. */
public JsonObject weights;
}

/** A file stored in the team (uploaded through the API or the dashboard). */
public static final class StoredFile {
public String id;
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/io/github/merchantprotocol/trueup/TrueUp.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.github.merchantprotocol.trueup.Models.Account;
import io.github.merchantprotocol.trueup.Models.Answers;
import io.github.merchantprotocol.trueup.Models.AuditResult;
import io.github.merchantprotocol.trueup.Models.EstimateResult;
import io.github.merchantprotocol.trueup.Models.MatchResult;
import io.github.merchantprotocol.trueup.Models.Model;
import io.github.merchantprotocol.trueup.Models.RunDetail;
Expand Down Expand Up @@ -220,6 +221,33 @@ public AuditResult auditStored(List<String> fileIds, String model) {
return gson.fromJson(request("POST", "/v1/audit", gson.toJson(body).getBytes(StandardCharsets.UTF_8), "application/json"), AuditResult.class);
}

// ---------------------------------------------------------------- estimate

/**
* Price a new job from past estimates: a domain file for the trade (.tu), at least 3 past estimates in any format,
* and one request describing the new job. One analysis.
*/
public EstimateResult estimate(List<Table> files) {
return estimate(files, null);
}

/** Price a request with {@code weights} (details.weights of an earlier estimate) instead of sending the history. */
public EstimateResult estimate(List<Table> files, JsonObject weights) {
if (files.isEmpty()) throw new TrueUpException.InvalidRequestException("Pass the domain file, past estimates and the request.", 0, "invalid_request", null);
List<String> fields = new ArrayList<>();
for (int i = 0; i < files.size(); i++) fields.add("files");
byte[] json = multipart("/v1/estimate", fields, files, new ReconcileOptions().weights(weights));
return gson.fromJson(new String(json, StandardCharsets.UTF_8), EstimateResult.class);
}

/** Price from files already stored in the team, by id. {@code model} (a saved estimate model id) may be null. */
public EstimateResult estimateStored(List<String> fileIds, String model) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("file_ids", fileIds);
if (model != null) body.put("model", model);
return gson.fromJson(request("POST", "/v1/estimate", gson.toJson(body).getBytes(StandardCharsets.UTF_8), "application/json"), EstimateResult.class);
}

// ---------------------------------------------------------------- stored files, runs, saved models

/** Upload one or more files to the team. Each comes back with its id, rows, columns and roles. */
Expand Down
25 changes: 22 additions & 3 deletions src/test/java/io/github/merchantprotocol/trueup/ApiTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import io.github.merchantprotocol.trueup.Models.AuditResult;
import io.github.merchantprotocol.trueup.Models.EstimateResult;
import io.github.merchantprotocol.trueup.Models.MatchResult;
import io.github.merchantprotocol.trueup.Models.ReconcileOptions;
import io.github.merchantprotocol.trueup.Models.RunDetail;
Expand All @@ -27,7 +28,7 @@

/**
* Integration tests against the live TrueUp API. Need TRUEUP_API_KEY (and optionally TRUEUP_BASE_URL).
* Each full run uses 8 analyses. Run in Docker: `just test` (or `docker compose run --rm test`).
* Each full run uses 10 analyses. Run in Docker: `just test` (or `docker compose run --rm test`).
*/
class ApiTest {
private static final Path FIXTURES = Path.of("src/test/resources/fixtures");
Expand Down Expand Up @@ -121,8 +122,7 @@ void storedFilesRunsAndModels() throws IOException {
assertEquals(7, run.result.stats.get("paired"));
RunPage page = tu.listRuns(1, null);
assertEquals(1, page.runs.size());
assertTrue(page.has_more);
assertNotEquals(page.runs.get(0).id, tu.listRuns(1, page.runs.get(0).id).runs.get(0).id);
if (page.has_more) assertNotEquals(page.runs.get(0).id, tu.listRuns(1, page.runs.get(0).id).runs.get(0).id);

String modelId = tu.createModel(result.run_id, "sdk test");
try {
Expand Down Expand Up @@ -170,4 +170,23 @@ void auditSixInvoicesThenOneAgainstTheSavedLaws() {
assertFalse(one.details.model.get("learned").getAsBoolean());
assertEquals("inv-1045.txt", one.findings.get(0).subject);
}

@Test
void estimateANewJobThenTheNextWithTheSavedModel() {
assumeTrue(live(), "needs TRUEUP_API_KEY");
TrueUp tu = TrueUp.builder().build();
List<Table> files = new ArrayList<>();
for (String n : List.of("barndo.tu", "01_anderson.csv", "02_brooks.csv", "03_carter.md", "04_dalton.txt", "05_ellis.json",
"06_foster.tsv", "07_garrison.txt", "08_hayes.csv", "09_iverson.csv", "10_jensen.md", "job_a.txt")) {
files.add(Table.file(FIXTURES.resolve("barndo/" + n)));
}
EstimateResult result = tu.estimate(files);
assertEquals("estimate", result.analysis);
assertEquals(10.0, result.stats.get("past estimates"));
double total = result.stats.get("total");
assertTrue(Math.abs(total - 292267) / 292267 < 0.05, "total " + total);
assertTrue(result.stats.get("low") < total);
EstimateResult next = tu.estimate(List.of(Table.file(FIXTURES.resolve("barndo/job_b.txt"))), result.details.weights);
assertFalse(next.details.model.get("learned").getAsBoolean());
}
}
40 changes: 40 additions & 0 deletions src/test/resources/fixtures/barndo/01_anderson.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
Estimate,Anderson Residence
Date,10/06/2025
Building,30x40 16' eave
Living area (sf),660
Baths,1
Overhead doors,2
Windows,15
Entry doors,4
Notes,"Fiberglass batt insulation in walls and ceiling. Ductless mini-split heat pumps. LVP flooring in the living area. Premium finish package: custom cabinets, quartz counters, fiberglass windows. Private well and septic system. 480 sq ft covered porch."

Description,Qty,Unit,Unit Price,Total
Site prep & grading,1200,SF,2.75,3302.33
Foundation - 4 in reinforced concrete slab,1200,SF,7.05,8459.96
Red iron frame + 26 ga panels,3536,SF,6.53,23096.76
Shell install - frame and sheet,3536,SF,3.34,11803.99
Rough carpentry interior,660,SF,7.04,4645.15
Windows - Pella fiberglass,15,EA,1283.58,19253.75
Premium entry door package,4,EA,2676.55,10706.20
Garage doors installed w/ openers,2,EA,2793.36,5586.71
Batts + vapor barrier,3536,SF,1.27,4502.36
Plumbing (rough-in and trim) - rough-in,1,LS,3853.11,3853.11
Plumbing (rough-in and trim) - trim-out,1,LS,3069.11,3069.11
Wiring and electrical fixtures - rough,1,LS,2582.98,2582.98
Wiring and electrical fixtures - finish,1,LS,2988.55,2988.55
Ductless mini-split system - equipment,1,LS,2568.80,2568.80
Ductless mini-split system - install,1,LS,1934.85,1934.85
Sheetrock,660,SF,5.73,3783.57
"Painting - interior, 2 coats",660,SF,3.35,2211.98
Luxury vinyl plank floors,660,SF,5.28,3487.06
Quartz countertops + custom cabinets,1,LS,26079.75,26079.75
"Finish carpentry - solid core doors, wide casing",660,SF,5.79,3822.79
Premium appliance & fixture allowance,1,LS,18579.51,18579.51
Gutters & downspouts,140,LF,10.82,1514.83
"Porch - covered, concrete floor",480,SF,42.37,20336.81
Drill well + pump,1,LS,14634.43,14634.43
Septic system installed,1,LS,17036.14,17036.14
Building permit + plan review,1200,SF,0.91,1088.57
Dumpster & final clean,1,LS,2949.61,2949.61
Builder margin,1,LS,28446.89,28446.89
TOTAL,,,,252326.55
35 changes: 35 additions & 0 deletions src/test/resources/fixtures/barndo/02_brooks.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Job: Brooks Residence
Prepared: November 12, 2025
Size: 50' x 80', 16 ft sidewalls; 2360 heated sq ft; 2 bath; 4 roll-up doors; 12 windows; 3 entry doors
Fiberglass batt insulation in walls and ceiling. Ductless mini-split heat pumps. Polished concrete floors in the living area. Premium finish package: custom cabinets, quartz counters, fiberglass windows. Private well and septic system. 240 sq ft covered porch.

Item,Quantity,UOM,Rate,Amount
"Excavation, pad prep and compaction",4000,SF,2.60,10411.89
Foundation - 4 in reinforced concrete slab,4000,SF,7.46,29836.27
Barndo shell kit,8480,SF,5.68,48132.87
Building erection labor & crane,8480,SF,3.42,29019.06
Stud walls & ceiling framing - living quarters,2360,SF,6.37,15032.76
Premium black-frame windows,12,EA,1128.85,13546.15
Custom front door + entries,3,EA,2432.03,7296.10
Garage doors installed w/ openers,4,EA,2466.66,9866.63
Batts + vapor barrier,8480,SF,1.10,9310.84
Plumbing labor & materials,2,EA,6902.28,13804.57
Wiring and electrical fixtures - rough,1,LS,11571.76,11571.76
Wiring and electrical fixtures - finish,1,LS,7883.44,7883.44
Mini splits (3 heads),2360,SF,7.08,16717.62
Subtotal - Shell & site,,,,222429.96
Gypsum board - level 4 finish,2360,SF,5.79,13670.58
Paint walls & ceilings,2360,SF,3.30,7793.90
Stained/polished slab finish,2360,SF,3.49,8247.42
Custom cabinets w/ quartz counters,1,LS,32061.21,32061.21
Solid core doors & craftsman trim,2360,SF,5.97,14084.80
Premium appliance & fixture allowance,1,LS,19297.41,19297.41
Rain gutters installed,260,LF,11.61,3018.91
"Porch - covered, concrete floor",240,SF,38.91,9339.14
Drill well + pump,1,LS,13894.10,13894.10
On-site septic system,1,LS,16994.97,16994.97
Permitting,4000,SF,0.79,3164.22
Construction cleanup / dumpsters,1,LS,2613.18,2613.18
Builder margin,1,LS,51615.95,51615.95
Subtotal - Interior & fees,,,,195795.79
Contract total,,,,418225.75
37 changes: 37 additions & 0 deletions src/test/resources/fixtures/barndo/03_carter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Barndominium estimate — Carter Residence

**Date:** 2025-12-09
**Building:** 50 x 80, 12' eave height
**Conditioned SF:** 2,660
**Bathrooms:** 2 · **Overhead doors:** 3 · **Windows:** 15 · **Entry doors:** 2

> Fiberglass batt insulation in walls and ceiling. Ducted heat pump for heating and cooling. Polished concrete floors in the living area. Premium finish package: custom cabinets, quartz counters, fiberglass windows. Private well and septic system. No porch.

| Scope | Qty | Unit | Amount |
|---|---:|---|---:|
| Clear lot and build building pad | 4,000 | SF | $9,084.51 |
| Monolithic slab foundation | 4,000 | SF | $29,903.46 |
| Barndo shell kit | 7,440 | SF | $46,592.72 |
| Erect steel building | 7,440 | SF | $25,108.24 |
| Rough carpentry interior | 2,660 | SF | $16,676.24 |
| Premium black-frame windows | 15 | EA | $18,076.92 |
| Premium entry door package | 2 | EA | $5,277.61 |
| Overhead doors 12x12 insulated | 3 | EA | $8,168.40 |
| Fiberglass batt insulation | 7,440 | SF | $9,655.22 |
| Plumbing (rough-in and trim) - rough-in | 1 | LS | $8,847.05 |
| Plumbing (rough-in and trim) - trim-out | 1 | LS | $6,222.19 |
| Electric - panel, circuits, lights | 2,660 | SF | $21,558.52 |
| HVAC - ducted heat pump system | 2,660 | SF | $23,792.56 |
| Sheetrock install and mud | 2,660 | SF | $15,276.51 |
| Paint walls & ceilings | 2,660 | SF | $8,001.91 |
| Stained/polished slab finish | 2,660 | SF | $10,117.64 |
| Custom cabinets w/ quartz counters | 1 | LS | $31,248.41 |
| Solid core doors & craftsman trim | 2,660 | SF | $15,285.59 |
| Allowance - stainless appliances, designer fixtures | 1 | LS | $17,558.23 |
| Rain gutters installed | 260 | LF | $2,973.75 |
| Well drilling and pump system | 1 | LS | $15,234.67 |
| Septic system installed | 1 | LS | $17,145.71 |
| Permits & fees | 4,000 | SF | $3,503.65 |
| Dumpster & final clean | 1 | LS | $2,706.90 |
| Contractor fee | 1 | LS | $49,945.92 |
| **Grand total** | | | **$417,962.53** |
33 changes: 33 additions & 0 deletions src/test/resources/fixtures/barndo/04_dalton.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
PROPOSAL — DALTON RESIDENCE
Date: Jan 20, 2026

Scope: 40x50 steel barndominium, 12' walls. 1,300 sf living, balance shop. three bathrooms, two overhead doors, 12 windows, four entry doors.
Fiberglass batt insulation in walls and ceiling. Ductless mini-split heat pumps. LVP flooring in the living area. Standard builder-grade finishes, laminate counters. City water and city sewer available at the lot. No porch.

Dirt work - cut/fill and grade (2,000 sf) ................. $5,141.26
Pour slab on grade incl. rebar (2,000 sf) ................. $16,802.50
Pre-engineered steel building materials (4,320 sf) ........ $24,638.86
Steel erection crew (4,320 sf) ............................ $13,935.63
Stud walls & ceiling framing - living quarters (1,300 sf) ... $9,178.52
Vinyl windows installed ................................... $8,884.23
Exterior entry doors ...................................... $5,738.70
Sectional overhead doors .................................. $5,439.56
R-19/R-30 fiberglass insulation (4,320 sf) ................ $4,730.06
Plumbing - supply, drain, fixtures set .................... $21,973.27
Electrical - 200A service, wiring, devices - rough ........ $5,560.11
Electrical - 200A service, wiring, devices - finish ....... $4,414.93
HVAC - ductless heat pumps (1,300 sf) ..................... $8,287.54
Sheetrock (1,300 sf) ...................................... $7,553.45
Painting - interior, 2 coats (1,300 sf) ................... $3,650.07
Luxury vinyl plank floors (1,300 sf) ...................... $6,863.84
Kitchen cabinets & laminate tops .......................... $15,889.33
Finish carpentry (1,300 sf) ............................... $5,659.92
Fixtures & appliances allowance ........................... $9,639.79
Gutters & downspouts (180 lf) ............................. $2,074.11
Municipal water hookup .................................... $3,713.71
City sewer hookup ......................................... $4,882.41
Permitting (2,000 sf) ..................................... $1,776.85
Cleanup and haul-off ...................................... $2,667.00
Contractor fee ............................................ $26,464.84

TOTAL PROPOSED PRICE ...................................... $225,560.49
Loading
Loading