feat: external outfit authoring for suggestions and pairings - #156
Open
jansitarski wants to merge 6 commits into
Open
feat: external outfit authoring for suggestions and pairings#156jansitarski wants to merge 6 commits into
jansitarski wants to merge 6 commits into
Conversation
Adds 'external' to the outfit_source enum — it names the write path (authored through the external authoring API), which the backend can attest to, consistent with the tagging origins — plus four nullable authoring attribute columns (season, formality, palette, notes) on outfits so authored suggestions and pairings can record the qualities the internal AI keeps implicit. Internal generation leaves them unset; existing rows are unaffected.
…airings
Adds the write surface an external agent uses to author the outfits the
internal LLM would otherwise generate:
- POST /api/v1/outfits/suggestions persists an authored suggestion as
Outfit(source=external), validating item ownership.
- POST /api/v1/pairings/item/{item_id} persists an authored pairing as
Outfit(source=external, source_item_id=item_id), mirroring the
GET /pairings/item/{item_id} route shape. The source item leads the
outfit when left out of the partner list, matching the internal
generator.
Both accept the new authoring attributes and are available regardless
of the AI flags; both reuse the studio's ownership semantics (403 with
OUTFIT_ITEM_OWNERSHIP). Outfit and pairing responses expose the new
fields. Pairing list/delete queries now treat externally-authored rows
anchored to a source item as pairings so they surface alongside
generated ones; external rows without a source item are suggestions and
stay out of the pairing surfaces.
The existing 503s on /outfits/suggest and /pairings/generate when text
AI is off are unchanged.
…apabilities The authoring endpoints for externally-authored suggestions and pairings now exist, so the features block reports the full external surface as available.
Covers suggestion and pairing authoring end to end: persistence as Outfit(source=external), request-order item positions, source-item leading, ownership rejection (403 OUTFIT_ITEM_OWNERSHIP), occasion and attribute validation bounds, unknown-field rejection, availability with internal AI disabled, pairing list/delete inclusion, and a default-on regression assertion that internally-generated rows report the authoring attributes as null.
POST /outfits/studio accepts the same optional season, formality, palette, and notes fields as the external authoring endpoints, so a manually composed outfit can carry them too. Source stays manual and the endpoint remains behind the studio kill switch; omitted attributes default to null and existing requests are unchanged.
Adds 'external' to the OutfitSource union and gives it a distinct badge in the outfit card, outfit history, and family feed. Without this the outfit card falls through to the 'AI' badge — misstating provenance — and the two Record<OutfitSource, ...> badge maps throw on the unknown key. Existing sources render unchanged.
jansitarski
marked this pull request as ready for review
July 30, 2026 22:51
Contributor
Author
|
@Anyesh What do you think about shipping an optional MCP server as part of this repo? After this PR the whole external surface is there (tagging, suggestions, pairings, labels), and since the backend is already Python it could be a small service on the official MCP SDK, reusing the existing schemas. Off by default like everything else in this series. I've been running the series end to end through my MCP server against a live deployment, so the API surface is proven. Happy to build the in-repo version as a follow-up PR. That would close #99. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This is phase 3 of making the internal AI optional so the backend can defer generation work to an external agent (e.g. Claude via an MCP server). Phase 1 (#113) added the capability switches and
/capabilities; phase 2 (#120) made item tagging externally ownable. This phase makes outfit suggestions and pairings externally authorable — with internal text generation off,/outfits/suggestand/pairings/generate503 by design, and until now there was no write surface for anything else to fill that role.externalvalue onoutfit_source— names the write path (authored through the external authoring API), consistent with phase 2's server-derivedauto|manualorigins. Never accepted from a request body.outfits:season,formality(length-capped strings; canonical vocabulary documented inapp/schemas/outfit.py, matching the item-tag precedent rather than enum enforcement),palette(JSONB list of colors,[]collapses to null),notes(text). Shared across the authoring and studio request schemas via oneOutfitAttributeFieldsbase. Existing rows and internal generation paths are unaffected.POST /outfits/suggestionspersists an authored suggestion: occasion validated against the existingVALID_OCCASIONS, item positions follow request order,scheduled_fordefaults to the user's local today, ownership violations return 403 withOUTFIT_ITEM_OWNERSHIP, rate-limited 20/min. Lands as a normalpendingsuggestion the user accepts or rejects in the app.POST /pairings/item/{item_id}persists an authored pairing around a source item:occasion="pairing"is set server-side, the source item is auto-prepended when absent from the partner list, unknown source item → 404, no partner items → 400. The route shape mirrors the existingGET /pairings/item/{item_id}and reusesPairingResponse.PAIRING_SOURCE_CLAUSE), so authored pairings appear on the item's pairings page. External rows without a source item are suggestions and stay out; internal pairings whose source item was deleted (SET NULL) remain listed — no regression.GET /capabilitiesflipsexternal_suggestionsandexternal_pairingstotrue, per the phase-1 contract (flags flip in the PR that ships the write surface).externaljoins theOutfitSourceunions and gets a labeled badge in the outfit card, history card, family feed, and pairing card — the existingRecord<OutfitSource, …>badge maps would throw on the unknown key, and the outfit card would otherwise mislabel agent-authored outfits as "AI". Badge labels go throughnext-intlwith entries added to all 8 locale catalogs (i18n:checkclean). The history calendar countsexternaltoward the on-demand indicator dot.Everything is additive and defaults to current behavior. Suggestion/pairing responses gain the four attribute fields; internally-generated rows report them as null. Phase 1's 503s on
/outfits/suggestand/pairings/generateare unchanged.Related Issue
Related to #99; phase 3 of the series (#113, #120)
Type of Change
Checklist
Testing
Test Environment
Tests Performed
backend/tests/test_external_authoring.py(22 tests): suggestion authoring with request-order positions and attribute round-trip;palette: []→ null; occasion validation; ownership 403; pairing authoring with source auto-prepend and dedup; unknown source 404; missing partner 400; authored pairings included in the item's pairing list and deletable; suggestions (no source item) excluded from pairing reads; internal-row regression (attributes null, listing unchanged); studio attribute passthrough; capabilities flip.453 passed);ruff checkandruff formatclean; migration upgrade and downgrade verified (e5f6a7b8c9d0, parented ond4e5f6a7b8c9).tsc --noEmitclean; vitest suite green (111 tests);i18n:check(keys, parity, scan) clean.scheduled_fordefaulting to the user's local date, and the External labeling/calendar indicators rendering on live data.Screenshots (if applicable)
Additional Notes
source=externalcomes from the write path only; there is no way to claim it from a request body, and no attempt to distinguish an agent from any other API client (same reasoning as phase 2'sauto|manual).weather_dataor per-itemlayer_type.pairingrows have never produced a calendar indicator (they rarely carryscheduled_for); that pre-existing behavior is left untouched.create_outfit_suggestion,create_item_pairing, attributes oncreate_outfit) and were validated end-to-end against this branch.