Ingeste locale
Intégrez un changelog sans déclencher le chemin de récupération à distance. Le chemin distant (releases admin source fetch → POST /v1/workflows/update → le DeterministicUpdateWorkflow du worker API, #1946) exécute l'extraction corps→enregistrements côté serveur (Haiku incrémental ou la boucle d'outils) et facture cette inférence — même lorsqu'il écrit zéro publications. Un agent Claude Code local est déjà un modèle capable avec des outils de récupération web + fichiers, il peut donc effectuer l'extraction par page en ligne (intégrée dans la session déjà en cours) et n'a besoin que d'un chemin d'écriture déterministe.
Principe fondamental : l'agent est l'extracteur. Vous récupérez, vous analysez, vous structurez les enregistrements, et vous les POSTez sur le endpoint batch, qui effectue un upsert idempotent simple sans IA lors de l'insertion. La boucle d'extraction distante ne s'exécute jamais.
Claude Code local uniquement. Cette compétence utilise l'outil Agent pour le fan-out de sous-agents et suppose un système de fichiers local persistant et les variables d'environnement RELEASES_API_* de la CLI. Elle se place à côté de seeding-playbooks comme compétence d'opérateur local uniquement — elle n'est pas déployée sur la flotte d'agents gérés.
Contrat de coûts
Énoncez-le explicitement à l'opérateur avant une exécution importante, et respectez-le :
- Dépenses : vos propres jetons de l'agent pour la récupération + l'extraction, plus ceux des sous-agents que vous déployez (votre choix de modèle —
sonnetpour la qualité,haikupour le volume). - NE dépense PAS : aucune extraction côté serveur (les passes Haiku incrémental / boucle d'outils du workflow de mise à jour), aucune extraction
web_fetchà distance.POST /v1/workflows/updaten'est jamais appelé. - Le endpoint batch n'exécute aucune IA lors de l'insertion — pas de résumé, pas de classifieur marketing, pas d'enrichissement de flux. Il effectue une
insertplus unwaitUntilpour un embed vectoriel. Les champs IA (title_generated/title_short/summary) sont intentionnellement laissés vides ; voir Enrichissement manuel ci-dessous pour les chemins de suivi délibérés.
Le signal négatif qui confirme qu'aucune exécution à distance n'a eu lieu : dans Axiom releases-cloudflare-logs, il n'y a pas de POST .../fetch?sessionId=det-… et pas d'événement extract-deps-worker pour la source.
Quand l'utiliser
- Intégrer une nouvelle source (ou remonter une source existante) de manière interactive dans Claude Code et vous souhaitez éviter le coût d'extraction à distance.
- Une récupération à distance a brûlé une boucle d'extraction complète et a écrit 0 publications (le mode d'échec Conductor / grand corps /
max_tokens) — extraire en ligne contourne entièrement la boucle. - La page est accessible par vos propres outils web (le refus ci-dessous est un choix de politique, pas une limite de récupération).
Quand NE PAS l'utiliser :
- Vous êtes dans une session d'agent géré. Ceci est local uniquement (fan-out d'outil Agent + système de fichiers local + environnement CLI). Les sessions MA doivent continuer à utiliser le chemin de récupération normal.
- La source a un flux propre et riche en contenu. Ajoutez-la simplement (
managing-sources) et laissez le pipeline cron la récupérer. L'ingeste locale est destinée aux cas où l'agent effectuant l'extraction en ligne est le bon outil, pas un remplacement pour les adaptateurs de flux/GitHub. - L'éditeur refuse (la pré-vérification refuse — voir Étape 1).
Workflow
Étape 1 — Pré-vérification (barrière de sécurité obligatoire)
Exécutez la vérification d'opt-out avant de récupérer ou d'écrire quoi que ce soit :
bun .claude/skills/local-ingest/preflight.ts <url> --json
Elle récupère robots.txt (en suivant les redirections apex→www), analyse la politique Cloudflare Content Signals, et se verrouille sur le code de sortie :
| Sortie | Verdict | Action |
|---|---|---|
0 | proceed | Permission ou absence — continuez. |
1 | refuse | ai-input=no déclaré. ARRÊTEZ. Informez l'utilisateur. Ne récupérez ni n'écrivez. |
2 | unknown | Impossible de récupérer/analyser robots.txt. Réessayez une fois ; si toujours inconnu, informez l'utilisateur — ne supposez pas "proceed". |
La barrière s'applique à ai-input uniquement. L'ingeste locale est une ingestion de recherche/d'entrée — alimenter un index de recherche — ce qui est exactement ce que ai-input régit. ai-train=no refuse l'utilisation pour l'entraînement du modèle, un usage que ce chemin n'effectue pas, donc ai-train=no seul (par ex. vercel.com) continue correctement.
C'est la leçon du Conductor. conductor.build sert Content-Signal: ai-train=no, search=yes, ai-input=no — son org/source est déjà en pause dans le registre exactement pour cette raison (releases admin playbook conductor). Une intégration locale ne doit pas silencieusement dépenser des jetons ou ingérer du contenu en violation d'un refus de l'éditeur. conductor.build doit être refusé (via son ai-input=no) — c'est la cible de régression pour cette barrière.
Le refus est un choix de politique, pas une limitation technique : web_fetch et CF Browser Rendering peuvent toujours récupérer ces pages (seul le endpoint /crawl de Cloudflare a appliqué le signal pour Conductor). Respectez le refus quand même. Ne passez outre qu'avec une autorisation explicite de l'éditeur documentée par l'opérateur — il n'y a pas de contournement silencieux.
La pré-vérification imprime également toutes les URL Sitemap: de robots.txt — conservez-les pour l'Étape 3.
Étape 2 — Résoudre la cible
Assurez-vous que l'org et la source existent avant d'écrire — les publications sont attachées à un source_id. Référez-vous à managing-sources pour les règles de création/nomination/primaire et à finding-changelogs pour déterminer si un flux structuré existe (si c'est le cas, préférez le pipeline normal). Ensuite :
- Lisez le playbook de l'org pour les indices d'analyse :
releases admin playbook <org>(voirmanaging-sources→ Playbooks). - Capturez le
sourceSluget l'IDsrc_…de la ligne créée/résolue — vous avez besoin de l'un des deux pour l'URL du batch.
Si la source est entièrement nouvelle, créez-la (par ex. releases admin source create "<name>" --url <url> --org <org> [--primary]) mais ne déclenchez pas de récupération — cela enverrait le workflow de mise à jour à distance que vous essayez d'éviter.
Étape 3 — Cartographier les pages
Classez la forme de la page (releases admin discovery evaluate <url> --json signale pageStructure: single-page | index | unknown) :
- Changelog sur une seule page → une récupération à l'Étape 4.
- Index → détail → énumérez les URL par publication. Préférez
/sitemap.xml(la pré-vérification l'a récupéré) filtré sur le chemin du changelog ; sinon, analysez le HTML de l'index pour les liens par publication. - Index rendu côté client → l'index est une coquille JS dont les cartes se chargent via un
fetchcôté client, donccurl/le scraping sans JS renvoie zéro entrée. Énumérez en rendant : pilotez l'index avec Claude dans Chrome (ou déployez un sous-agent de rendu), paginez si la liste est fenêtrée, et collectez les liens par publication. Vérifiez ensuite si les pages de détail sont rendues statiquement — elles le sont souvent même lorsque l'index ne l'est pas — et si c'est le cas, récupérez-les en masse aveccurldirectement (HTML→markdown localement, pas de rendu, pas de facture IA). Leshelp.harvey.ai/release-notesde Harvey sont exactement cela : index rendu côté client, pages de détail statiques. Ne concluez pas "aucune publication" à partir d'un scraping JS vide de l'index.
Limitez-vous à une fenêtre récente (par défaut ~25–50 publications les plus récentes) et enregistrez ce que vous avez sauté — ne tronquez jamais silencieusement. Les ré-exécutions sont idempotentes, donc un backfill plus profond plus tard ajoute simplement des lignes.
Étape 4 — Récupération + extraction
Réutilisez les conventions de parsing-changelogs pour le type/version/date/média (voir Parité ci-dessous).
- Quelques pages : récupérez et extrayez en ligne dans cet agent.
- Beaucoup de pages : déployez des sous-agents parallèles — une tranche d'URL chacun, chacun retournant des enregistrements dans le schéma du batch. Utilisez le modèle de distribution par outil
Agentdeseeding-playbooks(et superpowers:dispatching-parallel-agents pour la discipline de fan-out). Ne pas récupérer les URL des publications dans l'agent parent — déléguez pour garder le contexte du parent propre. - Mise en garde de sauvegarde par le parent : il peut être interdit aux sous-agents d'effectuer l'écriture batch (limites de permissions). Prévoyez-le — demandez à chaque sous-agent de retourner ses enregistrements dans son résultat, et laissez le parent (vous) effectuer les écritures à l'Étape 5. Même modèle que
seeding-playbookspour la sauvegarde de notes. - Extrayez les médias — n'expédiez pas que du texte. Récupérez les images/vidéos de chaque corps de publication et remplissez
media, en dépaquetant les wrappers d'optimiseur_next/image/_vercel/imagevers l'URL CDN sous-jacente (normalizeMediaUrldepackages/rendering/src/media-url.ts). Un premier passage uniquement textuel est un oubli courant qui n'est détecté qu'à la révision de l'opérateur (Harvey a expédié du texte uniquement, puis a dû réextraire_next/image→cdn.sanity.ioet réensemencer) ; le re-POST d'enrichissement estmode: "upsert-content"(Étape 5).
Chaque enregistrement doit correspondre au schéma du batch (voir Étape 5) et aux règles de parité.
Optionnel — extraction déterministe (Approche A, monorepo uniquement) : au lieu d'extraire manuellement, vous pouvez appeler les bibliothèques d'extraction de production dans packages/adapters/src/extract/ — extractFromBody, extractWithTools, runDirectFetchExtraction. ExtractDeps.cloudflare est nullable et repo peut être un noop, donc elles s'exécutent avec juste une clé Anthropic (preuve : scripts/smoke-toolloop.ts). Cela échange les jetons d'agent contre des jetons d'API Anthropic directs mais reproduit exactement la boucle d'outils de production. Ces packages sont private: true — accessibles uniquement depuis le monorepo, jamais depuis la CLI légère. La valeur par défaut reste l'Approche B (l'agent est l'extracteur).
Étape 5 — Écriture (upsert par batch)
POSTez les enregistrements vers le endpoint batch par morceaux (~50/requête maintient les requêtes limitées ; le handler effectue le découpage du binding D1 côté serveur) :
# Utilisez le chemin limité à l'org. La forme bare /v1/sources/:slug n'accepte QU'UN ID
# typé `src_…` — un slug humain renvoie 400 bare_slug_rejected (les slugs sont
# limités à l'org, #690). Avec un ID typé, la forme bare fonctionne aussi :
# POST $RELEASES_API_URL/v1/orgs/<orgSlug>/sources/<sourceSlug>/releases/batch
# POST $RELEASES_API_URL/v1/sources/<src_…>/releases/batch
curl -sS -X POST "$RELEASES_API_URL/v1/orgs/<orgSlug>/sources/<sourceSlug>/releases/batch" \
-H "Authorization: Bearer $RELEASES_API_KEY" \
-H "Content-Type: application/json" \
-d @chunk.json # { "releases": [ … ] }
Champs par publication (la forme acceptable réelle — workers/api/src/routes/sources.ts, postReleasesBatchHandler) :
{
"version": "1.4.0", // optionnel ; omettez/null pour les sources datées ou les blogs
"title": "Mode sombre", // obligatoire
"content": "Markdown body…", // obligatoire
"url": "https://…/changelog/dark-mode", // optionnel mais TOUJOURS à remplir — c'est la clé de déduplication
"contentHash": "…", // optionnel
"publishedAt": "2026-05-30", // optionnel ; ISO-8601 (voir Parité)
"media": "https://…/shot.png", // optionnel
"type": "feature", // optionnel ; "feature" | "rollup"
"prerelease": false, // optionnel
}
- Idempotent. Upserts sur
UNIQUE(source_id, url), remplit sans écraser : en cas de collision, il ne remplit le contenu/média que lorsque la ligne stockée est vide, donc une ré-exécution n'écrase jamais une bonne ligne. Sans danger pour les ré-exécutions et les pages — la mêmeurlmet à jour plutôt que de dupliquer. - Re-POST d'enrichissement (
mode: "upsert-content"). Parce que la valeur par défaut est "remplir uniquement", re-POSTer un contenu plus riche pour une URL qui a déjà du contenu est un no-op — le stub reste. Pour un deuxième passage délibéré (d'abord les résumés d'index, puis les corps de page de détail complets + médias), ajoutez"mode": "upsert-content"au niveau supérieur du corps du batch : cela écrase le contenu/média lors d'une collision sur la même URL et saute le pré-filtre de déduplication de titre de scraping (#1526). Utilisez-le uniquement pour un enrichissement intentionnel ; le chemin par défaut reste "remplir uniquement" afin qu'une récupération de routine ne puisse pas écraser. L'urldoit être identique à la ligne ensemencée — l'enrichissement s'appuie sur l'URL, donc un changement de schéma d'URL insère un doublon à la place. Si le schéma d'URL a changé, réensemencez :releases release delete --source <src_…> --hardpuis un nouveau/batch(mode par défaut). - Auth. Nécessite la portée
write; laRELEASES_API_KEYstatique (root) la satisfait..envcharge automatiquementRELEASES_API_URL+RELEASES_API_KEYdans ce dépôt. - Pas d'IA lors de l'insertion (voir Contrat de coûts).
Aide au découpage (lit un tableau records.json, POSTe par lots de 50) :
bun -e '
const orgSlug = process.argv[1], sourceSlug = process.argv[2], file = process.argv[3];
const all = JSON.parse(await Bun.file(file).text());
const base = process.env.RELEASES_API_URL, key = process.env.RELEASES_API_KEY;
for (let i = 0; i < all.length; i += 50) {
const releases = all.slice(i, i + 50);
const res = await fetch(`${base}/v1/orgs/${orgSlug}/sources/${sourceSlug}/releases/batch`, {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
body: JSON.stringify({ releases }),
});
console.log(`chunk ${i / 50}: ${res.status} ${(await res.text()).slice(0, 200)}`);
}
' <orgSlug> <sourceSlug> records.json
Étape 6 — Valider
releases tail <slug> --json # ou : get_latest_releases (outil typé)
Confirmez que les publications ont des titres, dates, contenu et médias non vides. Si une ligne est vide, corrigez l'extraction et réexécutez — l'upsert par défaut remplit les lignes vides sans risque. Si une ligne a déjà un contenu fin mais non vide (par exemple un stub de résumé d'index que vous souhaitez maintenant remplacer par le corps complet), une réexécution simple est un no-op — utilisez le re-POST d'enrichissement mode: "upsert-content" de l'Étape 5.
Vous récupérez une source, pas en intégrer une nouvelle ? Cette étape confirme seulement ce qui a été écrit — elle ne dit rien sur ce qui ne l'a pas été. Avant de dire à quelqu'un qu'une source est "récupérée" ou "rien ne manque", consultez backfilling-sources → Avant de déclarer une source "propre" ou "récupérée" pour la vérification ponctuelle des pages en direct requise.
Étape 7 — Note du playbook
Enregistrez comment cette source a été ingérée afin que les futurs agents n'aient pas à le redériver. Via manage_playbook (action update_notes) ou releases admin playbook <org> --notes-file -. Notez que la source a été ingérée localement (pas encore sur le chemin cron), sa cadence observée, et toute particularité d'extraction. Suivez les règles de rédaction du playbook de managing-sources (voix impérative, "keep-test", ### Instructions de récupération / ### Pièges / ### Couverture). Ne ...
[le contenu était tronqué pour la traduction]