Kategorie-Wissensbasis pro Einkaufsliste statt global scopen #178

Closed
opened 2026-09-07 10:06:28 +02:00 by lena · 4 comments
Collaborator

Story: Kategorie-Wissensbasis pro Einkaufsliste statt global scopen

As a Nutzer eines Haushalts mit eigener Einkaufsliste,
I want to dass gelernte Produkt-zu-Kategorie-Zuordnungen nur innerhalb meiner eigenen Liste gelten,
so that die Kategorie-Vorschläge zu meinem eigenen Kategorie-System passen, statt von fremden Haushalten mit anderen Kategorie-Namen beeinflusst zu werden.

Kontext (verifiziert im Code):

  • ProductSectionKnowledgeEntity ist aktuell app-weit, nicht listen-gebunden — eigener Kommentar im Code: "app-wide (not list-scoped) product-name -> generic-section-name knowledge base ... grown by every user who adds a genuinely new product to any shopping list". Felder: nur Id, ProductName, SectionName — kein ShoppingListId, kein UserId, nicht mal ein Unique-Index auf ProductName (bewusst, um mehrere Sektionsnamen pro Produkt zu erlauben).
  • ShoppingCategoryResolver.css ResolveFromKnowledgeBase liest ohne jede Filterung nach Liste/Nutzer aus der gesamten Tabelle; ContributeToKnowledgeBase schreibt ebenso ungefiltert global.
  • Entscheidung mit dem Menschen (2026-09-07): Scope wird auf pro Einkaufsliste umgestellt (nicht global, nicht pro Nutzer).
  • Die Speisekammer hat kein server-seitiges Äquivalent (nur die geteilte, rein client-seitige Embedding-Ähnlichkeits-Vorschlags-Badge über useCategorySuggestions/useUncategorizedCategorySuggestions) — betrifft diese Story nicht direkt.

Acceptance criteria:

  • ProductSectionKnowledgeEntity bekommt eine ShoppingListId-Zuordnung (Migration erforderlich).
  • ResolveFromKnowledgeBase schlägt nur noch Sektionen vor, die aus derselben Einkaufsliste gelernt wurden.
  • ContributeToKnowledgeBase schreibt neue Zuordnungen mit der ShoppingListId der Liste, auf der das Produkt tatsächlich angelegt wurde.
  • Eine neue, leere Einkaufsliste hat entsprechend keine automatischen Kategorie-Vorschläge mehr aus fremden Listen (bewusster Trade-off, siehe Story-Beschreibung).

Out of scope for this story:

  • Die rein client-seitige Embedding-Vorschlags-Badge (useCategorySuggestions) — unverändert, betrifft ein anderes System.
  • Die Speisekammer — hat aktuell keine serverseitige Wissensbasis.

Open questions: (escalate to human if unanswered)

  • Migrationspfad für die heute bestehenden, global gesammelten Einträge: komplett verwerfen (jede Liste startet bei Null), oder als einmaliger "Seed" pro bestehender Liste übernehmen, bevor der Scope greift? Die Entity trägt laut Kommentar ohnehin schon CSV-geseedete Einträge — bleiben die weiterhin global/allen Listen als Fallback zugänglich, oder werden auch sie pro Liste dupliziert?
## Story: Kategorie-Wissensbasis pro Einkaufsliste statt global scopen **As a** Nutzer eines Haushalts mit eigener Einkaufsliste, **I want to** dass gelernte Produkt-zu-Kategorie-Zuordnungen nur innerhalb meiner eigenen Liste gelten, **so that** die Kategorie-Vorschläge zu meinem eigenen Kategorie-System passen, statt von fremden Haushalten mit anderen Kategorie-Namen beeinflusst zu werden. **Kontext (verifiziert im Code):** - `ProductSectionKnowledgeEntity` ist aktuell **app-weit**, nicht listen-gebunden — eigener Kommentar im Code: "app-wide (not list-scoped) product-name -> generic-section-name knowledge base ... grown by every user who adds a genuinely new product to any shopping list". Felder: nur `Id`, `ProductName`, `SectionName` — kein `ShoppingListId`, kein `UserId`, nicht mal ein Unique-Index auf `ProductName` (bewusst, um mehrere Sektionsnamen pro Produkt zu erlauben). - `ShoppingCategoryResolver.cs`s `ResolveFromKnowledgeBase` liest ohne jede Filterung nach Liste/Nutzer aus der gesamten Tabelle; `ContributeToKnowledgeBase` schreibt ebenso ungefiltert global. - Entscheidung mit dem Menschen (2026-09-07): Scope wird auf **pro Einkaufsliste** umgestellt (nicht global, nicht pro Nutzer). - Die Speisekammer hat kein server-seitiges Äquivalent (nur die geteilte, rein client-seitige Embedding-Ähnlichkeits-Vorschlags-Badge über `useCategorySuggestions`/`useUncategorizedCategorySuggestions`) — betrifft diese Story nicht direkt. **Acceptance criteria:** - [ ] `ProductSectionKnowledgeEntity` bekommt eine `ShoppingListId`-Zuordnung (Migration erforderlich). - [ ] `ResolveFromKnowledgeBase` schlägt nur noch Sektionen vor, die aus derselben Einkaufsliste gelernt wurden. - [ ] `ContributeToKnowledgeBase` schreibt neue Zuordnungen mit der `ShoppingListId` der Liste, auf der das Produkt tatsächlich angelegt wurde. - [ ] Eine neue, leere Einkaufsliste hat entsprechend keine automatischen Kategorie-Vorschläge mehr aus fremden Listen (bewusster Trade-off, siehe Story-Beschreibung). **Out of scope for this story:** - Die rein client-seitige Embedding-Vorschlags-Badge (`useCategorySuggestions`) — unverändert, betrifft ein anderes System. - Die Speisekammer — hat aktuell keine serverseitige Wissensbasis. **Open questions:** (escalate to human if unanswered) - Migrationspfad für die heute bestehenden, global gesammelten Einträge: komplett verwerfen (jede Liste startet bei Null), oder als einmaliger "Seed" pro bestehender Liste übernehmen, bevor der Scope greift? Die Entity trägt laut Kommentar ohnehin schon CSV-geseedete Einträge — bleiben die weiterhin global/allen Listen als Fallback zugänglich, oder werden auch sie pro Liste dupliziert?
lena self-assigned this 2026-09-07 17:59:33 +02:00
Author
Collaborator

Claimed. Starting implementation.

Architect decision on the open migration question (no cross-agent disagreement, resolving without human escalation per the escalation criteria in 00_team_overview.md):

Existing global ProductSectionKnowledgeEntity rows are discarded, not seeded per list. The entity has no ShoppingListId on any existing row, so there is no factual basis to attribute a historical entry to one particular list over another - any seeding choice (e.g. "seed into every list") would fabricate list-specific history that never actually happened on that list, and directly contradicts this story's own accepted trade-off that a list without its own learned history gets no suggestions. Discarding is also the simpler, lower-risk migration (drop rows in the same migration that adds the FK, no name-matching heuristics, no risk of silently misattributing a CSV-seeded generic entry to the wrong household's list).

Plan: add ShoppingListId (non-nullable) to ProductSectionKnowledgeEntity via migration that also truncates existing rows; scope ResolveFromKnowledgeBase/ContributeToKnowledgeBase in ShoppingCategoryResolver.cs to the current list; add handler + component tests.

Claimed. Starting implementation. Architect decision on the open migration question (no cross-agent disagreement, resolving without human escalation per the escalation criteria in 00_team_overview.md): **Existing global `ProductSectionKnowledgeEntity` rows are discarded, not seeded per list.** The entity has no `ShoppingListId` on any existing row, so there is no factual basis to attribute a historical entry to one particular list over another - any seeding choice (e.g. "seed into every list") would fabricate list-specific history that never actually happened on that list, and directly contradicts this story's own accepted trade-off that a list without its own learned history gets no suggestions. Discarding is also the simpler, lower-risk migration (drop rows in the same migration that adds the FK, no name-matching heuristics, no risk of silently misattributing a CSV-seeded generic entry to the wrong household's list). Plan: add `ShoppingListId` (non-nullable) to `ProductSectionKnowledgeEntity` via migration that also truncates existing rows; scope `ResolveFromKnowledgeBase`/`ContributeToKnowledgeBase` in `ShoppingCategoryResolver.cs` to the current list; add handler + component tests.
Author
Collaborator

Done, pushed as ba32410.

Scope: ProductSectionKnowledgeEntity (product-name -> category-section-name auto-categorization knowledge base) is now scoped per ShoppingListId instead of app-wide.

Changes:

  • ProductSectionKnowledgeEntity gets a required ShoppingListId FK (cascade-delete with the list), mirroring ShoppingCategoryEntity's existing pattern.
  • ShoppingCategoryResolver.ResolveFromKnowledgeBase/ContributeToKnowledgeBase both filter reads and writes by the caller's ShoppingListId - already authorized upstream via AuthorizeShoppingListAccessForCurrentUserQuery on CreateShoppingProductCommand.
  • Migration AddShoppingListIdToProductSectionKnowledge deletes existing rows (including the #90 CSV seed) before adding the FK - see the earlier comment on this issue for why (no existing row carries any list attribution to migrate from, and the story's own AC already accepts a list with no learned history getting no suggestions).

Tests: updated existing CreateShoppingProductCommandHandlerTests to set ShoppingListId on inserted knowledge rows, added a new test (Does_not_use_a_knowledge_base_entry_learned_on_a_different_list) proving cross-list leakage no longer happens, and asserted the contributed row carries the correct ShoppingListId.

Verification: dotnet build Cqs.sln -c Release (0 errors) and Docker-free Common.Tests (119/119) + ReactUi build both green locally. Docker was unavailable in this session (confirmed via docker.exe version timing out), so the Testcontainers-backed Checkly.Tests for this change - including the new list-scoping test - could not run locally; relying on the real CI run for that (git.butzei.de/robert/todo/actions), which was still queued behind other pushes at push time (FIFO runner queue under concurrent loop sessions, per existing team-memory note - not a build failure). Self-review + the security-review skill both ran clean (the change narrows a pre-existing cross-tenant read/write, doesn't introduce one).

Out of scope (per the story): the client-side embedding-similarity suggestion badge (useCategorySuggestions) and the pantry, which has no server-side equivalent - both untouched.

Done, pushed as ba32410. **Scope:** `ProductSectionKnowledgeEntity` (product-name -> category-section-name auto-categorization knowledge base) is now scoped per `ShoppingListId` instead of app-wide. **Changes:** - `ProductSectionKnowledgeEntity` gets a required `ShoppingListId` FK (cascade-delete with the list), mirroring `ShoppingCategoryEntity`'s existing pattern. - `ShoppingCategoryResolver.ResolveFromKnowledgeBase`/`ContributeToKnowledgeBase` both filter reads and writes by the caller's `ShoppingListId` - already authorized upstream via `AuthorizeShoppingListAccessForCurrentUserQuery` on `CreateShoppingProductCommand`. - Migration `AddShoppingListIdToProductSectionKnowledge` deletes existing rows (including the #90 CSV seed) before adding the FK - see the earlier comment on this issue for why (no existing row carries any list attribution to migrate from, and the story's own AC already accepts a list with no learned history getting no suggestions). **Tests:** updated existing `CreateShoppingProductCommandHandlerTests` to set `ShoppingListId` on inserted knowledge rows, added a new test (`Does_not_use_a_knowledge_base_entry_learned_on_a_different_list`) proving cross-list leakage no longer happens, and asserted the contributed row carries the correct `ShoppingListId`. **Verification:** `dotnet build Cqs.sln -c Release` (0 errors) and Docker-free `Common.Tests` (119/119) + `ReactUi` build both green locally. Docker was unavailable in this session (confirmed via `docker.exe version` timing out), so the Testcontainers-backed `Checkly.Tests` for this change - including the new list-scoping test - could not run locally; relying on the real CI run for that (`git.butzei.de/robert/todo/actions`), which was still queued behind other pushes at push time (FIFO runner queue under concurrent loop sessions, per existing team-memory note - not a build failure). Self-review + the `security-review` skill both ran clean (the change narrows a pre-existing cross-tenant read/write, doesn't introduce one). **Out of scope (per the story):** the client-side embedding-similarity suggestion badge (`useCategorySuggestions`) and the pantry, which has no server-side equivalent - both untouched.
lena closed this issue 2026-09-07 18:09:04 +02:00
Author
Collaborator

Correction from direct human feedback (2026-09-07), reverted in f7ead53.

The per-list isolation implemented above was the wrong direction. Actual product intent, stated directly: every shopping list (new or existing) should keep drawing suggestions from the full pooled knowledge base - the 200 pre-seeded CSV categories plus everything learned across every hosted list - specifically to minimize manual categorization work, not to wall lists off from each other's history.

f7ead53 cleanly reverts ba32410 (ProductSectionKnowledgeEntity/ShoppingCategoryResolver/CreateShoppingProductCommandHandler/tests/comment all restored byte-identical to their pre-#178 content, verified via git diff) and drops the AddShoppingListIdToProductSectionKnowledge migration outright rather than adding a compensating one - confirmed safe because it was never applied anywhere (CI was still queued and Docker was unavailable in this environment for the entire ~15 minutes between the two commits).

Net effect: the app-wide knowledge base from #90 is unchanged/restored. Closing this back out - no further action pending. If per-list customization is wanted in the future, it needs a fresh story with this corrected premise (pooled base, not isolated).

**Correction from direct human feedback (2026-09-07), reverted in f7ead53.** The per-list isolation implemented above was the wrong direction. Actual product intent, stated directly: every shopping list (new or existing) should keep drawing suggestions from the full pooled knowledge base - the 200 pre-seeded CSV categories plus everything learned across every hosted list - specifically to minimize manual categorization work, not to wall lists off from each other's history. f7ead53 cleanly reverts ba32410 (`ProductSectionKnowledgeEntity`/`ShoppingCategoryResolver`/`CreateShoppingProductCommandHandler`/tests/comment all restored byte-identical to their pre-#178 content, verified via `git diff`) and drops the `AddShoppingListIdToProductSectionKnowledge` migration outright rather than adding a compensating one - confirmed safe because it was never applied anywhere (CI was still queued and Docker was unavailable in this environment for the entire ~15 minutes between the two commits). Net effect: the app-wide knowledge base from #90 is unchanged/restored. Closing this back out - no further action pending. If per-list customization is wanted in the future, it needs a fresh story with this corrected premise (pooled base, not isolated).
lena reopened this issue 2026-09-12 09:26:43 +02:00
Author
Collaborator

Erneut aufgegriffen im Rahmen von #190 (2026-09-12), mit fresh direktem Feedback vom Menschen - keine Neu-Verhandlung der damaligen Entscheidung, sondern eine bewusst andere.

Zur Erinnerung: dieses Issue wurde ursprünglich als "pro Einkaufsliste" implementiert (ba32410), dann nach direktem Feedback wieder auf "app-weit geteilt" zurückgesetzt (f7ead53), mit der Begründung, jede Liste solle weiter von der vollen gemeinsamen Wissensbasis profitieren.

Im Rahmen von #190 (geteilte Barcode-Wissensbasis) wurde dieselbe Grundfrage für ein eng verwandtes System erneut gestellt. Diesmal lautet die explizite Entscheidung: weder rein app-weit noch rein pro-Liste, sondern geteilt innerhalb der Kombination aus Einkaufsliste + der mit ihr verknüpften Vorratsschrank-Liste - dieselbe Holzhausen-Grenze, die #174 für Kategorien bereits etabliert hat.

ProductSectionKnowledgeEntity (dieses Issue) bekommt dieselbe Behandlung wie die neue ProductBarcodeKnowledgeEntity (#190): eine ShoppingListId-Spalte, Lookups/Writes in ShoppingCategoryResolver entsprechend gefiltert. Bereits bestehende, app-weit gesammelte Einträge werden verworfen (keine faktische Zuordnungsgrundlage zu einer bestimmten Liste) - jede Liste lernt Namens-Kategorie-Zuordnungen ab jetzt wieder neu, dafür geteilt mit ihrer eigenen Vorratsschrank-Liste statt mit fremden Haushalten.

Umgesetzt und gepusht als Teil von 9cf7f088 (#190s Backend-Commit). Migration 20260911224536_AddShoppingListIdToProductSectionKnowledge.

**Erneut aufgegriffen im Rahmen von #190 (2026-09-12), mit fresh direktem Feedback vom Menschen - keine Neu-Verhandlung der damaligen Entscheidung, sondern eine bewusst andere.** Zur Erinnerung: dieses Issue wurde ursprünglich als "pro Einkaufsliste" implementiert (ba32410), dann nach direktem Feedback wieder auf "app-weit geteilt" zurückgesetzt (f7ead53), mit der Begründung, jede Liste solle weiter von der vollen gemeinsamen Wissensbasis profitieren. Im Rahmen von #190 (geteilte Barcode-Wissensbasis) wurde dieselbe Grundfrage für ein eng verwandtes System erneut gestellt. Diesmal lautet die explizite Entscheidung: **weder rein app-weit noch rein pro-Liste, sondern geteilt innerhalb der Kombination aus Einkaufsliste + der mit ihr verknüpften Vorratsschrank-Liste** - dieselbe Holzhausen-Grenze, die #174 für Kategorien bereits etabliert hat. `ProductSectionKnowledgeEntity` (dieses Issue) bekommt dieselbe Behandlung wie die neue `ProductBarcodeKnowledgeEntity` (#190): eine `ShoppingListId`-Spalte, Lookups/Writes in `ShoppingCategoryResolver` entsprechend gefiltert. Bereits bestehende, app-weit gesammelte Einträge werden verworfen (keine faktische Zuordnungsgrundlage zu einer bestimmten Liste) - jede Liste lernt Namens-Kategorie-Zuordnungen ab jetzt wieder neu, dafür geteilt mit ihrer eigenen Vorratsschrank-Liste statt mit fremden Haushalten. Umgesetzt und gepusht als Teil von 9cf7f088 (#190s Backend-Commit). Migration `20260911224536_AddShoppingListIdToProductSectionKnowledge`.
lena closed this issue 2026-09-12 09:27:11 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
robert/todo#178
No description provided.