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
10 changes: 6 additions & 4 deletions packages/flow/docs/GENERATE_CONTRACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,12 @@ user, to the value every demonstration held, or to a copy of a column of the
row a key points to (an invoice line's price from its product). A key, a
link to a row the write added, the year, month, week or day of a date the
task gives, and a generated token (a UUID) are recognized as such. A link
and a column that holds the row's own key need two demonstrations; the own
key must sit in the same text in every row (`INV-{key}`), never as a mere
substring. Such a column is listed in the question, never checked or
left open on its own. A date part must differ between the demonstrations: a constant
needs two demonstrations. A column that is not a copy and, in two or more
demonstrations, holds the row's own key in the same text (`INV-{key}`) is
listed in the question as holding the row's own key, and is left unchecked
only if the answer names it, like any column that varies. A copy is
checked as a copy even when its values read as the own key (prices 15 and
25 on rows 1 and 2). A date part must differ between the demonstrations: a constant
that happens to equal the month of every demonstration's date is a
constant. A copy is taken only through a declared key or a column named
after its table, and only when the source differs between demonstrations:
Expand Down
30 changes: 13 additions & 17 deletions packages/flow/openadapt_flow/generate/demo_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,13 +686,6 @@ def column_roles(
if is_link:
out[name] = ColumnRole("link")
continue
template = None if one else _key_template(t, values, rows)
if template is not None:
out[name] = ColumnRole(
"varies",
detail={"why": "holds the row's own key", "template": template},
)
continue
part = None if one else _date_part(values, [d for d, _ in rows])
if part is not None:
out[name] = ColumnRole(
Expand All @@ -716,6 +709,11 @@ def column_roles(
if copied is not None:
out[name] = ColumnRole("copied", detail=copied)
continue
# Only once no copy fits: a copied price of 15 and 25 on rows 1 and
# 2 reads as ``{key}5``, and a copy is checked where this isn't.
if not one and _holds_own_key(t, values, rows):
out[name] = ColumnRole("varies", detail={"why": "holds the row's own key"})
continue
distinct = {v for v in values}
if col.kind == "text" and distinct <= {None, ""}:
# Free text the task doesn't give and the demonstrations left
Expand Down Expand Up @@ -757,27 +755,25 @@ def column_roles(
return out


def _key_template(
def _holds_own_key(
t: Table,
values: Sequence[Optional[str]],
rows: Sequence[tuple["Demo", Mapping[str, Optional[str]]]],
) -> Optional[str]:
"""``prefix{key}suffix`` when every row holds its own single-column key
between the same prefix and suffix (``INV-{key}``), else None."""
) -> bool:
"""Does every row hold its own single-column key between the same prefix
and suffix (``INV-{key}``)? Only the question's wording uses this: such
a column is never checked unless the answer leaves it out."""

if len(t.primary_key) != 1 or not values:
return None
return False
shapes = set()
for v, (_, r) in zip(values, rows):
key = r.get(t.primary_key[0])
if v is None or key is None or v == key or str(key) not in v:
return None
return False
i = v.index(str(key))
shapes.add((v[:i], v[i + len(str(key)) :]))
if len(shapes) != 1:
return None
prefix, suffix = next(iter(shapes))
return prefix + "{key}" + suffix
return len(shapes) == 1


def _date_part(
Expand Down
25 changes: 13 additions & 12 deletions packages/flow/tests/test_generate_coincidences_r1.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,12 @@ def demo(kind: str, values: dict[str, Any], write: Callable[[], Any]) -> None:
first = json.loads((work / "out" / "report.json").read_text())
questions = (work / "out" / "QUESTIONS.md").read_text()
answers = {q["key"]: q["proposal"] for q in first["questions"]}
# The person names the two columns that hold a row's own key, or what
# reads like one (a sale's price, an audit row's reference).
# The person names the column that holds a row's own key (an audit row's
# reference). A sale's price only reads like one: it is a copy.
bk = answers["bookkeeping"]
bk = json.loads(json.dumps(bk)) if isinstance(bk, dict) else {"accept": True}
named = bk.setdefault("unchecked", {})
for table, col in (("sales", "price"), ("job_audit", "ref")):
for table, col in (("job_audit", "ref"),):
if col not in named.setdefault(table, []):
named[table].append(col)
answers["bookkeeping"] = bk
Expand Down Expand Up @@ -320,19 +320,20 @@ def test_a_constant_equal_to_its_source_in_every_demo_is_asked(
# -- review round 3 ------------------------------------------------------------


def test_a_value_that_reads_as_the_own_key_is_asked_never_templated(
shop: dict[str, Any],
@pytest.mark.parametrize("price,confirmed", [(None, True), (45, False)])
def test_a_copy_that_reads_as_the_own_key_is_checked_as_a_copy(
shop: dict[str, Any], price: Any, confirmed: bool
) -> None:
"""Sales 1 and 2 copied prices 15 and 25, which read as ``{key}5``. The
column is listed in the question; nothing checks that template, so the
app's real price for sale 3 (40) passes once the person names it."""
column is a copy of the product's price, never asked about, so a wrong
45 on the next sale is refuted and the real price (40) passes."""

assert "sales: " in shop["questions"] and "price" in shop["questions"]
refused_first = shop["first"]["kinds"]["create_sale"]["refused"]
assert any("sales.price" in r for r in refused_first), refused_first
v = {"product": "Doohickey", "ref": "S-3"}
out = _run(shop, "create_sale", v, lambda: shop["app"].sale(v))
assert out.startswith("CONFIRMED"), out
assert not any("sales.price" in r for r in refused_first), refused_first
assert "sales.price" not in json.dumps(shop["first"]["questions"])
v = {"product": "Doohickey", "ref": f"S-{price}"}
out = _run(shop, "create_sale", v, lambda: shop["app"].sale(v, price=price))
assert out.startswith("CONFIRMED") is confirmed, out


def test_an_audit_row_holding_its_own_key_can_be_named(shop: dict[str, Any]) -> None:
Expand Down
Loading