@meuecommerce/frete-adapter-node 0.8.1 → 0.10.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.
@@ -2,13 +2,14 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.DIMENSION_PROMPT_VERSION = void 0;
4
4
  exports.createOpenAIDimensionEstimator = createOpenAIDimensionEstimator;
5
+ const frete_1 = require("@meuecommerce/frete");
5
6
  const openAIResponsesAgentRunner_js_1 = require("./openAIResponsesAgentRunner.js");
6
7
  /**
7
8
  * Bump whenever the prompt or the schema changes. It is written next to every
8
9
  * `L3` record so a prompt change can invalidate stale estimates in background
9
10
  * instead of at checkout.
10
11
  */
11
- exports.DIMENSION_PROMPT_VERSION = "2026-09-04.responses.v1";
12
+ exports.DIMENSION_PROMPT_VERSION = "2026-09-11.responses.v3-detalhes";
12
13
  /**
13
14
  * Strict JSON schema for the response.
14
15
  *
@@ -29,8 +30,20 @@ const DIMENSIONS_SCHEMA = {
29
30
  length: { type: "number", description: "Comprimento em cm" },
30
31
  width: { type: "number", description: "Largura em cm" },
31
32
  height: { type: "number", description: "Altura em cm" },
33
+ attrs: {
34
+ type: "object",
35
+ description: "Como o produto ocupa espaço. Na dúvida, false — cada traço afrouxa o encaixe.",
36
+ properties: {
37
+ fragile: { type: "boolean", description: "Quebra se receber peso em cima (vidro, cerâmica, ovo)" },
38
+ compressible: { type: "boolean", description: "Cede quando espremido (roupa, tecido, espuma, travesseiro)" },
39
+ nestable: { type: "boolean", description: "Unidades iguais encaixam uma na outra (tigela, copo, cone, pote)" },
40
+ keepUpright: { type: "boolean", description: "Precisa viajar em pé (líquido, bolo, planta)" },
41
+ },
42
+ required: ["fragile", "compressible", "nestable", "keepUpright"],
43
+ additionalProperties: false,
44
+ },
32
45
  },
33
- required: ["name", "length", "width", "height"],
46
+ required: ["name", "length", "width", "height", "attrs"],
34
47
  additionalProperties: false,
35
48
  },
36
49
  },
@@ -41,6 +54,28 @@ const DIMENSIONS_SCHEMA = {
41
54
  function largestBox(boxes) {
42
55
  return boxes.reduce((largest, current) => current.width * current.height * current.length > largest.width * largest.height * largest.length ? current : largest);
43
56
  }
57
+ /**
58
+ * Uma entrada da lista numerada: a linha de identificação e, recuado embaixo
59
+ * dela, o contexto que a loja tiver mandado.
60
+ *
61
+ * A separação é o que faz a resposta voltar: a primeira linha é curta e é dela
62
+ * que sai o `name` que o modelo copia. Antes de `details` existir, quem tinha
63
+ * descrição e categoria para mandar empurrava tudo para dentro do `name`, a
64
+ * lista numerada virava um bloco de cinco linhas por item, e o modelo
65
+ * respondia o título limpo — correto, e impossível de casar com o que foi
66
+ * perguntado.
67
+ */
68
+ function descreverProduto(p, i) {
69
+ const linha = `${i + 1}. ${p.name} - ${p.quantity}x (${p.weight}kg cada)`;
70
+ if (!p.details?.trim())
71
+ return linha;
72
+ const contexto = p.details
73
+ .trim()
74
+ .split("\n")
75
+ .map((l) => ` ${l.trim()}`)
76
+ .join("\n");
77
+ return `${linha}\n${contexto}`;
78
+ }
44
79
  function createPrompt(products, boxes, samples) {
45
80
  const box = largestBox(boxes);
46
81
  const maxWidth = Math.floor(box.width * 0.8);
@@ -52,9 +87,32 @@ function createPrompt(products, boxes, samples) {
52
87
  : `\n📋 INSTRUÇÕES (SEM EXEMPLOS):\n1. Considere a embalagem comercial típica brasileira\n2. Use conhecimento sobre produtos similares\n3. Ajuste baseado no peso\n`;
53
88
  return (`Você é um especialista em dimensões de produtos brasileiros para e-commerce.\n\n` +
54
89
  `🚨 LIMITES FÍSICOS OBRIGATÓRIOS:\n- Largura máxima: ${maxWidth}cm\n- Altura máxima: ${maxHeight}cm\n- Comprimento máximo: ${maxLength}cm\n(Maior caixa: ${box.name ?? "—"})\n\n` +
55
- `📦 PRODUTOS PARA ESTIMAR:\n${products.map((p, i) => `${i + 1}. ${p.name} - ${p.quantity}x (${p.weight}kg cada)`).join("\n")}\n` +
90
+ `📦 PRODUTOS PARA ESTIMAR:\n${products.map(descreverProduto).join("\n")}\n` +
56
91
  samplesSection +
57
- `\n⚠️ REGRAS: respeite os limites; preserve os nomes EXATOS; retorne length×width×height em cm.`);
92
+ `\n🧱 COMO O PRODUTO OCUPA ESPAÇO (campo attrs, um por produto):\n` +
93
+ `- fragile: quebra se levar peso em cima — vidro, cerâmica, garrafa, ovo\n` +
94
+ `- compressible: cede quando espremido — roupa, tecido, espuma, travesseiro, pelúcia\n` +
95
+ `- nestable: unidades IGUAIS encaixam uma na outra — tigela, copo, cone, pote, vaso\n` +
96
+ `- keepUpright: precisa viajar em pé — líquido, tinta, bolo, planta\n` +
97
+ `Na dúvida responda false. Cada traço afrouxa o encaixe, e afrouxar errado faz o pacote não fechar.\n` +
98
+ `\n⚠️ REGRAS: respeite os limites; retorne length×width×height em cm; ` +
99
+ `copie em "name" o nome EXATO da linha numerada do produto, sem as linhas recuadas de contexto — ` +
100
+ `é por ele que a resposta é reconhecida, e um nome diferente descarta a estimativa.`);
101
+ }
102
+ /**
103
+ * Só os traços verdadeiros, e nada quando não há nenhum.
104
+ *
105
+ * O schema obriga o modelo a responder os quatro booleanos, então quase todo
106
+ * produto volta com `{fragile:false, compressible:false, ...}`. Guardar isso é
107
+ * gravar ruído em cada linha da memória e, pior, apagar a diferença entre
108
+ * "sabemos que é um tijolo" e "não perguntamos" — que é justamente o que o
109
+ * empacotador precisa distinguir.
110
+ */
111
+ function attrsVerdadeiros(attrs) {
112
+ if (!attrs)
113
+ return null;
114
+ const so = Object.fromEntries(Object.entries(attrs).filter(([, v]) => v === true));
115
+ return Object.keys(so).length ? { attrs: so } : null;
58
116
  }
59
117
  /** Apply the largest-box physical limits + a minimum-volume floor to AI output. */
60
118
  function validateAndOptimize(aiResult, products, boxes) {
@@ -71,7 +129,10 @@ function validateAndOptimize(aiResult, products, boxes) {
71
129
  if (height > maxHeight)
72
130
  height = maxHeight;
73
131
  const volume = length * width * height;
74
- const original = products.find((p) => p.name === product.name);
132
+ // Mesma normalização que liga a resposta de volta ao produto na memória.
133
+ // Comparando as strings cruas, um espaço a mais fazia o peso sumir e o
134
+ // piso de volume cair para o de um produto de 100 g.
135
+ const original = products.find((p) => (0, frete_1.productNameKey)(p.name) === (0, frete_1.productNameKey)(product.name));
75
136
  const minVolume = (original?.weight || 0.1) * 500;
76
137
  if (volume > 0 && volume < minVolume) {
77
138
  const scale = Math.cbrt(minVolume / volume);
@@ -84,6 +145,7 @@ function validateAndOptimize(aiResult, products, boxes) {
84
145
  length: Math.round(length * 10) / 10,
85
146
  width: Math.round(width * 10) / 10,
86
147
  height: Math.round(height * 10) / 10,
148
+ ...(attrsVerdadeiros(product.attrs) ?? {}),
87
149
  };
88
150
  });
89
151
  return { products: optimized };
@@ -25,7 +25,7 @@ type FetchLike = typeof globalThis.fetch;
25
25
  * `L3` record so a prompt change can invalidate stale estimates in background
26
26
  * instead of at checkout.
27
27
  */
28
- export declare const DIMENSION_PROMPT_VERSION = "2026-09-04.responses.v1";
28
+ export declare const DIMENSION_PROMPT_VERSION = "2026-09-11.responses.v3-detalhes";
29
29
  export interface OpenAIDimensionEstimatorOptions {
30
30
  apiKey?: string;
31
31
  fetch?: FetchLike;
@@ -1,10 +1,11 @@
1
+ import { productNameKey } from "@meuecommerce/frete";
1
2
  import { createOpenAIResponsesAgentRunner } from "./openAIResponsesAgentRunner.js";
2
3
  /**
3
4
  * Bump whenever the prompt or the schema changes. It is written next to every
4
5
  * `L3` record so a prompt change can invalidate stale estimates in background
5
6
  * instead of at checkout.
6
7
  */
7
- export const DIMENSION_PROMPT_VERSION = "2026-09-04.responses.v1";
8
+ export const DIMENSION_PROMPT_VERSION = "2026-09-11.responses.v3-detalhes";
8
9
  /**
9
10
  * Strict JSON schema for the response.
10
11
  *
@@ -25,8 +26,20 @@ const DIMENSIONS_SCHEMA = {
25
26
  length: { type: "number", description: "Comprimento em cm" },
26
27
  width: { type: "number", description: "Largura em cm" },
27
28
  height: { type: "number", description: "Altura em cm" },
29
+ attrs: {
30
+ type: "object",
31
+ description: "Como o produto ocupa espaço. Na dúvida, false — cada traço afrouxa o encaixe.",
32
+ properties: {
33
+ fragile: { type: "boolean", description: "Quebra se receber peso em cima (vidro, cerâmica, ovo)" },
34
+ compressible: { type: "boolean", description: "Cede quando espremido (roupa, tecido, espuma, travesseiro)" },
35
+ nestable: { type: "boolean", description: "Unidades iguais encaixam uma na outra (tigela, copo, cone, pote)" },
36
+ keepUpright: { type: "boolean", description: "Precisa viajar em pé (líquido, bolo, planta)" },
37
+ },
38
+ required: ["fragile", "compressible", "nestable", "keepUpright"],
39
+ additionalProperties: false,
40
+ },
28
41
  },
29
- required: ["name", "length", "width", "height"],
42
+ required: ["name", "length", "width", "height", "attrs"],
30
43
  additionalProperties: false,
31
44
  },
32
45
  },
@@ -37,6 +50,28 @@ const DIMENSIONS_SCHEMA = {
37
50
  function largestBox(boxes) {
38
51
  return boxes.reduce((largest, current) => current.width * current.height * current.length > largest.width * largest.height * largest.length ? current : largest);
39
52
  }
53
+ /**
54
+ * Uma entrada da lista numerada: a linha de identificação e, recuado embaixo
55
+ * dela, o contexto que a loja tiver mandado.
56
+ *
57
+ * A separação é o que faz a resposta voltar: a primeira linha é curta e é dela
58
+ * que sai o `name` que o modelo copia. Antes de `details` existir, quem tinha
59
+ * descrição e categoria para mandar empurrava tudo para dentro do `name`, a
60
+ * lista numerada virava um bloco de cinco linhas por item, e o modelo
61
+ * respondia o título limpo — correto, e impossível de casar com o que foi
62
+ * perguntado.
63
+ */
64
+ function descreverProduto(p, i) {
65
+ const linha = `${i + 1}. ${p.name} - ${p.quantity}x (${p.weight}kg cada)`;
66
+ if (!p.details?.trim())
67
+ return linha;
68
+ const contexto = p.details
69
+ .trim()
70
+ .split("\n")
71
+ .map((l) => ` ${l.trim()}`)
72
+ .join("\n");
73
+ return `${linha}\n${contexto}`;
74
+ }
40
75
  function createPrompt(products, boxes, samples) {
41
76
  const box = largestBox(boxes);
42
77
  const maxWidth = Math.floor(box.width * 0.8);
@@ -48,9 +83,32 @@ function createPrompt(products, boxes, samples) {
48
83
  : `\n📋 INSTRUÇÕES (SEM EXEMPLOS):\n1. Considere a embalagem comercial típica brasileira\n2. Use conhecimento sobre produtos similares\n3. Ajuste baseado no peso\n`;
49
84
  return (`Você é um especialista em dimensões de produtos brasileiros para e-commerce.\n\n` +
50
85
  `🚨 LIMITES FÍSICOS OBRIGATÓRIOS:\n- Largura máxima: ${maxWidth}cm\n- Altura máxima: ${maxHeight}cm\n- Comprimento máximo: ${maxLength}cm\n(Maior caixa: ${box.name ?? "—"})\n\n` +
51
- `📦 PRODUTOS PARA ESTIMAR:\n${products.map((p, i) => `${i + 1}. ${p.name} - ${p.quantity}x (${p.weight}kg cada)`).join("\n")}\n` +
86
+ `📦 PRODUTOS PARA ESTIMAR:\n${products.map(descreverProduto).join("\n")}\n` +
52
87
  samplesSection +
53
- `\n⚠️ REGRAS: respeite os limites; preserve os nomes EXATOS; retorne length×width×height em cm.`);
88
+ `\n🧱 COMO O PRODUTO OCUPA ESPAÇO (campo attrs, um por produto):\n` +
89
+ `- fragile: quebra se levar peso em cima — vidro, cerâmica, garrafa, ovo\n` +
90
+ `- compressible: cede quando espremido — roupa, tecido, espuma, travesseiro, pelúcia\n` +
91
+ `- nestable: unidades IGUAIS encaixam uma na outra — tigela, copo, cone, pote, vaso\n` +
92
+ `- keepUpright: precisa viajar em pé — líquido, tinta, bolo, planta\n` +
93
+ `Na dúvida responda false. Cada traço afrouxa o encaixe, e afrouxar errado faz o pacote não fechar.\n` +
94
+ `\n⚠️ REGRAS: respeite os limites; retorne length×width×height em cm; ` +
95
+ `copie em "name" o nome EXATO da linha numerada do produto, sem as linhas recuadas de contexto — ` +
96
+ `é por ele que a resposta é reconhecida, e um nome diferente descarta a estimativa.`);
97
+ }
98
+ /**
99
+ * Só os traços verdadeiros, e nada quando não há nenhum.
100
+ *
101
+ * O schema obriga o modelo a responder os quatro booleanos, então quase todo
102
+ * produto volta com `{fragile:false, compressible:false, ...}`. Guardar isso é
103
+ * gravar ruído em cada linha da memória e, pior, apagar a diferença entre
104
+ * "sabemos que é um tijolo" e "não perguntamos" — que é justamente o que o
105
+ * empacotador precisa distinguir.
106
+ */
107
+ function attrsVerdadeiros(attrs) {
108
+ if (!attrs)
109
+ return null;
110
+ const so = Object.fromEntries(Object.entries(attrs).filter(([, v]) => v === true));
111
+ return Object.keys(so).length ? { attrs: so } : null;
54
112
  }
55
113
  /** Apply the largest-box physical limits + a minimum-volume floor to AI output. */
56
114
  function validateAndOptimize(aiResult, products, boxes) {
@@ -67,7 +125,10 @@ function validateAndOptimize(aiResult, products, boxes) {
67
125
  if (height > maxHeight)
68
126
  height = maxHeight;
69
127
  const volume = length * width * height;
70
- const original = products.find((p) => p.name === product.name);
128
+ // Mesma normalização que liga a resposta de volta ao produto na memória.
129
+ // Comparando as strings cruas, um espaço a mais fazia o peso sumir e o
130
+ // piso de volume cair para o de um produto de 100 g.
131
+ const original = products.find((p) => productNameKey(p.name) === productNameKey(product.name));
71
132
  const minVolume = (original?.weight || 0.1) * 500;
72
133
  if (volume > 0 && volume < minVolume) {
73
134
  const scale = Math.cbrt(minVolume / volume);
@@ -80,6 +141,7 @@ function validateAndOptimize(aiResult, products, boxes) {
80
141
  length: Math.round(length * 10) / 10,
81
142
  width: Math.round(width * 10) / 10,
82
143
  height: Math.round(height * 10) / 10,
144
+ ...(attrsVerdadeiros(product.attrs) ?? {}),
83
145
  };
84
146
  });
85
147
  return { products: optimized };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meuecommerce/frete-adapter-node",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "description": "Node implementation of @meuecommerce/frete ports (Correios HTTP client) using global fetch. Used by the Shopify/Fly deployment and the MCP server.",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -31,7 +31,7 @@
31
31
  "access": "public"
32
32
  },
33
33
  "dependencies": {
34
- "@meuecommerce/frete": "^0.8.1"
34
+ "@meuecommerce/frete": "^0.10.0"
35
35
  },
36
36
  "module": "./dist/index.js",
37
37
  "repository": {