Skip to content

Commit cd67f32

Browse files
Merge pull request #4 from merchantprotocol/feat/estimate
Estimate: price a new job from past estimates
2 parents e99d69e + 52a872f commit cd67f32

17 files changed

Lines changed: 682 additions & 4 deletions

File tree

‎README.md‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,21 @@ trueup.audit(List.of(Table.file("inv-1050.txt")), result.details.weights);
106106

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

109+
## Estimate
110+
111+
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:
112+
113+
```java
114+
EstimateResult result = trueup.estimate(List.of(Table.file("barndo.tu"), Table.file("01_anderson.csv"), /* … */ Table.file("job_a.txt")));
115+
System.out.println(result.headline);
116+
// job_a.txt: $292,267 (80% range $248,742 – $335,792) from 10 past estimates.
117+
118+
// The next job, with what was learned (no need to send the history again):
119+
trueup.estimate(List.of(Table.file("job_b.txt")), result.details.weights);
120+
```
121+
122+
`estimateStored(fileIds, model)` prices from stored files. One analysis per call.
123+
109124
## Stored files, runs and saved models
110125

111126
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:
@@ -179,7 +194,7 @@ TrueUp.builder()
179194
The tests run in Docker against the live API:
180195

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

‎src/main/java/io/github/merchantprotocol/trueup/Models.java‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,29 @@ public static final class AuditDetails {
102102
public static final class Law { public String scope; public String law; public String held; }
103103
}
104104

105+
/** The answer to an estimate call: the new job priced part by part (findings of kind priced_line), with an 80% range. */
106+
public static final class EstimateResult {
107+
public String analysis;
108+
public String title;
109+
public String headline;
110+
/** total, low, high (the 80% range), categories priced, past estimates, ... */
111+
public Map<String, Double> stats;
112+
public List<Finding> findings;
113+
public EstimateDetails details;
114+
public List<String> inputs;
115+
public String engine;
116+
public String run_id;
117+
}
118+
119+
/** The job's scope and total, and what was learned. */
120+
public static final class EstimateDetails {
121+
public List<JsonObject> scope;
122+
public JsonObject total;
123+
public JsonObject model;
124+
/** The trade and its labeled past estimates: pass back to {@link TrueUp#estimate(List, JsonObject)} with just a request. */
125+
public JsonObject weights;
126+
}
127+
105128
/** A file stored in the team (uploaded through the API or the dashboard). */
106129
public static final class StoredFile {
107130
public String id;

‎src/main/java/io/github/merchantprotocol/trueup/TrueUp.java‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import io.github.merchantprotocol.trueup.Models.Account;
99
import io.github.merchantprotocol.trueup.Models.Answers;
1010
import io.github.merchantprotocol.trueup.Models.AuditResult;
11+
import io.github.merchantprotocol.trueup.Models.EstimateResult;
1112
import io.github.merchantprotocol.trueup.Models.MatchResult;
1213
import io.github.merchantprotocol.trueup.Models.Model;
1314
import io.github.merchantprotocol.trueup.Models.RunDetail;
@@ -220,6 +221,33 @@ public AuditResult auditStored(List<String> fileIds, String model) {
220221
return gson.fromJson(request("POST", "/v1/audit", gson.toJson(body).getBytes(StandardCharsets.UTF_8), "application/json"), AuditResult.class);
221222
}
222223

224+
// ---------------------------------------------------------------- estimate
225+
226+
/**
227+
* Price a new job from past estimates: a domain file for the trade (.tu), at least 3 past estimates in any format,
228+
* and one request describing the new job. One analysis.
229+
*/
230+
public EstimateResult estimate(List<Table> files) {
231+
return estimate(files, null);
232+
}
233+
234+
/** Price a request with {@code weights} (details.weights of an earlier estimate) instead of sending the history. */
235+
public EstimateResult estimate(List<Table> files, JsonObject weights) {
236+
if (files.isEmpty()) throw new TrueUpException.InvalidRequestException("Pass the domain file, past estimates and the request.", 0, "invalid_request", null);
237+
List<String> fields = new ArrayList<>();
238+
for (int i = 0; i < files.size(); i++) fields.add("files");
239+
byte[] json = multipart("/v1/estimate", fields, files, new ReconcileOptions().weights(weights));
240+
return gson.fromJson(new String(json, StandardCharsets.UTF_8), EstimateResult.class);
241+
}
242+
243+
/** Price from files already stored in the team, by id. {@code model} (a saved estimate model id) may be null. */
244+
public EstimateResult estimateStored(List<String> fileIds, String model) {
245+
Map<String, Object> body = new LinkedHashMap<>();
246+
body.put("file_ids", fileIds);
247+
if (model != null) body.put("model", model);
248+
return gson.fromJson(request("POST", "/v1/estimate", gson.toJson(body).getBytes(StandardCharsets.UTF_8), "application/json"), EstimateResult.class);
249+
}
250+
223251
// ---------------------------------------------------------------- stored files, runs, saved models
224252

225253
/** Upload one or more files to the team. Each comes back with its id, rows, columns and roles. */

‎src/test/java/io/github/merchantprotocol/trueup/ApiTest.java‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import static org.junit.jupiter.api.Assumptions.assumeTrue;
1010

1111
import io.github.merchantprotocol.trueup.Models.AuditResult;
12+
import io.github.merchantprotocol.trueup.Models.EstimateResult;
1213
import io.github.merchantprotocol.trueup.Models.MatchResult;
1314
import io.github.merchantprotocol.trueup.Models.ReconcileOptions;
1415
import io.github.merchantprotocol.trueup.Models.RunDetail;
@@ -27,7 +28,7 @@
2728

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

127127
String modelId = tu.createModel(result.run_id, "sdk test");
128128
try {
@@ -170,4 +170,23 @@ void auditSixInvoicesThenOneAgainstTheSavedLaws() {
170170
assertFalse(one.details.model.get("learned").getAsBoolean());
171171
assertEquals("inv-1045.txt", one.findings.get(0).subject);
172172
}
173+
174+
@Test
175+
void estimateANewJobThenTheNextWithTheSavedModel() {
176+
assumeTrue(live(), "needs TRUEUP_API_KEY");
177+
TrueUp tu = TrueUp.builder().build();
178+
List<Table> files = new ArrayList<>();
179+
for (String n : List.of("barndo.tu", "01_anderson.csv", "02_brooks.csv", "03_carter.md", "04_dalton.txt", "05_ellis.json",
180+
"06_foster.tsv", "07_garrison.txt", "08_hayes.csv", "09_iverson.csv", "10_jensen.md", "job_a.txt")) {
181+
files.add(Table.file(FIXTURES.resolve("barndo/" + n)));
182+
}
183+
EstimateResult result = tu.estimate(files);
184+
assertEquals("estimate", result.analysis);
185+
assertEquals(10.0, result.stats.get("past estimates"));
186+
double total = result.stats.get("total");
187+
assertTrue(Math.abs(total - 292267) / 292267 < 0.05, "total " + total);
188+
assertTrue(result.stats.get("low") < total);
189+
EstimateResult next = tu.estimate(List.of(Table.file(FIXTURES.resolve("barndo/job_b.txt"))), result.details.weights);
190+
assertFalse(next.details.model.get("learned").getAsBoolean());
191+
}
173192
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Estimate,Anderson Residence
2+
Date,10/06/2025
3+
Building,30x40 16' eave
4+
Living area (sf),660
5+
Baths,1
6+
Overhead doors,2
7+
Windows,15
8+
Entry doors,4
9+
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."
10+
11+
Description,Qty,Unit,Unit Price,Total
12+
Site prep & grading,1200,SF,2.75,3302.33
13+
Foundation - 4 in reinforced concrete slab,1200,SF,7.05,8459.96
14+
Red iron frame + 26 ga panels,3536,SF,6.53,23096.76
15+
Shell install - frame and sheet,3536,SF,3.34,11803.99
16+
Rough carpentry interior,660,SF,7.04,4645.15
17+
Windows - Pella fiberglass,15,EA,1283.58,19253.75
18+
Premium entry door package,4,EA,2676.55,10706.20
19+
Garage doors installed w/ openers,2,EA,2793.36,5586.71
20+
Batts + vapor barrier,3536,SF,1.27,4502.36
21+
Plumbing (rough-in and trim) - rough-in,1,LS,3853.11,3853.11
22+
Plumbing (rough-in and trim) - trim-out,1,LS,3069.11,3069.11
23+
Wiring and electrical fixtures - rough,1,LS,2582.98,2582.98
24+
Wiring and electrical fixtures - finish,1,LS,2988.55,2988.55
25+
Ductless mini-split system - equipment,1,LS,2568.80,2568.80
26+
Ductless mini-split system - install,1,LS,1934.85,1934.85
27+
Sheetrock,660,SF,5.73,3783.57
28+
"Painting - interior, 2 coats",660,SF,3.35,2211.98
29+
Luxury vinyl plank floors,660,SF,5.28,3487.06
30+
Quartz countertops + custom cabinets,1,LS,26079.75,26079.75
31+
"Finish carpentry - solid core doors, wide casing",660,SF,5.79,3822.79
32+
Premium appliance & fixture allowance,1,LS,18579.51,18579.51
33+
Gutters & downspouts,140,LF,10.82,1514.83
34+
"Porch - covered, concrete floor",480,SF,42.37,20336.81
35+
Drill well + pump,1,LS,14634.43,14634.43
36+
Septic system installed,1,LS,17036.14,17036.14
37+
Building permit + plan review,1200,SF,0.91,1088.57
38+
Dumpster & final clean,1,LS,2949.61,2949.61
39+
Builder margin,1,LS,28446.89,28446.89
40+
TOTAL,,,,252326.55
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
Job: Brooks Residence
2+
Prepared: November 12, 2025
3+
Size: 50' x 80', 16 ft sidewalls; 2360 heated sq ft; 2 bath; 4 roll-up doors; 12 windows; 3 entry doors
4+
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.
5+
6+
Item,Quantity,UOM,Rate,Amount
7+
"Excavation, pad prep and compaction",4000,SF,2.60,10411.89
8+
Foundation - 4 in reinforced concrete slab,4000,SF,7.46,29836.27
9+
Barndo shell kit,8480,SF,5.68,48132.87
10+
Building erection labor & crane,8480,SF,3.42,29019.06
11+
Stud walls & ceiling framing - living quarters,2360,SF,6.37,15032.76
12+
Premium black-frame windows,12,EA,1128.85,13546.15
13+
Custom front door + entries,3,EA,2432.03,7296.10
14+
Garage doors installed w/ openers,4,EA,2466.66,9866.63
15+
Batts + vapor barrier,8480,SF,1.10,9310.84
16+
Plumbing labor & materials,2,EA,6902.28,13804.57
17+
Wiring and electrical fixtures - rough,1,LS,11571.76,11571.76
18+
Wiring and electrical fixtures - finish,1,LS,7883.44,7883.44
19+
Mini splits (3 heads),2360,SF,7.08,16717.62
20+
Subtotal - Shell & site,,,,222429.96
21+
Gypsum board - level 4 finish,2360,SF,5.79,13670.58
22+
Paint walls & ceilings,2360,SF,3.30,7793.90
23+
Stained/polished slab finish,2360,SF,3.49,8247.42
24+
Custom cabinets w/ quartz counters,1,LS,32061.21,32061.21
25+
Solid core doors & craftsman trim,2360,SF,5.97,14084.80
26+
Premium appliance & fixture allowance,1,LS,19297.41,19297.41
27+
Rain gutters installed,260,LF,11.61,3018.91
28+
"Porch - covered, concrete floor",240,SF,38.91,9339.14
29+
Drill well + pump,1,LS,13894.10,13894.10
30+
On-site septic system,1,LS,16994.97,16994.97
31+
Permitting,4000,SF,0.79,3164.22
32+
Construction cleanup / dumpsters,1,LS,2613.18,2613.18
33+
Builder margin,1,LS,51615.95,51615.95
34+
Subtotal - Interior & fees,,,,195795.79
35+
Contract total,,,,418225.75
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Barndominium estimate — Carter Residence
2+
3+
**Date:** 2025-12-09
4+
**Building:** 50 x 80, 12' eave height
5+
**Conditioned SF:** 2,660
6+
**Bathrooms:** 2 · **Overhead doors:** 3 · **Windows:** 15 · **Entry doors:** 2
7+
8+
> 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.
9+
10+
| Scope | Qty | Unit | Amount |
11+
|---|---:|---|---:|
12+
| Clear lot and build building pad | 4,000 | SF | $9,084.51 |
13+
| Monolithic slab foundation | 4,000 | SF | $29,903.46 |
14+
| Barndo shell kit | 7,440 | SF | $46,592.72 |
15+
| Erect steel building | 7,440 | SF | $25,108.24 |
16+
| Rough carpentry interior | 2,660 | SF | $16,676.24 |
17+
| Premium black-frame windows | 15 | EA | $18,076.92 |
18+
| Premium entry door package | 2 | EA | $5,277.61 |
19+
| Overhead doors 12x12 insulated | 3 | EA | $8,168.40 |
20+
| Fiberglass batt insulation | 7,440 | SF | $9,655.22 |
21+
| Plumbing (rough-in and trim) - rough-in | 1 | LS | $8,847.05 |
22+
| Plumbing (rough-in and trim) - trim-out | 1 | LS | $6,222.19 |
23+
| Electric - panel, circuits, lights | 2,660 | SF | $21,558.52 |
24+
| HVAC - ducted heat pump system | 2,660 | SF | $23,792.56 |
25+
| Sheetrock install and mud | 2,660 | SF | $15,276.51 |
26+
| Paint walls & ceilings | 2,660 | SF | $8,001.91 |
27+
| Stained/polished slab finish | 2,660 | SF | $10,117.64 |
28+
| Custom cabinets w/ quartz counters | 1 | LS | $31,248.41 |
29+
| Solid core doors & craftsman trim | 2,660 | SF | $15,285.59 |
30+
| Allowance - stainless appliances, designer fixtures | 1 | LS | $17,558.23 |
31+
| Rain gutters installed | 260 | LF | $2,973.75 |
32+
| Well drilling and pump system | 1 | LS | $15,234.67 |
33+
| Septic system installed | 1 | LS | $17,145.71 |
34+
| Permits & fees | 4,000 | SF | $3,503.65 |
35+
| Dumpster & final clean | 1 | LS | $2,706.90 |
36+
| Contractor fee | 1 | LS | $49,945.92 |
37+
| **Grand total** | | | **$417,962.53** |
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
PROPOSAL — DALTON RESIDENCE
2+
Date: Jan 20, 2026
3+
4+
Scope: 40x50 steel barndominium, 12' walls. 1,300 sf living, balance shop. three bathrooms, two overhead doors, 12 windows, four entry doors.
5+
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.
6+
7+
Dirt work - cut/fill and grade (2,000 sf) ................. $5,141.26
8+
Pour slab on grade incl. rebar (2,000 sf) ................. $16,802.50
9+
Pre-engineered steel building materials (4,320 sf) ........ $24,638.86
10+
Steel erection crew (4,320 sf) ............................ $13,935.63
11+
Stud walls & ceiling framing - living quarters (1,300 sf) ... $9,178.52
12+
Vinyl windows installed ................................... $8,884.23
13+
Exterior entry doors ...................................... $5,738.70
14+
Sectional overhead doors .................................. $5,439.56
15+
R-19/R-30 fiberglass insulation (4,320 sf) ................ $4,730.06
16+
Plumbing - supply, drain, fixtures set .................... $21,973.27
17+
Electrical - 200A service, wiring, devices - rough ........ $5,560.11
18+
Electrical - 200A service, wiring, devices - finish ....... $4,414.93
19+
HVAC - ductless heat pumps (1,300 sf) ..................... $8,287.54
20+
Sheetrock (1,300 sf) ...................................... $7,553.45
21+
Painting - interior, 2 coats (1,300 sf) ................... $3,650.07
22+
Luxury vinyl plank floors (1,300 sf) ...................... $6,863.84
23+
Kitchen cabinets & laminate tops .......................... $15,889.33
24+
Finish carpentry (1,300 sf) ............................... $5,659.92
25+
Fixtures & appliances allowance ........................... $9,639.79
26+
Gutters & downspouts (180 lf) ............................. $2,074.11
27+
Municipal water hookup .................................... $3,713.71
28+
City sewer hookup ......................................... $4,882.41
29+
Permitting (2,000 sf) ..................................... $1,776.85
30+
Cleanup and haul-off ...................................... $2,667.00
31+
Contractor fee ............................................ $26,464.84
32+
33+
TOTAL PROPOSED PRICE ...................................... $225,560.49

0 commit comments

Comments
 (0)