commit d17f335f02e068ea59429f5de58a4ee5b1b4d96f
parent 067e7ce760f162cceecc2066a58f3496089420fe
Author: triesap <tyson@radroots.org>
Date: Mon, 21 Sep 2026 13:03:41 +0000
core: add source-span candidate discovery
Diffstat:
2 files changed, 126 insertions(+), 0 deletions(-)
diff --git a/src/hyf_core/normalization/candidates.mojo b/src/hyf_core/normalization/candidates.mojo
@@ -0,0 +1,94 @@
+from std.collections import List
+
+
+@fieldwise_init
+struct Candidate(Copyable, Movable):
+ var kind: String
+ var text: String
+ var start: Int
+ var end: Int
+
+
+def _is_digit_byte(byte: UInt8) -> Bool:
+ return byte >= UInt8(ord("0")) and byte <= UInt8(ord("9"))
+
+
+def _backward_number_start(text: String, unit_start: Int) -> Int:
+ var bytes = text.as_bytes()
+ var index = unit_start - 1
+ while index >= 0:
+ var byte = bytes[index]
+ if byte == UInt8(ord(" ")) or byte == UInt8(ord("\t")):
+ index -= 1
+ continue
+ break
+ var end = index
+ while index >= 0 and _is_digit_byte(bytes[index]):
+ index -= 1
+ if index == end:
+ return -1
+ return index + 1
+
+
+def discover_candidates(
+ text: String, known_products: List[String], known_units: List[String], known_dates: List[String]
+) -> List[Candidate]:
+ var candidates = List[Candidate]()
+ var lowered = text.lower()
+
+ for product in known_products:
+ if product.strip() == "":
+ continue
+ var index = lowered.find(product.lower())
+ if index >= 0:
+ candidates.append(
+ Candidate(
+ kind="product",
+ text=String(product),
+ start=index,
+ end=index + product.byte_length(),
+ )
+ )
+
+ for unit in known_units:
+ if unit.strip() == "":
+ continue
+ var index = lowered.find(unit.lower())
+ if index >= 0:
+ var number_start = _backward_number_start(text, index)
+ if number_start >= 0:
+ candidates.append(
+ Candidate(
+ kind="quantity",
+ text=String(text[byte=number_start:index + unit.byte_length()]),
+ start=number_start,
+ end=index + unit.byte_length(),
+ )
+ )
+
+ for date_word in known_dates:
+ if date_word.strip() == "":
+ continue
+ var index = lowered.find(date_word.lower())
+ if index >= 0:
+ candidates.append(
+ Candidate(
+ kind="date",
+ text=String(date_word),
+ start=index,
+ end=index + date_word.byte_length(),
+ )
+ )
+ return candidates^
+
+
+def candidate_kinds(candidates: List[Candidate]) -> List[String]:
+ var kinds = List[String]()
+ for candidate in candidates:
+ var seen = False
+ for kind in kinds:
+ if kind == candidate.kind:
+ seen = True
+ if not seen:
+ kinds.append(String(candidate.kind))
+ return kinds^
diff --git a/tests/test_domain.mojo b/tests/test_domain.mojo
@@ -559,3 +559,35 @@ def test_execution_metadata_is_separate_from_business_outcome() raises:
_ = execution_meta("degraded", 0, None, None, None)
with assert_raises():
_ = execution_meta("weird", 0, None, None, None)
+
+
+from hyf_core.normalization.candidates import Candidate, candidate_kinds, discover_candidates
+
+
+def test_candidate_discovery_spans_and_unknown_products() raises:
+ var products = List[String]()
+ products.append("roma tomatoes")
+ var units = List[String]()
+ units.append("lb")
+ var dates = List[String]()
+ dates.append("friday")
+ var candidates = discover_candidates(
+ "Got about 80 lb of Roma tomatoes. Can deliver Friday.", products, units, dates
+ )
+ var kinds = candidate_kinds(candidates)
+ assert_true(len(kinds) == 3)
+ var saw_quantity = False
+ for candidate in candidates:
+ if candidate.kind == "quantity":
+ assert_equal(candidate.text, "80 lb")
+ saw_quantity = True
+ assert_true(saw_quantity)
+
+ var unknown_products = List[String]()
+ unknown_products.append("kohlrabi")
+ var empty_units = List[String]()
+ var empty_dates = List[String]()
+ assert_equal(
+ len(discover_candidates("mystery greens", unknown_products, empty_units, empty_dates)),
+ 0,
+ )