@fluentcommerce/ai-skills 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @fluentcommerce/ai-skills might be problematic. Click here for more details.

Files changed (168) hide show
  1. package/README.md +866 -622
  2. package/bin/cli.mjs +2112 -1973
  3. package/content/cli/agents/fluent-cli/agent.json +149 -149
  4. package/content/cli/agents/fluent-cli.md +132 -132
  5. package/content/cli/skills/fluent-bootstrap/SKILL.md +214 -181
  6. package/content/cli/skills/fluent-cli-index/SKILL.md +1 -1
  7. package/content/cli/skills/fluent-cli-mcp-cicd/SKILL.md +117 -1
  8. package/content/cli/skills/fluent-cli-reference/SKILL.md +1040 -1031
  9. package/content/cli/skills/fluent-cli-retailer/SKILL.md +27 -2
  10. package/content/cli/skills/fluent-cli-settings/SKILL.md +21 -1
  11. package/content/cli/skills/fluent-connect/SKILL.md +937 -886
  12. package/content/cli/skills/fluent-module-deploy/SKILL.md +63 -5
  13. package/content/cli/skills/fluent-profile/SKILL.md +73 -0
  14. package/content/cli/skills/fluent-workflow/SKILL.md +360 -310
  15. package/content/dev/agents/fluent-backend-dev/AGENT.md +58 -0
  16. package/content/dev/agents/fluent-backend-dev/agent.json +69 -0
  17. package/content/dev/agents/fluent-backend-dev.md +287 -0
  18. package/content/dev/agents/fluent-dev/AGENT.md +98 -0
  19. package/content/dev/agents/fluent-dev/agent.json +14 -2
  20. package/content/dev/agents/fluent-dev.md +194 -525
  21. package/content/dev/agents/fluent-frontend-dev/AGENT.md +63 -0
  22. package/content/dev/agents/fluent-frontend-dev/agent.json +52 -0
  23. package/content/dev/agents/fluent-frontend-dev.md +323 -0
  24. package/content/dev/skills/fluent-archive/SKILL.md +234 -0
  25. package/content/dev/skills/fluent-build/SKILL.md +312 -192
  26. package/content/dev/skills/fluent-connection-analysis/SKILL.md +422 -386
  27. package/content/dev/skills/fluent-custom-code/SKILL.md +15 -9
  28. package/content/dev/skills/fluent-data-module-scaffold/SKILL.md +19 -2
  29. package/content/dev/skills/fluent-e2e-test/SKILL.md +501 -394
  30. package/content/dev/skills/fluent-event-api/SKILL.md +962 -945
  31. package/content/dev/skills/fluent-feature-explain/SKILL.md +680 -603
  32. package/content/dev/skills/fluent-feature-plan/PLAN_TEMPLATE.md +27 -2
  33. package/content/dev/skills/fluent-feature-plan/SKILL.md +478 -227
  34. package/content/dev/skills/fluent-feature-status/SKILL.md +335 -0
  35. package/content/dev/skills/fluent-feedback/SKILL.md +221 -0
  36. package/content/dev/skills/fluent-implementation-map/SKILL.md +644 -0
  37. package/content/dev/skills/fluent-job-batch/SKILL.md +10 -0
  38. package/content/dev/skills/fluent-module-scaffold/SKILL.md +64 -2
  39. package/content/dev/skills/fluent-module-validate/SKILL.md +778 -775
  40. package/content/dev/skills/fluent-mystique-analyze/SKILL.md +817 -0
  41. package/content/dev/skills/fluent-mystique-builder/COMPONENT_TEMPLATE.md +81 -0
  42. package/content/dev/skills/fluent-mystique-builder/README.md +63 -0
  43. package/content/dev/skills/fluent-mystique-builder/SKILL.md +1294 -0
  44. package/content/dev/skills/fluent-mystique-builder/components/INDEX.md +92 -0
  45. package/content/dev/skills/fluent-mystique-builder/components/fc.accordion.md +48 -0
  46. package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.fulfilmentpack.md +20 -0
  47. package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.multiparcel.md +21 -0
  48. package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.returnitems.md +21 -0
  49. package/content/dev/skills/fluent-mystique-builder/components/fc.action.field.wavepick.md +21 -0
  50. package/content/dev/skills/fluent-mystique-builder/components/fc.action.inline.md +24 -0
  51. package/content/dev/skills/fluent-mystique-builder/components/fc.activity.entity.md +25 -0
  52. package/content/dev/skills/fluent-mystique-builder/components/fc.analytics.viz.md +20 -0
  53. package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.column.md +111 -0
  54. package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.json.md +20 -0
  55. package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.jsoneditor.md +54 -0
  56. package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.locationId.md +51 -0
  57. package/content/dev/skills/fluent-mystique-builder/components/fc.attribute.retailerId.md +52 -0
  58. package/content/dev/skills/fluent-mystique-builder/components/fc.button.bar.md +57 -0
  59. package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.download.md +53 -0
  60. package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.inline.compatibility.md +60 -0
  61. package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.inline.md +53 -0
  62. package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.md +24 -0
  63. package/content/dev/skills/fluent-mystique-builder/components/fc.button.print.pick.md +61 -0
  64. package/content/dev/skills/fluent-mystique-builder/components/fc.buttons.add.reject.md +20 -0
  65. package/content/dev/skills/fluent-mystique-builder/components/fc.card.attribute.md +73 -0
  66. package/content/dev/skills/fluent-mystique-builder/components/fc.card.attributes.grid.md +40 -0
  67. package/content/dev/skills/fluent-mystique-builder/components/fc.card.image.md +37 -0
  68. package/content/dev/skills/fluent-mystique-builder/components/fc.card.map.point.md +24 -0
  69. package/content/dev/skills/fluent-mystique-builder/components/fc.card.multi.md +79 -0
  70. package/content/dev/skills/fluent-mystique-builder/components/fc.card.product.md +27 -0
  71. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.area.md +34 -0
  72. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.area.wrapper.feed.md +98 -0
  73. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.bar.md +52 -0
  74. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.bar.wrapper.source.md +104 -0
  75. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.gauge.md +28 -0
  76. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.gauge.wrapper.threshold.md +118 -0
  77. package/content/dev/skills/fluent-mystique-builder/components/fc.chart.line.md +32 -0
  78. package/content/dev/skills/fluent-mystique-builder/components/fc.conditional.md +62 -0
  79. package/content/dev/skills/fluent-mystique-builder/components/fc.dashboard.threshold.md +65 -0
  80. package/content/dev/skills/fluent-mystique-builder/components/fc.daterange.wrapper.forwarder.md +56 -0
  81. package/content/dev/skills/fluent-mystique-builder/components/fc.drawer.button.md +21 -0
  82. package/content/dev/skills/fluent-mystique-builder/components/fc.event.detail.md +20 -0
  83. package/content/dev/skills/fluent-mystique-builder/components/fc.events.search.md +21 -0
  84. package/content/dev/skills/fluent-mystique-builder/components/fc.field.daterange.md +83 -0
  85. package/content/dev/skills/fluent-mystique-builder/components/fc.field.filterComplex.md +106 -0
  86. package/content/dev/skills/fluent-mystique-builder/components/fc.field.intrange.md +82 -0
  87. package/content/dev/skills/fluent-mystique-builder/components/fc.field.multistring.md +50 -0
  88. package/content/dev/skills/fluent-mystique-builder/components/fc.filterPanel.md +53 -0
  89. package/content/dev/skills/fluent-mystique-builder/components/fc.json.editor.md +22 -0
  90. package/content/dev/skills/fluent-mystique-builder/components/fc.json.viewer.md +21 -0
  91. package/content/dev/skills/fluent-mystique-builder/components/fc.list.customAction.md +79 -0
  92. package/content/dev/skills/fluent-mystique-builder/components/fc.list.md +116 -0
  93. package/content/dev/skills/fluent-mystique-builder/components/fc.list.wrapper.bppmetrics.md +69 -0
  94. package/content/dev/skills/fluent-mystique-builder/components/fc.list.wrapper.feed.md +65 -0
  95. package/content/dev/skills/fluent-mystique-builder/components/fc.list.wrapper.source.md +64 -0
  96. package/content/dev/skills/fluent-mystique-builder/components/fc.modal.button.addItem.md +60 -0
  97. package/content/dev/skills/fluent-mystique-builder/components/fc.modal.button.md +21 -0
  98. package/content/dev/skills/fluent-mystique-builder/components/fc.mutation.inline.md +88 -0
  99. package/content/dev/skills/fluent-mystique-builder/components/fc.mystique.collapsible.attributes.md +83 -0
  100. package/content/dev/skills/fluent-mystique-builder/components/fc.mystique.collapsible.text.md +33 -0
  101. package/content/dev/skills/fluent-mystique-builder/components/fc.mystique.link.md +30 -0
  102. package/content/dev/skills/fluent-mystique-builder/components/fc.order.itemDetails.md +20 -0
  103. package/content/dev/skills/fluent-mystique-builder/components/fc.order.shipmentDetails.md +20 -0
  104. package/content/dev/skills/fluent-mystique-builder/components/fc.page.filter.select.md +87 -0
  105. package/content/dev/skills/fluent-mystique-builder/components/fc.page.md +64 -0
  106. package/content/dev/skills/fluent-mystique-builder/components/fc.page.refresh.md +48 -0
  107. package/content/dev/skills/fluent-mystique-builder/components/fc.page.section.column.md +71 -0
  108. package/content/dev/skills/fluent-mystique-builder/components/fc.page.section.header.md +61 -0
  109. package/content/dev/skills/fluent-mystique-builder/components/fc.page.section.md +59 -0
  110. package/content/dev/skills/fluent-mystique-builder/components/fc.page.wizard.md +45 -0
  111. package/content/dev/skills/fluent-mystique-builder/components/fc.page.wizard.summary.md +56 -0
  112. package/content/dev/skills/fluent-mystique-builder/components/fc.progress.circular.md +20 -0
  113. package/content/dev/skills/fluent-mystique-builder/components/fc.provider.graphql.md +71 -0
  114. package/content/dev/skills/fluent-mystique-builder/components/fc.quantity.list.md +87 -0
  115. package/content/dev/skills/fluent-mystique-builder/components/fc.repeater.md +56 -0
  116. package/content/dev/skills/fluent-mystique-builder/components/fc.reports.ipuipc.md +54 -0
  117. package/content/dev/skills/fluent-mystique-builder/components/fc.return.rowExpansion.md +19 -0
  118. package/content/dev/skills/fluent-mystique-builder/components/fc.scanner.barcode.md +21 -0
  119. package/content/dev/skills/fluent-mystique-builder/components/fc.scanner.barcodeFilter.md +72 -0
  120. package/content/dev/skills/fluent-mystique-builder/components/fc.scanner.camera.md +20 -0
  121. package/content/dev/skills/fluent-mystique-builder/components/fc.settingForm.md +64 -0
  122. package/content/dev/skills/fluent-mystique-builder/components/fc.sourcing.profile.drawer.button.md +19 -0
  123. package/content/dev/skills/fluent-mystique-builder/components/fc.sourcing.profile.modal.button.md +64 -0
  124. package/content/dev/skills/fluent-mystique-builder/components/fc.sourcing.strategy.modal.button.md +20 -0
  125. package/content/dev/skills/fluent-mystique-builder/components/fc.stepper.md +20 -0
  126. package/content/dev/skills/fluent-mystique-builder/components/fc.tab.content.md +56 -0
  127. package/content/dev/skills/fluent-mystique-builder/components/fc.tabs.card.md +64 -0
  128. package/content/dev/skills/fluent-mystique-builder/components/fc.tabs.md +69 -0
  129. package/content/dev/skills/fluent-mystique-builder/components/fc.tile.metric.md +73 -0
  130. package/content/dev/skills/fluent-mystique-builder/components/fc.workflow.provider.md +77 -0
  131. package/content/dev/skills/fluent-mystique-builder/validate-docs.ps1 +260 -0
  132. package/content/dev/skills/fluent-mystique-scaffold/SKILL.md +1830 -0
  133. package/content/dev/skills/fluent-mystique-validate/SKILL.md +646 -0
  134. package/content/dev/skills/fluent-pre-deploy-check/SKILL.md +1144 -1108
  135. package/content/dev/skills/fluent-retailer-config/SKILL.md +1162 -1111
  136. package/content/dev/skills/fluent-rollback/SKILL.md +387 -0
  137. package/content/dev/skills/fluent-rule-scaffold/SKILL.md +515 -385
  138. package/content/dev/skills/fluent-scope-decompose/SKILL.md +1123 -1021
  139. package/content/dev/skills/fluent-session-audit-export/SKILL.md +880 -632
  140. package/content/dev/skills/fluent-session-summary/SKILL.md +320 -195
  141. package/content/dev/skills/fluent-settings/SKILL.md +160 -1
  142. package/content/dev/skills/fluent-source-onboard/SKILL.md +31 -3
  143. package/content/dev/skills/fluent-sourcing/SKILL.md +1185 -0
  144. package/content/dev/skills/fluent-system-monitoring/SKILL.md +771 -767
  145. package/content/dev/skills/fluent-test-data/SKILL.md +514 -513
  146. package/content/dev/skills/fluent-trace/SKILL.md +1169 -1143
  147. package/content/dev/skills/fluent-transition-api/SKILL.md +364 -346
  148. package/content/dev/skills/fluent-use-case-discover/SKILL.md +593 -0
  149. package/content/dev/skills/fluent-use-case-discover/SPEC_TEMPLATE.md +281 -0
  150. package/content/dev/skills/fluent-version-manage/SKILL.md +53 -2
  151. package/content/dev/skills/fluent-workflow-analyzer/SKILL.md +995 -959
  152. package/content/dev/skills/fluent-workflow-builder/SKILL.md +668 -319
  153. package/content/dev/skills/fluent-workflow-deploy/SKILL.md +480 -267
  154. package/content/dev/skills/fluent-workspace-tree/SKILL.md +281 -0
  155. package/content/mcp-extn/agents/fluent-mcp.md +133 -69
  156. package/content/mcp-extn/skills/fluent-mcp-tools/SKILL.md +812 -461
  157. package/content/mcp-official/agents/fluent-mcp-core.md +91 -91
  158. package/content/mcp-official/skills/fluent-mcp-core/SKILL.md +94 -94
  159. package/content/rfl/skills/fluent-rfl-assess/SKILL.md +172 -172
  160. package/docs/CAPABILITY_MAP.md +106 -77
  161. package/docs/DEPLOYMENT_PROMOTION_RUNBOOK.md +218 -0
  162. package/docs/DESIGN-implementation-map.md +698 -0
  163. package/docs/DEV_WORKFLOW.md +814 -802
  164. package/docs/FLOW_RUN.md +142 -142
  165. package/docs/GETTING_STARTED.md +427 -0
  166. package/docs/USE_CASES.md +909 -52
  167. package/metadata.json +184 -156
  168. package/package.json +3 -2
@@ -0,0 +1,1185 @@
1
+ ---
2
+ name: fluent-sourcing
3
+ description: "Comprehensive reference for the Fluent Commerce Responsive Sourcing Framework. Covers sourcing profiles, strategies, conditions, criteria, scoring algorithms, permutation search, settings schema, workflow integration, custom extensions, and operational GraphQL patterns. Triggers on \"sourcing\", \"sourcing profile\", \"sourcing strategy\", \"sourcing condition\", \"sourcing criteria\", \"fulfilment allocation\", \"location scoring\", \"order sourcing\", \"where to fulfil\"."
4
+ user-invocable: true
5
+ read-only: true
6
+ allowed-tools: Bash, Read, Write, Edit, Glob, Grep
7
+ argument-hint: "[--profile <PROFILE>] [--retailer <RETAILER_REF>] [--operation query|create|debug]"
8
+ ---
9
+
10
+ # Fluent Sourcing Framework Reference
11
+
12
+ Expert reference for the Fluent Commerce Responsive Sourcing Framework (v2.1+). Covers sourcing profiles, strategies, conditions, criteria, scoring algorithms, permutation search, settings, workflow integration, custom extensions, and operational GraphQL patterns.
13
+
14
+ **Source:** `util-sourcing-2.1.0.jar` decompiled analysis + Fluent Commerce documentation.
15
+
16
+ ## Ownership Boundary
17
+
18
+ | Task | This Skill | Chain To |
19
+ |------|-----------|----------|
20
+ | Understand sourcing concepts, scoring, algorithms | Yes | — |
21
+ | Generate sourcing profile JSON configurations | Yes | — |
22
+ | Debug why a location was selected/excluded | Yes | `/fluent-trace` for event-level forensics |
23
+ | Build custom sourcing condition/criterion Java code | Yes (skeleton) | `/fluent-rule-scaffold` for full module wiring |
24
+ | Create/modify workflows that invoke sourcing | No | `/fluent-workflow-builder` |
25
+ | Deploy sourcing-related settings | No | `/fluent-settings` |
26
+ | Query live sourcing profiles via MCP | No | `/fluent-mcp-tools` |
27
+ | Trace sourcing event execution | No | `/fluent-trace` |
28
+ | Configure retailer locations/networks | No | `/fluent-retailer-config` |
29
+ | Run E2E sourcing tests | No | `/fluent-e2e-test` |
30
+
31
+ ## Skill Type: Reference
32
+
33
+ This is a **reference skill** — it provides domain knowledge and query patterns but does not modify code or Fluent environments. Use it to understand sourcing concepts, generate configuration JSON, and debug allocation decisions. Implementation changes (workflow edits, rule scaffolding, settings) should be delegated to the appropriate implementation skills listed in the Ownership Boundary above.
34
+
35
+ ## When to Use
36
+
37
+ - **"How does sourcing work?"** — Conceptual Architecture section
38
+ - **"What sourcing conditions/criteria are available?"** — Conditions and Criteria sections
39
+ - **"Why was this location selected/excluded?"** — Debugging & Troubleshooting section
40
+ - **"How do I configure a sourcing profile?"** — Sourcing Profiles section
41
+ - **"How do I add custom sourcing logic?"** — Custom Extension Guide section
42
+ - **"What settings control sourcing?"** — Settings Reference section
43
+ - **"How does sourcing connect to workflows?"** — Workflow Integration section
44
+ - **"How does the scoring algorithm work?"** — Scoring & Normalization subsection
45
+
46
+ ## Conceptual Architecture
47
+
48
+ ### Evaluation Flow
49
+
50
+ ```mermaid
51
+ graph TD
52
+ Start[Order Sourcing Request] --> Load[Load Sourcing Profile]
53
+ Load --> Strategies[Evaluate Strategies by Priority]
54
+ Strategies --> Condition{All Conditions Met?}
55
+ Condition -- No --> NextStrategy[Next Strategy]
56
+ NextStrategy --> Strategies
57
+ Condition -- Yes --> Type{Primary or Fallback?}
58
+ Type -- Primary --> MustFull[Must Source ALL Items]
59
+ Type -- Fallback --> CanPartial[Can Source PARTIAL Items]
60
+ MustFull --> Criteria[Execute Criteria Chain]
61
+ CanPartial --> Criteria
62
+ Criteria --> Exclusion[Exclusion Criteria: Remove Locations score = -1.0]
63
+ Exclusion --> Scoring[Sorter Criteria: Score Remaining Locations]
64
+ Scoring --> Normalize[Normalize Scores to 0.0 - 1.0]
65
+ Normalize --> Sort[Sort by Final Score DESC]
66
+ Sort --> Split{Items Need Split?}
67
+ Split -- Yes maxSplit > 1 --> Permutation[Permutation Search]
68
+ Split -- No single location --> Select[Select Top Location]
69
+ Permutation --> Plan[Build Fulfilment Plan]
70
+ Select --> Plan
71
+ Plan --> End[Return FulfilmentPlan]
72
+ ```
73
+
74
+ ### Entity Model
75
+
76
+ | Entity | Parent | Description | Key Fields |
77
+ |--------|--------|-------------|------------|
78
+ | **SourcingProfile** | — | Root container for sourcing configuration | `ref`, `status`, `catalogueRef`, `networkRef`, `maxSplit`, `virtualCatalogueRef` |
79
+ | **SourcingStrategy** | SourcingProfile | A prioritized rule definition with conditions and criteria | `ref`, `priority`, `status`, `type` (PRIMARY/FALLBACK) |
80
+ | **SourcingCondition** | SourcingStrategy | Boolean gatekeeper — determines IF a strategy applies | `name`, `type`, `params` |
81
+ | **SourcingCriterion** | SourcingStrategy | Numeric scorer/filter — EXCLUDES or RANKS a location | `name`, `type`, `params` |
82
+
83
+ ### Profile Lifecycle
84
+
85
+ Profiles follow a versioned lifecycle:
86
+
87
+ | Status | Description | Can Be Evaluated? |
88
+ |--------|-------------|-------------------|
89
+ | `DRAFT` | Under construction | No |
90
+ | `ACTIVE` | Live — used by workflow rules | Yes |
91
+ | `INACTIVE` | Archived — no longer evaluated | No |
92
+
93
+ Only **one version** of a profile (by ref) can be `ACTIVE` at a time. Activating a new version automatically deactivates the previous one.
94
+
95
+ ### SourcingContext Model
96
+
97
+ The `SourcingContext` is the runtime data object passed to conditions and criteria. It contains:
98
+
99
+ | Path | Type | Description |
100
+ |------|------|-------------|
101
+ | `order` | Order | The full order entity |
102
+ | `order.totalPrice` | Float | Order total value |
103
+ | `order.attributes[]` | List | Order-level custom attributes |
104
+ | `fulfilmentChoice` | Object | Selected fulfilment option |
105
+ | `fulfilmentChoice.type` | String | e.g., `HD`, `CC`, `SFS` |
106
+ | `fulfilmentChoice.deliveryAddress` | Address | Delivery destination (lat/lng for distance) |
107
+ | `items[]` | List | Order items to source |
108
+ | `items[].productRef` | String | Product reference |
109
+ | `items[].requestedQuantity` | Integer | Quantity needed |
110
+ | `locations[]` | List | Candidate locations from network |
111
+ | `locations[].ref` | String | Location reference |
112
+ | `locations[].type` | String | e.g., `WAREHOUSE`, `STORE` |
113
+ | `locations[].address` | Address | Location coordinates |
114
+ | `locations[].attributes[]` | List | Location custom attributes |
115
+ | `inventoryPositions[]` | List | Stock data per location per product |
116
+
117
+ Conditions use JSON path expressions to navigate this model. For example, `fulfilmentChoice.type` extracts the delivery type, `order.attributes[?(@.name=='priority')].value` extracts a custom attribute.
118
+
119
+ ## Sourcing Profiles
120
+
121
+ ### Profile Structure
122
+
123
+ A Sourcing Profile is configured as a GraphQL entity and loaded at runtime by the `CreateFulfilmentWithSourcingProfile` workflow rule.
124
+
125
+ ```json
126
+ {
127
+ "ref": "SOURCING_PROFILE_HD",
128
+ "status": "ACTIVE",
129
+ "retailerId": 1,
130
+ "catalogueRef": "DEFAULT:1",
131
+ "virtualCatalogueRef": "FC:DEFAULT:1",
132
+ "networkRef": "HD_NETWORK",
133
+ "maxSplit": 2,
134
+ "sourcingStrategies": [
135
+ {
136
+ "ref": "STRATEGY_HD_STANDARD",
137
+ "priority": 10,
138
+ "status": "ACTIVE",
139
+ "type": "PRIMARY",
140
+ "sourcingConditions": [],
141
+ "sourcingCriteria": []
142
+ }
143
+ ]
144
+ }
145
+ ```
146
+
147
+ **Field Reference:**
148
+
149
+ | Field | Required | Description |
150
+ |-------|----------|-------------|
151
+ | `ref` | Yes | Unique identifier for the profile |
152
+ | `status` | Yes | `DRAFT`, `ACTIVE`, or `INACTIVE` |
153
+ | `retailerId` | Yes | Owning retailer ID |
154
+ | `catalogueRef` | No | Product catalogue for inventory lookup |
155
+ | `virtualCatalogueRef` | No | Virtual catalogue (overrides catalogueRef) |
156
+ | `networkRef` | No | Default network for location candidates |
157
+ | `maxSplit` | No | Maximum number of fulfilment locations (default: 1) |
158
+ | `sourcingStrategies` | Yes | Ordered list of strategies |
159
+
160
+ ### Create a Sourcing Profile
161
+
162
+ See the **GraphQL Operations** section for the full `createSourcingProfile` mutation.
163
+
164
+ ### Activate a Sourcing Profile
165
+
166
+ Activation is typically done by updating the profile status to `ACTIVE`. See **GraphQL Operations > Update & Activate**.
167
+
168
+ ## Sourcing Strategies
169
+
170
+ ### Primary vs Fallback
171
+
172
+ | Aspect | Primary | Fallback |
173
+ |--------|---------|----------|
174
+ | **Type** | `PRIMARY` | `FALLBACK` |
175
+ | **Requirement** | Must source ALL order items | Can source PARTIAL items |
176
+ | **Evaluation** | Tried first (lower priority number = higher priority) | Tried only if no Primary strategy can source all items |
177
+ | **Result** | Single fulfilment plan for entire order | May produce multiple partial fulfilments |
178
+ | **Use Case** | Standard fulfilment from one location | Split shipment, partial availability |
179
+
180
+ ### Strategy Configuration
181
+
182
+ | Field | Type | Description |
183
+ |-------|------|-------------|
184
+ | `ref` | String | Unique strategy identifier |
185
+ | `priority` | Integer | Evaluation order (lower = first). Must be unique within a profile. |
186
+ | `status` | String | `ACTIVE` or `INACTIVE` |
187
+ | `type` | String | `PRIMARY` or `FALLBACK` |
188
+ | `sourcingConditions` | Array | Conditions that must ALL pass for this strategy to apply |
189
+ | `sourcingCriteria` | Array | Criteria chain for filtering and ranking locations |
190
+
191
+ ### Multi-Strategy Example
192
+
193
+ ```json
194
+ {
195
+ "ref": "SOURCING_PROFILE_MULTI",
196
+ "maxSplit": 3,
197
+ "sourcingStrategies": [
198
+ {
199
+ "ref": "HD_EXPRESS",
200
+ "priority": 10,
201
+ "status": "ACTIVE",
202
+ "type": "PRIMARY",
203
+ "sourcingConditions": [
204
+ {
205
+ "name": "IsExpress",
206
+ "type": "fc.sourcing.condition.path",
207
+ "params": {
208
+ "path": "fulfilmentChoice.type",
209
+ "operator": "equals",
210
+ "value": "HD_EXPRESS"
211
+ }
212
+ }
213
+ ],
214
+ "sourcingCriteria": [
215
+ {
216
+ "name": "NearbyOnly",
217
+ "type": "fc.sourcing.criterion.locationDistanceExclusion",
218
+ "params": { "value": 25.0, "valueUnit": "KILOMETRES" }
219
+ },
220
+ {
221
+ "name": "ClosestFirst",
222
+ "type": "fc.sourcing.criterion.locationDistance"
223
+ }
224
+ ]
225
+ },
226
+ {
227
+ "ref": "HD_STANDARD",
228
+ "priority": 20,
229
+ "status": "ACTIVE",
230
+ "type": "PRIMARY",
231
+ "sourcingConditions": [
232
+ {
233
+ "name": "IsHD",
234
+ "type": "fc.sourcing.condition.path",
235
+ "params": {
236
+ "path": "fulfilmentChoice.type",
237
+ "operator": "in",
238
+ "value": ["HD", "HD_EXPRESS"]
239
+ }
240
+ }
241
+ ],
242
+ "sourcingCriteria": [
243
+ {
244
+ "name": "HasStock",
245
+ "type": "fc.sourcing.criterion.inventoryAvailabilityExclusion"
246
+ },
247
+ {
248
+ "name": "ByDistance",
249
+ "type": "fc.sourcing.criterion.locationDistance"
250
+ },
251
+ {
252
+ "name": "ByCapacity",
253
+ "type": "fc.sourcing.criterion.locationDailyCapacity"
254
+ }
255
+ ]
256
+ },
257
+ {
258
+ "ref": "HD_FALLBACK_SPLIT",
259
+ "priority": 100,
260
+ "status": "ACTIVE",
261
+ "type": "FALLBACK",
262
+ "sourcingConditions": [],
263
+ "sourcingCriteria": [
264
+ {
265
+ "name": "HasStock",
266
+ "type": "fc.sourcing.criterion.inventoryAvailabilityExclusion"
267
+ },
268
+ {
269
+ "name": "ByStock",
270
+ "type": "fc.sourcing.criterion.inventoryAvailability"
271
+ }
272
+ ]
273
+ }
274
+ ]
275
+ }
276
+ ```
277
+
278
+ ## Sourcing Conditions
279
+
280
+ ### Condition Model
281
+
282
+ Conditions are boolean gates on a strategy. **ALL conditions must pass** (AND logic) for the strategy to be evaluated. If any condition fails, the engine skips to the next strategy.
283
+
284
+ There is one built-in condition type: `fc.sourcing.condition.path`.
285
+
286
+ **Class:** `com.fluentcommerce.util.sourcing.condition.DefaultSourcingCondition`
287
+
288
+ This condition extracts a value from the `SourcingContext` using a JSON path expression, then applies an operator against an expected value.
289
+
290
+ ### Operators
291
+
292
+ (Source: `SourcingConditionOperatorRegistry.java`)
293
+
294
+ | Operator | Description | Value Type | Example |
295
+ |----------|-------------|------------|---------|
296
+ | `equals` | Exact match (case-sensitive) | Any | `"value": "HD"` |
297
+ | `not_equals` | Not equal | Any | `"value": "CC"` |
298
+ | `greater_than` | Numeric greater than | Number | `"value": 100.0` |
299
+ | `greater_than_or_equals` | Numeric >= | Number | `"value": 50` |
300
+ | `less_than` | Numeric less than | Number | `"value": 10` |
301
+ | `less_than_or_equals` | Numeric <= | Number | `"value": 5` |
302
+ | `in` | Value is in list | Array | `"value": ["HD", "CC"]` |
303
+ | `not_in` | Value is not in list | Array | `"value": ["PICKUP"]` |
304
+ | `between` | Numeric range (inclusive) | Array[2] | `"value": [10, 100]` |
305
+ | `exists` | Path exists (non-null) | Boolean | `"value": true` |
306
+
307
+ > **Important:** Operators are **case-sensitive, lowercase**. Using `EQUALS` or `Equals` will not match.
308
+
309
+ ### conditionScope
310
+
311
+ When the JSON path resolves to an **array** (e.g., `items[].productRef`), the `conditionScope` parameter controls how the condition evaluates:
312
+
313
+ | Scope | Behavior |
314
+ |-------|----------|
315
+ | `ALL` | Every element in the array must match |
316
+ | `ANY` | At least one element must match |
317
+ | `NONE` | No element may match |
318
+
319
+ If `conditionScope` is omitted on a non-array path, it is ignored.
320
+
321
+ ### Condition Examples
322
+
323
+ **Simple equality:**
324
+ ```json
325
+ {
326
+ "name": "IsHomeDelivery",
327
+ "type": "fc.sourcing.condition.path",
328
+ "params": {
329
+ "path": "fulfilmentChoice.type",
330
+ "operator": "equals",
331
+ "value": "HD"
332
+ }
333
+ }
334
+ ```
335
+
336
+ **Numeric range:**
337
+ ```json
338
+ {
339
+ "name": "HighValueOrder",
340
+ "type": "fc.sourcing.condition.path",
341
+ "params": {
342
+ "path": "order.totalPrice",
343
+ "operator": "greater_than",
344
+ "value": 200.0
345
+ }
346
+ }
347
+ ```
348
+
349
+ **List membership:**
350
+ ```json
351
+ {
352
+ "name": "IsDeliveryType",
353
+ "type": "fc.sourcing.condition.path",
354
+ "params": {
355
+ "path": "fulfilmentChoice.type",
356
+ "operator": "in",
357
+ "value": ["HD", "HD_EXPRESS", "SFS"]
358
+ }
359
+ }
360
+ ```
361
+
362
+ **Custom attribute with exists:**
363
+ ```json
364
+ {
365
+ "name": "HasPriority",
366
+ "type": "fc.sourcing.condition.path",
367
+ "params": {
368
+ "path": "order.attributes[?(@.name=='priority')].value",
369
+ "operator": "exists",
370
+ "value": true
371
+ }
372
+ }
373
+ ```
374
+
375
+ ### Custom Conditions
376
+
377
+ To add custom condition logic beyond `fc.sourcing.condition.path`, see the **Custom Extension Guide** section below. Custom conditions implement the `SourcingCondition` interface and are registered in the `SourcingConditionTypeRegistry`.
378
+
379
+ ## Sourcing Criteria
380
+
381
+ ### Criteria Model
382
+
383
+ Criteria filter and rank candidate locations. They execute **in order** within a strategy, building up a refined and scored list.
384
+
385
+ Each criterion returns a `float`:
386
+ - **`-1.0`** — Exclude the location (removed from candidates)
387
+ - **`0.0+`** — A score (higher = better)
388
+
389
+ Criteria are classified by tag:
390
+ - **Exclusion** — Returns `-1.0` to remove locations that fail a hard constraint
391
+ - **Sorter** — Returns a numeric score to rank locations
392
+
393
+ ### Built-in Criteria Reference
394
+
395
+ (Source: `SourcingCriteriaTypeRegistry.java`)
396
+
397
+ | Type | Tag | Description | Key Params |
398
+ |------|-----|-------------|------------|
399
+ | `fc.sourcing.criterion.locationDistanceExclusion` | Exclusion | Exclude locations beyond a distance threshold | `value` (max distance), `valueUnit` (`KILOMETRES` or `MILES`) |
400
+ | `fc.sourcing.criterion.locationTypeExclusion` | Exclusion | Exclude locations of specified types | `value` (list of type strings, e.g., `["STORE"]`) |
401
+ | `fc.sourcing.criterion.locationNetworkExclusion` | Exclusion | Exclude locations not in the specified network | `value` (network ref string) |
402
+ | `fc.sourcing.criterion.inventoryAvailabilityExclusion` | Exclusion | Exclude locations with insufficient stock for all order items | *(none)* |
403
+ | `fc.sourcing.criterion.locationDistance` | Sorter | Score by distance from delivery address (closest = highest score) | *(none)* |
404
+ | `fc.sourcing.criterion.locationDistanceBanded` | Sorter | Score by distance using configurable bands | `bands` (list of `{from, to, score}`) |
405
+ | `fc.sourcing.criterion.locationDailyCapacity` | Sorter | Score by remaining daily fulfilment capacity | *(none)* |
406
+ | `fc.sourcing.criterion.networkPriority` | Sorter | Score by the location's network priority field | *(none)* |
407
+ | `fc.sourcing.criterion.inventoryAvailability` | Sorter | Score by stock quantity (more stock = higher score) | *(none)* |
408
+ | `fc.sourcing.criterion.inventoryAvailabilityBanded` | Sorter | Score by stock quantity using configurable bands | `bands` (list of `{from, to, score}`) |
409
+ | `fc.sourcing.criterion.orderValue` | Sorter | Score by order value handling capability | *(none)* |
410
+
411
+ > **Note on Parameter Names:** In `util-sourcing-2.1.0`, the JSON parsing logic uses generic keys like `"value"` and `"valueUnit"` (via `params.get("value")`). Newer versions may use descriptive names like `maxDistance`, `distanceUnit`, `excludedTypes`. Always check your specific version.
412
+
413
+ ### Scoring and Normalization
414
+
415
+ (Source: `SourcingCriteriaUtils.java`, `BaseSourcingCriterion.normalize()`)
416
+
417
+ **Step 1: Raw Scoring**
418
+ Each criterion calculates a raw score for every candidate location.
419
+
420
+ **Step 2: Normalization**
421
+ Raw scores are normalized to a `0.0` – `1.0` range across all candidates for that criterion:
422
+
423
+ ```
424
+ normalizedScore = (rawScore - minScore) / (maxScore - minScore)
425
+ ```
426
+
427
+ If all scores are equal (`maxScore == minScore`), all locations get `1.0`.
428
+
429
+ **Step 3: Distance Inversion**
430
+ `LocationDistanceCriterion` overrides normalization — it **inverts** the score so that shorter distance = higher score:
431
+
432
+ ```
433
+ normalizedScore = 1.0 - ((distance - minDistance) / (maxDistance - minDistance))
434
+ ```
435
+
436
+ **Step 4: Aggregation**
437
+ After all criteria run, each location has a list of normalized scores. These are combined (typically averaged) to produce a final composite score. Locations are sorted by final score descending.
438
+
439
+ **Worked Example:**
440
+
441
+ Three locations scored by `locationDistance` (raw = km from delivery address):
442
+ | Location | Raw Distance | After Normalization | After Inversion |
443
+ |----------|-------------|--------------------|-----------------|
444
+ | Store A | 5 km | (5-5)/(50-5) = 0.0 | 1.0 - 0.0 = **1.0** |
445
+ | Store B | 20 km | (20-5)/(50-5) = 0.33 | 1.0 - 0.33 = **0.67** |
446
+ | Store C | 50 km | (50-5)/(50-5) = 1.0 | 1.0 - 1.0 = **0.0** |
447
+
448
+ Result: Store A (closest) wins with score 1.0.
449
+
450
+ ### Banding Pattern
451
+
452
+ Banded criteria use configurable ranges instead of continuous scoring:
453
+
454
+ ```json
455
+ {
456
+ "name": "DistanceBands",
457
+ "type": "fc.sourcing.criterion.locationDistanceBanded",
458
+ "params": {
459
+ "bands": [
460
+ { "from": 0, "to": 10, "score": 1.0 },
461
+ { "from": 10, "to": 50, "score": 0.5 },
462
+ { "from": 50, "to": 100, "score": 0.1 }
463
+ ]
464
+ }
465
+ }
466
+ ```
467
+
468
+ Locations falling outside all bands receive a score of `0.0`. The `inventoryAvailabilityBanded` criterion uses the same pattern with stock quantity thresholds.
469
+
470
+ ### Exclusion Pattern
471
+
472
+ Exclusion criteria are hard filters — they return `-1.0` for locations that fail the constraint, immediately removing them from the candidate pool. Exclusions should always be placed **before** sorters in the criteria chain to reduce the scoring workload.
473
+
474
+ ```json
475
+ {
476
+ "name": "ExcludeFarLocations",
477
+ "type": "fc.sourcing.criterion.locationDistanceExclusion",
478
+ "params": {
479
+ "value": 100.0,
480
+ "valueUnit": "KILOMETRES"
481
+ }
482
+ }
483
+ ```
484
+
485
+ ### Custom Criteria
486
+
487
+ To add custom scoring or filtering logic, see the **Custom Extension Guide** section below. Custom criteria extend `BaseSourcingCriterion` and are registered in the `SourcingCriteriaTypeRegistry`.
488
+
489
+ ## Permutation Search
490
+
491
+ ### Algorithm Overview
492
+
493
+ (Source: `SourcingUtils.findPlanForAllItems()`)
494
+
495
+ When `maxSplit > 1`, the engine performs a **permutation search** to find the optimal combination of locations:
496
+
497
+ 1. **Start with split count = 1** — Try to source all items from a single location
498
+ 2. **Increment split count** — If no single location works, try 2-location combinations, then 3, etc.
499
+ 3. **Stop at maxSplit** — Never exceed the configured maximum
500
+ 4. **Fewer splits always wins** — A 1-location plan is always preferred over a 2-location plan, regardless of scores
501
+
502
+ Within each split level, the engine:
503
+ - Takes the top-scored locations from the criteria chain
504
+ - Tests all permutations to find combinations that can collectively source all items
505
+ - Selects the combination with the highest aggregate score
506
+
507
+ ### maxSplit Behavior
508
+
509
+ | maxSplit | Behavior |
510
+ |----------|----------|
511
+ | `1` (default) | Single location must have all items. No split. |
512
+ | `2` | Try 1 location first, then 2-location combos |
513
+ | `3+` | Try 1, then 2, ..., up to N. First successful split level wins. |
514
+ | Not set | Defaults to 1 |
515
+
516
+ > **Performance Note:** Permutation search is combinatorial. With 100 candidate locations and `maxSplit=3`, the search space is large. Use exclusion criteria aggressively to reduce the candidate pool before permutation.
517
+
518
+ ## Settings Reference
519
+
520
+ ### Sourcing Settings Keys
521
+
522
+ (Source: `SourcingConditionTypeRegistry.java`, `SourcingCriteriaTypeRegistry.java`)
523
+
524
+ | Setting Key | Scope | Purpose |
525
+ |-------------|-------|---------|
526
+ | `fc.rubix.order.sourcing.conditions` | GLOBAL | Registry of built-in sourcing condition types |
527
+ | `fc.rubix.order.sourcing.conditions.custom` | ACCOUNT / RETAILER | Registry of custom sourcing condition types |
528
+ | `fc.rubix.order.sourcing.criteria` | GLOBAL | Registry of built-in sourcing criteria types |
529
+ | `fc.rubix.order.sourcing.criteria.custom` | ACCOUNT / RETAILER | Registry of custom sourcing criteria types |
530
+
531
+ ### Querying Settings
532
+
533
+ Use MCP tools to inspect current sourcing settings:
534
+
535
+ ```
536
+ Tool: graphql.query
537
+ Query: { settings(context: "RETAILER", contextId: <RETAILER_ID>) { edges { node { name value } } } }
538
+ Filter: name starts with "fc.rubix.order.sourcing"
539
+ ```
540
+
541
+ ### Custom Schema Registration
542
+
543
+ To register a custom condition or criterion type, create a setting at the `ACCOUNT` or `RETAILER` scope with the appropriate key. The value is a JSON array of type definitions:
544
+
545
+ ```json
546
+ {
547
+ "name": "fc.rubix.order.sourcing.criteria.custom",
548
+ "context": "ACCOUNT",
549
+ "contextId": 1,
550
+ "value": "[{\"name\":\"Custom Carrier Preference\",\"type\":\"ext.sourcing.criterion.preferredCarrier\",\"class\":\"com.example.PreferredCarrierCriterion\"}]"
551
+ }
552
+ ```
553
+
554
+ ## Workflow Integration
555
+
556
+ ### Workflow Rules
557
+
558
+ | Rule | Class | Purpose |
559
+ |------|-------|---------|
560
+ | `CreateFulfilmentWithSourcingProfile` | `com.fluentcommerce.rule.sourcing.CreateFulfilmentWithSourcingProfile` | Main rule — loads profile, runs evaluation, creates fulfilment |
561
+ | `CreatePartialFulfilmentWithSourcingProfile` | `com.fluentcommerce.rule.sourcing.CreatePartialFulfilmentWithSourcingProfile` | Handles partial sourcing from FALLBACK strategies |
562
+ | `CreateRejectedFulfilment` | `com.fluentcommerce.rule.sourcing.CreateRejectedFulfilment` | Creates a rejected fulfilment when no strategy can source |
563
+
564
+ ### Typical Workflow Pattern
565
+
566
+ ```mermaid
567
+ sequenceDiagram
568
+ participant WF as Order Workflow
569
+ participant Rule as CreateFulfilmentWithSourcingProfile
570
+ participant SP as SourcingProfile
571
+ participant Ctx as SourcingContext
572
+ participant Loc as Location Candidates
573
+
574
+ WF->>Rule: Event: SourceOrder (status: BOOKED)
575
+ Rule->>SP: Load profile by ref from rule props
576
+ Rule->>Ctx: Build SourcingContext (order, items, fulfilmentChoice)
577
+ Rule->>Loc: Query candidate locations from network
578
+ SP->>Ctx: Evaluate strategies (priority order)
579
+ Note over Ctx: For each strategy:<br/>1. Check all conditions<br/>2. Run criteria chain<br/>3. Score & rank locations
580
+ Ctx-->>Rule: FulfilmentPlan (locations + items)
581
+ alt Plan found
582
+ Rule->>WF: Create FULFILMENT entity
583
+ else No plan
584
+ Rule->>WF: Trigger CreateRejectedFulfilment
585
+ end
586
+ ```
587
+
588
+ ### Rule Configuration
589
+
590
+ The sourcing rule is configured in a workflow ruleset with the profile ref as a property:
591
+
592
+ ```json
593
+ {
594
+ "name": "RULESET::SOURCE_ORDER",
595
+ "description": "Source the order using the responsive sourcing profile",
596
+ "type": "ORDER",
597
+ "eventType": "NORMAL",
598
+ "rules": [
599
+ {
600
+ "name": "{{fluent.rule.sourcing.CreateFulfilmentWithSourcingProfile}}",
601
+ "props": {
602
+ "sourcingProfileRef": "SOURCING_PROFILE_HD",
603
+ "eventName": "FulfilmentCreated"
604
+ }
605
+ }
606
+ ],
607
+ "triggers": [
608
+ {
609
+ "status": "BOOKED"
610
+ }
611
+ ],
612
+ "userActions": []
613
+ }
614
+ ```
615
+
616
+ User action note (strict contract):
617
+ - This sourcing ruleset is backend-driven and typically keeps `userActions: []`.
618
+ - If you add user actions, set explicit `eventName` values and keep `context`/`attributes` to documented fields only.
619
+ - Validate action visibility with `workflow.transitions` (`module` vs `module: "all"`) and cross-check `/fluent-workflow-builder` + `/fluent-transition-api`.
620
+
621
+ ## GraphQL Operations
622
+
623
+ ### Query Profile with Full Strategy Tree
624
+
625
+ ```graphql
626
+ query GetSourcingProfile($ref: String!) {
627
+ sourcingProfile(ref: $ref) {
628
+ id
629
+ ref
630
+ status
631
+ catalogueRef
632
+ virtualCatalogueRef
633
+ networkRef
634
+ maxSplit
635
+ sourcingStrategies {
636
+ edges {
637
+ node {
638
+ id
639
+ ref
640
+ priority
641
+ status
642
+ type
643
+ sourcingConditions {
644
+ edges {
645
+ node {
646
+ id
647
+ name
648
+ type
649
+ params
650
+ }
651
+ }
652
+ }
653
+ sourcingCriteria {
654
+ edges {
655
+ node {
656
+ id
657
+ name
658
+ type
659
+ params
660
+ }
661
+ }
662
+ }
663
+ }
664
+ }
665
+ }
666
+ }
667
+ }
668
+ ```
669
+
670
+ ### Create Complete Profile
671
+
672
+ ```graphql
673
+ mutation CreateSourcingProfile($input: CreateSourcingProfileInput!) {
674
+ createSourcingProfile(input: $input) {
675
+ id
676
+ ref
677
+ status
678
+ }
679
+ }
680
+ ```
681
+
682
+ **Input variable structure:**
683
+ ```json
684
+ {
685
+ "input": {
686
+ "ref": "SOURCING_PROFILE_HD",
687
+ "retailerId": 1,
688
+ "status": "DRAFT",
689
+ "catalogueRef": "DEFAULT:1",
690
+ "networkRef": "HD_NETWORK",
691
+ "maxSplit": 2,
692
+ "sourcingStrategies": [
693
+ {
694
+ "ref": "STRATEGY_HD_STANDARD",
695
+ "priority": 10,
696
+ "status": "ACTIVE",
697
+ "type": "PRIMARY",
698
+ "sourcingConditions": [
699
+ {
700
+ "name": "IsHD",
701
+ "type": "fc.sourcing.condition.path",
702
+ "params": "{\"path\":\"fulfilmentChoice.type\",\"operator\":\"equals\",\"value\":\"HD\"}"
703
+ }
704
+ ],
705
+ "sourcingCriteria": [
706
+ {
707
+ "name": "HasStock",
708
+ "type": "fc.sourcing.criterion.inventoryAvailabilityExclusion",
709
+ "params": "{}"
710
+ },
711
+ {
712
+ "name": "ByDistance",
713
+ "type": "fc.sourcing.criterion.locationDistance",
714
+ "params": "{}"
715
+ }
716
+ ]
717
+ }
718
+ ]
719
+ }
720
+ }
721
+ ```
722
+
723
+ > **Note:** The `params` field in GraphQL mutations is a **JSON string**, not a JSON object. Stringify the params before sending.
724
+
725
+ ### Update & Activate
726
+
727
+ ```graphql
728
+ mutation UpdateSourcingProfile($input: UpdateSourcingProfileInput!) {
729
+ updateSourcingProfile(input: $input) {
730
+ id
731
+ ref
732
+ status
733
+ }
734
+ }
735
+ ```
736
+
737
+ To activate a profile:
738
+ ```json
739
+ {
740
+ "input": {
741
+ "id": "<PROFILE_ID>",
742
+ "status": "ACTIVE"
743
+ }
744
+ }
745
+ ```
746
+
747
+ ### Required Permissions
748
+
749
+ | Operation | Permission |
750
+ |-----------|-----------|
751
+ | Query profiles | `SOURCING_PROFILE:READ` |
752
+ | Create profile | `SOURCING_PROFILE:CREATE` |
753
+ | Update profile | `SOURCING_PROFILE:UPDATE` |
754
+ | Delete profile | `SOURCING_PROFILE:DELETE` |
755
+
756
+ ## Common Use Cases
757
+
758
+ ### 1. Region-Specific Sourcing
759
+
760
+ Different strategies for different delivery regions:
761
+
762
+ ```json
763
+ {
764
+ "sourcingStrategies": [
765
+ {
766
+ "ref": "METRO_EXPRESS",
767
+ "priority": 10,
768
+ "type": "PRIMARY",
769
+ "sourcingConditions": [
770
+ {
771
+ "name": "MetroZone",
772
+ "type": "fc.sourcing.condition.path",
773
+ "params": {
774
+ "path": "fulfilmentChoice.deliveryAddress.attributes[?(@.name=='zone')].value",
775
+ "operator": "equals",
776
+ "value": "METRO"
777
+ }
778
+ }
779
+ ],
780
+ "sourcingCriteria": [
781
+ { "name": "Within10km", "type": "fc.sourcing.criterion.locationDistanceExclusion", "params": { "value": 10.0, "valueUnit": "KILOMETRES" } },
782
+ { "name": "ByCapacity", "type": "fc.sourcing.criterion.locationDailyCapacity" }
783
+ ]
784
+ },
785
+ {
786
+ "ref": "REGIONAL_STANDARD",
787
+ "priority": 20,
788
+ "type": "PRIMARY",
789
+ "sourcingConditions": [],
790
+ "sourcingCriteria": [
791
+ { "name": "HasStock", "type": "fc.sourcing.criterion.inventoryAvailabilityExclusion" },
792
+ { "name": "ByDistance", "type": "fc.sourcing.criterion.locationDistance" }
793
+ ]
794
+ }
795
+ ]
796
+ }
797
+ ```
798
+
799
+ ### 2. Ship-from-Store Priority
800
+
801
+ Prefer stores over warehouses when stock is available:
802
+
803
+ ```json
804
+ {
805
+ "sourcingCriteria": [
806
+ { "name": "HasStock", "type": "fc.sourcing.criterion.inventoryAvailabilityExclusion" },
807
+ { "name": "ExcludeWarehouses", "type": "fc.sourcing.criterion.locationTypeExclusion", "params": { "value": ["WAREHOUSE"] } },
808
+ { "name": "ByDistance", "type": "fc.sourcing.criterion.locationDistance" }
809
+ ]
810
+ }
811
+ ```
812
+
813
+ Note: This excludes warehouses entirely. For a softer approach, use `networkPriority` with stores in a higher-priority network.
814
+
815
+ ### 3. Seasonal Capacity Management
816
+
817
+ Use banded criteria to prefer locations with high remaining capacity during peak season:
818
+
819
+ ```json
820
+ {
821
+ "sourcingCriteria": [
822
+ { "name": "HasStock", "type": "fc.sourcing.criterion.inventoryAvailabilityExclusion" },
823
+ {
824
+ "name": "CapacityBands",
825
+ "type": "fc.sourcing.criterion.inventoryAvailabilityBanded",
826
+ "params": {
827
+ "bands": [
828
+ { "from": 100, "to": 999999, "score": 1.0 },
829
+ { "from": 50, "to": 99, "score": 0.7 },
830
+ { "from": 10, "to": 49, "score": 0.3 },
831
+ { "from": 0, "to": 9, "score": 0.0 }
832
+ ]
833
+ }
834
+ },
835
+ { "name": "ByDistance", "type": "fc.sourcing.criterion.locationDistance" }
836
+ ]
837
+ }
838
+ ```
839
+
840
+ ### 4. Click-and-Collect (Same Store)
841
+
842
+ For CC orders, source from the selected pickup location:
843
+
844
+ ```json
845
+ {
846
+ "sourcingStrategies": [
847
+ {
848
+ "ref": "CC_PICKUP",
849
+ "priority": 10,
850
+ "type": "PRIMARY",
851
+ "sourcingConditions": [
852
+ {
853
+ "name": "IsCC",
854
+ "type": "fc.sourcing.condition.path",
855
+ "params": { "path": "fulfilmentChoice.type", "operator": "equals", "value": "CC" }
856
+ }
857
+ ],
858
+ "sourcingCriteria": [
859
+ { "name": "HasStock", "type": "fc.sourcing.criterion.inventoryAvailabilityExclusion" },
860
+ {
861
+ "name": "PickupStoreOnly",
862
+ "type": "fc.sourcing.criterion.locationNetworkExclusion",
863
+ "params": { "value": "CC_NETWORK" }
864
+ }
865
+ ]
866
+ }
867
+ ]
868
+ }
869
+ ```
870
+
871
+ ## Debugging and Troubleshooting
872
+
873
+ ### Common Failures
874
+
875
+ | Symptom | Probable Cause | Diagnosis |
876
+ |---------|----------------|-----------|
877
+ | All strategies skipped | Conditions don't match context | Check operator case (must be lowercase), verify JSON path resolves, check `conditionScope` for array paths |
878
+ | All locations excluded | Exclusion criteria too aggressive | Check `locationDistanceExclusion` threshold, verify inventory data exists, check location types |
879
+ | Wrong location selected | Scoring/normalization issue | Review criteria order, check if distance inversion is being applied correctly |
880
+ | `ClassNotFoundException` | Custom type not registered | Verify setting `fc.rubix.order.sourcing.criteria.custom` exists at correct scope |
881
+ | Parameter ignored | Wrong JSON key name | Check if source expects `"value"` vs descriptive name for your version |
882
+ | Profile not loaded | Wrong ref in rule props | Verify `sourcingProfileRef` in workflow ruleset matches profile `ref` exactly |
883
+ | Partial fulfilment not created | No FALLBACK strategy | Add a FALLBACK strategy for split scenarios |
884
+
885
+ ### Diagnosis Patterns
886
+
887
+ **1. Verify profile is active and loadable:**
888
+ ```
889
+ Tool: graphql.query
890
+ Query: { sourcingProfile(ref: "SOURCING_PROFILE_HD") { id ref status maxSplit } }
891
+ ```
892
+
893
+ **2. Check strategy conditions match runtime context:**
894
+ ```
895
+ Tool: event.flowInspect
896
+ entityType: ORDER
897
+ entityId: <ORDER_ID>
898
+ eventName: SourceOrder
899
+ compact: false
900
+ ```
901
+
902
+ Review the flow inspection output for condition evaluation results.
903
+
904
+ **3. Verify inventory data exists for candidate locations:**
905
+ ```
906
+ Tool: graphql.query
907
+ Query: {
908
+ inventoryPositions(
909
+ catalogue: { ref: "DEFAULT:1" }
910
+ productRef: "<PRODUCT_REF>"
911
+ ) {
912
+ edges { node { locationRef quantity status } }
913
+ }
914
+ }
915
+ ```
916
+
917
+ **4. Check settings for custom type registration:**
918
+ ```
919
+ Tool: graphql.query
920
+ Query: {
921
+ settings(
922
+ context: "ACCOUNT"
923
+ contextId: 1
924
+ name: "fc.rubix.order.sourcing.criteria.custom"
925
+ ) { edges { node { name value } } }
926
+ }
927
+ ```
928
+
929
+ ## Key Java Classes
930
+
931
+ (Source: decompiled `util-sourcing-2.1.0.jar`)
932
+
933
+ | Class | Package | Purpose |
934
+ |-------|---------|---------|
935
+ | `SourcingUtils` | `com.fluentcommerce.util.sourcing` | Main orchestrator — `findPlanBasedOnStrategies()`, `findPlanForAllItems()` |
936
+ | `SourcingContextUtils` | `com.fluentcommerce.util.sourcing` | Builds and manages the `SourcingContext` runtime object |
937
+ | `SourcingConditionUtils` | `com.fluentcommerce.util.sourcing.condition` | Evaluates conditions against context using operator registry |
938
+ | `SourcingCriteriaUtils` | `com.fluentcommerce.util.sourcing.criteria` | Executes criteria chain, normalization, aggregation |
939
+ | `SourcingConditionTypeRegistry` | `com.fluentcommerce.util.sourcing.condition` | Maps condition type strings to implementing classes |
940
+ | `SourcingCriteriaTypeRegistry` | `com.fluentcommerce.util.sourcing.criteria` | Maps criterion type strings to implementing classes |
941
+ | `LocationUtils` | `com.fluentcommerce.util.sourcing` | Haversine distance calculation, 10-minute location cache |
942
+ | `OrderUtils` | `com.fluentcommerce.util.sourcing` | Order item extraction and quantity calculation |
943
+
944
+ ### Extension Points
945
+
946
+ | Interface / Base Class | Implement For |
947
+ |----------------------|---------------|
948
+ | `SourcingCondition` | Custom condition type (boolean gate) |
949
+ | `BaseSourcingCriterion` | Custom criterion type (scorer/filter) |
950
+ | `SourcingConditionOperator` | Custom comparison operator |
951
+
952
+ ## Custom Extension Guide
953
+
954
+ ### Building a Custom Condition
955
+
956
+ **Step 1: Implement `SourcingCondition` interface**
957
+
958
+ ```java
959
+ package com.example.sourcing.condition;
960
+
961
+ import com.fluentcommerce.util.sourcing.condition.SourcingCondition;
962
+ import com.fluentcommerce.util.sourcing.model.SourcingContext;
963
+ import com.fasterxml.jackson.databind.JsonNode;
964
+
965
+ public class GeofenceCondition implements SourcingCondition {
966
+
967
+ private double minLat;
968
+ private double maxLat;
969
+ private double minLng;
970
+ private double maxLng;
971
+
972
+ @Override
973
+ public void init(JsonNode params) {
974
+ this.minLat = params.get("minLat").asDouble();
975
+ this.maxLat = params.get("maxLat").asDouble();
976
+ this.minLng = params.get("minLng").asDouble();
977
+ this.maxLng = params.get("maxLng").asDouble();
978
+ }
979
+
980
+ @Override
981
+ public boolean evaluate(SourcingContext context) {
982
+ double lat = context.getFulfilmentChoice()
983
+ .getDeliveryAddress().getLatitude();
984
+ double lng = context.getFulfilmentChoice()
985
+ .getDeliveryAddress().getLongitude();
986
+
987
+ return lat >= minLat && lat <= maxLat
988
+ && lng >= minLng && lng <= maxLng;
989
+ }
990
+ }
991
+ ```
992
+
993
+ **Step 2: Register in `SourcingConditionTypeRegistry`**
994
+
995
+ Add to your module's initialization:
996
+ ```java
997
+ SourcingConditionTypeRegistry.register(
998
+ "ext.sourcing.condition.geofence",
999
+ GeofenceCondition.class
1000
+ );
1001
+ ```
1002
+
1003
+ **Step 3: Create the setting**
1004
+
1005
+ Register at ACCOUNT or RETAILER scope:
1006
+ ```json
1007
+ {
1008
+ "name": "fc.rubix.order.sourcing.conditions.custom",
1009
+ "context": "ACCOUNT",
1010
+ "contextId": 1,
1011
+ "value": "[{\"name\":\"Geofence\",\"type\":\"ext.sourcing.condition.geofence\",\"class\":\"com.example.sourcing.condition.GeofenceCondition\"}]"
1012
+ }
1013
+ ```
1014
+
1015
+ **Step 4: Use in a strategy**
1016
+
1017
+ ```json
1018
+ {
1019
+ "name": "InSydneyMetro",
1020
+ "type": "ext.sourcing.condition.geofence",
1021
+ "params": {
1022
+ "minLat": -34.0,
1023
+ "maxLat": -33.5,
1024
+ "minLng": 150.8,
1025
+ "maxLng": 151.4
1026
+ }
1027
+ }
1028
+ ```
1029
+
1030
+ ### Building a Custom Criterion
1031
+
1032
+ **Step 1: Extend `BaseSourcingCriterion`**
1033
+
1034
+ ```java
1035
+ package com.example.sourcing.criterion;
1036
+
1037
+ import com.fluentcommerce.util.sourcing.criterion.BaseSourcingCriterion;
1038
+ import com.fluentcommerce.util.sourcing.criterion.annotation.SourcingCriterionInfo;
1039
+ import com.fluentcommerce.util.sourcing.criterion.annotation.SourcingCriterionParam;
1040
+ import com.fluentcommerce.util.sourcing.criterion.annotation.SourcingCriterionParamSelectComponentOption;
1041
+ import com.fluentcommerce.util.sourcing.model.CriterionContext;
1042
+ import com.fasterxml.jackson.databind.JsonNode;
1043
+
1044
+ @SourcingCriterionInfo(
1045
+ name = "Preferred Carrier",
1046
+ type = "ext.sourcing.criterion.preferredCarrier",
1047
+ tags = {"Sorter"},
1048
+ description = "Boost locations that support the customer's preferred carrier"
1049
+ )
1050
+ public class PreferredCarrierCriterion extends BaseSourcingCriterion {
1051
+
1052
+ @SourcingCriterionParam(
1053
+ name = "carrierAttributeName",
1054
+ component = "input",
1055
+ description = "Name of the location attribute holding supported carriers"
1056
+ )
1057
+ private String carrierAttributeName;
1058
+
1059
+ @SourcingCriterionParam(
1060
+ name = "boostFactor",
1061
+ component = "select",
1062
+ description = "Score multiplier when carrier matches"
1063
+ )
1064
+ @SourcingCriterionParamSelectComponentOption(name = "High", value = "2.0")
1065
+ @SourcingCriterionParamSelectComponentOption(name = "Medium", value = "1.5")
1066
+ @SourcingCriterionParamSelectComponentOption(name = "Low", value = "1.2")
1067
+ private double boostFactor;
1068
+
1069
+ @Override
1070
+ public void parseParams(JsonNode params) {
1071
+ this.carrierAttributeName = params.has("carrierAttributeName")
1072
+ ? params.get("carrierAttributeName").asText()
1073
+ : "supportedCarriers";
1074
+ this.boostFactor = params.has("boostFactor")
1075
+ ? params.get("boostFactor").asDouble()
1076
+ : 1.5;
1077
+ }
1078
+
1079
+ @Override
1080
+ public float apply(CriterionContext context) {
1081
+ // Get customer's preferred carrier from order attributes
1082
+ String preferredCarrier = getOrderAttribute(
1083
+ context, "preferredCarrier");
1084
+
1085
+ if (preferredCarrier == null) {
1086
+ return 1.0f; // No preference — neutral score
1087
+ }
1088
+
1089
+ // Check if location supports this carrier
1090
+ String supported = getLocationAttribute(
1091
+ context, carrierAttributeName);
1092
+
1093
+ if (supported != null && supported.contains(preferredCarrier)) {
1094
+ return (float) boostFactor; // Boosted score
1095
+ }
1096
+
1097
+ return 1.0f; // Base score
1098
+ }
1099
+ }
1100
+ ```
1101
+
1102
+ **Annotation Reference:**
1103
+
1104
+ | Annotation | Purpose |
1105
+ |-----------|---------|
1106
+ | `@SourcingCriterionInfo` | Declares the criterion name, type key, tags (Exclusion/Sorter), and description |
1107
+ | `@SourcingCriterionParam` | Declares a configurable parameter with name, UI component type, and description |
1108
+ | `@SourcingCriterionParamSelectComponentOption` | For `component="select"` params — defines dropdown options with name/value pairs |
1109
+
1110
+ **Step 2: Register in `SourcingCriteriaTypeRegistry`**
1111
+
1112
+ ```java
1113
+ SourcingCriteriaTypeRegistry.register(
1114
+ "ext.sourcing.criterion.preferredCarrier",
1115
+ PreferredCarrierCriterion.class
1116
+ );
1117
+ ```
1118
+
1119
+ **Step 3: Create the setting**
1120
+
1121
+ ```json
1122
+ {
1123
+ "name": "fc.rubix.order.sourcing.criteria.custom",
1124
+ "context": "ACCOUNT",
1125
+ "contextId": 1,
1126
+ "value": "[{\"name\":\"Preferred Carrier\",\"type\":\"ext.sourcing.criterion.preferredCarrier\",\"class\":\"com.example.sourcing.criterion.PreferredCarrierCriterion\"}]"
1127
+ }
1128
+ ```
1129
+
1130
+ **Step 4: Use in a strategy**
1131
+
1132
+ ```json
1133
+ {
1134
+ "name": "CarrierBoost",
1135
+ "type": "ext.sourcing.criterion.preferredCarrier",
1136
+ "params": {
1137
+ "carrierAttributeName": "supportedCarriers",
1138
+ "boostFactor": 1.5
1139
+ }
1140
+ }
1141
+ ```
1142
+
1143
+ ### Testing Custom Extensions
1144
+
1145
+ 1. **Unit test** — Mock `SourcingContext` and `CriterionContext`, verify scores
1146
+ 2. **Integration test** — Deploy to sandbox, create a test profile with the custom type, run `/fluent-e2e-test`
1147
+ 3. **Verify registration** — Query the custom settings to confirm type is registered:
1148
+ ```
1149
+ Tool: graphql.query
1150
+ Query: { settings(name: "fc.rubix.order.sourcing.criteria.custom", context: "ACCOUNT", contextId: 1) { edges { node { value } } } }
1151
+ ```
1152
+
1153
+ ## Integration with Other Skills
1154
+
1155
+ | Task | Skill | How |
1156
+ |------|-------|-----|
1157
+ | Build workflow that triggers sourcing | `/fluent-workflow-builder` | Add `CreateFulfilmentWithSourcingProfile` to a ruleset |
1158
+ | Scaffold custom criterion Java class | `/fluent-rule-scaffold` | Generate boilerplate extending `BaseSourcingCriterion` |
1159
+ | Deploy sourcing settings | `/fluent-settings` | Create/update `fc.rubix.order.sourcing.*` keys |
1160
+ | Query live profiles via MCP | `/fluent-mcp-tools` | Use `graphql.query` with the GraphQL patterns above |
1161
+ | Trace sourcing event execution | `/fluent-trace` | Inspect `SourceOrder` event flow and rule outputs |
1162
+ | Configure locations/networks | `/fluent-retailer-config` | Set up the location network referenced by profiles |
1163
+ | Create test orders for sourcing | `/fluent-test-data` | Generate orders with specific fulfilment choices |
1164
+ | Run E2E sourcing flow | `/fluent-e2e-test` | Create order → source → verify fulfilment plan |
1165
+ | Analyze existing sourcing workflows | `/fluent-workflow-analyzer` | Map status graph and ruleset connections |
1166
+
1167
+ ## Learning Capture
1168
+
1169
+ This is an analysis skill — follow the **Learning Capture Protocol** (see CLAUDE.md Cross-Skill Conventions). After sourcing analysis, when the user confirms or corrects findings about sourcing profile configuration, strategy behavior, or scoring quirks, write the confirmed learning to the `implementation-learnings.md` auto-memory file under the relevant account heading.
1170
+
1171
+ ## Tips
1172
+
1173
+ - **Only one ACTIVE version** of a profile ref can exist. Activating a new version deactivates the old one.
1174
+ - **Conditions use AND logic** — ALL conditions in a strategy must pass. For OR logic, create separate strategies.
1175
+ - **Exclusion score is `-1.0`** — Any criterion returning `-1.0` permanently removes that location from consideration for the current strategy.
1176
+ - **Normalization scope is per-criterion** — Each criterion normalizes independently across all remaining candidates, not across all criteria.
1177
+ - **Location cache TTL is 10 minutes** — `LocationUtils` caches location data. Changes to location attributes may take up to 10 minutes to be reflected in sourcing.
1178
+ - **maxSplit increases computation** — Permutation search is combinatorial. Use exclusion criteria to reduce candidate count before permutation.
1179
+ - **Operators are lowercase** — `equals`, not `EQUALS` or `Equals`. This is the most common configuration error.
1180
+ - **params is a JSON string in mutations** — When creating profiles via GraphQL, stringify the params object.
1181
+ - **Order criteria carefully** — Exclusions first (reduce pool), then sorters (rank remaining). This improves both performance and correctness.
1182
+ - **networkRef on profile is the default** — Individual strategies can override with `locationNetworkExclusion` criterion.
1183
+ - **`virtualCatalogueRef` overrides `catalogueRef`** — If both are set, the virtual catalogue is used for inventory lookups.
1184
+ - **Use `exists` operator for optional attributes** — Check if an attribute exists before comparing its value in a separate condition.
1185
+ - **Test with `/fluent-trace`** — After configuring a profile, send a test order event and use flow inspection to verify the sourcing decision path.