← Files Fitness LedgerARCHIVED FILE

skills/nutrition-ledger/scripts/product_identity.py

4.88 KB · Sep 30, 2026 · 23:15 UTC

↓ Download file

"""Deterministic product identity and formulation-version primitives.

These helpers are intentionally independent of persistence. Callers can use
the result to decide whether to reuse a food master, create a new version, or
ask for clarification before mutating the canonical ledger.
"""

from __future__ import annotations

from dataclasses import dataclass
import hashlib
import json
import re
from typing import Any, Iterable, Mapping


_FORMULATION_FIELDS = (
    "serving_size",
    "serving_unit",
    "serving_weight_g",
    "calories",
    "protein_g",
    "carbohydrates_g",
    "fat_g",
    "fiber_g",
    "total_sugars_g",
    "added_sugars_g",
    "saturated_fat_g",
    "sodium_mg",
    "potassium_mg",
    "ingredients",
)


def _clean(value: Any) -> Any:
    if isinstance(value, str):
        return re.sub(r"\s+", " ", value.strip()).casefold()
    if isinstance(value, float):
        return round(value, 6)
    if isinstance(value, Mapping):
        return {str(k): _clean(value[k]) for k in sorted(value)}
    if isinstance(value, (list, tuple)):
        return [_clean(item) for item in value]
    return value


def normalized_gtin(record: Mapping[str, Any]) -> str | None:
    """Return a digits-only GTIN/UPC, or ``None`` when absent/invalid."""
    raw = record.get("gtin") or record.get("upc")
    if raw is None:
        return None
    digits = re.sub(r"\D", "", str(raw))
    return digits or None


def formulation_fingerprint(record: Mapping[str, Any]) -> str:
    """Create a stable change-detection fingerprint for declared formulation."""
    payload = {field: _clean(record.get(field)) for field in _FORMULATION_FIELDS}
    encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
    return hashlib.sha256(encoded.encode("utf-8")).hexdigest()


def identity_key(record: Mapping[str, Any]) -> tuple[Any, ...]:
    """Return normalized identity attributes used for deterministic matching."""
    return (
        normalized_gtin(record),
        _clean(record.get("brand_owner") or record.get("brand")),
        _clean(record.get("manufacturer")),
        _clean(record.get("product_name") or record.get("food_name") or record.get("food_product")),
        _clean(record.get("variant")),
        _clean(record.get("package_size")),
    )


def _identity_without_gtin(record: Mapping[str, Any]) -> tuple[Any, ...]:
    return identity_key(record)[1:]


@dataclass(frozen=True)
class IdentityResolution:
    status: str
    selected: Mapping[str, Any] | None
    candidates: tuple[Mapping[str, Any], ...]
    reason: str


def resolve_identity(query: Mapping[str, Any], masters: Iterable[Mapping[str, Any]]) -> IdentityResolution:
    """Resolve a query against masters without guessing through ambiguity."""
    records = tuple(masters)
    gtin = normalized_gtin(query)
    if gtin:
        gtin_matches = tuple(m for m in records if normalized_gtin(m) == gtin)
        if len(gtin_matches) == 1:
            match = gtin_matches[0]
            if query.get("formulation_hash") and query["formulation_hash"] != match.get("formulation_hash"):
                return IdentityResolution("changed_formulation", None, gtin_matches, "GTIN matches but formulation fingerprint changed")
            return IdentityResolution("exact", match, gtin_matches, "exact GTIN match")
        if len(gtin_matches) > 1:
            return IdentityResolution("ambiguous", None, gtin_matches, "duplicate masters share the same GTIN")

    key = identity_key(query)
    matches = tuple(m for m in records if key[0] is None and _identity_without_gtin(m) == _identity_without_gtin(query))
    if len(matches) == 1:
        return IdentityResolution("exact", matches[0], matches, "exact non-GTIN identity match")
    if len(matches) > 1:
        return IdentityResolution("ambiguous", None, matches, "multiple masters match without a barcode")
    return IdentityResolution("unresolved", None, (), "insufficient identity evidence")


def version_master(master: Mapping[str, Any], observed: Mapping[str, Any]) -> dict[str, Any]:
    """Return a versioned master, preserving the prior master when formulation changes."""
    old = dict(master)
    old_hash = old.get("formulation_hash") or formulation_fingerprint(old)
    new = dict(observed)
    new_hash = new.get("formulation_hash") or formulation_fingerprint(new)
    if old_hash == new_hash:
        old["formulation_hash"] = old_hash
        return old
    new["formulation_hash"] = new_hash
    prior_id = old.get("food_master_id")
    new.setdefault("food_master_version", int(old.get("food_master_version") or 1) + 1)
    if new.get("food_master_id") in (None, prior_id):
        new["food_master_id"] = f"{prior_id or 'fm'}-v{new['food_master_version']}"
    new["supersedes_food_master_id"] = prior_id
    new["status"] = new.get("status", "active")
    return new

SHA-256: 585d602680b7bd42b55286baff08643cfab3f72b702abce2b90861ca6a1f980d