Categories resolve through the §1.3 machine tree (backend/taxonomy_data/,
generated from docs/plans/garment-taxonomy.md by generate_taxonomy_tree.py):
tee/T-shirt/T-shirts → T-Shirt, sub-types fold to parents, anything else →
empty + taxonomy_review flag. Colours collapse to bare CANONICAL_COLOURS by
measured hex (blue-gray → whichever it is more of), unknown → Unknown +
review. taxonomy_review is ingest bookkeeping: excluded from the item
sidecar all_tags and from Save-to-Library GarmentTag rows. 28 new tests +
2 adapter regression tests; test file registered in ci.yml + deploy-dev.yml.
The bearer token no longer lands in any script-readable storage. Login and
Google sign-in set an HttpOnly + Secure + SameSite=Lax `sapl_session` cookie,
and the frontend stores only the non-secret user snapshot (any legacy
`sapl_token` key is removed on sight). The Bearer path is unchanged for
machine clients — tests, scripts and the QA harnesses keep using the body
token / Authorization header — so nothing that cannot hold a cookie breaks.
Backend
- routers/auth.py: `_set_session_cookie` / `_clear_session_cookie` helpers;
`current_user` now resolves header OR cookie; new idempotent POST /logout.
- config.py: AUTH_COOKIE_NAME (sapl_session) + AUTH_COOKIE_SECURE (default
true; set false only for a plain-http localhost dev run).
Frontend
- axios uses `withCredentials` and no longer attaches an Authorization
header; direct fetches (techpack previews/downloads, seasons, gallery,
org chart) send `credentials: 'include'`.
- sign-out controls call `logout()` (clears the server cookie, then the
local snapshot) before navigating — a local-only clear would leave the
session alive server-side.
- `isAuthed()` reads the user snapshot; the 401 interceptor still heals a
stale snapshot.
CSP
- script-src loses 'unsafe-inline': the built bundle carries ZERO inline
scripts (only /assets/*.js and /registerSW.js, both 'self') and Google's
documented GSI policy needs none. style-src keeps it (React style
attributes, the login <style> block, GSI's injected styles).
Tests
- backend/tests/test_auth_session_cookie.py (offline; wired into the CI
router-test step): cookie attributes, cookie/header/anonymous resolution,
logout expiry, and no cookie on a failed login.
- frontend/src/lib/auth.test.ts: token never persisted; legacy key cleanup.
- seasons.test.ts signIn() now seeds the user snapshot.
Note: sessions from pre-#1346 builds end at the next request after deploy —
everyone signs in once more.
The garment's identity is its style number (QR scan URL, hangtag, every TNA
join key). Pattern number is a pattern-master reference that routinely lands
after the sample, so it must not gate an approval.
- contract: MANDATORY_FIELDS = (style_number, fabric_quality, counters) in
services/qr_fields.py, mirrored by frontend/src/lib/qrFields.ts; the
QR_MANDATORY_FIELDS / missing_qr_fields names stay as aliases so the gates,
the QR endpoint and the CI test list keep working.
- approval gates + POST /api/plm/{id}/qr now require the style number; it is
listed exactly once (no double-listing in the refusal payload).
- closed the identity hole in PUT /api/garments/{id}: blank, whitespace-only
and explicit JSON null are all 422 STYLE_NUMBER_REQUIRED and the row is left
untouched. The edit form used to send null when the field was emptied, which
silently erased the number.
- form: Style number carries the "· required" marker and refuses to save;
Pattern no. loses its "· QR" marker and is marked optional.
- capture drafts are safe: garment_capture_save_service reserves a style number
at save time, so no draft is blocked by the new rule.
- tests: inverted the null-clears-the-number case, added pattern-less-approval
and per-field blocking coverage.
Existing style-less rows are deliberately left un-approvable until someone
fills the number in (no backfill, no data writes).
A 0.01ms keyframe still only STARTS on the frame after the element is inserted,
so the panel's first painted frame was the keyframe's `from` state (opacity 0)
even with prefers-reduced-motion: reduce. Verified live on dev by the harness's
8a check ('panel fully opaque on the first frame' -> measured 0).
`animation: none` paints the element's natural style immediately and removes
the row stagger's delays with it.
Also decides the panel's open/close from an imperative `phase` ref instead of
the render closure, so a close+re-open landing in ONE task re-opens the panel
instead of being swallowed as a second close (harness 6b).
And fixes the harness itself: it sampled the panel synchronously right after a
programmatic .click(), i.e. before React's commit, which reported 'panel never
mounted' for a working panel (checks 1/2/3a/3b/8a); the sample now starts on the
first animation frame, and the gates scan every sample. It also fell back from
channel='chrome' (not installed on the verifying VPS) to bundled chromium.
Pratik: the "From the deck" strips showed the same ~8 pages under every
category. The filter included any page that merely mentioned a category, and
most deck pages are full looks (colour+fabric+silhouette) → qualified for all.
Replace with assignDeckPages: each page goes to a single category — its
page_type enum (colour_direction/fabric/print/silhouette) wins, else its
dominant content array. No page repeats across sections; each strip is
genuinely relevant. Verified on a real 14-page decode.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pratik feedback — the decode read as a wall of text. The deck pages were
already rendered + tagged during analysis but never displayed. Add a
"From the deck" thumbnail strip (deckPagesFor + DeckStrip) to the Colour /
Fabric / Print / Silhouette sections, sourced from page_results by category
(ranked by relevance, click → lightbox). Zero extra AI cost.
Also render the Fabric/Print/Silhouette sections even when no AI swatches
were generated (previously hidden) — deck strip + brief text — so a
text-only brief still surfaces the buyer's imagery.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main was 17 commits behind + diverged from uat; promoted via cherry-pick of
the 7 session commits onto main (261b0e6), Dokploy deploy verified live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Concept/Product Studio + moodboard outputs were one-shot generators whose
images never entered the library. Persist them as first-class, origin-tagged
entries so the public library stays clean.
- Garment.origin (made|ai_concept) + origin_source; rbac self-heal
(existing rows default to 'made').
- filter_search excludes ai_concept by default (concepts never leak into the
main library, studio pickers, similar, or boards); origin=ai_concept|all
param threaded through /garments + /search/filter; origin in serializer.
- POST /api/garments/concept: copies a generator image into the library store
and creates an origin=ai_concept garment (no style number / QR / production).
- Frontend: Library Made/Concepts tab toggle; amber CONCEPT badge on cards;
concept cards titled by description; saveConcept client fn; "Save to Library"
on Concept Studio + Product Studio results + moodboard starter-kit cards.
Validated live: concept created → excluded from Made grid, shown in Concepts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Technical flat sketches generated unconditionally and the Design Starter
Kits section was assembled regardless of what the designer chose — so
picking only "print & fabric swatches" still produced garment flats + a
full starter-kit block they never asked for (wasted image spend on a
garment design they won't use).
Starter kits are a garment-design deliverable, so tie them to the garment
opt-in: wants_garments = generate_visuals AND "concepts" in visual_types
now gates construction-spec drafting, flat-sketch generation, and the
starter-kit assembly. Swatches-only runs skip all of it (frontend already
hides an empty section); "garment concepts"/"everything" get the full kit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Fabric direction" cards showed in-house garment matches for fabrics
SAPL doesn't stock. Calibrating against the live gemini-embedding-2 index
proved cosine similarity can't judge availability (a suede query scores
0.69 > cotton poplin 0.66; nonsense 0.51), so availability must be a DB
fact.
- _fabric_status: vocabulary-driven, matches STRUCTURED fabric fields only
(composition/weave/texture/pattern), never free-text description — kills
the blend/soft/coloured false-positives; replaces loose check_fabric_library
in the moodboard grounding.
- _match_library_per_item: gap-gate — a sourcing-gap fabric surfaces zero
in-house garments.
- embed_texts: gemini-embedding-2 returns ONE embedding for contents=[N],
silently collapsing N per-item queries to 1; embed per-text instead
(restores silhouette/print matching that was dropped by a zip).
- _enrich_matches: calibrated similarity floor (moodboard_match_min_similarity
0.58, env MOODBOARD_MATCH_MIN_SIM) drops nonsense-tier hits.
- Frontend: subtle library-status hint on fabric cards (in / similar / not in
library) so an empty gap card doesn't read as broken.
Validated live vs real DB: tweed/suede/terry→gap (no in-house), cotton
poplin/twill→have, satin/organza+viscose→similar.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the static pulsing-dots in PdfDecodeProgress with the "cat Mark
loading" dotLottie. Move the file into src/assets (no space) so it goes
through the vite ?url bundle + cache-busting like the other loaders, and
add a DecodingLottie preset to LottieLoader.
Docs: archive the granular 2026-07-25 scraper iteration log to
STATUS-ARCHIVE.md and slim STATUS.md to a summary; mark the moodboard
interactive-flow work as shipped (da212d0, live on UAT).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Notifications (models/notification.py, services/notification_service.py,
routers/notifications.py):
- The bell — a notification lands for the content owner (+ thread participants)
when someone likes/comments on their post or a board/brief/trend they shared.
Self-actions don't notify; ownerless system events (ranges/orders) don't spam.
- Announcements (admin/management-gated) fan out to every active user and post
to the feed with a 📢.
- Frontend: NotificationBell in TopNav (unread badge + dropdown) + an Announce
toggle in the feed composer. Verified smoke_test_notifications 8/8.
Department-scoped RBAC (services/rbac.py):
- One role per department managing only its own features on top of the common
surfaces (library/boards/ingest): Admin (global) · Designer · Merchant ·
Planner · Auditor · Leadership · Viewer. Users/Data stay Admin-only.
- Seed keeps code-owned system roles in sync and retires removed ones (0-user
guard). Personas map to the roles (feed_digest._persona).
- A Designer sees only the design + common nav; shared boards open for all
(common) but shared briefs/trends stay department-gated.
Admin consolidation (Admin.tsx, routers/users.py):
- Users tab Role/Department become read-only labels (Status + Actions remain);
the Roles tab "add someone" now sets role + department together.
Verified: feed 31/31, notifications 8/8, playwright 13/13; tsc -b + build +
py_compile clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dehx6goqDV2TiQ92dFFWGP
- design-library-feed.md / design-library-plan.md: the "OS for design" home-feed
plan + reconciliation.
- wfx-oc-api-dummy-review-2026-07-16.md: field-by-field review of WFX's dummy JSON;
grain settled (OC = whole order, deliveries nested), StyleID/OrderCode added,
history via date-range, docs via temporary per-OC SharePoint URL (download+keep),
Saved-OC handling = Pratik's Option A (track from email, lot number links later).
- STATUS.md + tna-governance-platform-plan.md: reflect the above.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dehx6goqDV2TiQ92dFFWGP
Sprint-0 items from Pratik's governance direction (all zero-decision, unblocked):
- Corpse-archive filter: services/wfx_service.orders_without_tna hides orders >120d
overdue (CORPSE_MAX_OVERDUE_DAYS, exclude_corpses param) so red stays meaningful —
scoped to /tna/no-tna (all-orders unaffected); response returns an `archived` count.
Also raised the /no-tna cap to 500 so the worklist has the full set to paginate.
- Plain-language milestone names: utils/tnaLabels.milestoneLabel maps raw Stitch jargon
("Bulk Flow Start IH Date") + Category-template names to merchant-readable labels;
applied to Create-TNA, LifecycleTimeline, and PERT (raw kept as a hover tooltip).
- Orders needing a plan: 5-per-page pagination (urgency bands become inline headers)
+ an "N archived" note next to the count.
- docs: governance platform plan (synthesis of the 9-Jul blueprint) + STATUS update.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the buyer-name fuzzy match with Sharavana's real model. The plan is chosen
by value-add category × fabric type; lead time is deterministic from the grid; the
engine selects the real synced Stitch template ("Category N - X Days") and back-
schedules its authoritative milestones (no hardcoding — Sharavana maintains them in
Stitch, we re-sync). services/tna_catalog.py holds only what's not in Stitch: the
(category × fabric) → tier grid + the WFX→category/fabric pre-fill guessers.
- recommend(): select "Category {N} - {gridLead} Days" mirror template, schedule_backward
its items; encoded catalog kept only as a fallback if a template hasn't synced.
- _resolve_order_context: returns garment/category/fabric pre-fill guesses from WfxOrder.
- POST /tna/draft: takes category/fabric_type/garment (defaulting to the guesses);
push builds from the selected mirror template. TnaRecommendation gains
category/fabric_type/garment (self-healed).
- Frontend TnaCreate: two dropdowns (value-add, fabric) + auto-derived lead, replacing
the template picker + free-text lead box.
- docs/plans/STATUS.md: P1/P2 progress + infra lessons.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Buildable-now foundation for auto-completing TNA milestones (no live-integration
dependency), per docs/plans/tna-milestone-autocomplete.md and Pratik's 3 Jul data
reality:
- services/tna/milestone_map.py — classify each milestone by source (WFX order/PO/
GRN · Stitch production · SAPL approval · manual) + coverage (AUTO_NOW/NEEDS_QR/
MANUAL).
- services/tna/reconcile.py — idempotent, monotonic completion + delay + order-risk
(never un-ticks a done milestone; measures delay vs the frozen plan).
- Phase 2b: models/tna_approval.py (append-only approval log) + services/tna/
approvals.py; decide_recommendation now records each planner decision, feeding the
Approvals milestone auto-tick with zero ERP dependency.
- scripts/smoke_test_tna_phase0.py — 24/24 passing.
Not wired into any live sync yet; ready for Phase 1 when Stitch's TNA Task API lands.
NOT deployed (prod deploys are on hold after the 7 Jul incident).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adopt Google's Open Knowledge Format (curated markdown + YAML frontmatter)
as KAARIGAR's domain-knowledge layer — not a RAG replacement (KAARIGAR is a
tool-calling agent), but the stable, curated knowledge it needs every turn to
reason correctly about SAPL.
- New OKF bundle backend/knowledge/ (glossary, wfx-oc + tna-milestones schemas,
tna-approval rule, stitch-ids join, when-to-use tool guide), interlinked.
- Loader services/hermes/knowledge.py: frontmatter-stripped, lru_cache, fail-soft.
- agent.py injects it into system_instruction (~2.5K tokens/turn).
- Tested live (real Gemini, local DB): tool routing + delay/owner + Library
facet behavior all match the curated rules.
Also: STATUS.md updated; TNA milestone auto-complete plan added.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
"show me beige dresses" now applies the real Refine facets (chips +
filtered grid + count) instead of listing one item and pulsing a card.
- Library.tsx reads URL facet params (?colour=&category=&fabric=&dna=
&season=&q=) -> native facetSel selections (substring match, _->space)
- search_library emits a "Show in Library" action with those facets; one
call both answers and filters the UI. Colour matching made lenient to
match the Library's Colour facet (counts agree: beige=8).
- prompt: browse intents -> filter UI (one-line reply, no long lists,
single call); factual -> text; TNA "take me to" -> navigate+highlight.
Verified: beige dresses -> /?colour=beige&category=dress; beige pieces
-> 8; "how many late" -> text only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
search_library was semantic-only, so 'black dresses' returned fuzzy,
polluted results (8 sources for 3 real matches). Now the model extracts
attributes into structured params (category/colour/fabric_family/
design_dna/season) matched exactly against tags; colour is role-aware
(dominant Hero colour). Citations == matched set.
Verified: black dresses -> 3 (all black-dominant); navy blouses -> 2;
knit tops -> 1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HERMES no longer just answers; it can drive the app. New `navigate` tool
lets the agent take the user to a page and highlight a specific item; the
backend returns actions[] with focus-aware paths (/tna?focus=order:...).
A global HermesFocusRunner scrolls to + pulses the matching
[data-hermes-focus] card once the page loads. AskHermes auto-runs the
action (cancellable). Tagged TNA order cards + Library garment cards.
Verified: "take me to the late Primark order" -> query_orders+navigate
-> /tna?focus=order:8863-1264674; broad info queries don't navigate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 of the master-brain unifier (scope-hermes-for-pratik.md §9-12).
An LLM agent whose tools are our own backend functions: the user asks in
natural language, the agent picks tool(s), they run server-side AS the
logged-in user (RBAC applies), and it answers over the real results with
clickable citations. Reuses the Market Scraper's Gemini function-calling
loop, generalised and made synchronous.
- services/hermes/tools.py: search_library + query_orders (portable,
{summary, rows, citations})
- services/hermes/agent.py: ask_hermes sync loop (never raises)
- routers/hermes.py: POST /api/hermes/ask (auth as current user)
- frontend AskHermes.tsx: floating Ask bar (⌘J) + grounded answer +
citation cards; mounted globally
Tools verified against real data locally; live Gemini answer verified on
prod (local key is 403 project-denied, same as Scraper/Concept Studio).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
History in Trends and Moodboard (and Boards) leaked across users
because those sections were scoped by department, not by user.
Rework them to per-user ownership:
- rbac: add can_see_all(), owner_scope_query(), can_access_owned()
- add created_by to TrendSearch + MoodboardDecode (boot-time self-heal);
Boards reuse existing owner_id
- scraper/moodboard/collections: stamp owner on create, scope every
list, and 404-guard every detail-GET + mutation by owner
- by-garment saved-state only reflects the caller's own boards
Library + Fabric stay public; TNA/Inbox/Review/Analytics stay
team-shared; Admins and 'all'-scope roles still see everyone's.
Verified: scripts/smoke_test_user_isolation.py 14/14.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Append a 2026-06-26 section to the HERMES scope note: promote HERMES from the TNA
WhatsApp coach to the atelier's brain across the whole app (Library/Studio/Trends/
Inbox/TNA). One persona, two surfaces (proactive push + an "Ask HERMES" bar); the
TNA coaching becomes its flagship skill. Notes the agent-with-tools design (reuses
our Gemini function-calling engine), grounding + RBAC, carried-over privacy
guardrails, and a 3-phase rollout (in-app Ask + morning brief first; person-level
WhatsApp + scorecards last, HR-gated).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The demo link's fillDemo helper hard-coded real demo credentials into the frontend
bundle, so anyone with the login URL or viewing source could sign in. Remove the
link, the fillDemo helper, and its CSS. Verified the creds are no longer present in
the built bundle.
Follow-up: the demo account still exists server-side (and the creds are now in git
history) — deactivate it or rotate its password via /admin for full closure.
tsc -b + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Same fit-to-page treatment: h-screen flex layout, compact custom header (drop
PageHeader), height-cap the two input slots (aspect-[3/4] -> h-36), tighten the
description/dimensions/aspect controls with Generate pinned at the bottom, and let
the result panel fill the column height. The old two aspect-[3/4] boxes pushed the
controls below the fold.
Fit smoke 3/3 PASS (scrollHeight==innerHeight at 900px, Generate visible, both
slots present). tsc -b + build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>