Branding path
The repeatable order in which an agent turns this baseline into a specific customer's B2B shop, using only skills and content changes. Each step names its skill, its tools, the input it needs from customer-context/, and the assert that proves it.
The two standing rules
RULE 1 — branding never edits a layer.
F:\code\Truvio.Commerce.Distribution is the source of the baseline, not a working tree. A
layer is a versioned unit that a gate proved. Editing layers\* to brand a demo means the next
`git pull --ff-only` either clobbers your work or refuses to fast-forward, and the edition you
claim to have composed is no longer the edition you ran. Brand through MCP writes against the
running instance and through files under `Files/`. If something genuinely belongs in the
Distribution, that is a Foundry PR (dw-demo-foldback), not an in-place edit.
The concrete forms of this rule as the skills state them:
"Customer overrides go in `<customer>_custom.css`, never by editing `theme-default`'s own
files." — dw-demo-swift/references/re-skin.md:46
"editing `theme-default`'s `default.*` Style assets in place … the re-skin hard rule forbids
and … the next layer update reverts."
— dw-demo-swift/references/styles-assets.md:104-105
"strip the offending column lines from the STAGED copy, never from the checked-out original
under `distribution\layers\base\`."
— dw-demo-swift/references/deserialize-flow.md:177
RULE 2 — customer-context is read-only.
`<demo>\customer-context\` holds what the customer handed over. Read it; never write it.
Transformed output goes to `<demo>\notes\` or `<demo>\extracts\`. A write to any path
containing `customer-context` is a hard abort with no approve branch. Detection signature at
end of phase: `git status --porcelain "customer-context/"` is empty.
Its sister is the customisations ledger: a write under Controllers/, Providers/, or any
*Controller.cs prompts Approve+log / Refactor instead / Cancel, and every approved row is a
deliberate trade-off you will show on the closing slide.Note for the person filing issues from this plan. There is currently no guarded-write preflight ondistribution\layers\anywhere in the skill set.** The only enforced globs are the controller/provider set andcustomer-context\(dw-demo-base/SKILL.md:149-163). Rule 1 above is assembled from three scattered statements indw-demo-swiftreferences; it is not stated as a contract anywhere the way Rule 2 is. That gap is Finding F-G1.
The overlay convention
customer-context/ is an OVERLAY on the baseline, not a replacement for it. The baseline
supplies structure and mechanism; customer-context supplies nouns — the company name, the
palette, the product families, the account roles, the currency, the price logic, the personas.
Branding is the act of walking the ordered steps below and, at each one, reading one specific
fact out of customer-context and writing it into the instance.
Derived artefacts live in notes/:
notes/wording.md the customer's own vocabulary, harvested from the intro-call notes
and applied across all demo copy ("speak the customer's words")
notes/qa/ QA screenshots and visual-QA evidence
notes/logs/ host logs
notes/snapshots/ DOM / accessibility dumps
notes/credentials.local.md persona passwords (gitignored, never in the tracked ledger)
Root allowlist — only these may sit at the demo root: DEMO-PLAN.md, CLAUDE.md,
CUSTOMISATIONS.md, .gitignore, .mcp.json, and directories. Everything else routes to notes/,
with the same redirect wording as the customer-context contract.
Name evidence for what it IS (`home-desktop-*.jpeg`), never for what it was captured during.
Security-suggestive names for non-secret dumps are forbidden — they read as leaked-secret
files to any human or scanner. Step 1 — Customer context
| Skill | dw-demo-base (dynamo: false) — references/customer-context.md, references/demo-tactics.md |
| Tools | none (filesystem) |
| Input | the whole customer-context/ folder |
| Do | Read every artefact. Produce notes/wording.md: the customer's own words for their products, their customers, their roles, their processes. Decide the single deep storyline — one buyer persona, one CSR persona, one shop, one locale. Write DEMO-PLAN.md at the demo root. |
| Assert | git status --porcelain "customer-context/" is empty, and notes/wording.md exists carrying at least the company name, the product noun, and the two persona role names. |
Demo philosophy that governs every later step: go deep, not wide. Personas floor of 2 (one buyer, one CSR, so impersonation has somewhere to land). Shops/channels 1 + 1. Locale: single home market. Customer-center sections: only the ones the storyline visits. The product catalogue is the deliberate exception — go deep AND wide there. Rich product data is cheap over MCP and makes the demo feel real; a product family does not have to justify itself against demo minutes, but every login, channel, locale and customer-center section does.
Step 2 — Theme overlay
| Skill | dw-swift-page-blocks (dynamo: true) "Color schemes & the design system"; dw-demo-swift (dynamo: false) references/styles-assets.md, references/re-skin.md |
| Tools | get_color_schemes -> save_color_schemes -> get_typographies -> save_typographies -> get_button_styles -> save_button_styles -> get_fonts |
| Input | the customer's palette, logo and typeface from the alignment deck in customer-context/ |
| Do | Read first, then write. Style writes are not patch-safe: a color-scheme save fully overwrites the submitted scheme's colors (omitted colors nulled, custom colors cleared) while sibling schemes in the group survive; typography, button and font saves replace the WHOLE object. |
| Assert | get_color_schemes returns your new values on the scheme ids the rows actually reference, and a rendered page shows the new palette. A CSS deploy asserts CSSOM rule presence, not byte presence — a rewriter cannot prove its own rule identity; re-parse with a parser that shares no code with it. |
What this site actually ships, and it is not what dw-swift-page-blocks' illustrative list says:
Group id: default (NOT `swift`)
Scheme ids: light #FFFFFF / #1F2933
lightgrey1 #F5F7FA / #1F2933
lightgrey2 #E4E7EB / #1F2933
dark #1F2933 / #F5F7FA
darksubtle #323F4B / #CBD2D9
primary #1F2933 / #F5F7FA (NOT #004fff)
secondary #3E4C59 / #F5F7FA
Typography id: default Inter, base 16, scale 1.2, headings Inter 600 / -0.02 tracking
Button id: default Shape 2, BorderSize 1, PaddingY 0.55, PaddingX 1.4
Area binds: AreaColorSchemeGroupId default, AreaColorSchemeId light,
AreaTypographyId default, AreaButtonStyleId defaultTier 1 of the re-skin ladder is Files/Templates/Designs/Swift-v2/Custom/default_custom.css, loaded by DefaultHeadInclude.cshtml AFTER swift.css and the Style asset sheets. Its :root tokens (--td-ink, --td-accent, …) are what a customer re-skin overrides first. The hard rule: the customer's own sheet is <customer>_custom.css; git diff --name-only -- '*custom.css' must never show any other path ending in custom.css.
Do a Step-0 zero-state pass before styling: the frontpage title and meta title carry the customer's own words (save_pages with the real metaTitle), so nothing generic survives the first screenshot.
Step 3 — Areas and languages
| Skill | dw-swift-building (dynamo: true) SKILL.md:88-96 "Update the Area"; dw-content-localization (dynamo: true, no references/ — SKILL.md only) |
| Tools | get_areas -> get_area_by_id -> save_areas -> (host restart) -> find_unresolvable_item_pages -> create_language_version -> get_translatable_content -> apply_translation |
| Input | company name, domain, home market (country / language / currency) |
| Do | Bind the per-environment Area values the layer deliberately did not serialize: AreaFrontpage (the site-root page id), AreaEcomShopId (SHOP1), AreaEcomLanguageId, AreaEcomCurrencyId, AreaEcomCountryCode, AreaDomain. Set AreaTypographyId and AreaButtonStyleId in the SAME save_areas call — an area with those empty renders the whole site as unstyled 16px serif, because Swift's heading and body scale is driven by the area's typography CSS variables. On this Distribution the correct ids are default / default, not the fonts / buttons that dw-swift-page-blocks gotcha 16 names. |
| Assert | get_area_by_id echoes the bound values, and after a host restart GET <site-root>/ resolves to the frontpage rather than 404. Gate after ANY area save: fetch / and require both <header data-swift-page-header=...> and <footer data-swift-page-footer=...> to be present. |
Never create a new area in the branding flow. Repurpose area 3.
If a second language is in scope, the ordered sub-flow is (dw-content-localization/SKILL.md:27-91):
get_areas / get_master_areas resolve the website the user means
find_unresolvable_item_pages(areaId) create_language_version FAILS on these — get the list
first and hand it to the user; do not guess a fix
create_language_version(sourceAreaId, culture, name)
loop: get_translatable_content(areaId, skip, take: 20) -> apply_translation(units)
WHILE HasMore is true, call again with skip = NextSkipStopping while HasMore is true leaves the rest of the page — typically the accordion at the bottom — in the source language, which is exactly the defect an un-looped call ships. Read per-page/field errors as you go and re-apply only the failures; do not halt on an individual field error. Capture every mirror's PageMenuText before a bulk change and diff after: a reorder_pages or a save_pages re-parent de-translates mirror labels silently, and set_page_menu cannot repair it — the repair is set_page_item_fields {pageId: <mirrorId>, fields: {Title: "<translated label>"}}.
Step 4 — Catalog and PIM data model
| Skill | dw-demo-pim (dynamo: false) references/canonical-setup-order.md §1 Variant A; dw-pim-modelling (dynamo: true) references/structural-model.md; dw-pim-completeness (dynamo: true); dw-pim-workflow (dynamo: true) |
| Input | the customer's product families, attributes and sample data files from customer-context/ |
Ordered tool sequence, Variant A (Storefront-first — this site has a storefront):
1 save_currencies, save_countries, save_languages
2 save_shops SHOP1 is a WHOLE-ENTITY REPLACE — read it back and send the
complete model, or a name-only save silently resets
ShopAutoBuildIndex, the image folder and patterns,
ShopOrderFlowId, ShopCompletionRules and ShopCreated. The save
response projects only id/name/usageType/topLevelProductGroups,
so the damage is invisible in the echo.
4 save_units, save_manufacturers
7 create_data_model_structure (or create_categories -> create_category_fields ->
save_groups -> assign_categories_to_data_models)
8 save_groups pass shopId on SUBGROUPS too, in the same call as parentGroupId
9 save_variant_groups, save_variant_options
10 create_products ignores languageId — products land on the master language
11 assign_data_model_to_products
12 patch_products_safe
13 get_product_asset_categories -> import_product_images_from_urls -> set_product_primary_image
14 assign_variant_groups_to_product -> create_variant_combinations -> get_variant_combinations
then fix ProductActive and ProductPrice, which
create_variant_combinations leaves NULL
15 (BOM) EcomProductItems — restart the host after; the ProductItem cache is startup-scoped
17 create_or_update_product_queries
21 create_or_update_completeness_rules -> assign_completion_rules_to_groups
23 create_dashboards -> get_available_widgets -> get_widget_parameters
-> add_widgets_to_dashboards (counts first, the grid last)
24 build_product_index -> wait_for_product_index FINAL REBUILD, after all mutationsID discipline: the PIM catalogue tools auto-assign entity ids and ignore ids you pass in. Capture items[].id from every create/save response and key every later step off the captured id. A blind re-run after a partial failure stacks duplicate, mis-IDed entities.
Assert: get_products count equals what you authored; one EcomShopGroupRelation row per group; verify_product_visibility on one SKU reports it visible in SHOP1; get_product_index_status shows the primary instance Completed with a non-zero documentCount; and GET <site-root>/shop shows non-zero cards with dw-error 0 and rendered rows equal to the header count.
Step 5 — Users and groups
| Skill | dw-users-permissions (dynamo: true) references/grant-mechanics.md, references/page-gating.md; dw-commerce-b2b (dynamo: true) references/account-shape.md |
| Tools | save_user_groups -> get_user_groups_by_system_name -> create_users -> assign_users_to_group -> save_user_addresses |
| Input | the customer's account structure and role names, from notes/wording.md |
| Do | Build on the three base groups (Customers 1325, Account Admin 1270, CSR 1292) rather than replacing them — surface-swift's page permission blocks reference those ids by number. Author the floor of two personas. Give every contact in one account the SAME customerNumber: four features compare that string exactly, and a per-contact suffix silently disables account-wide favourites, CSR account listing and contract pricing. Passwords go to notes/credentials.local.md, referenced by pointer from the ledger. |
| Assert | get_users_by_group_id returns the expected members for each group; get_users_by_customer_number on the account number returns every contact. Then, signed in as a Customers-only persona, GET <site-root>/customer-center/csr/accounts is denied. Never verify a grant as Angel / BuiltInAdmin / Administrator — those bypass every check; create a Default-type user in the target group. The permission model is cached in-process — recycle before believing a negative check. |
Two ceilings to say out loud at this step rather than route around: there is no MCP password tool (create_users makes a login with no usable password — setting it is an admin screen), and there is no MCP impersonation tool (Edit user > Impersonation tab).
Step 6 — Pricing and assortments
| Skill | dw-commerce-catalog (dynamo: true); dw-commerce-b2b (dynamo: true) references/dc-scoping.md; dw-data-write-effects (dynamo: true) |
| Input | the customer's price logic — list vs contract, quantity breaks, per-account catalogue scoping — from the pricing extracts in customer-context/ |
save_currencies set the new default currency to Rate 100 BEFORE anything else; every
other rate is relative to it. The shipped USD$$ENU row is Rate 1 against
a default EUR of 100 — copy that shape and every USD price is 100x wrong.
Changing the default currency owes a host restart BEFORE seeding.
save_prices customer-number-scoped contract rows are charged end-to-end by the stock
DefaultPriceProvider with zero custom code. A customer-GROUP-scoped price
(PriceUserGroupId) is NOT reachable from save_prices — say so.
force_price_recalculation
save_assortments -> assign_groups_to_assortment -> assign_products_to_assortment
-> assign_shops_to_assortment -> assign_permissions_to_assortment
flag_assortments_for_rebuild -> build_assortments -> get_assortments_for_build (confirm empty)
check_assortment_product_accessAssert: get_prices_by_product_id returns the contract row; the cart line for the contract customer, signed in, shows the contract amount — force_price_recalculation recomputes without a frontend user price context, so it can never prove a customer-scoped price. check_assortment_product_access reports access for an in-scope user and none for an out-of-scope one. An assortment write not followed by flag_assortments_for_rebuild + build_assortments leaves the customer seeing the old catalogue. Before switching an assortment Active on, assert its item count is non-zero: activating an empty assortment takes the whole catalogue away from everyone who holds it.
Step 7 — Checkout
| Skill | dw-commerce-orders (dynamo: true) references/checkout-configuration.md, references/order-states-and-quotes.md |
| Tools | get_payments / get_shippings -> toggle_payment_active / toggle_shipping_active -> update_payment / update_shipping -> sort_payments / sort_shippings -> create_shipping_method -> save_payment_translation / save_shipping_translation |
| Input | the customer's payment terms (on-account vs card), delivery model, and any custom checkout field |
| Do | Deactivate the stock Danish-named rows the base ships (Faktura, Kreditkort, Firmalevering, Hjemmelevering) unless the customer is Danish. Keep PAY2 Invoice active if subscriptions are in the storyline — it is the IRecurring method. A custom checkout field is an admin-screen operation: create_order_field errors on every call on this platform line. |
| Assert | GET <site-root>/cart/ is 200, and a driven cart reaching <site-root>/cart/checkout-user renders at least one payment option and one shipping option. An all-inactive method list still returns 200. Drive the cart with a BROWSER User-Agent and follow redirects — curl and wget defaults fail both gates and the failure looks like a broken cart. |
Step 8 — Verification
| Skill | dw-swift-page-design (dynamo: true) "Verify + summary"; dw-swift-building (dynamo: true) references/layout-verification.md; dw-demo-base (dynamo: false) references/visual-qa.md, references/pii-sweep.md |
| Tools | fetch_frontend_page_html, get_frontend_health, get_grid_rows_by_page_id, get_paragraphs_by_page_id |
1. Render check. fetch_frontend_page_html every branded page. The body must show real content
and NOT: raw {"Label":...} JSON, overlapping garbled text, "the selected option no longer
exist", or "Error executing template". Reading the stored structure back is NOT enough — a
page with perfectly valid rows and paragraphs still renders broken if one Template name,
button value or scheme id is wrong, and that exact failure looks fine in
get_paragraphs_by_page_id.
2. Placeholder sweep. Zero occurrences of /placeholder/i across every rendered page. That is
the marker surface-swift deliberately planted.
3. Structure read-back. get_grid_rows_by_page_id + get_paragraphs_by_page_id match the plan.
4. Browser measurements — four defects clear every check above and still ship a broken page,
because each needs a measurement rather than a read:
a. Horizontal overflow at phone width. Assert BOTH
window.innerWidth === requested AND document.body.scrollWidth === window.innerWidth.
Either leg alone certifies a broken page: scrollWidth alone reads zero once the
browser widens the layout viewport to fit unshrinkable content (measured:
body.scrollWidth 652 / innerWidth 652 / requested 390 — a 262px stretch reported as no
overflow). Read scrollWidth at TWO device widths before hunting: the same number at
both means a fixed-width box in normal flow, which eliminates every responsive suspect
in one reading. Name the offender by RIGHT EDGE, never by width.
b. A control that is present and styled but sits under a card's stretched row link, so
clicks land on the anchor. Hit-test it.
c. An anchor coloured by its row's DECLARED color scheme rather than by the background the
page actually paints. Measure the rendered contrast pair, not the declared token.
d. A row that renders nothing while still paying its spacing.
Measure BOTH auth states — Swift's mobile header renders a wider control when nobody is
signed in. Adjudicate overflow by measurement, never from a full-page capture: a closed
off-canvas panel parked off-screen looks identical to a stretched canvas, and an unclickable
control looks perfect. Verify at more than one width — a wrap-dependent bug can measure
0.0px at one viewport and fail at another.
5. PII and vendor-boilerplate sweep. BLOCKING, before the demo is shown, published,
screenshared or handed over — true of a cleanly built demo, not only a cloned host.
Renaming user rows fixes nothing: order snapshots, address rows, token labels, log text and
JSON merge-field snapshots each hold an independent copy. Scan EVERY string column,
classify by SAMPLING values (not by table name), fix, re-scan — fixing one layer exposes the
next. Crawl the rendered corpus signed in as each persona, customer-centre order pages
included: the order-time identity snapshot is invisible to a user-table check and to an
anonymous crawl alike. Sweep the stock vendor boilerplate too — privacy / cookie / terms
pages, corporate addresses, the email-recipient author list. A term-grep cannot find
placeholder data containing none of your terms, so add locale-SHAPED patterns and keep the
rendered-page eyeball pass as a required step. Never copy a leaked value forward into notes,
commits, tickets or transcripts — record the class and the count.
6. Closing ledger. Run the CUSTOMISATIONS.md audit; its output is the closing
customisation-budget slide, and the format IS the slide — do not embellish it.