Orient yourself first
npm run crm -- app inspect --json
Read valid, then problems[], then limitations[], in that order. Every problem is fixed or reported before anything is built on top of it, and every limitation is a hard boundary on what you may claim. Then read packages[], capabilities[], resources[], actions[], policies[] and providers[]: that list is what exists. A capability absent from the report does not exist, whatever a record name, a label or a document suggests.
If the repository documents this skill names are absent, you are in a project built from this framework rather than in the framework itself. The inspection report is then the source of truth and those documents are optional background — do not guess at their contents, and do not assume a path exists because this skill names it.
providers[] reports declared metadata only. A provider entry never means the provider is configured, credentialed or reachable — PROVIDER_HEALTH_UNKNOWN is in limitations[] for exactly that reason.
Background, where they exist: ARCHITECTURE.md, DECISIONS.md (ADR-015) and docs/LEAD_INTELLIGENCE.md. They are the deeper source for the rules below, not a prerequisite for them — the rules stand on their own.
Integrate an enrichment provider
- Define it code-first:
{ name, version, label, capabilities: ['company'], async enrichCompany({ lead }, { now }) } returning { fields: { companyDomain, companyName, country (A-Z2), employeeRange, industry, revenueRange, language (a-z2) }, confidence (0–100 int), sourceRef, expiresAt?, partial? }. Out-of-contract output is refused (PROVIDER_INVALID) — never loosen the normalizer.
- Register it in
packages/intelligence/generated/index.js (static import, like actions/pipelines).
- The provider is called in the action's
prepare phase — never inside a DB transaction. Do not add DB writes to a provider.
- Test failure paths with a deterministic fixture (outage →
PROVIDER_FAILED, hang → PROVIDER_TIMEOUT, bad shapes → PROVIDER_INVALID): nothing persisted, honest failed trace.
- Real paid providers need human-approved credentials and are out of scope until then.
Create a scoring model version
{ name, version, label, minScore?, maxScore?, rules: [{ key, label, weight (non-zero int), evaluate(context) }] } — evaluate gets frozen { lead, snapshot, signals, evaluatedAt }, must be total, deterministic, side-effect-free (no network, no LLM, no DB writes) and return boolean or { matched, reason }.
- Declare every threshold in
config. The fingerprint is a declared-definition fingerprint: it captures the definition's source and its declared config (frozen into ctx.config), NOT values captured by closures or out-of-file helpers. A closure-held threshold changes silently — put it in config or as a literal.
- Never edit a registered version. Changing rules or config = a NEW version registered alongside the old (the persisted fingerprint check stops the app on in-place edits). Rollback = publish a new version whose rules come from an earlier one.
- Test explainability: contributions sum to the total, appear in declared rule order, and the same inputs reproduce the same score. Test the bounds clamp and behavior without snapshot/signals.
Create a routing policy version
{ name, version, label, route(context) } — context gives frozen { lead, score, snapshot, targets (eligible, with currentLoad), allTargets, rank, routedAt }; return { target, rule } choosing an ELIGIBLE target, or null for the fallback queue. Use rank() (priority desc → load asc → key asc) for ties — never randomness.
- Targets are declared data (
key, kind, active, countries, languages, skills, capacity, priority, scoreMin/Max); capacity works against the in-transaction count of assigned leads. Same versioning rules as scoring models.
- Test: deterministic selection, capacity exclusion (full target skipped), fallback with recorded reason,
ALREADY_ASSIGNED on reroute, LEAD_NOT_SCORED before scoring, one final assignment under concurrency.
Test policy fingerprints
Boot the app twice: same source must boot cleanly; a changed registered version must fail with "source changed after registration". Runs must retain the old version + fingerprint after new versions appear.
Never claim manual override before RBAC
There is no Sales-Manager reassignment action and none may be added before the Production Spine (auth, tenancy, RBAC). Routing targets are trusted identifiers, not users. Keep the JTBD matrix honest.
Finish with npm run verify and the starter (node examples/starters/b2b-lead-qualification/install.mjs).
1---2name: build-lead-intelligence3description: Add or extend Lead Intelligence in an Accordo project - enrichment providers, versioned explainable scoring models, versioned routing policies and routing targets. Use for enrich/score/route work on leads. Do not use for CRUD module changes (create-crm-module) or approval processes (create-crm-workflow).4---56## Orient yourself first78```bash9npm run crm -- app inspect --json10```1112Read `valid`, then `problems[]`, then `limitations[]`, in that order. Every problem is fixed or reported before anything is built on top of it, and **every limitation is a hard boundary on what you may claim.** Then read `packages[]`, `capabilities[]`, `resources[]`, `actions[]`, `policies[]` and `providers[]`: that list is what exists. A capability absent from the report does not exist, whatever a record name, a label or a document suggests.1314If the repository documents this skill names are absent, you are in a project built from this framework rather than in the framework itself. The inspection report is then the source of truth and those documents are optional background — do not guess at their contents, and do not assume a path exists because this skill names it.1516`providers[]` reports declared metadata only. A provider entry never means the provider is configured, credentialed or reachable — `PROVIDER_HEALTH_UNKNOWN` is in `limitations[]` for exactly that reason.1718**Background, where they exist:** `ARCHITECTURE.md`, `DECISIONS.md` (ADR-015) and `docs/LEAD_INTELLIGENCE.md`. They are the deeper source for the rules below, not a prerequisite for them — the rules stand on their own.1920## Integrate an enrichment provider21221. Define it code-first: `{ name, version, label, capabilities: ['company'], async enrichCompany({ lead }, { now }) }` returning `{ fields: { companyDomain, companyName, country (A-Z2), employeeRange, industry, revenueRange, language (a-z2) }, confidence (0–100 int), sourceRef, expiresAt?, partial? }`. Out-of-contract output is refused (`PROVIDER_INVALID`) — never loosen the normalizer.232. Register it in `packages/intelligence/generated/index.js` (static import, like actions/pipelines).243. The provider is called in the action's `prepare` phase — never inside a DB transaction. Do not add DB writes to a provider.254. Test failure paths with a deterministic fixture (outage → `PROVIDER_FAILED`, hang → `PROVIDER_TIMEOUT`, bad shapes → `PROVIDER_INVALID`): nothing persisted, honest failed trace.265. Real paid providers need human-approved credentials and are out of scope until then.2728## Create a scoring model version29301. `{ name, version, label, minScore?, maxScore?, rules: [{ key, label, weight (non-zero int), evaluate(context) }] }` — `evaluate` gets frozen `{ lead, snapshot, signals, evaluatedAt }`, must be total, deterministic, side-effect-free (no network, no LLM, no DB writes) and return `boolean` or `{ matched, reason }`.312. **Declare every threshold in `config`.** The fingerprint is a *declared-definition* fingerprint: it captures the definition's source and its declared `config` (frozen into `ctx.config`), NOT values captured by closures or out-of-file helpers. A closure-held threshold changes silently — put it in `config` or as a literal.323. **Never edit a registered version.** Changing rules or config = a NEW version registered alongside the old (the persisted fingerprint check stops the app on in-place edits). Rollback = publish a new version whose rules come from an earlier one.334. Test explainability: contributions sum to the total, appear in declared rule order, and the same inputs reproduce the same score. Test the bounds clamp and behavior without snapshot/signals.3435## Create a routing policy version36371. `{ name, version, label, route(context) }` — context gives frozen `{ lead, score, snapshot, targets (eligible, with currentLoad), allTargets, rank, routedAt }`; return `{ target, rule }` choosing an ELIGIBLE target, or `null` for the fallback queue. Use `rank()` (priority desc → load asc → key asc) for ties — never randomness.382. Targets are declared data (`key, kind, active, countries, languages, skills, capacity, priority, scoreMin/Max`); capacity works against the in-transaction count of assigned leads. Same versioning rules as scoring models.393. Test: deterministic selection, capacity exclusion (full target skipped), fallback with recorded reason, `ALREADY_ASSIGNED` on reroute, `LEAD_NOT_SCORED` before scoring, one final assignment under concurrency.4041## Test policy fingerprints4243Boot the app twice: same source must boot cleanly; a changed registered version must fail with "source changed after registration". Runs must retain the old version + fingerprint after new versions appear.4445## Never claim manual override before RBAC4647There is no Sales-Manager reassignment action and none may be added before the Production Spine (auth, tenancy, RBAC). Routing targets are trusted identifiers, not users. Keep the JTBD matrix honest.4849Finish with `npm run verify` and the starter (`node examples/starters/b2b-lead-qualification/install.mjs`).