@intelex-ia/sdk 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.
package/README.md CHANGED
@@ -1,207 +1,310 @@
1
- # `@intelex-ia/sdk`
2
-
3
- SDK pra consumir a **Content API IntelexIA** em sites headless (Next.js, Astro etc.). Multiplataforma Fase 2b.
4
-
5
- > **O que você ganha:** `getArticles` / `getArticleBySlug` tipados, um `<ArticleJsonLd>` pra injetar o schema JSON-LD, e um `articleToMetadata` pra plugar no `generateMetadata` do Next.js App Router. Zero dependências (usa `fetch` global).
6
-
7
- ## Instalação
8
-
9
- ```bash
10
- pnpm add @intelex-ia/sdk
11
- # ou
12
- npm install @intelex-ia/sdk
13
- ```
14
-
15
- > **Enquanto não publicamos no npm**, instale via tarball/git URL (peça pro time IntelexIA).
16
-
17
- ## Configuração
18
-
19
- 1. No painel da IntelexIA, **cadastre o site como "Headless (código)"** (vai pedir só domain + opcional revalidate URL/secret).
20
- 2. Abra o site na lista → seção **"Tokens de API (Content API)"** → **Gerar token**. Você recebe `itx_…` (mostrado uma única vez).
21
- 3. Coloque o token em `.env.local`:
22
-
23
- ```bash
24
- INTELEXIA_TOKEN=itx_seu_token_aqui
25
- INTELEXIA_API_URL=https://api.innovarticles.intelexia.com.br # opcional, é o default
26
- ```
27
-
28
- ## Uso (Next.js App Router)
29
-
30
- ### Cliente — `lib/intelexia.ts`
31
-
32
- ```ts
33
- import { createClient } from "@intelex-ia/sdk";
34
-
35
- export const intelexia = createClient({
36
- token: process.env.INTELEXIA_TOKEN!,
37
- // ISR: revalida a cada 60s (ou use webhook revalidate pra invalidar
38
- // sob demanda — ver mais abaixo).
39
- init: { next: { revalidate: 60, tags: ["intelexia-articles"] } },
40
- });
41
- ```
42
-
43
- ### Listagem — `app/blog/page.tsx` (Server Component)
44
-
45
- ```tsx
46
- import { intelexia } from "@/lib/intelexia";
47
- import Link from "next/link";
48
-
49
- export default async function BlogIndex() {
50
- const { items } = await intelexia.getArticles({ limit: 20 });
51
- return (
52
- <main>
53
- <h1>Blog</h1>
54
- <ul>
55
- {items.map((a) => (
56
- <li key={a.id}>
57
- <Link href={`/blog/${a.slug}`}>{a.title}</Link>
58
- {a.publishedAt && (
59
- <time>{new Date(a.publishedAt).toLocaleDateString("pt-BR")}</time>
60
- )}
61
- </li>
62
- ))}
63
- </ul>
64
- </main>
65
- );
66
- }
67
- ```
68
-
69
- ### Post — `app/blog/[slug]/page.tsx` (com SEO completo)
70
-
71
- ```tsx
72
- import { intelexia } from "@/lib/intelexia";
73
- import { ArticleJsonLd, articleToMetadata } from "@intelex-ia/sdk/react";
74
- import { IntelexiaApiError } from "@intelex-ia/sdk";
75
- import { notFound } from "next/navigation";
76
-
77
- async function fetchArticle(slug: string) {
78
- try {
79
- return await intelexia.getArticleBySlug(slug);
80
- } catch (err) {
81
- if (err instanceof IntelexiaApiError && err.status === 404) notFound();
82
- throw err;
83
- }
84
- }
85
-
86
- export async function generateMetadata({
87
- params,
88
- }: {
89
- params: Promise<{ slug: string }>;
90
- }) {
91
- const { slug } = await params;
92
- const { article } = await fetchArticle(slug);
93
- return articleToMetadata(article); // → title, description, OG, Twitter, keywords
94
- }
95
-
96
- export default async function PostPage({
97
- params,
98
- }: {
99
- params: Promise<{ slug: string }>;
100
- }) {
101
- const { slug } = await params;
102
- const { article } = await fetchArticle(slug);
103
- return (
104
- <article>
105
- {/* Schema JSON-LD (Article/Breadcrumb/FAQ/HowTo/Org/Person) */}
106
- <ArticleJsonLd article={article} />
107
-
108
- <h1>{article.title}</h1>
109
- {article.featuredImageUrl && (
110
- <img src={article.featuredImageUrl} alt={article.title ?? ""} />
111
- )}
112
- <div
113
- dangerouslySetInnerHTML={{ __html: article.contentHtml ?? "" }}
114
- />
115
- </article>
116
- );
117
- }
118
- ```
119
-
120
- ### Webhook de revalidate — `app/api/revalidate/route.ts`
121
-
122
- Quando um artigo publica no painel, o worker IntelexIA faz um `POST` no seu endpoint de revalidate (configurado no cadastro do site). Implemente assim:
123
-
124
- ```ts
125
- import { NextResponse } from "next/server";
126
- import { revalidatePath, revalidateTag } from "next/cache";
127
-
128
- export async function POST(req: Request) {
129
- // Autentica com o secret cadastrado no painel (header X ou body)
130
- const headerSecret = req.headers.get("x-revalidate-secret");
131
- const body = (await req.json().catch(() => ({}))) as {
132
- secret?: string;
133
- slug?: string | null;
134
- domain?: string;
135
- };
136
- const got = headerSecret ?? body.secret ?? "";
137
- if (!process.env.INTELEXIA_REVALIDATE_SECRET || got !== process.env.INTELEXIA_REVALIDATE_SECRET) {
138
- return NextResponse.json({ ok: false, error: "unauthorized" }, { status: 401 });
139
- }
140
-
141
- // Revalida a página do post (se veio slug) e o índice do blog.
142
- if (body.slug) revalidatePath(`/blog/${body.slug}`);
143
- revalidatePath("/blog");
144
- revalidateTag("intelexia-articles");
145
-
146
- return NextResponse.json({ ok: true, revalidated: { slug: body.slug ?? null } });
147
- }
148
- ```
149
-
150
- E no `.env.local`:
151
-
152
- ```bash
153
- INTELEXIA_REVALIDATE_SECRET=cole-o-mesmo-segredo-do-painel
154
- ```
155
-
156
- > O worker envia o secret tanto em `x-revalidate-secret` quanto no body (`{ secret, slug, domain }`), tem timeout de 15s e é **fire-and-forget** (se o seu endpoint falhar, o artigo continua publicado e a ISR atualiza pelo `revalidate: 60`).
157
-
158
- ## API
159
-
160
- ### `createClient(config)`
161
-
162
- | Opção | Tipo | Default |
163
- | -------- | -------------------- | ------------------------------------------------- |
164
- | `token` | `string` (obrig.) | — |
165
- | `apiUrl` | `string` | `https://api.innovarticles.intelexia.com.br` |
166
- | `fetch` | `typeof fetch` | `globalThis.fetch` |
167
- | `init` | `RequestInit` | `undefined` (passe `{ next: { revalidate: N } }`) |
168
-
169
- ### `client.getArticles({ limit?, offset?, init? })`
170
-
171
- Resposta: `{ ok, mode, lifecycle, count, items: ArticleListItem[] }`.
172
-
173
- ### `client.getArticleBySlug(slug, { init? })`
174
-
175
- Resposta: `{ ok, mode, lifecycle, article: Article }`. **404 → joga `IntelexiaApiError`** (use `try/catch` + `notFound()`).
176
-
177
- ### `mode: "full" | "plain"`
178
-
179
- - **`full`** (sites `active`/`paused`): inclui schema JSON-LD, metaTitle/Description, keyword(s), interlinking interno no `contentHtml`.
180
- - **`plain`** (site `cancelled`): só conteúdo cru — schema/meta/keywords vêm `null`, interlinking removido. O `ArticleJsonLd` e o `articleToMetadata` lidam graciosamente (omitem o que não tem).
181
-
182
- A decisão é **server-side e não-negociável** — o cliente não escolhe o modo.
183
-
184
- ### Helpers `/react`
185
-
186
- - `<ArticleJsonLd article={article} />` — renderiza `<script type="application/ld+json">` com o `schemaJson`. No-op se ausente.
187
- - `articleToMetadata(article)` → objeto compatível com o `Metadata` do Next.js App Router (title, description, keywords, openGraph, twitter, customMetaTags como `other`).
188
-
189
- ## Erros
190
-
191
- ```ts
192
- import { IntelexiaApiError } from "@intelex-ia/sdk";
193
-
194
- try {
195
- await intelexia.getArticleBySlug("post-inexistente");
196
- } catch (err) {
197
- if (err instanceof IntelexiaApiError) {
198
- console.error(err.status, err.message); // 404, "Artigo não encontrado."
199
- }
200
- }
201
- ```
202
-
203
- ## Compatibilidade
204
-
205
- - Node 18+ (fetch nativo), Edge Runtime, Browser.
206
- - React 18+ (peer dep opcional — só pros helpers `/react`).
207
- - TypeScript: tipos publicados no `dist`.
1
+ # `@intelex-ia/sdk`
2
+
3
+ SDK pra consumir a **Content API IntelexIA** em sites headless (Next.js, Astro etc.). Multiplataforma Fase 2b.
4
+
5
+ > **O que você ganha:** `getArticles` / `getArticleBySlug` tipados, `mergePosts()` pra unir conteúdo antigo + novo do painel, `<ArticleJsonLd>` pra injetar schema JSON-LD, e `articleToMetadata` pra plugar no `generateMetadata` do Next.js App Router. Zero dependências (usa `fetch` global).
6
+
7
+ ## Instalação rápida (recomendado)
8
+
9
+ Se você está começando agora, use o CLI — ele detecta a estrutura do seu projeto e configura tudo automaticamente:
10
+
11
+ ```bash
12
+ npx @intelex-ia/cli init
13
+ ```
14
+
15
+ Cria os arquivos no lugar certo (App Router ou Pages Router, com ou sem `src/`), configura `.env.local`, gera um `INTELEXIA.md` completo na raiz e instala o SDK pra você.
16
+
17
+ ## Instalação manual
18
+
19
+ ```bash
20
+ npm install @intelex-ia/sdk
21
+ # ou pnpm add @intelex-ia/sdk / yarn add @intelex-ia/sdk
22
+ ```
23
+
24
+ ## Configuração
25
+
26
+ 1. No painel da IntelexIA, **cadastre o site como "Headless (código)"** (vai pedir domain + URL e secret de revalidate).
27
+ 2. Abra o site na lista → seção **"Tokens de API (Content API)"** → **Gerar token**. Você recebe `itx_…` (mostrado uma única vez).
28
+ 3. Coloque em `.env.local`:
29
+
30
+ ```bash
31
+ INTELEXIA_API_TOKEN=itx_seu_token_aqui
32
+ INTELEXIA_API_URL=https://api.innovarticles.intelexia.com.br # opcional, é o default
33
+ INTELEXIA_REVALIDATE_SECRET=cole-o-mesmo-segredo-do-painel
34
+ ```
35
+
36
+ ## Três modos de integração
37
+
38
+ Cada cliente tem uma situação diferente. Escolha o modo que faz sentido:
39
+
40
+ | Modo | Quando usar | O que muda no `/blog` |
41
+ |---|---|---|
42
+ | **🔀 Additive (recomendado)** | Cliente já tem posts antigos e quer **adicionar** os do painel sem perder os antigos | `mergePosts()` une os dois feeds, dedupa por slug, ordena por data |
43
+ | **🔁 Replace** | Cliente migrou tudo pro painel — só os novos contam | Substitui `getAllPosts()` por `client.getArticles()` |
44
+ | **🆕 Isolated** | Cliente quer manter `/blog` antigo intacto e criar rota nova só pra IA | Cria `/blog/ia` separado, blog antigo zero alterado |
45
+
46
+ ---
47
+
48
+ ### Modo 1: Additive (cliente existente — caso mais comum)
49
+
50
+ ```tsx
51
+ // app/blog/page.tsx
52
+ import { intelexia } from "@/lib/intelexia";
53
+ import { mergePosts } from "@intelex-ia/sdk";
54
+ import { posts as localPosts } from "@/lib/posts"; // seu array atual
55
+ import Link from "next/link";
56
+
57
+ export default async function BlogIndex() {
58
+ // 1) Busca os artigos do painel
59
+ const { items: remote } = await intelexia.getArticles({ limit: 50 });
60
+
61
+ // 2) Mistura com os seus posts antigos
62
+ const all = mergePosts({
63
+ local: localPosts,
64
+ remote,
65
+ mapLocal: (p) => ({
66
+ slug: p.slug,
67
+ title: p.title,
68
+ excerpt: p.excerpt,
69
+ publishedAt: p.date,
70
+ featuredImageUrl: null,
71
+ category: p.category,
72
+ }),
73
+ // Opcional — defaults sensatos:
74
+ // dedupe: "remote-wins" (painel substitui local se slug colide)
75
+ // sortBy: "date-desc" (mais recente primeiro)
76
+ });
77
+
78
+ return (
79
+ <ul>
80
+ {all.map((p) => (
81
+ <li key={p.slug}>
82
+ <Link href={`/blog/${p.slug}`}>{p.title}</Link>
83
+ <time>{new Date(p.publishedAt).toLocaleDateString("pt-BR")}</time>
84
+ {p.source === "remote" && <span className="badge-ia">✨ IA</span>}
85
+ </li>
86
+ ))}
87
+ </ul>
88
+ );
89
+ }
90
+ ```
91
+
92
+ Pro post individual, tenta o painel primeiro, cai pro local se 404:
93
+
94
+ ```tsx
95
+ // app/blog/[slug]/page.tsx
96
+ import { intelexia } from "@/lib/intelexia";
97
+ import { IntelexiaApiError } from "@intelex-ia/sdk";
98
+ import { ArticleJsonLd, articleToMetadata } from "@intelex-ia/sdk/react";
99
+ import { getPostBySlug } from "@/lib/posts";
100
+ import { notFound } from "next/navigation";
101
+
102
+ export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
103
+ const { slug } = await params;
104
+
105
+ // 1) Tenta painel
106
+ try {
107
+ const { article } = await intelexia.getArticleBySlug(slug);
108
+ return (
109
+ <article>
110
+ <ArticleJsonLd article={article} />
111
+ <h1>{article.title}</h1>
112
+ <div dangerouslySetInnerHTML={{ __html: article.contentHtml ?? "" }} />
113
+ </article>
114
+ );
115
+ } catch (err) {
116
+ if (!(err instanceof IntelexiaApiError) || err.status !== 404) throw err;
117
+ }
118
+
119
+ // 2) Fallback: post antigo local
120
+ const local = getPostBySlug(slug);
121
+ if (!local) notFound();
122
+ return (
123
+ <article>
124
+ <h1>{local.title}</h1>
125
+ {/* renderiza o local do jeito que você já fazia */}
126
+ </article>
127
+ );
128
+ }
129
+ ```
130
+
131
+ ### Modo 2: Replace (migração completa)
132
+
133
+ ```tsx
134
+ // app/blog/page.tsx
135
+ import { intelexia } from "@/lib/intelexia";
136
+
137
+ export default async function BlogIndex() {
138
+ const { items } = await intelexia.getArticles({ limit: 20 });
139
+ return (
140
+ <ul>
141
+ {items.map((a) => (
142
+ <li key={a.id}>
143
+ <Link href={`/blog/${a.slug}`}>{a.title}</Link>
144
+ </li>
145
+ ))}
146
+ </ul>
147
+ );
148
+ }
149
+ ```
150
+
151
+ Pro post individual, sem fallback:
152
+
153
+ ```tsx
154
+ import { intelexia } from "@/lib/intelexia";
155
+ import { IntelexiaApiError } from "@intelex-ia/sdk";
156
+ import { ArticleJsonLd, articleToMetadata } from "@intelex-ia/sdk/react";
157
+ import { notFound } from "next/navigation";
158
+
159
+ async function fetchArticle(slug: string) {
160
+ try {
161
+ return await intelexia.getArticleBySlug(slug);
162
+ } catch (err) {
163
+ if (err instanceof IntelexiaApiError && err.status === 404) notFound();
164
+ throw err;
165
+ }
166
+ }
167
+
168
+ export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
169
+ const { slug } = await params;
170
+ const { article } = await fetchArticle(slug);
171
+ return articleToMetadata(article);
172
+ }
173
+
174
+ export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
175
+ const { slug } = await params;
176
+ const { article } = await fetchArticle(slug);
177
+ return (
178
+ <article>
179
+ <ArticleJsonLd article={article} />
180
+ <h1>{article.title}</h1>
181
+ <div dangerouslySetInnerHTML={{ __html: article.contentHtml ?? "" }} />
182
+ </article>
183
+ );
184
+ }
185
+ ```
186
+
187
+ ### Modo 3: Isolated (rota nova, blog antigo intacto)
188
+
189
+ Cria `app/blog/ia/page.tsx` e `app/blog/ia/[slug]/page.tsx` com o mesmo código do modo Replace, mas em rota separada (`/blog/ia` em vez de `/blog`). Zero alteração no blog existente.
190
+
191
+ ---
192
+
193
+ ## Cliente — `lib/intelexia.ts`
194
+
195
+ ```ts
196
+ import { createClient } from "@intelex-ia/sdk";
197
+
198
+ export const intelexia = createClient({
199
+ token: process.env.INTELEXIA_API_TOKEN!,
200
+ // ISR: revalida a cada 60s; o webhook revalidate invalida sob demanda
201
+ init: { next: { revalidate: 60, tags: ["intelexia-articles"] } },
202
+ });
203
+ ```
204
+
205
+ ## Webhook de revalidate — `app/api/revalidate/route.ts`
206
+
207
+ Quando um artigo publica/edita no painel, nosso worker faz `POST` no seu endpoint. Implemente assim:
208
+
209
+ ```ts
210
+ import { NextResponse } from "next/server";
211
+ import { revalidatePath, revalidateTag } from "next/cache";
212
+
213
+ export async function POST(req: Request) {
214
+ const headerSecret = req.headers.get("x-revalidate-secret");
215
+ const body = (await req.json().catch(() => ({}))) as {
216
+ secret?: string;
217
+ slug?: string | null;
218
+ domain?: string;
219
+ };
220
+ const got = headerSecret ?? body.secret ?? "";
221
+ if (!process.env.INTELEXIA_REVALIDATE_SECRET || got !== process.env.INTELEXIA_REVALIDATE_SECRET) {
222
+ return NextResponse.json({ ok: false, error: "unauthorized" }, { status: 401 });
223
+ }
224
+
225
+ if (body.slug) revalidatePath(`/blog/${body.slug}`);
226
+ revalidatePath("/blog");
227
+ revalidateTag("intelexia-articles");
228
+
229
+ return NextResponse.json({ ok: true, revalidated: { slug: body.slug ?? null } });
230
+ }
231
+ ```
232
+
233
+ > O worker envia o secret tanto no header `x-revalidate-secret` quanto no body, tem timeout de 15s e é **fire-and-forget** (se seu endpoint falhar, o artigo continua publicado e a ISR atualiza pelo `revalidate: 60`).
234
+
235
+ ## API
236
+
237
+ ### `createClient(config)`
238
+
239
+ | Opção | Tipo | Default |
240
+ | -------- | -------------------- | ------------------------------------------------- |
241
+ | `token` | `string` (obrig.) | — |
242
+ | `apiUrl` | `string` | `https://api.innovarticles.intelexia.com.br` |
243
+ | `fetch` | `typeof fetch` | `globalThis.fetch` |
244
+ | `init` | `RequestInit` | `undefined` (passe `{ next: { revalidate: N } }`) |
245
+
246
+ ### `client.getArticles({ limit?, offset?, init? })`
247
+
248
+ Resposta: `{ ok, mode, lifecycle, count, items: ArticleListItem[] }`.
249
+
250
+ ### `client.getArticleBySlug(slug, { init? })`
251
+
252
+ Resposta: `{ ok, mode, lifecycle, article: Article }`. **404 → joga `IntelexiaApiError`** (use `try/catch` + `notFound()`).
253
+
254
+ ### `mergePosts({ local, remote, mapLocal, dedupe?, sortBy? })` 🆕 0.2.0
255
+
256
+ | Opção | Tipo | Default |
257
+ | ---------- | ------------------------------------------------- | ---------------- |
258
+ | `local` | `readonly TLocal[]` (qualquer shape) | — |
259
+ | `remote` | `readonly ArticleListItem[]` | — |
260
+ | `mapLocal` | `(post: TLocal) => Omit<UnifiedPost,"source"> \| null` | — |
261
+ | `dedupe` | `"remote-wins" \| "local-wins" \| "throw"` | `"remote-wins"` |
262
+ | `sortBy` | `"date-desc" \| "date-asc" \| "none"` | `"date-desc"` |
263
+
264
+ Retorna `UnifiedPost[]` com `source: "local" | "remote"`. `mapLocal` retornando `null` filtra o post (útil pra esconder rascunhos). Slugs duplicados resolvidos pela estratégia `dedupe`. Veja exemplo no Modo Additive acima.
265
+
266
+ ### `mode: "full" | "plain"`
267
+
268
+ - **`full`** (sites `active`/`paused`): inclui schema JSON-LD, metaTitle/Description, keyword(s), interlinking interno no `contentHtml`.
269
+ - **`plain`** (site `cancelled`): só conteúdo cru — schema/meta/keywords vêm `null`, interlinking removido. O `ArticleJsonLd` e o `articleToMetadata` lidam graciosamente (omitem o que não tem).
270
+
271
+ A decisão é **server-side e não-negociável** — o cliente não escolhe o modo.
272
+
273
+ ### Helpers `/react`
274
+
275
+ - `<ArticleJsonLd article={article} />` — renderiza `<script type="application/ld+json">` com o `schemaJson`. No-op se ausente.
276
+ - `articleToMetadata(article)` → objeto compatível com o `Metadata` do Next.js App Router (title, description, keywords, openGraph, twitter, customMetaTags como `other`).
277
+
278
+ ## Erros
279
+
280
+ ```ts
281
+ import { IntelexiaApiError, MergeConflictError } from "@intelex-ia/sdk";
282
+
283
+ try {
284
+ await intelexia.getArticleBySlug("post-inexistente");
285
+ } catch (err) {
286
+ if (err instanceof IntelexiaApiError) {
287
+ console.error(err.status, err.message); // 404, "Artigo não encontrado."
288
+ }
289
+ }
290
+
291
+ try {
292
+ mergePosts({ local, remote, mapLocal, dedupe: "throw" });
293
+ } catch (err) {
294
+ if (err instanceof MergeConflictError) {
295
+ console.error(`Conflito de slug: ${err.slug}`);
296
+ }
297
+ }
298
+ ```
299
+
300
+ ## Compatibilidade
301
+
302
+ - **Node** 18+ (fetch nativo), Edge Runtime, Browser.
303
+ - **React** 18+ (peer dep opcional — só pros helpers `/react`).
304
+ - **TypeScript:** tipos publicados no `dist`.
305
+ - **Frameworks:** Next.js (App Router e Pages Router), Astro, Remix, SvelteKit, Vite, qualquer JS.
306
+
307
+ ## Changelog
308
+
309
+ - **0.2.0** — adiciona `mergePosts()` + tipo `UnifiedPost` + `MergeConflictError` (3 modos de integração).
310
+ - **0.1.0** — release inicial: `createClient`, `getArticles`, `getArticleBySlug`, `<ArticleJsonLd>`, `articleToMetadata`.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=autoria.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoria.test.d.ts","sourceRoot":"","sources":["../src/autoria.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { autoriaDoArtigo, SELO_IA } from "./react.js";
3
+ /**
4
+ * A autoria do artigo tem duas regras duras, e as duas vêm de fora da
5
+ * engenharia:
6
+ *
7
+ * 1. `author` só vale quando é `Organization`. Texto produzido com apoio de IA
8
+ * e assinado por uma PESSOA é o que os Termos do Google proíbem desde
9
+ * 30/07/2026. O profissional aparece como REVISOR.
10
+ *
11
+ * 2. A autoria NUNCA some. Sem cadastro, assina a equipe da empresa — e o selo
12
+ * de apoio de IA sai do mesmo jeito. Artigo no ar sem dizer quem assina foi
13
+ * exatamente o defeito medido no siteIA em 21/10/2026.
14
+ */
15
+ describe("autoriaDoArtigo", () => {
16
+ const comOrganization = {
17
+ schemaJson: {
18
+ "@graph": [
19
+ {
20
+ "@type": "BlogPosting",
21
+ author: { "@type": "Organization", name: "Equipe Editorial Parceria" },
22
+ contributor: {
23
+ "@type": "Person",
24
+ name: "Mauro Roger Pinto",
25
+ jobTitle: "Contador responsável técnico",
26
+ identifier: "CRC-SC 020357/O-4",
27
+ },
28
+ dateModified: "2026-10-21",
29
+ publisher: { "@type": "Organization", name: "Parceria Assessoria Contábil" },
30
+ },
31
+ ],
32
+ },
33
+ };
34
+ it("usa a Organization como autor e o contributor como revisor", () => {
35
+ const a = autoriaDoArtigo(comOrganization);
36
+ expect(a.autor).toBe("Equipe Editorial Parceria");
37
+ expect(a.revisor).toBe("Mauro Roger Pinto, Contador responsável técnico, CRC-SC 020357/O-4");
38
+ expect(a.revisadoEm).toBe("2026-10-21");
39
+ expect(a.temCadastro).toBe(true);
40
+ });
41
+ it("IGNORA author do tipo Person — e cai para a equipe da empresa", () => {
42
+ const a = autoriaDoArtigo({
43
+ schemaJson: {
44
+ "@type": "BlogPosting",
45
+ author: { "@type": "Person", name: "Fulano de Tal" },
46
+ publisher: { "@type": "Organization", name: "Contabilidade Acme" },
47
+ },
48
+ });
49
+ expect(a.autor).not.toContain("Fulano");
50
+ expect(a.autor).toBe("Equipe Editorial Contabilidade Acme");
51
+ expect(a.temCadastro).toBe(false);
52
+ });
53
+ it("sem autoria no schema, assina a equipe tirada do publisher", () => {
54
+ const a = autoriaDoArtigo({
55
+ schemaJson: { "@type": "BlogPosting", publisher: { "@type": "Organization", name: "Acme" } },
56
+ });
57
+ expect(a.autor).toBe("Equipe Editorial Acme");
58
+ expect(a.revisor).toBeNull();
59
+ expect(a.temCadastro).toBe(false);
60
+ });
61
+ it("a prop `empresa` vence o publisher do schema", () => {
62
+ const a = autoriaDoArtigo({ schemaJson: { publisher: { "@type": "Organization", name: "Nome Velho" } } }, { empresa: "Nome Novo" });
63
+ expect(a.autor).toBe("Equipe Editorial Nome Novo");
64
+ });
65
+ it("sem schema nenhum ainda assina — nunca devolve vazio", () => {
66
+ for (const vazio of [null, undefined, "", 0, [], "texto solto"]) {
67
+ const a = autoriaDoArtigo({ schemaJson: vazio });
68
+ expect(a.autor.length).toBeGreaterThan(0);
69
+ expect(a.temCadastro).toBe(false);
70
+ }
71
+ });
72
+ it("revisor sem cargo nem registro não gera pontuação solta", () => {
73
+ const a = autoriaDoArtigo({
74
+ schemaJson: {
75
+ author: { "@type": "Organization", name: "Equipe" },
76
+ contributor: { "@type": "Person", name: "Ana Souza" },
77
+ },
78
+ });
79
+ expect(a.revisor).toBe("Ana Souza");
80
+ });
81
+ it("o selo é o texto único do grupo", () => {
82
+ expect(SELO_IA).toBe("Apoio de IA · Revisão humana");
83
+ });
84
+ });
85
+ //# sourceMappingURL=autoria.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoria.test.js","sourceRoot":"","sources":["../src/autoria.test.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -6,4 +6,5 @@
6
6
  */
7
7
  export { createClient, IntelexiaApiError, type ClientConfig, type ArticlesListResponse, type ArticleResponse, type IntelexiaClient, } from "./client.js";
8
8
  export type { Lifecycle, Mode, Article, ArticleListItem, CustomMetaTag, ErrorResponse, } from "./types.js";
9
+ export { mergePosts, MergeConflictError, type UnifiedPost, type MergePostsOptions, } from "./merge.js";
9
10
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,SAAS,EACT,IAAI,EACJ,OAAO,EACP,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,EACjB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAC;AAErB,YAAY,EACV,SAAS,EACT,IAAI,EACJ,OAAO,EACP,eAAe,EACf,aAAa,EACb,aAAa,GACd,MAAM,YAAY,CAAC;AAIpB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,KAAK,WAAW,EAChB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -5,4 +5,7 @@
5
5
  * `@intelex-ia/sdk/react`.
6
6
  */
7
7
  export { createClient, IntelexiaApiError, } from "./client.js";
8
+ // Merge helpers (0.2.0+) — pro caso real "cliente já tem posts antigos
9
+ // e quer adicionar os do painel sem perder os antigos".
10
+ export { mergePosts, MergeConflictError, } from "./merge.js";
8
11
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,GAKlB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,YAAY,EACZ,iBAAiB,GAKlB,MAAM,aAAa,CAAC;AAWrB,uEAAuE;AACvE,wDAAwD;AACxD,OAAO,EACL,UAAU,EACV,kBAAkB,GAGnB,MAAM,YAAY,CAAC"}