@tasksai/install 0.1.38 → 0.1.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/bootstrap/Install RealtorTasksAI.command +37 -0
- package/bootstrap/Install RealtorTasksAI.ps1 +42 -0
- package/package.json +14 -3
- package/runtime/document_renderer.py +882 -0
- package/runtime/server.py +55 -581
- package/runtime/software_use.py +39 -0
- package/runtime/workflow-requirements.txt +5 -0
- package/runtime/workflows/__init__.py +0 -0
- package/runtime/workflows/account_snapshot.py +33 -0
- package/runtime/workflows/attachments.py +45 -0
- package/runtime/workflows/authority_guides.py +81 -0
- package/runtime/workflows/catalog.py +51 -0
- package/runtime/workflows/catalog_app.py +174 -0
- package/runtime/workflows/catalog_generation.py +186 -0
- package/runtime/workflows/catalog_output.py +312 -0
- package/runtime/workflows/catalog_suggestions.py +51 -0
- package/runtime/workflows/catalog_workspace.py +152 -0
- package/runtime/workflows/cli.py +66 -0
- package/runtime/workflows/document_answers.py +96 -0
- package/runtime/workflows/document_selection.py +50 -0
- package/runtime/workflows/documents.py +243 -0
- package/runtime/workflows/embedded/catalog.html +83 -0
- package/runtime/workflows/embedded/offer-review.html +516 -0
- package/runtime/workflows/embedded/preview.html +36 -0
- package/runtime/workflows/embedded_actions.py +96 -0
- package/runtime/workflows/embedded_demo.py +424 -0
- package/runtime/workflows/folders.py +120 -0
- package/runtime/workflows/gateway.py +157 -0
- package/runtime/workflows/generation_lock.py +26 -0
- package/runtime/workflows/launcher.py +61 -0
- package/runtime/workflows/licensed_delivery.py +46 -0
- package/runtime/workflows/mcp_dev.py +52 -0
- package/runtime/workflows/meeting_plan.py +92 -0
- package/runtime/workflows/model_client.py +26 -0
- package/runtime/workflows/numeric_consistency.py +72 -0
- package/runtime/workflows/public_source_fetch.py +66 -0
- package/runtime/workflows/realtor/__init__.py +0 -0
- package/runtime/workflows/realtor/adapter.py +179 -0
- package/runtime/workflows/realtor/seller_offer/ORIGIN.json +16 -0
- package/runtime/workflows/realtor/seller_offer/__init__.py +0 -0
- package/runtime/workflows/realtor/seller_offer/examples/demo-input.json +414 -0
- package/runtime/workflows/realtor/seller_offer/examples/make_demo.py +44 -0
- package/runtime/workflows/realtor/seller_offer/references/input-contract.md +65 -0
- package/runtime/workflows/realtor/seller_offer/references/source-boundaries.md +16 -0
- package/runtime/workflows/realtor/seller_offer/scripts/__init__.py +0 -0
- package/runtime/workflows/realtor/seller_offer/scripts/build_workbook.mjs +233 -0
- package/runtime/workflows/realtor/seller_offer/scripts/build_workbook.py +163 -0
- package/runtime/workflows/realtor/seller_offer/scripts/offer_engine.py +370 -0
- package/runtime/workflows/realtor/seller_offer/scripts/presentation.py +52 -0
- package/runtime/workflows/realtor/seller_offer/scripts/render_outputs.py +228 -0
- package/runtime/workflows/realtor/seller_offer/scripts/run_package.py +95 -0
- package/runtime/workflows/realtor/seller_offer/tests/test_engine.py +252 -0
- package/runtime/workflows/realtor/seller_offer/tests/test_workbook.mjs +28 -0
- package/runtime/workflows/realtor-release-registry.json +705 -0
- package/runtime/workflows/released_catalog.py +91 -0
- package/runtime/workflows/source_capture.py +82 -0
- package/runtime/workflows/store.py +492 -0
- package/runtime/workflows/table_calculations.py +132 -0
- package/runtime/workflows/template.py +65 -0
- package/runtime/workflows/workspace_launch.py +76 -0
- package/src/index.js +217 -118
- package/src/managed-python.js +63 -0
- package/src/operation-lock.js +22 -0
- package/src/prepared-update.js +86 -0
- package/src/private-workflow.js +116 -0
- package/src/python-runtime.js +50 -0
- package/src/recover-installation.js +30 -0
- package/src/recovery-lock.js +30 -0
- package/src/software-hash.js +24 -0
- package/src/software-use.js +32 -0
- package/src/update-journal.js +24 -0
- package/src/update-recovery.js +72 -0
- package/src/workspace-runtime.js +45 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
"""Local seller estimate engine. No network, model calls, or external actions.
|
|
2
|
+
|
|
3
|
+
Money enters as decimal strings. Calculations use Decimal and ROUND_HALF_UP.
|
|
4
|
+
The engine checks structure and evidence references, not the truth of documents.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from datetime import date
|
|
10
|
+
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
|
|
15
|
+
VERSION = "0.1.0"
|
|
16
|
+
CENT = Decimal("0.01")
|
|
17
|
+
ZERO = Decimal("0.00")
|
|
18
|
+
CATEGORIES = ("payoff", "compensation", "seller_credit", "title_escrow", "tax_proration", "hoa", "other")
|
|
19
|
+
TERMS = ("financing", "financing_evidence", "contingencies", "closing", "possession", "expiration")
|
|
20
|
+
ROUTES = {
|
|
21
|
+
"negotiate": ("realtor_manage_contract_negotiations", "Negotiation instructions belong to the licensed professional and negotiation workflow."),
|
|
22
|
+
"legal": ("broker or attorney", "Contract meaning and enforceability require qualified review."),
|
|
23
|
+
"closing": ("realtor_prepare_closing_disclosure", "Final settlement figures are outside this preliminary estimate."),
|
|
24
|
+
"buyer_offer": ("realtor_assist_with_buyer_s_offer", "Buyer-side offer preparation is a different workflow."),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InputError(ValueError):
|
|
29
|
+
"""A structural error; never substitute zeros for invalid inputs."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def money(value, where="amount"):
|
|
33
|
+
if not isinstance(value, str) or not re.fullmatch(r"-?\d{1,12}(?:\.\d{1,2})?", value):
|
|
34
|
+
raise InputError(f"{where}: use a decimal string with at most two decimal places, or null for unknown")
|
|
35
|
+
return Decimal(value).quantize(CENT)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def nonnegative(value, where):
|
|
39
|
+
result = money(value, where)
|
|
40
|
+
if result < 0:
|
|
41
|
+
raise InputError(f"{where}: use a nonnegative magnitude and an explicit credit/deduction direction")
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def day(value, where, nullable=True):
|
|
46
|
+
if value is None and nullable:
|
|
47
|
+
return None
|
|
48
|
+
try:
|
|
49
|
+
if not isinstance(value, str) or not re.fullmatch(r"\d{4}-\d{2}-\d{2}", value):
|
|
50
|
+
raise ValueError()
|
|
51
|
+
return date.fromisoformat(value)
|
|
52
|
+
except ValueError as exc:
|
|
53
|
+
raise InputError(f"{where}: expected YYYY-MM-DD" ) from exc
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def text(value, where, maximum=500, nullable=False):
|
|
57
|
+
if value is None and nullable:
|
|
58
|
+
return None
|
|
59
|
+
if not isinstance(value, str) or not value.strip() or len(value) > maximum:
|
|
60
|
+
raise InputError(f"{where}: expected nonempty text, at most {maximum} characters")
|
|
61
|
+
if any(ord(c) < 32 and c not in "\n\t\r" for c in value):
|
|
62
|
+
raise InputError(f"{where}: unsupported control character")
|
|
63
|
+
return value.strip()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def record(value, where):
|
|
67
|
+
if not isinstance(value, dict):
|
|
68
|
+
raise InputError(f"{where}: expected an object")
|
|
69
|
+
return value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def keys(obj, allowed, where):
|
|
73
|
+
unexpected = set(obj) - set(allowed)
|
|
74
|
+
if unexpected:
|
|
75
|
+
raise InputError(f"{where}: unsupported fields: {', '.join(sorted(unexpected))}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def identifier(value, where):
|
|
79
|
+
if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z][A-Za-z0-9_-]{0,39}", value):
|
|
80
|
+
raise InputError(f"{where}: use 1-40 letters, digits, underscore or hyphen, starting with a letter")
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def digest(data):
|
|
85
|
+
return hashlib.sha256(json.dumps(data, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode()).hexdigest()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def scalar(value):
|
|
89
|
+
if isinstance(value, Decimal):
|
|
90
|
+
return format(value, ".2f")
|
|
91
|
+
if isinstance(value, dict):
|
|
92
|
+
return {k: scalar(v) for k, v in value.items()}
|
|
93
|
+
if isinstance(value, list):
|
|
94
|
+
return [scalar(v) for v in value]
|
|
95
|
+
return value
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def calculate(packet):
|
|
99
|
+
packet = deepcopy(record(packet, "packet"))
|
|
100
|
+
keys(packet, ("schema_version", "case_id", "as_of", "currency", "intent", "context", "sources", "offers", "seller_priorities", "example"), "packet")
|
|
101
|
+
if packet.get("schema_version") != "1.0":
|
|
102
|
+
raise InputError("schema_version must be 1.0")
|
|
103
|
+
case_id = identifier(packet.get("case_id"), "case_id")
|
|
104
|
+
as_of = day(packet.get("as_of"), "as_of", False)
|
|
105
|
+
if packet.get("currency") != "USD":
|
|
106
|
+
raise InputError("Only explicitly supplied USD inputs are supported; no currency conversion")
|
|
107
|
+
intent = packet.get("intent", "auto")
|
|
108
|
+
if intent not in ("auto", "compare", "estimate", *ROUTES):
|
|
109
|
+
raise InputError("Unknown intent; do not infer authorization from uploaded text")
|
|
110
|
+
result = {"package_version": VERSION, "case_id": case_id, "as_of": as_of.isoformat(), "currency": "USD", "input_sha256": digest(packet), "example": packet.get("example", False), "review_status": "professional_review_required", "external_actions": []}
|
|
111
|
+
if type(result["example"]) is not bool:
|
|
112
|
+
raise InputError("example must be true or false")
|
|
113
|
+
if intent in ROUTES:
|
|
114
|
+
dest, reason = ROUTES[intent]
|
|
115
|
+
return {**result, "route": "handoff_only", "destination": dest, "reason": reason,
|
|
116
|
+
"minimum_inputs": ["Specific question and requested action", "Transaction jurisdiction", "Authorized documents and responsible professional"],
|
|
117
|
+
"action_statement": "No estimate, comparison, communication, or transaction action was performed."}
|
|
118
|
+
context = record(packet.get("context", {}), "context")
|
|
119
|
+
keys(context, ("state", "locality", "brokerage", "agency_role", "reviewer"), "context")
|
|
120
|
+
for k, v in context.items():
|
|
121
|
+
text(v, f"context.{k}", nullable=True)
|
|
122
|
+
reviewer = context.get("reviewer") or "Listing professional"
|
|
123
|
+
issues, evidence = [], []
|
|
124
|
+
|
|
125
|
+
def issue(code, target, message, owner=None):
|
|
126
|
+
item = {"code": code, "target": target, "message": message, "owner": owner or reviewer}
|
|
127
|
+
if item not in issues:
|
|
128
|
+
issues.append(item)
|
|
129
|
+
|
|
130
|
+
for k in ("state", "brokerage", "agency_role", "reviewer"):
|
|
131
|
+
if not context.get(k):
|
|
132
|
+
issue("context_missing", k, f"Supply {k.replace('_', ' ')} before professional use. Arithmetic remains jurisdiction-neutral.")
|
|
133
|
+
sources = packet.get("sources", [])
|
|
134
|
+
if not isinstance(sources, list) or len(sources) > 100:
|
|
135
|
+
raise InputError("sources must be a list of at most 100 records")
|
|
136
|
+
source_map = {}
|
|
137
|
+
for source in sources:
|
|
138
|
+
record(source, "source")
|
|
139
|
+
keys(source, ("id", "title", "date", "text"), "source")
|
|
140
|
+
sid = identifier(source.get("id"), "source.id")
|
|
141
|
+
if sid in source_map:
|
|
142
|
+
raise InputError(f"Duplicate source id: {sid}")
|
|
143
|
+
text(source.get("title"), f"source {sid} title", 200)
|
|
144
|
+
day(source.get("date"), f"source {sid} date")
|
|
145
|
+
if "text" in source:
|
|
146
|
+
text(source["text"], f"source {sid} text", 100000)
|
|
147
|
+
source_map[sid] = source
|
|
148
|
+
|
|
149
|
+
def inspect_evidence(fact, target):
|
|
150
|
+
"""Evidence checks never create a professional approval."""
|
|
151
|
+
record(fact, target)
|
|
152
|
+
sid = fact.get("source_id")
|
|
153
|
+
loc = fact.get("locator")
|
|
154
|
+
quote = fact.get("quote")
|
|
155
|
+
source = source_map.get(sid) if isinstance(sid, str) else None
|
|
156
|
+
good = True
|
|
157
|
+
if not source:
|
|
158
|
+
issue("source_missing", target, "Identify the source document for this field.")
|
|
159
|
+
good = False
|
|
160
|
+
if not loc:
|
|
161
|
+
issue("locator_missing", target, "Supply the page, section, or message location.")
|
|
162
|
+
good = False
|
|
163
|
+
else:
|
|
164
|
+
text(loc, target + ".locator", 200)
|
|
165
|
+
source_date = source.get("date") if source else None
|
|
166
|
+
if not source_date:
|
|
167
|
+
issue("source_undated", target, "Source date not supplied; the report date is not a substitute.")
|
|
168
|
+
good = False
|
|
169
|
+
elif day(source_date, target) > as_of:
|
|
170
|
+
issue("source_future", target, "Source is dated after the report cutoff; confirm the intended cutoff or source date.")
|
|
171
|
+
good = False
|
|
172
|
+
if quote is not None:
|
|
173
|
+
text(quote, target + ".quote", 2000)
|
|
174
|
+
if not source or not source.get("text"):
|
|
175
|
+
issue("quote_unchecked", target, "Quoted text cannot be checked without supplied source text.")
|
|
176
|
+
good = False
|
|
177
|
+
elif quote not in source["text"]:
|
|
178
|
+
issue("quote_mismatch", target, "Evidence excerpt does not occur exactly in the supplied source text.")
|
|
179
|
+
good = False
|
|
180
|
+
verifier = fact.get("verified_by")
|
|
181
|
+
verified_on = fact.get("verified_on")
|
|
182
|
+
if verifier is not None or verified_on is not None:
|
|
183
|
+
if not verifier or not verified_on:
|
|
184
|
+
raise InputError(f"{target}: verification requires both a separately identified verifier and date")
|
|
185
|
+
text(verifier, target + ".verified_by", 120)
|
|
186
|
+
vd = day(verified_on, target + ".verified_on", False)
|
|
187
|
+
if not good or str(verifier).lower() in ("verified", "complete", str(sid).lower()) or vd > as_of or (source_date and vd < day(source_date, target)):
|
|
188
|
+
raise InputError(f"{target}: invalid verification identity, evidence, or date")
|
|
189
|
+
else:
|
|
190
|
+
issue("input_unverified", target, "Confirm this extracted field against the source before relying on it.")
|
|
191
|
+
evidence.append({"target": target, "source_id": sid, "source_title": source.get("title") if source else None, "source_date": source_date, "locator": loc, "quote": quote, "verified_by": verifier, "verified_on": verified_on, "reference_checks": good})
|
|
192
|
+
return good and bool(verifier and verified_on)
|
|
193
|
+
|
|
194
|
+
fact_keys = ("value", "source_id", "locator", "quote", "verified_by", "verified_on")
|
|
195
|
+
|
|
196
|
+
def fact(obj, target, numeric=False, allow_negative=False):
|
|
197
|
+
obj = record(obj or {"value": None}, target)
|
|
198
|
+
keys(obj, fact_keys, target)
|
|
199
|
+
value = obj.get("value")
|
|
200
|
+
if value is None:
|
|
201
|
+
issue("value_missing", target, "Value not supplied; do not treat the blank as zero or a waived term.")
|
|
202
|
+
elif numeric:
|
|
203
|
+
money(value, target) if allow_negative else nonnegative(value, target)
|
|
204
|
+
else:
|
|
205
|
+
text(value, target)
|
|
206
|
+
checked = inspect_evidence(obj, target)
|
|
207
|
+
return value, checked
|
|
208
|
+
|
|
209
|
+
priorities = packet.get("seller_priorities", [])
|
|
210
|
+
if not isinstance(priorities, list) or len(priorities) > 10:
|
|
211
|
+
raise InputError("seller_priorities must be at most 10 sourced facts")
|
|
212
|
+
for i, p in enumerate(priorities):
|
|
213
|
+
fact(p, f"seller_priorities.{i}")
|
|
214
|
+
if not priorities:
|
|
215
|
+
issue("priorities_missing", "seller_priorities", "Ask the seller which objective terms matter. No priority weights have been invented.")
|
|
216
|
+
offers = packet.get("offers", [])
|
|
217
|
+
if not isinstance(offers, list) or not 1 <= len(offers) <= 6:
|
|
218
|
+
raise InputError("Supply 1-6 offers; split larger groups only after confirming the full comparison remains available")
|
|
219
|
+
route = "single_offer_estimate" if len(offers) == 1 else "multiple_offer_comparison"
|
|
220
|
+
if intent == "estimate" and len(offers) != 1:
|
|
221
|
+
raise InputError("intent estimate accepts one offer; use compare or auto for multiple offers")
|
|
222
|
+
if intent == "compare" and len(offers) == 1:
|
|
223
|
+
issue("single_offer_routed", "workflow", "One offer supplied: completed the bundled single-offer estimate, not a cross-offer comparison.")
|
|
224
|
+
used_ids, computed = set(), []
|
|
225
|
+
for offer in offers:
|
|
226
|
+
record(offer, "offer")
|
|
227
|
+
keys(offer, ("id", "label", "price", "earnest_money", "terms", "costs", "claimed_net"), "offer")
|
|
228
|
+
oid = identifier(offer.get("id"), "offer.id")
|
|
229
|
+
if oid in used_ids:
|
|
230
|
+
raise InputError(f"Duplicate offer id: {oid}")
|
|
231
|
+
used_ids.add(oid)
|
|
232
|
+
label = text(offer.get("label"), f"{oid}.label", 40)
|
|
233
|
+
price_raw, price_checked = fact(offer.get("price"), f"{oid}.price", True)
|
|
234
|
+
price = None if price_raw is None else nonnegative(price_raw, f"{oid}.price")
|
|
235
|
+
if price == ZERO:
|
|
236
|
+
raise InputError(f"{oid}.price must exceed zero")
|
|
237
|
+
deposit_raw, _ = fact(offer.get("earnest_money"), f"{oid}.earnest_money", True)
|
|
238
|
+
terms = record(offer.get("terms", {}), f"{oid}.terms")
|
|
239
|
+
keys(terms, TERMS, f"{oid}.terms")
|
|
240
|
+
term_values = {}
|
|
241
|
+
for key in TERMS:
|
|
242
|
+
term_values[key], _ = fact(terms.get(key), f"{oid}.{key}")
|
|
243
|
+
closing = day(term_values["closing"], f"{oid}.closing")
|
|
244
|
+
costs = offer.get("costs", [])
|
|
245
|
+
if not isinstance(costs, list) or len(costs) > 50:
|
|
246
|
+
raise InputError(f"{oid}.costs must be a list of at most 50 items")
|
|
247
|
+
present, line_ids, duplicate_refs, lines = set(), set(), set(), []
|
|
248
|
+
base_deductions, base_credits, low_adjust, high_adjust = ZERO, ZERO, ZERO, ZERO
|
|
249
|
+
numeric_complete = price is not None
|
|
250
|
+
estimate_complete = price_checked
|
|
251
|
+
finite_range = price is not None
|
|
252
|
+
for item in costs:
|
|
253
|
+
record(item, f"{oid}.cost")
|
|
254
|
+
keys(item, ("id", "label", "category", "direction", "payer", "treatment", "amount", "source_id", "locator", "quote", "verified_by", "verified_on", "valid_through", "payoff_basis", "note"), f"{oid}.cost")
|
|
255
|
+
cid = identifier(item.get("id"), f"{oid}.cost.id")
|
|
256
|
+
target = f"{oid}.{cid}"
|
|
257
|
+
if cid in line_ids:
|
|
258
|
+
raise InputError(f"Duplicate cost id: {target}")
|
|
259
|
+
line_ids.add(cid)
|
|
260
|
+
text(item.get("label"), target + ".label", 100)
|
|
261
|
+
cat = item.get("category")
|
|
262
|
+
if cat not in CATEGORIES:
|
|
263
|
+
raise InputError(f"{target}: invalid category; earnest money is informational, never an extra deduction or credit")
|
|
264
|
+
present.add(cat)
|
|
265
|
+
direction, payer, treatment = item.get("direction"), item.get("payer"), item.get("treatment")
|
|
266
|
+
if direction not in ("deduction", "credit") or payer not in ("seller", "buyer", "unknown") or treatment not in ("base", "conditional", "excluded"):
|
|
267
|
+
raise InputError(f"{target}: direction, payer, and treatment must be explicit allowed values")
|
|
268
|
+
note = item.get("note")
|
|
269
|
+
if note is not None:
|
|
270
|
+
text(note, target + ".note", 500)
|
|
271
|
+
if (payer != "seller" or treatment != "base" or direction == "credit") and not note:
|
|
272
|
+
raise InputError(f"{target}: explain the allocation, exclusion, condition, or credit basis in note")
|
|
273
|
+
checked = inspect_evidence(item, target)
|
|
274
|
+
estimate_complete &= checked
|
|
275
|
+
amount = record(item.get("amount"), target + ".amount")
|
|
276
|
+
kind = amount.get("kind")
|
|
277
|
+
value, detail = None, "Amount unknown"
|
|
278
|
+
if kind == "fixed":
|
|
279
|
+
keys(amount, ("kind", "value"), target + ".amount")
|
|
280
|
+
if amount.get("value") is not None:
|
|
281
|
+
value = nonnegative(amount["value"], target)
|
|
282
|
+
detail = "Supplied fixed amount"
|
|
283
|
+
elif kind == "percent":
|
|
284
|
+
keys(amount, ("kind", "rate"), target + ".amount")
|
|
285
|
+
rate = amount.get("rate")
|
|
286
|
+
if rate is not None:
|
|
287
|
+
if not isinstance(rate, str) or not re.fullmatch(r"\d{1,3}(?:\.\d{1,6})?", rate) or Decimal(rate) > 100:
|
|
288
|
+
raise InputError(f"{target}: rate must be a decimal string from 0 to 100 percent")
|
|
289
|
+
if price is not None:
|
|
290
|
+
value = (price * Decimal(rate) / 100).quantize(CENT, rounding=ROUND_HALF_UP)
|
|
291
|
+
detail = f"Price x {rate}% rounded to cents"
|
|
292
|
+
elif kind == "per_diem":
|
|
293
|
+
keys(amount, ("kind", "daily_rate", "days"), target + ".amount")
|
|
294
|
+
days = amount.get("days")
|
|
295
|
+
daily = amount.get("daily_rate")
|
|
296
|
+
if days is not None and (type(days) is not int or not 0 <= days <= 3660):
|
|
297
|
+
raise InputError(f"{target}: days must be an explicit nonnegative integer, at most 3660")
|
|
298
|
+
if daily is not None:
|
|
299
|
+
rate = nonnegative(daily, target + ".daily_rate")
|
|
300
|
+
if days is not None:
|
|
301
|
+
value = (rate * days).quantize(CENT)
|
|
302
|
+
detail = f"{days} supplied days x ${rate}/day"
|
|
303
|
+
if not note:
|
|
304
|
+
raise InputError(f"{target}: per-diem items need a note identifying the authorized day-count basis")
|
|
305
|
+
else:
|
|
306
|
+
raise InputError(f"{target}: unsupported amount kind")
|
|
307
|
+
duplicate_key = (cat, item.get("source_id"), item.get("locator"), json.dumps(amount, sort_keys=True), direction, payer, treatment)
|
|
308
|
+
if duplicate_key in duplicate_refs:
|
|
309
|
+
raise InputError(f"{target}: duplicate financial line; reconcile the source instead of double counting")
|
|
310
|
+
duplicate_refs.add(duplicate_key)
|
|
311
|
+
if cat == "payoff":
|
|
312
|
+
basis = item.get("payoff_basis")
|
|
313
|
+
if basis not in ("payoff_quote", "no_debt_confirmed", "balance_only", "unknown"):
|
|
314
|
+
raise InputError(f"{target}: explicit payoff_basis required")
|
|
315
|
+
valid = day(item.get("valid_through"), target + ".valid_through")
|
|
316
|
+
if basis in ("balance_only", "unknown"):
|
|
317
|
+
issue("payoff_not_quote", target, "A current balance is not a dated payoff quote. Obtain an authorized payoff estimate.", "Title or settlement professional")
|
|
318
|
+
estimate_complete = False
|
|
319
|
+
elif basis == "no_debt_confirmed":
|
|
320
|
+
if value != ZERO:
|
|
321
|
+
raise InputError(f"{target}: no_debt_confirmed must be an explicit zero")
|
|
322
|
+
elif not valid or not closing or valid < closing:
|
|
323
|
+
issue("payoff_date_uncovered", target, "Payoff quote does not establish coverage through the proposed closing date. No interest adjustment has been invented.", "Title or settlement professional")
|
|
324
|
+
estimate_complete = False
|
|
325
|
+
if value is None and payer != "buyer" and treatment != "excluded":
|
|
326
|
+
numeric_complete = False
|
|
327
|
+
finite_range = False
|
|
328
|
+
estimate_complete = False
|
|
329
|
+
issue("cost_unknown", target, "Seller impact is unquantified. The known subtotal is not a complete net estimate.")
|
|
330
|
+
base = ZERO
|
|
331
|
+
low = high = ZERO
|
|
332
|
+
if payer == "buyer" or treatment == "excluded":
|
|
333
|
+
if not checked:
|
|
334
|
+
numeric_complete = False
|
|
335
|
+
finite_range = False
|
|
336
|
+
# Record the exclusion visibly; no silent payor assumptions.
|
|
337
|
+
elif value is not None:
|
|
338
|
+
signed = -value if direction == "deduction" else value
|
|
339
|
+
if payer == "unknown" or treatment == "conditional":
|
|
340
|
+
low, high = min(ZERO, signed), max(ZERO, signed)
|
|
341
|
+
issue("conditional_cost", target, "Base excludes this unresolved amount. The separate range shows its potential seller impact.")
|
|
342
|
+
estimate_complete = False
|
|
343
|
+
else:
|
|
344
|
+
base = signed
|
|
345
|
+
if direction == "deduction":
|
|
346
|
+
base_deductions += value
|
|
347
|
+
else:
|
|
348
|
+
base_credits += value
|
|
349
|
+
low_adjust += low
|
|
350
|
+
high_adjust += high
|
|
351
|
+
lines.append({**item, "computed_amount": value, "calculation": detail, "base_effect": base, "low_adjustment": low, "high_adjustment": high, "evidence_confirmed": checked})
|
|
352
|
+
missing_categories = sorted(set(CATEGORIES) - present)
|
|
353
|
+
for cat in missing_categories:
|
|
354
|
+
issue("category_unreviewed", f"{oid}.{cat}", f"Review {cat.replace('_', ' ')}. Supply a sourced amount, explicit confirmed zero, or supported exclusion.")
|
|
355
|
+
lines.append({"id": f"missing_{cat}", "label": f"Unreviewed {cat.replace('_', ' ')}", "category": cat, "direction": "deduction", "payer": "unknown", "treatment": "conditional", "amount": {"kind": "fixed", "value": None}, "source_id": None, "locator": None, "note": "No figure supplied; no zero or exclusion has been assumed.", "computed_amount": None, "calculation": "Amount unknown", "base_effect": ZERO, "low_adjustment": ZERO, "high_adjustment": ZERO, "evidence_confirmed": False})
|
|
356
|
+
if missing_categories:
|
|
357
|
+
estimate_complete = numeric_complete = finite_range = False
|
|
358
|
+
known = None if price is None else price + base_credits - base_deductions
|
|
359
|
+
if "claimed_net" in offer:
|
|
360
|
+
claimed, _ = fact(offer["claimed_net"], f"{oid}.claimed_net", True, allow_negative=True)
|
|
361
|
+
if claimed is not None and known is not None and money(claimed) != known:
|
|
362
|
+
issue("prior_net_mismatch", oid, f"Supplied prior net differs from the calculated known subtotal by ${abs(money(claimed) - known):,.2f}. Resolve scope and input differences.")
|
|
363
|
+
computed.append({"id": oid, "label": label, "price": price, "earnest_money": None if deposit_raw is None else money(deposit_raw), "terms": term_values, "costs": lines,
|
|
364
|
+
"base_deductions": base_deductions, "base_credits": base_credits, "known_subtotal": known,
|
|
365
|
+
"net_low": known + low_adjust if finite_range else None, "net_high": known + high_adjust if finite_range else None,
|
|
366
|
+
"numerically_complete": numeric_complete, "estimate_complete": bool(estimate_complete and numeric_complete),
|
|
367
|
+
"subtotal_label": "Estimated net from supplied inputs" if estimate_complete and numeric_complete else "Known subtotal only - unresolved inputs remain"})
|
|
368
|
+
result.update({"route": route, "context": context, "sources": [{k: s.get(k) for k in ("id", "title", "date")} for s in sources], "seller_priorities": [p.get("value") for p in priorities], "offers": computed, "verification_queue": issues, "evidence": evidence,
|
|
369
|
+
"limits": ["Preliminary estimate, not a closing statement, valuation, legal opinion, or tax calculation.", "No state, local, MLS, brokerage, or approved-form requirements were resolved by this calculator.", "Evidence checks validate references and recorded confirmations, not source truth or legal applicability.", "Earnest money is shown for comparison only; it is not added to or deducted from total transaction proceeds.", "One listing professional must reconcile inputs, approve presentation, and obtain qualified advice where needed."]})
|
|
370
|
+
return scalar(result)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""One presentation contract for conversation, Word and spreadsheet outputs."""
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
import hashlib
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def cash(value):
|
|
7
|
+
return f"${Decimal(value):,.2f}" if value is not None else "Input needed"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def presentation(result):
|
|
11
|
+
if result["route"] == "handoff_only":
|
|
12
|
+
return {"offers": [], "questions": [], "review_checks": []}
|
|
13
|
+
offers = []
|
|
14
|
+
for offer in result["offers"]:
|
|
15
|
+
all_base_known = all(c["computed_amount"] is not None or c["payer"] == "buyer" or c["treatment"] == "excluded" for c in offer["costs"])
|
|
16
|
+
subtotal = offer["known_subtotal"] if all_base_known else None
|
|
17
|
+
bounds = [offer["net_low"], offer["net_high"]]
|
|
18
|
+
offers.append({"id": offer["id"], "label": offer["label"], "reported_subtotal": subtotal,
|
|
19
|
+
"subtotal_text": cash(subtotal),
|
|
20
|
+
"range_text": "Not established - resolve inputs" if None in bounds else cash(bounds[0]) if bounds[0] == bounds[1] else f"{cash(bounds[0])} to {cash(bounds[1])}",
|
|
21
|
+
"scope": "Financial inputs accounted for" if offer["estimate_complete"] else "Unresolved financial inputs"})
|
|
22
|
+
groups = {}
|
|
23
|
+
checks = {}
|
|
24
|
+
references = {e["target"]: e for e in result["evidence"]}
|
|
25
|
+
labels = {}
|
|
26
|
+
for offer in result["offers"]:
|
|
27
|
+
for field in ("price", "earnest_money", *offer["terms"]):
|
|
28
|
+
labels[f"{offer['id']}.{field}"] = f"{offer['label']} {field.replace('_', ' ')}"
|
|
29
|
+
for cost in offer["costs"]:
|
|
30
|
+
labels[f"{offer['id']}.{cost['id']}"] = f"{offer['label']} {cost['label']}"
|
|
31
|
+
for issue in result["verification_queue"]:
|
|
32
|
+
if issue["code"] == "single_offer_routed":
|
|
33
|
+
continue
|
|
34
|
+
if issue["code"] == "input_unverified":
|
|
35
|
+
source = references.get(issue["target"], {})
|
|
36
|
+
key = (source.get("source_id") or "Missing source", issue["owner"])
|
|
37
|
+
group = checks.setdefault(key, {"source": source.get("source_title") or "Missing source", "owner": issue["owner"], "fields": []})
|
|
38
|
+
group["fields"].append(issue["target"])
|
|
39
|
+
continue
|
|
40
|
+
source = references.get(issue["target"], {})
|
|
41
|
+
grouped_target = f"source:{source['source_id']}" if issue["code"] in ("source_undated", "source_future") and source.get("source_id") else issue["target"]
|
|
42
|
+
key = (grouped_target, issue["owner"])
|
|
43
|
+
question = groups.setdefault(key, {"id": hashlib.sha256(repr(key).encode()).hexdigest()[:16],
|
|
44
|
+
"target": grouped_target, "label": (source.get("source_title") or grouped_target) if grouped_target.startswith('source:') else labels.get(grouped_target, grouped_target.replace('_',' ')),
|
|
45
|
+
"targets": [], "owner": issue["owner"], "messages": [], "codes": []})
|
|
46
|
+
if issue["target"] not in question["targets"]:
|
|
47
|
+
question["targets"].append(issue["target"])
|
|
48
|
+
if issue["message"] not in question["messages"]:
|
|
49
|
+
question["messages"].append(issue["message"])
|
|
50
|
+
question["codes"].append(issue["code"])
|
|
51
|
+
return {"offers": offers, "questions": list(groups.values()), "review_checks": list(checks.values()),
|
|
52
|
+
"basis": "Where a seller amount is unquantified, the comparison subtotal is Input needed in both files. Known components remain in the cost detail; missing amounts are never zero."}
|