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
79 changes: 68 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

**nomnom stores nothing about food; it computes what you feed it.**

`nomnomcli` is an agent-first nutrition ledger. Version 0.3 ships zero food records: no food
`nomnomcli` is an agent-first nutrition ledger. Version 0.4 ships zero food records: no food
database, synonym corpus, or piece-weight table is hidden in the package. It resolves food at
runtime, performs nutrition arithmetic in code, and stores successful logs plus a user-owned cache
in SQLite. There is no LLM in the program and no invented nutrition fallback.
Expand All @@ -17,6 +17,7 @@ flowchart LR
N --> C[(user food cache)]
N -->|free-text search| O[Open Food Facts API]
N -->|when key is configured| U[USDA FoodData Central API]
A -->|barcode or extracted label facts| N
N -->|resolved JSON or structured error| A
```

Expand Down Expand Up @@ -63,7 +64,8 @@ Resolution is deterministic and ordered:
2. exact match in the user's `food_cache`;
3. token-overlap search in that cache;
4. Open Food Facts free-text search;
5. USDA FoodData Central, when a setup key or `NOMNOM_USDA_KEY` is configured;
5. a safe USDA FoodData Central generic proxy, when a setup key or
`NOMNOM_USDA_KEY` is configured;
6. actionable JSON error—never a guessed food.

Open Food Facts candidates need at least 0.5 normalized token overlap between the query and
Expand All @@ -72,7 +74,8 @@ values must be present, finite, and greater than zero. A rejected result returns
`off_low_confidence` with the candidate and alternatives, and is neither cached nor logged.

Successful API results are cached in the user's database, so the same food can resolve locally
later. Existing v0.2 cache records, logs, and recipes are preserved when v0.3 opens the database.
later. Existing cache records, logs, recipes, and aliases are preserved when v0.4 opens the
database.
`nomnom search QUERY` searches this user cache; it is not a packaged food catalog.

Open Food Facts free-text search goes directly through the official legacy v1 endpoint,
Expand Down Expand Up @@ -109,15 +112,61 @@ Without a key, a food that OFF cannot resolve returns `usda_key_required`, the s
the same signup URL. USDA search requires complete positive kcal/protein/fat/carbs, scores query
token overlap together with data type and category, prefers Foundation and SR Legacy, and enforces
a confidence floor. Weak matches return `usda_low_confidence` with candidate alternatives and are
never cached. Accepted matches cache `source=usda`, `fdc_id`, and any returned serving-field
provenance.
never cached.

The default generic policy is `allow_for_unbranded`. A USDA result becomes a generic proxy only
when it is an unbranded Foundation, SR Legacy, or Survey (FNDDS) record with an FDC id and every
normalized query token is covered by its name. Brand/SKU-like or unmatched input and branded USDA
records return `exact_resolution_required` without cache or log writes. Accepted proxies expose
`resolution_mode=generic_proxy`, `source=usda`, the FDC `source_id`, `provenance=usda`, confidence,
and an explicit assumption in log JSON.

Choose a stricter policy in the user config:

```toml
[resolution]
generic_proxy_policy = "ask" # or "exact_only"
```

`NOMNOM_GENERIC_PROXY_POLICY` overrides the file. `ask` returns
`generic_proxy_confirmation_required` with the candidate but writes nothing; `exact_only` requires
barcode or package-label capture. Supported values are `allow_for_unbranded`, `ask`, and
`exact_only`.

Set `NOMNOM_OFFLINE=1` to prevent all remote food lookup. Set `NOMNOM_DISABLE_OFF=1` to skip OFF
while retaining USDA when its key is configured.

### Pin a label manually
## Capture an exact packaged product

When an exact packaged product is needed, use its barcode or ask the user for a package photo. A
barcode capture calls only the Open Food Facts v2 product endpoint; it never sends free text:

```sh
nomnom capture barcode "0123456789012" --json
```

If the barcode is absent or OFF lacks complete core nutrition, the agent reads the supplied label
photo and passes the extracted per-100 g facts to the CLI:

```sh
nomnom capture label \
--name "chicken pastrami" --brand "Example" \
--kcal 110 --protein 20 --fat 2 --carbs 3 \
--serving-grams 75 \
--source-note "image:sha256:LOCAL_REFERENCE" --json
```

`--source-note` is required. Use a nonempty local or opaque image/barcode reference that lets the
user trace the facts without putting the image itself in SQLite. The CLI has no OCR or vision
dependency, never receives or stores the photo, never estimates missing macros, and rejects
non-finite/negative values or a non-positive serving weight without writing. Both capture paths
persist `resolution_mode=exact_product`, source identity, provenance, and the normalized nutrition
facts; the canonical name can then be used in an alias and logged offline.

### Legacy manual pin

`nomnom add` remains a manual operation. Use only verified per-100 g label values:
`nomnom add` remains available for existing manual workflows. For a new packaged product, prefer
the source-backed capture commands above. Use only verified per-100 g label values:

```sh
nomnom add \
Expand Down Expand Up @@ -197,6 +246,13 @@ an `error` object and exit with status 2. Important codes include:
no near match was cached.
- `usda_invalid_nutrition`: every USDA candidate lacked one or more complete positive core values.
- `usda_key_required`: configure the free FDC key or pin verified values.
- `generic_proxy_confirmation_required`: show the named USDA candidate and ask before changing the
configured policy; nothing was cached or logged.
- `exact_resolution_required`: request the barcode or a package photo for source-backed capture.
- `invalid_barcode` / `barcode_not_found` / `barcode_nutrition_incomplete`: correct the barcode or
request a package photo; failed captures write nothing.
- `invalid_source_note` / `invalid_nutrition`: correct the extracted label facts; failed captures
write nothing.
- `piece_weight_unknown`: ask for grams or add a verified `--piece-grams` value.
- `alias_target_not_found`: add/resolve the exact cached target or remove the stale alias.
- `openfoodfacts_unavailable` / `usda_unavailable`: retry later or use a manual label.
Expand Down Expand Up @@ -227,14 +283,15 @@ Recipe ingredients use the same runtime resolver. An unresolved ingredient fails
instead of storing partial nutrition.

User data defaults to `~/.local/share/nomnomcli/nomnom.sqlite3`. Override it with
`NOMNOM_DB_PATH`. Schema v3 upgrades preserve cached foods, logs, and recipes in place and add the
user-only alias table.
`NOMNOM_DB_PATH`. Schema v4 upgrades preserve cached foods, logs, recipes, and aliases in place and
add resolution mode, source identity/note, provenance, and assumption fields to the food cache.

## Agent skill and development

The repository agent workflow is [`skill/SKILL.md`](skill/SKILL.md). It teaches agents to use
nomnom's JSON, follow OFF → USDA → manual add → structured error, and never estimate nutrition in
their own context.
nomnom's JSON, accept only safe generic proxies, request a barcode/package photo for exact products,
capture extracted label facts with a source note, and never estimate nutrition in their own
context.

```sh
python -m pip install -e '.[dev]'
Expand Down
71 changes: 71 additions & 0 deletions docs/plans.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,76 @@
# Plans

## Issue #19 Source
- Task: Implement the v0.4 zero-friction source-backed capture slice.
- Canonical input: GitHub issue #19 plus the user's explicit default-policy override.
- Repo context: provider policy, runtime resolver, capture CLI, user SQLite schema, tests, README, and agent skill.
- Last updated: 2026-07-21

## Issue #19 Assumptions
- `allow_for_unbranded` is the default despite the issue body's older `ask` default.
- A USDA proxy is eligible only when the returned record passes existing nutrition/confidence checks, is a generic data type with no brand, has an FDC id, and covers every normalized query token; unmatched brand/SKU-like input therefore stays exact-only.
- Package-photo OCR/vision remains agent-side. The CLI accepts only extracted facts and a mandatory local source reference and never stores the image.
- Schema version 4 is the additive migration boundary already started on this branch; issue #19 completes that explicit v3-to-v4 migration rather than introducing a second version number.

## Issue #19 Milestone Order
| ID | Title | Depends on | Status |
| --- | --- | --- | --- |
| M13 | Add failing policy, proxy, capture, migration, and smoke contracts | M12 | [x] |
| M14 | Implement v4 provenance and deterministic capture/resolution | M13 | [x] |
| M15 | Document v0.4 agent flow and privacy contract | M14 | [x] |
| M16 | Run full validation, smoke, audit, and local commit | M15 | [x] |

## M13. Add failing issue #19 acceptance contracts `[x]`
### Goal
- Freeze the user-visible policy, JSON, endpoint, persistence, migration, and clean-install behavior before runtime implementation.

### Validation
```sh
pytest -q tests/test_config.py tests/test_foods.py tests/test_off.py tests/test_cli.py tests/test_db.py
```

### Stop-and-Fix Rule
- Record expected RED failures before production changes; no live provider traffic or personal-image fixture may enter tests.

## M14. Implement v4 provenance and deterministic capture/resolution `[x]`
### Goal
- Source-backed unbranded USDA proxies and exact OFF/package captures persist and replay with explicit mode and provenance.

### Validation
```sh
pytest -q tests/test_config.py tests/test_foods.py tests/test_off.py tests/test_cli.py tests/test_db.py
```

### Stop-and-Fix Rule
- Reject incomplete nutrition, unsafe/branded generic substitution, blank provenance, and failed capture without cache or log writes.

## M15. Document v0.4 agent flow and privacy contract `[x]`
### Goal
- README and agent skill give exact commands and direct agents to request a photo—not manual label lookup—when exact package facts are needed.

### Validation
```sh
pytest -q tests/test_install.py tests/test_cli.py
ruff check .
```

### Stop-and-Fix Rule
- Keep docs aligned with executable syntax and stable JSON fields before release validation.

## M16. Run full validation, smoke, audit, and local commit `[x]`
### Goal
- Produce one coherent, validated local conventional commit with no push or PR.

### Validation
```sh
pytest -q
ruff check .
git diff --check
```

### Stop-and-Fix Rule
- Do not commit until full tests, Ruff, literal isolated-DB smoke, and diff audit all pass.

## Issue #17 Source
- Task: Fix the Open Food Facts full-text provider contract without live-test traffic.
- Canonical input: GitHub issue #17 and the user's required behavior.
Expand Down
15 changes: 13 additions & 2 deletions docs/status.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Status

## Snapshot
- Current phase: issue #17 complete
- Current phase: issue #19 complete
- Plan file: `docs/plans.md`
- Status: green
- Last updated: 2026-07-21
Expand All @@ -15,21 +15,27 @@
- Added mocked OFF v2 resolution with branded priority, alternatives, barcode/cache migration, clear failures, and `nomnom add`.
- Removed placeholder profiles from the offline seed and bundled 431-food database; byte-deterministic updates and data-quality gates pass.
- Passed 72 tests, Ruff, version 0.2.0, and the exact isolated v0.2 smoke; removed the `/tmp` smoke database.
- Completed v0.4 source-backed capture: safe default USDA generic proxies, exact OFF v2 barcode capture, agent-extracted label capture, durable provenance, and additive schema-v4 migration.
- Passed 155 tests, Ruff, diff audit, and the literal isolated schema-v4 capture/alias/offline-log/error smoke.

## In Progress
- None.

## Next
- Push `feat/off-fulltext-contract` and open the issue #17 pull request after independent verification.
- None; issue #19 is complete and committed locally with no push or PR.

## Decisions Made
- Default generic policy is `allow_for_unbranded`; this explicit user decision supersedes issue #19's older `ask` default.
- Generic proxy safety requires a generic USDA type, no returned brand, an FDC id, complete validated core nutrition, accepted confidence, and full query-token coverage.
- Package photo extraction stays outside the dependency-free CLI; only extracted facts and a mandatory source note enter the user database.
- Route all OFF free text to legacy v1 `/cgi/search.pl`; never pass `search_terms` to v2.
- Report OFF product/barcode reachability separately from full-text resolution readiness.
- Use stdlib `argparse` — keeps runtime dependencies to `requests` only.
- Use SQLite for shipped foods and per-user mutable state — matches the offline-first contract.
- Reuse `scripts/build_mini_db.py --update-existing` for deterministic offline v0.2 data repair without shrinking the tracked USDA corpus.

## Assumptions In Force
- Schema v4 is completed additively from the existing v3-to-v4 boundary and preserves every v3 table and row.
- Issue #17 tests use only mocked/replay transports and never live OFF traffic.
- Agent confirmation is an operating pattern, not a pending database transaction in v0.1.
- Named brands are never resolved to bundled generic foods; manual cache entries are the offline escape hatch.
Expand Down Expand Up @@ -58,8 +64,12 @@ ruff check .
| 2026-07-19 | M8 | README, skill, version, planning docs | `pytest -q`; `ruff check .`; exact isolated smoke; diff/junk audit | 72 pass; clean | commit |
| 2026-07-21 | M9 | OFF, food confidence, doctor/setup contract tests | `PYTHONPATH=. pytest -q tests/test_off.py tests/test_foods.py tests/test_config.py tests/test_cli.py`; focused setup test | RED: missing product probe; RED: missing status explanation | M10 |
| 2026-07-21 | M10–M11 | OFF client, onboarding/CLI, README, skill, tests | focused pytest; full pytest; full Ruff | 62 pass; 126 pass; clean | M12 |
| 2026-07-21 | issue #19 preflight | issue, providers, resolver, schema, CLI, tests, docs, skill | `pytest -q`; repository inspection | 126 pass; clean baseline | M13 |
| 2026-07-21 | M13 | policy, proxy, capture, and migration acceptance tests | focused pytest; full local pytest | RED contracts and 4 partial-worktree failures recorded | M14 |
| 2026-07-21 | M14–M16 | config, resolver, OFF, schema, CLI, docs, skill, tests | `pytest -q`; `ruff check .`; `git diff --check`; literal temp-DB smoke | 155 pass; clean; smoke pass | local commit |

## Smoke / Demo Checklist
- [x] Fresh temp DB: help/version, capture label, alias, log, and invalid structured capture error.
- [x] Russian mixed-item log works and persists.
- [x] Today stats reproduce logged totals.
- [x] Fixture recipe imports and logs.
Expand All @@ -71,3 +81,4 @@ ruff check .
- [x] Version and documentation report 0.2.0 behavior.
- [x] OFF free text uses v1 with no unfiltered v2 fallback.
- [x] Doctor and setup distinguish product/barcode reachability from full-text readiness.
- [x] Version and documentation report v0.4 generic-proxy and source-backed capture behavior.
41 changes: 41 additions & 0 deletions docs/test-plan.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,46 @@
# Test Plan

## Issue #19 Source
- Task: Validate the v0.4 zero-friction source-backed capture slice.
- Plan file: `docs/plans.md`
- Status file: `docs/status.md`
- Last updated: 2026-07-21

## Issue #19 Validation Scope
- In scope: default/config/env proxy policy, generic USDA eligibility and visible assumptions, branded/SKU denial, OFF v2 barcode lookup, package-label capture, aliases/log replay, v3-to-v4 preservation, docs/skill, and isolated fresh-DB CLI smoke.
- Out of scope: LLM/OCR/cloud-vision dependencies, live API traffic, real/personal photos, repository nutrition records, and macro estimation.

## Issue #19 Fixtures and Network Rules
- Provider responses are mocked synthetic OFF/USDA payloads only; barcode assertions inspect the exact v2 product URL and absence of free-text parameters.
- Package-label tests pass synthetic agent-extracted numbers and opaque image/barcode reference tokens; no image is stored or committed.

## Issue #19 Test Levels

### Unit
- Policy precedence/default/invalid values; barcode syntax and complete nutrients; generic data type/brand/query-token safety; finite non-negative label values and positive serving grams.

### Integration
- Automatic unbranded USDA proxy caches and logs `generic_proxy`, canonical name, `source=usda`, FDC `source_id`, confidence, and explicit assumption.
- `ask`, `exact_only`, and branded inputs return structured actions without cache/log writes.
- Exact OFF and package-label captures preserve source, source id/note, provenance, mode, and later alias/log behavior.
- Explicit v3-to-v4 migration preserves cache, logs, recipes, and aliases while legacy rows remain readable.

### End-to-End / Smoke
- A clean temp database runs help/version, capture label, alias creation, offline log, and invalid structured capture input.

## Issue #19 Negative / Edge Cases
- Invalid barcode and OFF missing/zero core nutrition are never cached.
- Blank/missing source note, negative/non-finite nutrition, and non-positive serving grams are structured failures without writes.
- Returned branded USDA records, generic records with unmatched query tokens, and any explicit SKU never become generic proxies.

## Issue #19 Acceptance Gates
- [x] Focused tests witnessed RED before implementation.
- [x] `pytest -q` — 155 passed.
- [x] `ruff check .` — clean.
- [x] Literal isolated temp-DB smoke passes.
- [x] `git diff --check` and scoped diff audit pass.
- [x] One local conventional commit; no push or PR.

## Issue #17 Source
- Task: Validate the OFF full-text provider contract from GitHub issue #17.
- Plan file: `docs/plans.md`
Expand Down
2 changes: 1 addition & 1 deletion nomnomcli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Deterministic nutrition tracking for humans and their agents."""

__version__ = "0.3.0"
__version__ = "0.4.0"
Loading
Loading