Skip to content

feat: external outfit authoring for suggestions and pairings - #156

Open
jansitarski wants to merge 6 commits into
Anyesh:mainfrom
jansitarski:feat/external-outfit-authoring
Open

feat: external outfit authoring for suggestions and pairings#156
jansitarski wants to merge 6 commits into
Anyesh:mainfrom
jansitarski:feat/external-outfit-authoring

Conversation

@jansitarski

@jansitarski jansitarski commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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/suggest and /pairings/generate 503 by design, and until now there was no write surface for anything else to fill that role.

  • New external value on outfit_source — names the write path (authored through the external authoring API), consistent with phase 2's server-derived auto|manual origins. Never accepted from a request body.
  • Four nullable authoring attribute columns on outfits: season, formality (length-capped strings; canonical vocabulary documented in app/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 one OutfitAttributeFields base. Existing rows and internal generation paths are unaffected.
  • POST /outfits/suggestions persists an authored suggestion: occasion validated against the existing VALID_OCCASIONS, item positions follow request order, scheduled_for defaults to the user's local today, ownership violations return 403 with OUTFIT_ITEM_OWNERSHIP, rate-limited 20/min. Lands as a normal pending suggestion 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 existing GET /pairings/item/{item_id} and reuses PairingResponse.
  • Pairing reads and deletes now match "internally generated, or externally authored around a source item" (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 /capabilities flips external_suggestions and external_pairings to true, per the phase-1 contract (flags flip in the PR that ships the write surface).
  • Studio composer accepts the same four attributes (separate droppable commit; gated by the existing studio kill switch — the authoring endpoints are not).
  • Frontend: external joins the OutfitSource unions and gets a labeled badge in the outfit card, history card, family feed, and pairing card — the existing Record<OutfitSource, …> badge maps would throw on the unknown key, and the outfit card would otherwise mislabel agent-authored outfits as "AI". Badge labels go through next-intl with entries added to all 8 locale catalogs (i18n:check clean). The history calendar counts external toward 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/suggest and /pairings/generate are unchanged.

Related Issue

Related to #99; phase 3 of the series (#113, #120)

Type of Change

  • New feature (non-breaking change that adds functionality)

Checklist

  • I have read the CONTRIBUTING guide
  • My code follows the project's coding style
  • I have added tests that prove my fix/feature works
  • New and existing tests pass locally
  • I have updated documentation as needed
  • My changes don't introduce new warnings or errors

Testing

Test Environment

  • Docker Compose
  • Kubernetes
  • Local development

Tests Performed

  • New 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.
  • Full backend suite green (453 passed); ruff check and ruff format clean; migration upgrade and downgrade verified (e5f6a7b8c9d0, parented on d4e5f6a7b8c9).
  • Frontend: tsc --noEmit clean; vitest suite green (111 tests); i18n:check (keys, parity, scan) clean.
  • End-to-end validated against a live Kubernetes deployment (internal AI off) through an MCP client: authored suggestion and pairing with full attribute round-trip, server-set occasion and source-item block, positions in request order, scheduled_for defaulting to the user's local date, and the External labeling/calendar indicators rendering on live data.

Screenshots (if applicable)

Screenshot from 2026-07-31 00-33-03 Screenshot from 2026-07-22 02-02-51 Screenshot from 2026-07-22 02-02-27

Additional Notes

  • Provenance is server-derived. source=external comes 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's auto|manual).
  • Non-goals, stated to bound the surface: no demand-signal/request queue for suggestions (the external agent is pull/conversation-driven); no suggestion-cache invalidation on external authoring (no correctness impact); authored outfits do not carry weather_data or per-item layer_type.
  • Internally-generated pairing rows have never produced a calendar indicator (they rarely carry scheduled_for); that pre-existing behavior is left untouched.
  • The branch is organized as six independently reviewable commits: enum + attribute columns + migration → authoring endpoints → capabilities flip → tests → studio extension (droppable) → frontend labeling (droppable).
  • The consuming MCP-side changes ship in feat(outfits): add the external outfit-authoring tools jansitarski/wardrowbe-mcp#29 (create_outfit_suggestion, create_item_pairing, attributes on create_outfit) and were validated end-to-end against this branch.

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 jansitarski changed the title feat: external outfit authoring for suggestions and pairings (phase 3) feat: external outfit authoring for suggestions and pairings Jul 30, 2026
@jansitarski
jansitarski marked this pull request as ready for review July 30, 2026 22:51
@jansitarski

jansitarski commented Jul 30, 2026

Copy link
Copy Markdown
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant