@craft-ng: Associer l’art de la composition & du state management dans Angular
<p>Quand je construis une feature Angular un peu sérieuse, je veux toujours la même chose:</p> <ul> <li>une seule source de vérité</li> <li>un flux de données clair</li> <li>un code composable</li> <li>une DX solide</li> <li>et surtout une type-safety qui m'évite de jouer aux devinettes</li> <li>des outils pour pensés pour simplifier l'UX/UI</li> </ul> <p>C'est exactement l'objectif de @craft-ng.</p> <p>Une lib complète de state management pour tous les types d'état d'une application:</p> <ul> <li> <strong>client state</strong>: états locaux, listes, UI, sélection...</li> <li> <strong>server state</strong>: chargement, cache, mutation, pagination, optimistic update...</li> <li> <strong>URL state</strong>: query params synchronisés, type-safe, avec fallback</li> </ul> <p>Des utilitaires prê
Quand je construis une feature Angular un peu sérieuse, je veux toujours la même chose:
-
une seule source de vérité
-
un flux de données clair
-
un code composable
-
une DX solide
-
et surtout une type-safety qui m'évite de jouer aux devinettes
-
des outils pour pensés pour simplifier l'UX/UI
C'est exactement l'objectif de @craft-ng.
Une lib complète de state management pour tous les types d'état d'une application:
-
client state: états locaux, listes, UI, sélection...
-
server state: chargement, cache, mutation, pagination, optimistic update...
-
URL state: query params synchronisés, type-safe, avec fallback
Des utilitaires prêts à l'emploi pour se rendre la vie plus facile.
Une approche Method-based ou Event-based pour s'adapter à tous les styles de code.
Qu'ils soient simples ou complexes, le principe reste toujours le même.
-
Les « primitives », basées sur les signals, ont chacune leur rôle et portent un state et sa logique.
-
Elles sont utilisables directement dans les composants et les services.
-
Elles suivent toutes le même principe : primitive(config, insertion1, insertion2, ...).
-
Les insertions servent à ajouter de la logique (modifiers, réactions, états dérivés, method-based/event-based...).
-
Ce pattern, combiné aux utilitaires de craft-ng insert..., permet d'obtenir un niveau inégalé de composition, offrant une gestion fluide aussi bien pour les cas simples que complexes.
-
Un store craft est disponible pour orchestrer ces primitives. Il peut être composé par d'autres stores, et être lui-même composable.
Dans cet article, je vais:
-
présenter la structure commune des primitives
-
montrer comment exposer méthodes, état dérivés, et réagir à un événement via les insertions
-
donner un exemple concret pour chaque primitive
-
faire un tour rapide des insertions utiles
-
expliquer pourquoi source$ (event-based) change vraiment la façon de structurer le state
-
terminer avec injectService et le store craft
⚠️ @craft-ng est une librairie experimentale. Je ne recommande pas de l'utiliser en production pour le moment. Cet article est avant tout un partage des concepts.
La doc: https://ng-angular-stack.github.io/craft/
1) Une structure commune à toutes les primitives
Que tu utilises state, query, mutation, asyncProcess ou queryParam, la logique de composition reste la même:
-
une configuration de base
-
des insertions pour exposer des méthodes / des états dérives / des réactions
import { computed } from '@angular/core';
const counter = state( 0, // config // insertion 1 ({ set, update }) => ({ increment: () => update((current) => current + 1), reset: () => set(0), }), // insertion 2 ({ state }) => ({ isOdd: computed(() => state() % 2 === 1), }), );
counter.increment(); counter.isOdd();`
Enter fullscreen mode
Exit fullscreen mode
Ce point est clé: tu n'apprends pas 5 APIs différentes, tu apprends un modèle mental unique.
2) Les primitives: fonctionnement + exemples concrets
Dans la pratique, chaque primtive apporte ses fonctionnalités qui lui sont propres, et le composant/service/store m'aide à les orchestrer.
state
state gère le client state synchrone. C'est la base pour modéliser un état client, global ou local, le composer, et le spécialiser.
Combiné à insertSelect, le state devient redoutable pour gérer des structures imbriquées de manière fluide et type-safe.
type User = { id: string; name: string; selected: boolean };
const usersState = state( { filters: { search: '' }, users: [] as User[], }, insertSelect('filters', ({ set }) => ({ set, })), );
usersState.selectFilters().set('@craft-ng');`
Enter fullscreen mode
Exit fullscreen mode
Ce que j'aime ici:
-
les méthodes suivent la structure du state
-
la lecture du code reste directe
Pourquoi avoir créé un state alors qu'il y a déjà les signals d'Angular ?
-
pour bénéficier du système de composition via les insertions
-
exposer les méthodes qui modifient l'état pour le rendre prédictif
-
encapsuler toute la logique qui lui est associée
mutation
mutation: sert a modifier (UPDATE/PUT/PATCH/DELETE) des données cote serveur.
Version méthode directe avec .mutate(...):
const updateUser = mutation({ method: (payload: { id: string; name: string }) => payload, loader: async ({ params }) => { const response = await fetch(const updateUser = mutation({ method: (payload: { id: string; name: string }) => payload, loader: async ({ params }) => { const response = await fetch(, { method: 'PATCH', body: JSON.stringify(params), }); return response.json() as User; }, });, { method: 'PATCH', body: JSON.stringify(params), }); return response.json() as User; }, });updateUser.mutate({ id: '42', name: 'Romain' });`
Enter fullscreen mode
Exit fullscreen mode
On peut aussi les appeler en parallèle, avec des identifiers, pour gérer des cas plus complexes (cf. l'exemple dans full-demo).
Pourquoi avoir créé une mutation alors qu'il y a déjà les resources d'Angular ?
-
pour bénéficier du système de composition via les insertions
-
permettre des appels api en parallèle via les identifiers
-
retourner des craftException typés en cas d'erreur métier (ex: validation), pour ne pas perdre d'information et offrir la meilleure UX/UI à tes utilisateurs
-
peut s'appeler comme une méthode directe myMutationRef.mutate(...)
query
query: gère le server state (chargement, valeur, erreur, cache) et peut tourner en parallèle via identifier (ex: pour faire de la pagination).
C'est la primitive qui est faîte pour représenter une ressource distante, avec des utilitaires pour gérer le cache, les updates liés aux mutations, la pagination...
Avec insertPaginationPlaceholderData + insertReactOnMutation, on obtient:
-
une pagination fluide
-
des updates réactifs liés aux mutations (optimistic update/patch, auto reload).
-
moins de code impératif
import { insertPaginationPlaceholderData, insertReactOnMutation, mutation, query, } from '@craft-ng/core';import { insertPaginationPlaceholderData, insertReactOnMutation, mutation, query, } from '@craft-ng/core';const updateUser = mutation({ method: (payload: { id: string; name: string }) => payload, loader: async ({ params }) => params, });
const page = signal(1);
const usersQuery = query(
{
params: page,
identifier: (page) => ${page},
loader: async ({ params: currentPage }) =>
fetch(/api/users?page=${currentPage}).then((r) => r.json()),
},
insertPaginationPlaceholderData,
insertReactOnMutation(updateUser, {
patch: {
name: ({ mutationParams }) => mutationParams.name,
},
}),
);`
Enter fullscreen mode
Exit fullscreen mode
Pourquoi avoir créé une query alors qu'il y a déjà les resources d'Angular ?
-
pour bénéficier du système de composition via les insertions
-
permettre des appels api en parallèle via les identifiers
-
retourner des craftException typés en cas d'erreur métier (ex: validation), pour ne pas perdre d'information et offrir la meilleure UX/UI à tes utilisateurs
asyncProcess
asyncProcess est idéal pour des traitements async qui ne sont pas strictement des queries/métiers CRUD (debounce, wrappers API natives, orchestration).
import { asyncProcess } from '@craft-ng/core';
const delaySearch = asyncProcess({ method: (term: string) => term, loader: async ({ params: term }) => { await new Promise((resolve) => setTimeout(resolve, 250)); return term; }, });
delaySearch.safeValue(); // undefined delaySearch.status(); // 'idle' delaySearch.method('@craft-ng'); delaySearch.status(); // 'loading' -> after 250ms -> 'resolved' delaySearch.safeValue(); // '@craft-ng'`
Enter fullscreen mode
Exit fullscreen mode
Pourquoi avoir créé un asyncProcess alors qu'il y a déjà les resources d'Angular ?
-
permet de profiter du système de composition via les insertions
-
retourner des craftException typés en cas d'erreur métier
queryParam
queryParam synchronise l'état avec l'URL, tout en restant type-safe (parse/serialize/fallback).
import { queryParam } from '@craft-ng/core';
const tableParams = queryParam( { state: { page: { fallbackValue: 1, parse: (v) => parseInt(v, 10), serialize: (v) => String(v), }, search: { fallbackValue: '', parse: (v) => v, serialize: (v) => v, }, }, }, ({ patch, reset }) => ({ patch, reset }), );
tableParams.patch({ page: 2 });`
Enter fullscreen mode
Exit fullscreen mode
Pourquoi avoir créé un queryParam alors qu'on peut utiliser withComponentInputBindingpour récupérer un query param dans un input ?
-
queryParam peut être utilisé dans un service providé au niveau du composant
-
possède une valeur de fallback en cas de non présence du query param ou d'une valeur invalide
-
permet de modifier ce query param via les insertions
-
profite du système de composition via les insertions
-
permet de retourner des craftException typés en cas d'erreur métier au parse d'un query param
Exemples de la doc qui m'ont inspiré
Si tu veux voir des versions plus complètes des patterns présentes ici, je te conseille particulièrement:
-
les exemples primitives (query, mutation, full demo): https://ng-angular-stack.github.io/craft/examples
-
l'approche list-with-pagination pour visualiser insertPaginationPlaceholderData en contexte
-
les exemples Pixel Art / Pixel Art Matrix pour voir insertSelect sur des structures plus profondes
-
la section exceptions pour les cas métier avec erreurs type-safe, pour ne pas perdre d'information et offrir la meilleure UX/UI à tes utilisateurs
Ces exemples m'ont servi de base pour structurer les snippets de cet article.
3) Exposer des méthodes et état dérivé avec les insertions (Method-based)
Tu peux partir simple, puis enrichir sans casser le contrat initial.
Method-based insertions
import { state } from '@craft-ng/core'; const counter = state(0, ({ update, set }) => ({ increment: () => update((current) => current + 1), decrement: () => update((current) => current - 1), reset: () => set(0), })); console.log(counter()); // 0 counter.increment(); console.log(counter()); // 1 counter.reset(); console.log(counter()); // 0import { state } from '@craft-ng/core'; const counter = state(0, ({ update, set }) => ({ increment: () => update((current) => current + 1), decrement: () => update((current) => current - 1), reset: () => set(0), })); console.log(counter()); // 0 counter.increment(); console.log(counter()); // 1 counter.reset(); console.log(counter()); // 0Enter fullscreen mode
Exit fullscreen mode
Source-based insertions (Event-based)
import { source$, state, on$ } from '@craft-ng/core';
const incrementTrigger$ = source$(); const resetTrigger$ = source$(); const counter = state(0, ({ set }) => ({ increment: on$(incrementTrigger$, () => set((v) => v + 1)), reset: on$(resetTrigger$, () => set(0)), })); console.log(counter()); // 0 incrementTrigger$.emit(); console.log(counter()); // 1 resetTrigger$.emit(); console.log(counter()); // 0`
Enter fullscreen mode
Exit fullscreen mode
Créer de la logique réutilisable est très simple
Tu peux extraire une insertion dans une fonction custom et la rebrancher partout:
const counter = state(0, (context) => myCustomFn(context));
Enter fullscreen mode
Exit fullscreen mode
Implémentation simple (dans cet esprit):
const myCustomFn = ({ update, set, state, }: { update: (updater: (v: number) => number) => void; set: (value: number) => void; state: Signal; }) => ({ increment: () => update((current) => current + 1), decrement: () => update((current) => current - 1), reset: () => set(0), isOdd: computed(() => state() % 2 === 1), });const myCustomFn = ({ update, set, state, }: { update: (updater: (v: number) => number) => void; set: (value: number) => void; state: Signal; }) => ({ increment: () => update((current) => current + 1), decrement: () => update((current) => current - 1), reset: () => set(0), isOdd: computed(() => state() % 2 === 1), });const myState = state(0, (context) => myCustomFn(context));
myState.increment(); myState.isOdd();`
Enter fullscreen mode
Exit fullscreen mode
Pour les cas plus poussés, j'étudie différents patterns pour que ca reste aussi simple que possible cote API et usage.
4) Tour rapide de quelques insertions utiles
insertPaginationPlaceholderData (query)
Pour garder les donnees de la page precedente pendant le chargement de la suivante. Resultat: UX plus fluide, moins de flicker.
insertReactOnMutation (query)
Pour synchroniser automatiquement le cache query avec le resultat d'une mutation (patch/optimistic/reload selon le besoin).
insertLocalStoragePersister (state/query/asyncProcess)
Pour persister et rehydrater automatiquement avec localStorage. Tres utile pour garder l'état entre sessions.
insertEntities (state)
Pour manipuler des collections avec des utilitaires prets a l'emploi (add, set, update, remove, upsert...), en restant type-safe.
insertSelect (state)
Pour cibler un sous-arbre d'état et exposer des méthodes/dérives au bon endroit. Hyper utile sur des structures imbriquées. (Prochainement disponible)
5) Pourquoi source$ est un vrai levier d'architecture
source$ est l'outil que j'utilise pour garder des states granulaires sans perdre la simplicite d'orchestration.
Cela correspond grosso-modo à un subject dans RxJS.
Cas 1: plusieurs states réagissent au même événement
Au lieu d'un gros state qui gère tout, plusieurs states petits et lisibles peuvent réagir au même trigger.
import { on$, source$, state } from '@craft-ng/core';
const resetFilters$ = source$();
const search = state('', ({ set }) => ({ set, reset: on$(resetFilters$, () => set('')), }));
const page = state(1, ({ set }) => ({ set, reset: on$(resetFilters$, () => set(1)), }));
resetFilters$.emit();`
Enter fullscreen mode
Exit fullscreen mode
Ca donne:
-
responsabilités claires
-
meilleure DX
-
flux de mise à jour plus facile à raisonner
Et surtout: tu peux commencer avec une méthode exposée, puis migrer vers une réaction on$ sans rearchitecture lourde.
Cas 2: state imbriqué + insertSelect
Dans des structures profondes, insertSelect permet d'associer des méthodes et des états dérivés à une niveau plus profond. Parfois, j'utilise source$ à un haut niveau, puis je réagis à cette source$ depuis des niveau imbriqués. Cela me permet de modifier l'état au plus proche de l'endroit où il est modifié.
Pour les states complexes avec des imbrications, le modèle mentale devient plus souple et plus facile à raisonner.
Cas 3: event-driven (et pont avec Observable)
source$ + on$ permettent de réagir à des événements, y compris depuis un Observable. Pour ceux qui aiment l'event-driven, c'est très naturel.
Et si tu veux rester dans un style state-driven et réagir à des changements d'état, il y a aussi:
- reactiveWritableSignal
Dans cet exemple, ce me permet de créer un linkedSignal, qui réagit à des changements d'états de d'autres signals. Cela me permet retirer les ids qui ont été supprimés de la sélection, sans devoir faire du code impératif pour écouter les changements de page et de suppression.
const selectedIds = reactiveWritableSignal([] as string[], (sync) => ({ resetWhenCurrentPageIsResolved: sync( users.currentPageStatus, ({ params, current }) => (params === 'resolved' ? [] : current), ), resetWhenBulkDeleteIsResolved: sync( bulkDelete.status, ({ params, current }) => (params === 'resolved' ? [] : current), ), })); // WritableSignalconst selectedIds = reactiveWritableSignal([] as string[], (sync) => ({ resetWhenCurrentPageIsResolved: sync( users.currentPageStatus, ({ params, current }) => (params === 'resolved' ? [] : current), ), resetWhenBulkDeleteIsResolved: sync( bulkDelete.status, ({ params, current }) => (params === 'resolved' ? [] : current), ), })); // WritableSignalEnter fullscreen mode
Exit fullscreen mode
-
afterRecomputation : qui déclenche son callBack si le résultat de sa source n'est pas undefined.
-
toSource: transforme un signal en source. La première lecture d'une source renverra toujours undefined, puis dès que la source change, le résultat sera synchronisé.
6) La philosophie continue avec injectService
injectService permet de construire une facade typée au-dessus d'un service Angular . Tu exposes uniquement ce qui est utile au cas d'usage, tu dérives proprement, et tu gardes la maitrise de l'API publique.
import { computed } from '@angular/core'; import { injectService } from '@craft-ng/core';import { computed } from '@angular/core'; import { injectService } from '@craft-ng/core';const checkout = injectService( CheckoutService, ({ cart, total, submitOrder }) => ({ total, itemCount: computed(() => cart().length), submit: submitOrder, }), ({ insertions }) => ({ canSubmit: computed(() => insertions.itemCount() > 0), }), );
checkout.canSubmit();`
Enter fullscreen mode
Exit fullscreen mode
7) Et au-dessus: le store craft
La lib expose aussi un store craft, toujours basé sur la composition, la type-safety et le découplage. Tu peux composer states, queries, mutations, sources, inputs et query params dans une architecture cohérente, sans perdre le contrôle fin.
Plus de détails dans un prochain article, sinon il y a la doc ;D
Conclusion
Si je devais résumer @craft-ng en une phrase: composer des briques simples pour gérer des logiques complexes, sans quitter un modèle déclaratif/reactif/type-safe.
Et la lib ne s'arrête pas là. A l'heure où j'écris cet article, d'autres utilitaires arrivent dans la même philosophie.
Le prochain utilitaire, si je devais n'en partager qu'un :
- un formulaire à la pointe de la technologie (en plus de tout ce que permet le signal form d'Angular):
création de formulaire en parallèle intégration avec les autres primitives (pour le submit, et les validations asynchrones) gestion fine des erreurs (validation, submit, async validators), tout est inféré, permettant d'avoir la liste exhaustive des erreurs associées à un champ. Gestion de la logique interdépendante grâce aux mécanismes de composition offerts par la lib.
(Actuellement, j'ai un wrapper du signalForm, mais j'ai 2 cas qui sont impossibles à gérer. J'attends un peu de voir si Angular permet d'étendre le signalForm, ou si je dois faire une implémentation custom pour garder la philosophie de composition et de type-safety.)
N'hésite pas à aller voir la doc ou à mettre une étoile sur le repo si tu veux suivre l'évolution de la lib, ou à me faire un retour si tu as des idées d'amélioration !
Je suis Romain Geffrault. Développeur Angular et créateur de @craft-ng Suis-moi pour plus de contenu sur Angular
Sign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
More about
versionupdateproductExclusive | The Sudden Fall of OpenAI’s Most Hyped Product Since ChatGPT - WSJ
<a href="https://news.google.com/rss/articles/CBMiogNBVV95cUxNVXJPcUFQbnkwYXJRQzNDVHFhTE5ZdUZrLUNPUjB3b05TV2tPTG9rdTUwV0J2WncxdE1KR2Z1c0x4aXFQdUlscGtZTDRwRG5yTkpmVzlHckxPaUdZcUZSZk1TOGdRSUdIVVBURlM2dG8xZHNKS2JqcXhJSmw4UllDZS1hUHd3bnlxWDhUa0ZFVzZZTXdTSk93eS1xSzllYkJ1WWxHTVNjZ3ROYWdnWTdxRmRyU0x3NTk0bnpQdnFDQVB6aXh2U01sX3BobHNFZGlhRFpVbXVnRkNkQlFBbDk5Q3E4dzRDUE9NT01HQm1NMUlMWFFaa1U1djhWdm15X1FZUGhJMXNPX05aX2w2UTI4MjdoNXgweHdzWTRkYkdSeUxEdnZPb09nZDFXX1pmZ2NGckZmVC1UV3YxSjhFamRXTzlRb3ZlWElBb3Vna0FPR09DRm5kWjFIVzhQRE1RVUtKS3hVWkFDcHZiU0x3SjY0OTRVRS0yR2VlRWtYMG8wb2V2YW1wdTlic2hwZ2wzTW9Uc0FqWGJsU3NGQ1pYdkJFeW1B?oc=5" target="_blank">Exclusive | The Sudden Fall of OpenAI’s Most Hyped Product Since ChatGPT</a> <font color="#6f6f6f">WSJ</font>
Exclusive | The Sudden Fall of OpenAI’s Most Hyped Product Since ChatGPT - WSJ
<a href="https://news.google.com/rss/articles/CBMiogNBVV95cUxPb1N4U0FYNGxVQlJ1eHNBbHE2d2p2NlBwS3RuTUFCeHZXY3B5U0NmZlRIVm9TVC10NGpSNlROQkQ2SENlRkhJRzFXWWdYUWFEdkh2Q081NnFCcXB4cmtkaEF1UjV1b1ZsOXBWdi0yV0tfTkM1TUZvV1NmeUVmc1V4N0luZ1RVT0hfYU5WMFo0MTBadVdsZDBNQkQ0cE1DVFNPOUc0TDAyaVU3STJXSDBCem1Pb1VfeHJqZzd5UVlCZVlDUFNQTjlVNU1pcjJZYnVnbWhORzRBQl8wQXZEWmEtOW9fUjFsX2t5ZkNINmIwR1dpcE5WYWtBdFNjLTNPUkltOU83ZS1qU1lod0JVdlpxeW1HNk5SMGRPa1IxMXp3eTFlbTdIckhhYjMyQ0x4VmpPWlI4VDR5M0NTZGVFMGxUQnBBVTBvUlB4UlNaZ3dDaEg3R0VIVGdRZDNCZGgtcFVDcEttbVJxSzkxMVQyWVo0Rk9vcTFKWWpUTEJsTloxVXdhX1FzdkE4M0ozZXpqZ0tYT0pZLTdCMnBlMzU5U05XUDJB?oc=5" target="_blank">Exclusive | The Sudden Fall of OpenAI’s Most Hyped Product Since ChatGPT</a> <font color="#6f6f6f">WSJ</font>

Webhook Best Practices: Retry Logic, Idempotency, and Error Handling
<h1> Webhook Best Practices: Retry Logic, Idempotency, and Error Handling </h1> <p>Most webhook integrations fail silently. A handler returns 500, the provider retries a few times, then stops. Your system never processed the event and no one knows.</p> <p>Webhooks are not guaranteed delivery by default. How reliably your integration works depends almost entirely on how you write the receiver. This guide covers the patterns that make webhook handlers production-grade: proper retry handling, idempotency, error response codes, and queue-based processing.</p> <h2> Understand the Delivery Model </h2> <p>Before building handlers, understand what you are dealing with:</p> <ul> <li>Providers send webhook events as HTTP POST requests</li> <li>They expect a 2xx response within a timeout (typically 5
Knowledge Map
Connected Articles — Knowledge Graph
This article is connected to other articles through shared AI topics and tags.
More in Products
UCL appoints Google DeepMind fellow to advance multilingual AI research - EdTech Innovation Hub
<a href="https://news.google.com/rss/articles/CBMisgFBVV95cUxQR3RqV1doQ2lCUFBMLTdSMjU1NEhDdHQ2dEhsbElyd1BLc0J6cE80VTBMYWxHdmk1a2h0NEJzckF6ZU5wN1dEUDR5aGJra1dGZUNEdExRMnFmWm1mUzFkU0tCZkpkdmNTME1JS0ZxSzlsVVNLQjFacEp1NXdJMlJfM3BQSTRlZENOWDlzQnJ1aVJ0amdZRndGYXpvN3pjaDdPMDJjcV9hdmhPTHJ5MkpEenBn?oc=5" target="_blank">UCL appoints Google DeepMind fellow to advance multilingual AI research</a> <font color="#6f6f6f">EdTech Innovation Hub</font>

Webhook Best Practices: Retry Logic, Idempotency, and Error Handling
<h1> Webhook Best Practices: Retry Logic, Idempotency, and Error Handling </h1> <p>Most webhook integrations fail silently. A handler returns 500, the provider retries a few times, then stops. Your system never processed the event and no one knows.</p> <p>Webhooks are not guaranteed delivery by default. How reliably your integration works depends almost entirely on how you write the receiver. This guide covers the patterns that make webhook handlers production-grade: proper retry handling, idempotency, error response codes, and queue-based processing.</p> <h2> Understand the Delivery Model </h2> <p>Before building handlers, understand what you are dealing with:</p> <ul> <li>Providers send webhook events as HTTP POST requests</li> <li>They expect a 2xx response within a timeout (typically 5

Why AI Agents Need a Trust Layer (And How We Built One)
<p><em>What happens when AI agents need to prove they're reliable before anyone trusts them with real work?</em></p> <h2> The Problem No One's Talking About </h2> <p>Every week, a new AI agent framework drops. Autonomous agents that can write code, send emails, book flights, manage databases. The capabilities are incredible.</p> <p>But here's the question nobody's answering: <strong>how do you know which agent to trust?</strong></p> <p>Right now, hiring an AI agent feels like hiring a contractor with no references, no portfolio, and no track record. You're just... hoping it works. And when it doesn't, there's no accountability trail.</p> <p>We kept running into this building our own multi-agent systems:</p> <ul> <li>Agent A says it can handle email outreach. Can it? Who knows.</li> <li>Age

Building a scoring engine with pure TypeScript functions (no ML, no backend)
<p>We needed to score e-commerce products across multiple dimensions: quality, profitability, market conditions, and risk.</p> <p>The constraints:</p> <ul> <li>Scores must update in real time</li> <li>Must run entirely in the browser (Chrome extension)</li> <li>Must be explainable (not a black box)</li> </ul> <p>We almost built an ML pipeline — training data, model serving, APIs, everything.</p> <p>Then we asked a simple question:</p> <p><strong>Do we actually need machine learning for this?</strong></p> <p>The answer was no.</p> <p>We ended up building several scoring engines in pure TypeScript.<br> Each one is a single function, under 100 lines, zero dependencies, and runs in under a millisecond.</p> <h2> What "pure function" means here </h2> <p>Each scoring engine follows 3 rules:</p> <
Discussion
Sign in to join the discussion
No comments yet — be the first to share your thoughts!