@open-mercato/core 0.10.1-develop.7772.1.9d3d6ce261 → 0.10.1-develop.7774.1.9a13ef7580

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.
Files changed (42) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/ai-tools.js.map +2 -2
  3. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js +1 -1
  4. package/dist/modules/customers/backend/customers/companies-v2/[id]/page.js.map +2 -2
  5. package/dist/modules/customers/backend/customers/deals/[id]/page.js +1 -1
  6. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  7. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js +1 -1
  8. package/dist/modules/customers/backend/customers/people-v2/[id]/page.js.map +2 -2
  9. package/dist/modules/customers/components/detail/CompanyDetailTabs.js +3 -2
  10. package/dist/modules/customers/components/detail/CompanyDetailTabs.js.map +2 -2
  11. package/dist/modules/customers/components/detail/DealDetailTabs.js +3 -2
  12. package/dist/modules/customers/components/detail/DealDetailTabs.js.map +2 -2
  13. package/dist/modules/customers/components/detail/PersonDetailTabs.js +3 -2
  14. package/dist/modules/customers/components/detail/PersonDetailTabs.js.map +2 -2
  15. package/dist/modules/sales/api/documents/factory.js +1 -1
  16. package/dist/modules/sales/api/documents/factory.js.map +2 -2
  17. package/dist/modules/sales/api/orders/route.js +5 -4
  18. package/dist/modules/sales/api/orders/route.js.map +2 -2
  19. package/dist/modules/sales/api/quotes/route.js +5 -4
  20. package/dist/modules/sales/api/quotes/route.js.map +2 -2
  21. package/dist/modules/sales/backend/sales/documents/[id]/page.js +23 -1
  22. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  23. package/dist/modules/sales/components/documents/SalesDocumentExtensionSpot.js +26 -0
  24. package/dist/modules/sales/components/documents/SalesDocumentExtensionSpot.js.map +7 -0
  25. package/dist/modules/sales/extension-points.js +26 -0
  26. package/dist/modules/sales/extension-points.js.map +2 -2
  27. package/package.json +7 -7
  28. package/src/modules/catalog/AGENTS.md +2 -2
  29. package/src/modules/customers/AGENTS.md +2 -2
  30. package/src/modules/customers/ai-tools.ts +6 -9
  31. package/src/modules/customers/backend/customers/companies-v2/[id]/page.tsx +1 -1
  32. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +1 -1
  33. package/src/modules/customers/backend/customers/people-v2/[id]/page.tsx +1 -1
  34. package/src/modules/customers/components/detail/CompanyDetailTabs.tsx +5 -1
  35. package/src/modules/customers/components/detail/DealDetailTabs.tsx +6 -2
  36. package/src/modules/customers/components/detail/PersonDetailTabs.tsx +6 -2
  37. package/src/modules/sales/api/documents/factory.ts +1 -1
  38. package/src/modules/sales/api/orders/route.ts +4 -3
  39. package/src/modules/sales/api/quotes/route.ts +4 -3
  40. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +19 -1
  41. package/src/modules/sales/components/documents/SalesDocumentExtensionSpot.tsx +44 -0
  42. package/src/modules/sales/extension-points.ts +26 -0
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { extensionPoints } from "@open-mercato/core/modules/sales/extension-points";
4
+ import { resolveExtensionPointPattern } from "@open-mercato/shared/modules/widgets/extension-points";
5
+ import { InjectionSpot } from "@open-mercato/ui/backend/injection/InjectionSpot";
6
+ function SalesDocumentExtensionSpot({
7
+ surface,
8
+ context,
9
+ data,
10
+ onDataChange
11
+ }) {
12
+ const pattern = surface === "stage-bar" ? extensionPoints.hosts.documentStageBar.pattern : extensionPoints.hosts.documentClosure.pattern;
13
+ return /* @__PURE__ */ jsx(
14
+ InjectionSpot,
15
+ {
16
+ spotId: resolveExtensionPointPattern(pattern, { kind: context.kind }),
17
+ context,
18
+ data,
19
+ onDataChange
20
+ }
21
+ );
22
+ }
23
+ export {
24
+ SalesDocumentExtensionSpot
25
+ };
26
+ //# sourceMappingURL=SalesDocumentExtensionSpot.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../src/modules/sales/components/documents/SalesDocumentExtensionSpot.tsx"],
4
+ "sourcesContent": ["'use client'\n\nimport { extensionPoints } from '@open-mercato/core/modules/sales/extension-points'\nimport { resolveExtensionPointPattern } from '@open-mercato/shared/modules/widgets/extension-points'\nimport { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'\n\nexport type SalesDocumentExtensionContext<TRecord extends { id: string }> = {\n kind: 'order' | 'quote'\n documentId: string | null\n record: TRecord | null\n formId: string\n resourceKind: 'sales.order' | 'sales.quote'\n resourceId?: string\n retryLastMutation: () => Promise<boolean>\n organizationId: string | null\n tenantId: string | null\n}\n\ntype SalesDocumentExtensionSpotProps<TRecord extends { id: string }> = {\n surface: 'stage-bar' | 'closure'\n context: SalesDocumentExtensionContext<TRecord>\n data: TRecord\n onDataChange: (data: TRecord) => void\n}\n\nexport function SalesDocumentExtensionSpot<TRecord extends { id: string }>({\n surface,\n context,\n data,\n onDataChange,\n}: SalesDocumentExtensionSpotProps<TRecord>) {\n const pattern = surface === 'stage-bar'\n ? extensionPoints.hosts.documentStageBar.pattern\n : extensionPoints.hosts.documentClosure.pattern\n\n return (\n <InjectionSpot\n spotId={resolveExtensionPointPattern(pattern, { kind: context.kind })}\n context={context}\n data={data}\n onDataChange={onDataChange}\n />\n )\n}\n"],
5
+ "mappings": ";AAoCI;AAlCJ,SAAS,uBAAuB;AAChC,SAAS,oCAAoC;AAC7C,SAAS,qBAAqB;AAqBvB,SAAS,2BAA2D;AAAA,EACzE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAA6C;AAC3C,QAAM,UAAU,YAAY,cACxB,gBAAgB,MAAM,iBAAiB,UACvC,gBAAgB,MAAM,gBAAgB;AAE1C,SACE;AAAA,IAAC;AAAA;AAAA,MACC,QAAQ,6BAA6B,SAAS,EAAE,MAAM,QAAQ,KAAK,CAAC;AAAA,MACpE;AAAA,MACA;AAAA,MACA;AAAA;AAAA,EACF;AAEJ;",
6
+ "names": []
7
+ }
@@ -17,6 +17,32 @@ const extensionPoints = defineModuleExtensionPoints({
17
17
  supported: ["render-widget"],
18
18
  source: "backend/sales/documents/[id]/page.tsx"
19
19
  }),
20
+ documentStageBar: injectionExtensionHost({
21
+ family: "detail",
22
+ pattern: "detail:sales.{kind}:stage-bar",
23
+ parameters: {
24
+ kind: { source: "SalesDocumentExtensionContext.kind", pattern: "^(order|quote)$" }
25
+ },
26
+ supported: ["render-widget"],
27
+ source: ["components/documents/SalesDocumentExtensionSpot.tsx", "backend/sales/documents/[id]/page.tsx"],
28
+ contextContract: "SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId",
29
+ dataContract: "Current sales order or quote record; onDataChange updates the in-page record only",
30
+ scopeContract: "organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization",
31
+ runtimeContract: "Rendered below the document header; no native stage transition or mutation lifecycle is added"
32
+ }),
33
+ documentClosure: injectionExtensionHost({
34
+ family: "detail",
35
+ pattern: "detail:sales.{kind}:closure",
36
+ parameters: {
37
+ kind: { source: "SalesDocumentExtensionContext.kind", pattern: "^(order|quote)$" }
38
+ },
39
+ supported: ["render-widget"],
40
+ source: ["components/documents/SalesDocumentExtensionSpot.tsx", "backend/sales/documents/[id]/page.tsx"],
41
+ contextContract: "SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId",
42
+ dataContract: "Current sales order or quote record; onDataChange updates the in-page record only",
43
+ scopeContract: "organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization",
44
+ runtimeContract: "Rendered below document details for closure extensions; no native closure dialog or mutation lifecycle is added"
45
+ }),
20
46
  documentDetail: injectionExtensionHost({
21
47
  family: "detail",
22
48
  pattern: "sales.document.detail.{kind}:{surface}",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/sales/extension-points.ts"],
4
- "sourcesContent": ["import { dataTableExtensionHost, defineModuleExtensionPoints, injectionExtensionHost } from '@open-mercato/shared/modules/widgets/extension-points'\n\nexport const extensionPoints = defineModuleExtensionPoints({\n moduleId: 'sales',\n hosts: {\n ordersTable: dataTableExtensionHost({ tableId: 'sales.orders', source: 'components/documents/SalesDocumentsTable.tsx' }),\n quotesTable: dataTableExtensionHost({ tableId: 'sales.quotes', source: 'components/documents/SalesDocumentsTable.tsx' }),\n paymentsTable: dataTableExtensionHost({ tableId: 'sales.payments', source: 'components/documents/PaymentsSection.tsx' }),\n orderItemColumns: injectionExtensionHost({\n family: 'detail',\n spotId: 'data-table:sales.order.items:columns',\n supported: ['column-widget'],\n source: 'components/documents/ItemsSection.tsx',\n }),\n orderShipping: injectionExtensionHost({\n family: 'detail',\n spotId: 'detail:sales.order:shipping',\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n documentDetail: injectionExtensionHost({\n family: 'detail',\n pattern: 'sales.document.detail.{kind}:{surface}',\n parameters: {\n kind: { source: 'SalesDocumentKind', pattern: '^(order|quote)$' },\n surface: { source: 'host declaration', pattern: '^(tabs|details)$' },\n },\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n },\n})\n\nexport default extensionPoints\n"],
5
- "mappings": "AAAA,SAAS,wBAAwB,6BAA6B,8BAA8B;AAErF,MAAM,kBAAkB,4BAA4B;AAAA,EACzD,UAAU;AAAA,EACV,OAAO;AAAA,IACL,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,eAAe,uBAAuB,EAAE,SAAS,kBAAkB,QAAQ,2CAA2C,CAAC;AAAA,IACvH,kBAAkB,uBAAuB;AAAA,MACvC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,eAAe,uBAAuB;AAAA,MACpC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,gBAAgB,uBAAuB;AAAA,MACrC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,qBAAqB,SAAS,kBAAkB;AAAA,QAChE,SAAS,EAAE,QAAQ,oBAAoB,SAAS,mBAAmB;AAAA,MACrE;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACF,CAAC;AAED,IAAO,2BAAQ;",
4
+ "sourcesContent": ["import { dataTableExtensionHost, defineModuleExtensionPoints, injectionExtensionHost } from '@open-mercato/shared/modules/widgets/extension-points'\n\nexport const extensionPoints = defineModuleExtensionPoints({\n moduleId: 'sales',\n hosts: {\n ordersTable: dataTableExtensionHost({ tableId: 'sales.orders', source: 'components/documents/SalesDocumentsTable.tsx' }),\n quotesTable: dataTableExtensionHost({ tableId: 'sales.quotes', source: 'components/documents/SalesDocumentsTable.tsx' }),\n paymentsTable: dataTableExtensionHost({ tableId: 'sales.payments', source: 'components/documents/PaymentsSection.tsx' }),\n orderItemColumns: injectionExtensionHost({\n family: 'detail',\n spotId: 'data-table:sales.order.items:columns',\n supported: ['column-widget'],\n source: 'components/documents/ItemsSection.tsx',\n }),\n orderShipping: injectionExtensionHost({\n family: 'detail',\n spotId: 'detail:sales.order:shipping',\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n documentStageBar: injectionExtensionHost({\n family: 'detail',\n pattern: 'detail:sales.{kind}:stage-bar',\n parameters: {\n kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },\n },\n supported: ['render-widget'],\n source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],\n contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',\n dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',\n scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',\n runtimeContract: 'Rendered below the document header; no native stage transition or mutation lifecycle is added',\n }),\n documentClosure: injectionExtensionHost({\n family: 'detail',\n pattern: 'detail:sales.{kind}:closure',\n parameters: {\n kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },\n },\n supported: ['render-widget'],\n source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],\n contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',\n dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',\n scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',\n runtimeContract: 'Rendered below document details for closure extensions; no native closure dialog or mutation lifecycle is added',\n }),\n documentDetail: injectionExtensionHost({\n family: 'detail',\n pattern: 'sales.document.detail.{kind}:{surface}',\n parameters: {\n kind: { source: 'SalesDocumentKind', pattern: '^(order|quote)$' },\n surface: { source: 'host declaration', pattern: '^(tabs|details)$' },\n },\n supported: ['render-widget'],\n source: 'backend/sales/documents/[id]/page.tsx',\n }),\n },\n})\n\nexport default extensionPoints\n"],
5
+ "mappings": "AAAA,SAAS,wBAAwB,6BAA6B,8BAA8B;AAErF,MAAM,kBAAkB,4BAA4B;AAAA,EACzD,UAAU;AAAA,EACV,OAAO;AAAA,IACL,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,aAAa,uBAAuB,EAAE,SAAS,gBAAgB,QAAQ,+CAA+C,CAAC;AAAA,IACvH,eAAe,uBAAuB,EAAE,SAAS,kBAAkB,QAAQ,2CAA2C,CAAC;AAAA,IACvH,kBAAkB,uBAAuB;AAAA,MACvC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,eAAe,uBAAuB;AAAA,MACpC,QAAQ;AAAA,MACR,QAAQ;AAAA,MACR,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,IACD,kBAAkB,uBAAuB;AAAA,MACvC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,sCAAsC,SAAS,kBAAkB;AAAA,MACnF;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ,CAAC,uDAAuD,uCAAuC;AAAA,MACvG,iBAAiB;AAAA,MACjB,cAAc;AAAA,MACd,eAAe;AAAA,MACf,iBAAiB;AAAA,IACnB,CAAC;AAAA,IACD,iBAAiB,uBAAuB;AAAA,MACtC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,sCAAsC,SAAS,kBAAkB;AAAA,MACnF;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ,CAAC,uDAAuD,uCAAuC;AAAA,MACvG,iBAAiB;AAAA,MACjB,cAAc;AAAA,MACd,eAAe;AAAA,MACf,iBAAiB;AAAA,IACnB,CAAC;AAAA,IACD,gBAAgB,uBAAuB;AAAA,MACrC,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,YAAY;AAAA,QACV,MAAM,EAAE,QAAQ,qBAAqB,SAAS,kBAAkB;AAAA,QAChE,SAAS,EAAE,QAAQ,oBAAoB,SAAS,mBAAmB;AAAA,MACrE;AAAA,MACA,WAAW,CAAC,eAAe;AAAA,MAC3B,QAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACF,CAAC;AAED,IAAO,2BAAQ;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.10.1-develop.7772.1.9d3d6ce261",
3
+ "version": "0.10.1-develop.7774.1.9a13ef7580",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -266,16 +266,16 @@
266
266
  "zod": "^4.4.3"
267
267
  },
268
268
  "peerDependencies": {
269
- "@open-mercato/ai-assistant": "0.10.1-develop.7772.1.9d3d6ce261",
270
- "@open-mercato/shared": "0.10.1-develop.7772.1.9d3d6ce261",
271
- "@open-mercato/ui": "0.10.1-develop.7772.1.9d3d6ce261",
269
+ "@open-mercato/ai-assistant": "0.10.1-develop.7774.1.9a13ef7580",
270
+ "@open-mercato/shared": "0.10.1-develop.7774.1.9a13ef7580",
271
+ "@open-mercato/ui": "0.10.1-develop.7774.1.9a13ef7580",
272
272
  "react": "^19.0.0",
273
273
  "react-dom": "^19.0.0"
274
274
  },
275
275
  "devDependencies": {
276
- "@open-mercato/ai-assistant": "0.10.1-develop.7772.1.9d3d6ce261",
277
- "@open-mercato/shared": "0.10.1-develop.7772.1.9d3d6ce261",
278
- "@open-mercato/ui": "0.10.1-develop.7772.1.9d3d6ce261",
276
+ "@open-mercato/ai-assistant": "0.10.1-develop.7774.1.9a13ef7580",
277
+ "@open-mercato/shared": "0.10.1-develop.7774.1.9a13ef7580",
278
+ "@open-mercato/ui": "0.10.1-develop.7774.1.9a13ef7580",
279
279
  "@testing-library/dom": "^10.4.1",
280
280
  "@testing-library/jest-dom": "^7.0.1",
281
281
  "@testing-library/react": "^16.3.3",
@@ -112,6 +112,6 @@ Two typed AI agents ship from `ai-agents.ts`. See `/framework/ai-assistant/agent
112
112
  | Agent ID | Mode | Policy | Purpose |
113
113
  |----------|------|--------|---------|
114
114
  | `catalog.catalog_assistant` | chat | `read-only` | General operator explorer for products, categories, variants, prices, offers, product media, tags, option schemas, and unit conversions via the base catalog tool pack + general-purpose packs. |
115
- | `catalog.merchandising_assistant` | chat | `read-only` (mutation-capable via per-tenant override unlocking `catalog.update_product`, `catalog.bulk_update_products`, `catalog.apply_attribute_extraction`, `catalog.update_product_media_descriptions`) | D18 demo agent: proposes descriptions, attribute extractions, title variants, and price adjustments for the current selection on the products list page. |
115
+ | `catalog.merchandising_assistant` | chat | `confirm-required`; tenant overrides may restrict to `read-only`, never escalate | D18 agent: proposes descriptions, attribute extractions, title variants, and price adjustments; whitelisted product, batch, attribute, and media writes require approval. See `ai-agents.ts`. |
116
116
 
117
- The merchandising assistant is the Phase 2 D18 demo. `<AiChat agent="catalog.merchandising_assistant" />` is injected via `MerchandisingAssistantSheet.tsx` on `/backend/catalog/catalog/products` (see `packages/core/src/modules/catalog/backend/catalog/products/page.tsx`). Mutation-capable authoring tools route through `prepareMutation` + the approval-card contract; confirmed batches refresh the DataTable via `catalog.product.updated` events on the DOM event bridge.
117
+ The merchandising assistant is the D18 demo. `<AiChat agent="catalog.merchandising_assistant" />` is injected via `MerchandisingAssistantSheet.tsx` on `/backend/catalog/products` (see `packages/core/src/modules/catalog/backend/catalog/products/page.tsx`). Mutation tools route through `prepareMutation` + the approval-card contract; confirmed batches refresh the DataTable via `catalog.product.updated` events on the DOM event bridge.
@@ -125,8 +125,8 @@ This module is the reference implementation for the AI framework. Copy `ai-agent
125
125
 
126
126
  | Agent ID | Mode | Policy | Purpose |
127
127
  |----------|------|--------|---------|
128
- | `customers.account_assistant` | chat | read-only (mutation-capable via per-tenant override that unlocks `customers.update_deal_stage`; see `packages/core/src/modules/customers/ai-agents.ts` for the whitelist and prompt) | Operator-facing assistant that explores people, companies, deals, activities, tasks, addresses, tags, and settings through the customers tool pack. |
129
- | `customers.update_deal_stage` | tool (mutation) | `destructive-confirm-required` — goes through `prepareMutation` + the approval card | Moves a deal between stages / flips status between open, won, lost. Declared via `defineAiTool` in `ai-tools.ts` and exposed only when the tenant mutation-policy override promotes the agent above read-only. |
128
+ | `customers.account_assistant` | chat | `confirm-required`; tenant overrides may restrict to `read-only`, never escalate | Explores people, companies, deals, activities, tasks, addresses, tags, and settings; whitelisted stage, comment, and activity writes require approval. See `ai-agents.ts`. |
129
+ | `customers.update_deal_stage` | tool (mutation) | Agent mutation policy applies through `prepareMutation` + the approval card | Moves deals between stages or open/won/lost statuses. Declared in `ai-tools/deals-pack.ts`; blocked when the agent is read-only. |
130
130
  | `customers.deal_analyzer` | chat, `executionEngine: 'stream-text'` | `confirm-required` | Multi-step agentic demo that exercises loop primitives from PRs #1856..#1869. Calls `customers.analyze_deals`, ranks deals by health score, then calls `customers.update_deal_stage` for the highest-value stalled deal — loop stops immediately after (loop.stopWhen). `loop.prepareStep` scopes the active tool set by step without overriding the model. It intentionally leaves `defaultProvider` and `defaultModel` unset so runtime resolution uses `OM_AI_PROVIDER` / `OM_AI_MODEL` or tenant/runtime overrides. |
131
131
  | `customers.deal_analyzer_tool_loop` | chat, `executionEngine: 'tool-loop-agent'` | `confirm-required` | Sibling agent identical to `customers.deal_analyzer` but dispatched via `ToolLoopAgent`. Proves the mutation-approval gate holds for both execution engines (TC-AI-AGENT-LOOP-006). |
132
132
 
@@ -1,18 +1,15 @@
1
1
  /**
2
- * Module-root AI tool contribution for the customers module
3
- * (Phase 1 WS-C, Step 3.9 — read-only Phase 1 surface).
2
+ * Module-root AI tool contribution for the customers module.
4
3
  *
5
4
  * The generator walks every module for a top-level `ai-tools.ts` and takes
6
5
  * the default/`aiTools` export as the contribution. This file aggregates the
7
- * six customers packs (people, companies, deals, activities+tasks,
8
- * addresses+tags, settings) so they all flow through the existing
6
+ * customers packs (people, companies, deals, activities+tasks,
7
+ * addresses+tags, settings, deal analysis) so they all flow through the existing
9
8
  * `ai-tools.generated.ts` pipeline without any generator changes.
10
9
  *
11
- * Mutation tools are deferred to Step 5.13+ under the pending-action contract;
12
- * every tool here is read-only and enforces tenant + organization scoping via
13
- * the existing encryption helpers. See
14
- * `.ai/runs/2026-04-18-ai-framework-unification/step-3.9-checks.md` for the
15
- * matrix of required features and decisions.
10
+ * Read and mutation tools enforce tenant + organization scoping. Mutations
11
+ * use the pending-action approval contract and command-backed writes;
12
+ * read-only agents cannot expose them. See the framework AI agents guide.
16
13
  */
17
14
  import peopleAiTools from './ai-tools/people-pack'
18
15
  import companiesAiTools from './ai-tools/companies-pack'
@@ -530,7 +530,7 @@ export default function CompanyDetailV2Page({ params }: { params?: { id?: string
530
530
  activeTab={activeTab}
531
531
  onTabChange={handleTabChange}
532
532
  isLoadingInjectedTabs={injectedTabsLoading}
533
- injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
533
+ injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label, priority: tab.priority }))}
534
534
  peopleCount={data.counts?.people ?? 0}
535
535
  dealsCount={dealCount}
536
536
  activitiesCount={data.counts?.activities ?? 0}
@@ -667,7 +667,7 @@ export default function DealDetailPage({ params }: { params?: { id?: string } })
667
667
  isLoadingInjectedTabs={injectedTabsLoading}
668
668
  activeTab={activeTab}
669
669
  onTabChange={handleTabChange}
670
- injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
670
+ injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label, priority: tab.priority }))}
671
671
  peopleCount={data.counts.people}
672
672
  companiesCount={data.counts.companies}
673
673
  >
@@ -568,7 +568,7 @@ export default function PersonDetailV2Page({ params }: { params?: { id?: string
568
568
  activeTab={activeTab}
569
569
  onTabChange={handleTabChange}
570
570
  isLoadingInjectedTabs={injectedTabsLoading}
571
- injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label }))}
571
+ injectedTabs={injectedTabs.map((tab) => ({ id: tab.id, label: tab.label, priority: tab.priority }))}
572
572
  activitiesCount={interactionCount}
573
573
  dealsCount={dealCount}
574
574
  companiesCount={companyCount}
@@ -32,6 +32,7 @@ type TabDef = {
32
32
  label: string
33
33
  icon?: React.ReactNode
34
34
  count?: React.ReactNode
35
+ priority?: number
35
36
  }
36
37
 
37
38
  export const COMPANY_DETAIL_TABS_COMPONENT_ID = 'section:customers.companies.detailTabs'
@@ -135,8 +136,11 @@ function DefaultCompanyDetailTabs({
135
136
  ...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
136
137
  id: tab.id as CompanyTabId,
137
138
  label: tab.label,
139
+ priority: tab.priority,
138
140
  })),
139
- ].filter((tab) => !hidden.has(tab.id))
141
+ ]
142
+ .sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0))
143
+ .filter((tab) => !hidden.has(tab.id))
140
144
  }, [builtInTabs, hiddenTabIds, injectedTabs])
141
145
 
142
146
  React.useEffect(() => {
@@ -23,6 +23,7 @@ type TabDef = {
23
23
  label: string
24
24
  icon?: React.ReactNode
25
25
  count?: React.ReactNode
26
+ priority?: number
26
27
  }
27
28
 
28
29
  export const DEAL_DETAIL_TABS_COMPONENT_ID = 'section:customers.deals.detailTabs'
@@ -30,7 +31,7 @@ export const DEAL_DETAIL_TABS_COMPONENT_ID = 'section:customers.deals.detailTabs
30
31
  export type DealDetailTabsProps = {
31
32
  activeTab: DealTabId
32
33
  onTabChange: (tab: DealTabId) => void
33
- injectedTabs?: Array<{ id: string; label: string }>
34
+ injectedTabs?: Array<{ id: string; label: string; priority?: number }>
34
35
  isLoadingInjectedTabs?: boolean
35
36
  hiddenTabIds?: string[]
36
37
  peopleCount?: number
@@ -110,8 +111,11 @@ function DefaultDealDetailTabs({
110
111
  ...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
111
112
  id: tab.id as DealTabId,
112
113
  label: tab.label,
114
+ priority: tab.priority,
113
115
  })),
114
- ].filter((tab) => !hidden.has(tab.id))
116
+ ]
117
+ .sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0))
118
+ .filter((tab) => !hidden.has(tab.id))
115
119
  }, [builtInTabs, hiddenTabIds, injectedTabs])
116
120
 
117
121
  React.useEffect(() => {
@@ -37,6 +37,7 @@ type TabDef = {
37
37
  label: string
38
38
  icon?: React.ReactNode
39
39
  count?: React.ReactNode
40
+ priority?: number
40
41
  }
41
42
 
42
43
  export const PERSON_DETAIL_TABS_COMPONENT_ID = 'section:customers.people.detailTabs'
@@ -44,7 +45,7 @@ export const PERSON_DETAIL_TABS_COMPONENT_ID = 'section:customers.people.detailT
44
45
  export type PersonDetailTabsProps = {
45
46
  activeTab: PersonTabId
46
47
  onTabChange: (tab: PersonTabId) => void
47
- injectedTabs?: Array<{ id: string; label: string }>
48
+ injectedTabs?: Array<{ id: string; label: string; priority?: number }>
48
49
  isLoadingInjectedTabs?: boolean
49
50
  hiddenTabIds?: string[]
50
51
  activitiesCount?: number
@@ -148,8 +149,11 @@ function DefaultPersonDetailTabs({
148
149
  ...injectedTabs.filter((tab, index) => !builtInTabs.some((native) => native.id === tab.id) && injectedTabs.findIndex((candidate) => candidate.id === tab.id) === index).map((tab) => ({
149
150
  id: tab.id as PersonTabId,
150
151
  label: tab.label,
152
+ priority: tab.priority,
151
153
  })),
152
- ].filter((tab) => !hidden.has(tab.id))
154
+ ]
155
+ .sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0))
156
+ .filter((tab) => !hidden.has(tab.id))
153
157
  }, [builtInTabs, hiddenTabIds, injectedTabs])
154
158
 
155
159
  React.useEffect(() => {
@@ -473,7 +473,7 @@ export function buildDocumentCrudOptions(binding: DocumentBinding) {
473
473
  indexer: {
474
474
  entityType: binding.entityId,
475
475
  },
476
- enrichers: binding.kind === 'order' ? { entityId: binding.entityId } : undefined,
476
+ enrichers: { entityId: binding.entityId },
477
477
  list: {
478
478
  schema: listSchema,
479
479
  entityId: binding.entityId,
@@ -3,8 +3,8 @@ import { SalesOrder } from '../../data/entities'
3
3
  import { E } from '#generated/entities.ids.generated'
4
4
  import { buildDocumentCrudOptions, buildDocumentOpenApi } from '../documents/factory'
5
5
 
6
- const crud = makeCrudRoute(
7
- buildDocumentCrudOptions({
6
+ const crud = makeCrudRoute({
7
+ ...buildDocumentCrudOptions({
8
8
  kind: 'order',
9
9
  entity: SalesOrder,
10
10
  entityId: E.sales.sales_order,
@@ -15,7 +15,8 @@ const crud = makeCrudRoute(
15
15
  manageFeature: 'sales.orders.manage',
16
16
  viewFeature: 'sales.orders.view',
17
17
  }),
18
- )
18
+ enrichers: { entityId: E.sales.sales_order },
19
+ })
19
20
 
20
21
  export const metadata = crud.metadata
21
22
  export const GET = crud.GET
@@ -3,8 +3,8 @@ import { SalesQuote } from '../../data/entities'
3
3
  import { E } from '#generated/entities.ids.generated'
4
4
  import { buildDocumentCrudOptions, buildDocumentOpenApi } from '../documents/factory'
5
5
 
6
- const crud = makeCrudRoute(
7
- buildDocumentCrudOptions({
6
+ const crud = makeCrudRoute({
7
+ ...buildDocumentCrudOptions({
8
8
  kind: 'quote',
9
9
  entity: SalesQuote,
10
10
  entityId: E.sales.sales_quote,
@@ -15,7 +15,8 @@ const crud = makeCrudRoute(
15
15
  manageFeature: 'sales.quotes.manage',
16
16
  viewFeature: 'sales.quotes.view',
17
17
  }),
18
- )
18
+ enrichers: { entityId: E.sales.sales_quote },
19
+ })
19
20
 
20
21
  export const metadata = crud.metadata
21
22
  export const GET = crud.GET
@@ -52,6 +52,7 @@ import type { AdjustmentRowData } from '@open-mercato/core/modules/sales/compone
52
52
  import { SalesShipmentsSection } from '@open-mercato/core/modules/sales/components/documents/ShipmentsSection'
53
53
  import { SalesReturnsSection } from '@open-mercato/core/modules/sales/components/documents/ReturnsSection'
54
54
  import { DocumentTotals } from '@open-mercato/core/modules/sales/components/documents/DocumentTotals'
55
+ import { SalesDocumentExtensionSpot, type SalesDocumentExtensionContext } from '@open-mercato/core/modules/sales/components/documents/SalesDocumentExtensionSpot'
55
56
  import { E } from '#generated/entities.ids.generated'
56
57
  import type { DictionarySelectLabels } from '@open-mercato/core/modules/dictionaries/components/DictionaryEntrySelect'
57
58
  import { useCurrencyDictionary } from '@open-mercato/core/modules/customers/components/detail/hooks/useCurrencyDictionary'
@@ -862,6 +863,8 @@ type StatusOption = {
862
863
 
863
864
  type DocumentRecord = {
864
865
  id: string
866
+ organizationId?: string | null
867
+ tenantId?: string | null
865
868
  orderNumber?: string | null
866
869
  quoteNumber?: string | null
867
870
  status?: string | null
@@ -1992,14 +1995,17 @@ export default function SalesDocumentDetailPage({
1992
1995
  contextId: mutationContextId,
1993
1996
  blockedMessage: t('ui.forms.flash.saveBlocked', 'Save blocked by validation'),
1994
1997
  })
1995
- const detailInjectionContext = React.useMemo(
1998
+ const detailInjectionContext = React.useMemo<SalesDocumentExtensionContext<DocumentRecord>>(
1996
1999
  () => ({
1997
2000
  kind,
2001
+ documentId: record?.id ?? null,
1998
2002
  record,
1999
2003
  formId: mutationContextId,
2000
2004
  resourceKind: `sales.${kind}`,
2001
2005
  resourceId: record?.id ?? undefined,
2002
2006
  retryLastMutation,
2007
+ organizationId: record?.organizationId ?? null,
2008
+ tenantId: record?.tenantId ?? null,
2003
2009
  }),
2004
2010
  [kind, mutationContextId, record, retryLastMutation],
2005
2011
  )
@@ -4779,6 +4785,12 @@ export default function SalesDocumentDetailPage({
4779
4785
  isDeleting={deleting}
4780
4786
  deleteLabel={t('sales.documents.detail.delete', 'Delete')}
4781
4787
  />
4788
+ <SalesDocumentExtensionSpot
4789
+ surface="stage-bar"
4790
+ context={detailInjectionContext}
4791
+ data={record}
4792
+ onDataChange={setRecord}
4793
+ />
4782
4794
  <div className="grid gap-4 md:grid-cols-4">
4783
4795
  <div className="md:col-span-3">
4784
4796
  <CustomerInlineEditor
@@ -5023,6 +5035,12 @@ export default function SalesDocumentDetailPage({
5023
5035
  />
5024
5036
  </div>
5025
5037
 
5038
+ <SalesDocumentExtensionSpot
5039
+ surface="closure"
5040
+ context={detailInjectionContext}
5041
+ data={record}
5042
+ onDataChange={setRecord}
5043
+ />
5026
5044
  <div className="space-y-4">
5027
5045
  <CustomDataSection
5028
5046
  title={t('sales.documents.detail.customData', 'Custom data')}
@@ -0,0 +1,44 @@
1
+ 'use client'
2
+
3
+ import { extensionPoints } from '@open-mercato/core/modules/sales/extension-points'
4
+ import { resolveExtensionPointPattern } from '@open-mercato/shared/modules/widgets/extension-points'
5
+ import { InjectionSpot } from '@open-mercato/ui/backend/injection/InjectionSpot'
6
+
7
+ export type SalesDocumentExtensionContext<TRecord extends { id: string }> = {
8
+ kind: 'order' | 'quote'
9
+ documentId: string | null
10
+ record: TRecord | null
11
+ formId: string
12
+ resourceKind: 'sales.order' | 'sales.quote'
13
+ resourceId?: string
14
+ retryLastMutation: () => Promise<boolean>
15
+ organizationId: string | null
16
+ tenantId: string | null
17
+ }
18
+
19
+ type SalesDocumentExtensionSpotProps<TRecord extends { id: string }> = {
20
+ surface: 'stage-bar' | 'closure'
21
+ context: SalesDocumentExtensionContext<TRecord>
22
+ data: TRecord
23
+ onDataChange: (data: TRecord) => void
24
+ }
25
+
26
+ export function SalesDocumentExtensionSpot<TRecord extends { id: string }>({
27
+ surface,
28
+ context,
29
+ data,
30
+ onDataChange,
31
+ }: SalesDocumentExtensionSpotProps<TRecord>) {
32
+ const pattern = surface === 'stage-bar'
33
+ ? extensionPoints.hosts.documentStageBar.pattern
34
+ : extensionPoints.hosts.documentClosure.pattern
35
+
36
+ return (
37
+ <InjectionSpot
38
+ spotId={resolveExtensionPointPattern(pattern, { kind: context.kind })}
39
+ context={context}
40
+ data={data}
41
+ onDataChange={onDataChange}
42
+ />
43
+ )
44
+ }
@@ -18,6 +18,32 @@ export const extensionPoints = defineModuleExtensionPoints({
18
18
  supported: ['render-widget'],
19
19
  source: 'backend/sales/documents/[id]/page.tsx',
20
20
  }),
21
+ documentStageBar: injectionExtensionHost({
22
+ family: 'detail',
23
+ pattern: 'detail:sales.{kind}:stage-bar',
24
+ parameters: {
25
+ kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },
26
+ },
27
+ supported: ['render-widget'],
28
+ source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],
29
+ contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',
30
+ dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',
31
+ scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',
32
+ runtimeContract: 'Rendered below the document header; no native stage transition or mutation lifecycle is added',
33
+ }),
34
+ documentClosure: injectionExtensionHost({
35
+ family: 'detail',
36
+ pattern: 'detail:sales.{kind}:closure',
37
+ parameters: {
38
+ kind: { source: 'SalesDocumentExtensionContext.kind', pattern: '^(order|quote)$' },
39
+ },
40
+ supported: ['render-widget'],
41
+ source: ['components/documents/SalesDocumentExtensionSpot.tsx', 'backend/sales/documents/[id]/page.tsx'],
42
+ contextContract: 'SalesDocumentExtensionContext: kind, documentId, record, formId, resourceKind, resourceId, retryLastMutation, organizationId, tenantId',
43
+ dataContract: 'Current sales order or quote record; onDataChange updates the in-page record only',
44
+ scopeContract: 'organizationId and tenantId from the loaded, server-scoped document; widget writes must enforce their own authorization',
45
+ runtimeContract: 'Rendered below document details for closure extensions; no native closure dialog or mutation lifecycle is added',
46
+ }),
21
47
  documentDetail: injectionExtensionHost({
22
48
  family: 'detail',
23
49
  pattern: 'sales.document.detail.{kind}:{surface}',