Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ on:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: satrank
POSTGRES_PASSWORD: satrank
POSTGRES_DB: satrank
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U satrank -d satrank"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
DATABASE_URL: postgresql://satrank:satrank@localhost:5432/satrank
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
Expand Down
49 changes: 49 additions & 0 deletions docs/phase-6.1/RELEASE-NOTES-DRAFT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# SatRank SDK 1.0.0 — Release Notes (draft)

> **Draft for manual publication.** Do NOT publish automatically.

Two SDKs promoted from `1.0.0-rc.1` / `1.0.0rc1` to stable `1.0.0`:
- `@satrank/sdk` (TypeScript, npm) — `sdk/satrank-sdk-1.0.0.tgz`
- `satrank` (Python, PyPI) — `python-sdk/dist/satrank-1.0.0-py3-none-any.whl` + `satrank-1.0.0.tar.gz`

## Highlights

- **One verb, hard budget.** `sr.fulfill({ intent, budget_sats })` / `await sr.fulfill(intent=..., budget_sats=...)` runs the full L402 flow: discover, pay, retry, report. Budget is a hard cap across attempts.
- **Three wallet drivers.** `LndWallet` (REST + macaroon), `NwcWallet` (NIP-47 encrypted over Nostr), `LnurlWallet` (LNbits-style HTTP). The driver contract is a two-method protocol: `payInvoice(bolt11, maxFeeSats)` + `isAvailable()`.
- **Typed error hierarchy.** `SatRankError` subclasses map to HTTP statuses (`ValidationSatRankError`, `PaymentRequiredError`, `BalanceExhaustedError`, `RateLimitedError`, …). `isRetryable()` (TS) tags 429/503/504/network/timeout for agent-side backoff.
- **NLP helper (EN).** `parseIntent('find me a cheap weather API for Paris under 50 sats')` → `{ category, keywords, budget_sats }`. Drop-in for `fulfill()`.

## Changes from RC

- Added `"consider_alternative"` to `AdvisoryBlock.recommendation` union (both SDKs). Server has always emitted four values; SDKs had three. This is **additive** and non-breaking for consumers pattern-matching on the existing three.
- Removed internal `ApiClient.getAgentVerdict()` in the TS SDK (never wired to the public surface).
- Narrative: "AI agents" → "autonomous agents on Bitcoin Lightning" in descriptions.
- TS README rewritten for the narrow 1.0 `SatRank` class (prior README still documented the 0.x `SatRankClient`).

## Phase 12C note

The `AgentSource 'observer_protocol' → 'attestation'` enum rename and the retirement of `BucketSource 'observer'` (Phase 12C, PR #14, currently unmerged) are **transparent to the SDK**. Neither SDK references these enums in its typed surface, so SDK consumers are unaffected whether Phase 12C ships before or after this SDK 1.0.

## Known issue (not blocking)

`error.code` differs between SDKs for known HTTP statuses:
- Python preserves the server's upstream `code` (e.g. `INVALID_CATEGORY`).
- TypeScript substitutes the class default (e.g. `VALIDATION_ERROR`).

Reconciling this is a breaking change for whichever side we adjust and is **deferred to a post-1.0 follow-up**. Consumers pattern-matching on `instanceof` (the recommended path) are unaffected.

## Verification

- 116 Python tests ✅, 125 TS tests ✅
- TS build ✅, Python mypy strict ✅, ruff ✅
- Live smoke against https://satrank.dev ✅ (`listCategories`, `resolveIntent` invalid path) — see `docs/phase-6.1/SDK-INTEGRATION-TEST.md`

## Publication checklist (manual — Romain)

- [ ] `cd sdk && npm publish` (requires npm login with 2FA)
- [ ] `cd python-sdk && twine upload dist/satrank-1.0.0*` (requires PyPI token)
- [ ] `git tag v1.0.0 && git push origin v1.0.0`
- [ ] `gh release create v1.0.0 --draft --notes-file docs/phase-6.1/RELEASE-NOTES-DRAFT.md`
- [ ] Announce on Nostr (SatRank npub)

None of the above is run by the automated loop. The GATE is explicit and enforced.
218 changes: 218 additions & 0 deletions docs/phase-6.1/SDK-DRIFT-AUDIT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
# Phase 6.1 — SDK drift audit (TypeScript + Python)

**Branche :** `phase-6.1-sdk` (branché depuis `origin/main`)
**Date :** 2026-04-22
**Base de comparaison prod :** `src/openapi.ts` + `src/app.ts` (endpoint wiring) à `origin/main`.
**Note Phase 12C :** PR #14 pas encore mergé. Les changements d'énum backend (`observer_protocol` → `attestation`, retrait `observer` de `BucketSource`) ne sont pas encore dans main, mais ils **n'impactent pas le SDK** (voir §4).

---

## 1. Méthodes SDK exposées

### 1.1 TypeScript — `@satrank/sdk` 1.0.0-rc.1

Surface publique (`sdk/src/index.ts`) :

| Classe / fonction | Signature | Depuis |
|--------------------------------|--------------------------------------------------------|--------|
| `new SatRank(opts)` | `SatRankOptions → SatRank` | rc.1 |
| `sr.fulfill(opts)` | `FulfillOptions → Promise<FulfillResult>` | rc.1 |
| `sr.listCategories()` | `() → Promise<IntentCategoriesResponse>` | rc.1 |
| `sr.resolveIntent(input)` | `{category,keywords?,budget_sats?,...} → Promise<IntentResponse>` | rc.1 |
| `parseIntent(text, opts?)` | subpath `@satrank/sdk/nlp` | rc.1 |
| `LndWallet(opts)` | subpath `@satrank/sdk/wallet` | rc.1 |
| `NwcWallet(opts)` + `parseNwcUri` | subpath `@satrank/sdk/wallet` | rc.1 |
| `LnurlWallet(opts)` | subpath `@satrank/sdk/wallet` | rc.1 |
| `deriveSharedSecret`, `nip04Encrypt/Decrypt` | subpath `@satrank/sdk/wallet` | rc.1 |
| Hiérarchie `SatRankError` | 12 sous-classes (`Balance...`, `Payment...`, etc.) | rc.1 |

### 1.2 Python — `satrank` 1.0.0rc1

Surface publique (`python-sdk/satrank/__init__.py`) :

| Symbole | Équivalent TS |
|--------------------------------|--------------------------|
| `SatRank(api_base=..., wallet=..., caller=...)` | `new SatRank()` |
| `sr.fulfill(intent=..., budget_sats=..., ...)` | `sr.fulfill()` |
| `sr.list_categories()` | `sr.listCategories()` |
| `sr.resolve_intent(...)` | `sr.resolveIntent()` |
| `LndWallet` / `NwcWallet` / `LnurlWallet` | `@satrank/sdk/wallet` |
| `parse_intent(...)` (satrank.nlp) | `@satrank/sdk/nlp` |
| 12 erreurs typées | miroir TS |

**Observation :** la surface Python est strictement le miroir de TS. Aucun décalage de méthode.

---

## 2. Endpoints prod consommés par le SDK

Le SDK est volontairement **narrow** : il n'appelle que 3 endpoints HTTP.

| Méthode SDK | Verb + path | Auth |
|-----------------------|--------------------------------|--------------------------------|
| `listCategories()` | `GET /api/intent/categories` | aucune (free discovery) |
| `resolveIntent()` | `POST /api/intent` | aucune (free discovery) |
| `fulfill()` (interne) | `POST /api/intent` puis fetch candidat + `POST /api/report` optionnel | L402 sur `/api/report` via `depositToken` |

`ApiClient` TS (`sdk/src/client/apiClient.ts`) expose aussi `getAgentVerdict()` **non re-exporté** via `SatRank` — mort-code inerte, pas dans la surface publique. À nettoyer en S2 (pas de drift prod, juste déchet interne).

---

## 3. Inventaire exhaustif des endpoints prod (source = `src/openapi.ts`)

26 endpoints routés sous `/api/*` :

**Agents / scoring** (non utilisés par le SDK 1.0 narrow) :
- `GET /agent/{hash}`
- `GET /agent/{hash}/verdict`
- `GET /agent/{hash}/history`
- `GET /agent/{hash}/attestations`
- `POST /verdicts` (batch 100)
- `GET /agents/top`
- `GET /agents/search`
- `GET /agents/movers`
- `GET /profile/{id}`

**Attestations / reports** :
- `POST /attestations` (free, X-API-Key)
- `POST /report` ✅ *utilisé par `fulfill()` auto-report*

**Système** :
- `GET /health`
- `GET /stats`
- `GET /stats/reports`
- `GET /version`
- `GET /openapi.json`

**Discovery / intent** ✅ *cœur du SDK 1.0* :
- `GET /intent/categories` ✅
- `POST /intent` ✅
- `GET /services`, `/services/best`, `/services/categories`
- `POST /services/register`
- `GET /endpoint/{url_hash}`

**Opérateurs (Phase 7)** :
- `POST /operator/register`, `GET /operators`, `GET /operator/{id}`

**Monétisation / paiement** :
- `POST /deposit` (2-phase invoice)
- `POST /probe` (paid, 5 credits)

**Monitoring / temps-réel** :
- `GET /watchlist`
- `GET /ping/{pubkey}`

**Conclusion :** le SDK couvre 3/26 endpoints (les 3 stables pour le flow discover-pay-deliver). Les 23 autres sont hors scope 1.0 et doivent le rester (pas de surface chargée).

---

## 4. Drifts identifiés

### 4.1 Drift narratif (MINOR — user-visible)

Brief Phase 6.1 demande : `"AI agents"` → `"autonomous agents on Bitcoin Lightning"`. Matches :

| Fichier | Ligne | Texte actuel |
|-----------------------------------|-------|--------------------------------------------------------------------|
| `sdk/package.json` | 4 | `"SatRank SDK 1.0 — sr.fulfill() for AI agents on Bitcoin Lightning"` |
| `sdk/README.md` | 3 | `Client SDK for the SatRank API. Trust scores for AI agents on Bitcoin Lightning.` |
| `python-sdk/pyproject.toml` | 8 | `"SatRank SDK for AI agents — discover, score, and pay Lightning-native HTTP services"` |

Aucun autre match dans `sdk/` et `python-sdk/` en dehors de README et métadonnées.

**Classification :** MINOR (texte marketing/description, pas d'API change).

### 4.2 README TypeScript désaligné (BREAKING docs)

`sdk/README.md` décrit une classe `SatRankClient` avec ~20 méthodes (`getScore`, `getTopAgents`, `decide`, `report`, `transact`, `watchNostr`, `deposit`, …) qui **n'existe plus dans `sdk/src/`**. La classe exportée est `SatRank` avec 3 méthodes (`fulfill`, `listCategories`, `resolveIntent`). Le README date de la surface SDK 0.x ; la réécriture Phase 6 (narrow 1.0) n'a pas touché la doc.

Impact consommateur :
- `import { SatRankClient } from '@satrank/sdk'` échouerait à la compilation → l'import n'est pas dans `index.ts`.
- Tous les exemples du README sont morts.

**Classification :** BREAKING au niveau documentation (code déjà aligné). Réécriture complète du README obligatoire en S2.

### 4.3 Union `recommendation` incomplète (MINOR — type drift additif)

Serveur (`src/types/index.ts:606` + `src/openapi.ts:558`) :
```ts
export type Recommendation = 'proceed' | 'proceed_with_caution' | 'consider_alternative' | 'avoid';
```

SDK TS (`sdk/src/types.ts:43`) :
```ts
recommendation: 'proceed' | 'proceed_with_caution' | 'avoid'; // 'consider_alternative' manquant
```

SDK Python (`python-sdk/satrank/types.py:88`) : mêmes 3 valeurs, `consider_alternative` absent.

Émis serveur via `src/utils/recommendation.ts:44` quand `advisoryLevel === 'orange'`. Endpoint concerné : `POST /api/intent` (bloc `advisory.recommendation` par candidat).

Impact : un pattern-matching TS exhaustif sur `candidate.advisory.recommendation` rate `consider_alternative` silencieusement. En Python, pas d'erreur runtime (TypedDict permissif) mais type-check `mypy --strict` passera quand même à cause du `Literal` permissif.

**Classification :** MINOR (ajout d'une valeur à une union — additive côté wire, mais consomme-breaking sur pattern-matching exhaustif TS). Correction obligatoire en S2/S3 pour refléter le contrat serveur.

### 4.4 Énums backend (aucun impact SDK)

Changements Phase 12C planifiés (pas encore sur `main` au moment de cet audit) :
- `AgentSource` : `'observer_protocol'` → `'attestation'` (rename)
- `BucketSource` : retrait de `'observer'` (sunset Observer Protocol)

**Recherche dans SDKs** : `grep -r "observer_protocol\|observer" sdk/ python-sdk/` → **0 matches**. Ni les types wire ni les docstrings ne référencent ces enums. Le SDK consomme uniquement des champs stables (`endpoint_url`, `bayesian.verdict`, `advisory.recommendation`, …).

**Classification :** NO-OP côté SDK. À mentionner dans CHANGELOG pour transparence, mais aucune édition de code.

### 4.5 Code mort interne (PATCH — cleanup)

`sdk/src/client/apiClient.ts:62` : méthode `getAgentVerdict()` implémentée, jamais appelée par `SatRank`. À supprimer en S2 (aligner le client sur la surface publique narrow).

**Classification :** PATCH interne, pas de drift fonctionnel.

---

## 5. Classification globale + proposition de version

| Drift | Sévérité | Action |
|--------------------------------|---------------------|---------------------------------|
| Narratif "AI agents" | MINOR | Rewrite descriptions |
| README TS désaligné | BREAKING (docs) | Réécriture complète README |
| Union `recommendation` | MINOR (type additif)| Ajouter `'consider_alternative'` TS + Python |
| Énums backend (12C) | NO-OP | Mention CHANGELOG |
| `getAgentVerdict` mort | PATCH interne | Supprimer méthode ApiClient |

### Proposition de bump

Les deux SDKs sont en **RC (1.0.0-rc.1 / 1.0.0rc1)**. Un RC n'a pas de garantie API — un consommateur qui a épinglé `1.0.0-rc.1` accepte des changements. La Phase 6.1 est le bon moment pour **promouvoir en GA 1.0.0** car :

1. La surface narrow `fulfill()/listCategories()/resolveIntent()` est stable depuis Phase 6 (merge 90ba9c0, 2026-04-19).
2. Les tests SDK sont verts (`sdk/tests/` + `python-sdk/tests/`).
3. Le seul ajout de contrat wire (`consider_alternative`) est additif — un consommateur rc.1 qui ne le match pas explicitement ne casse pas (la valeur arrive comme string au runtime).
4. Le README est mensonger sur la surface publique actuelle — un GA propre corrige le tort.

**Cibles :**
- TypeScript : `1.0.0-rc.1` → **`1.0.0`** (GA)
- Python : `1.0.0rc1` → **`1.0.0`** (GA, aligné)

---

## 6. Estimation d'effort S2→S6

| Étape | Contenu | Estim. |
|-------|------------------------------------------------------|-----------|
| S2 | TS : union `recommendation`, narrative, README rewrite, cleanup `getAgentVerdict`, bump 1.0.0, `npm run build` + `npm test` | 2h |
| S3 | Python : union `recommendation` (typing.Literal), pyproject narrative, pytest, bump 1.0.0 | 1h |
| S4 | Intégration vs `https://satrank.dev/api/health` + `/api/intent/categories` + `/api/agents/top` (2 SDKs) | 1h |
| S5 | `npm pack` + `python -m build`, CHANGELOGs, RELEASE-NOTES-DRAFT | 1.5h |
| S6 | Report final, commit, push, draft PR #15 avec checklist de publication | 1h |

**Total estimé :** 6.5h. Dans la borne basse du brief user (7-11h). Aucune étape ne dépasse 90 min isolément → pas de report envisageable.

---

## 7. Règles cardinales rappelées (self-check)

- ❌ **PUBLISH GATE absolu** : `npm publish`, `twine upload`, `gh release create`, `git tag v*` → interdits. S5 produit les artefacts dans `sdk/` et `python-sdk/dist/` uniquement.
- ❌ **LND non-négociable** : aucun `openchannel`, `closechannel`, ni ops LN.
- ✅ Si `/api/health` renvoie 500 en S4 → stop immédiat, log, pas de tentative fix prod.
- ✅ Sur ambiguïté : préférer supprimer que conserver (code mort → delete).
- ✅ Branche `phase-6.1-sdk` → pousser avec draft PR #15 pour review Romain.
71 changes: 71 additions & 0 deletions docs/phase-6.1/SDK-INTEGRATION-TEST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Phase 6.1 — SDK Integration Test Report

**Date:** 2026-04-22
**Target:** https://satrank.dev (prod)
**SDKs:** `@satrank/sdk@1.0.0` (TS), `satrank@1.0.0` (Python)

## Endpoint health

| Endpoint | Status | Notes |
|---|---|---|
| `GET /api/health` | 200 | `schemaVersion=41`, `agentsIndexed=8186`, `dbStatus=ok`, `lndStatus=ok` |
| `GET /api/intent/categories` | 200 | `{ "categories": [] }` — registry currently empty, shape matches SDK contract |
| `POST /api/intent` (unknown category) | 400 | `code=INVALID_CATEGORY` — correctly rejected |
| `GET /api/agents/top` | 200 | Not in SDK surface (removed in Phase 6 narrowing); verified wire shape for later reference |

Prod is healthy. No STOP condition triggered.

## TypeScript SDK smoke

Ran `@satrank/sdk@1.0.0` from freshly-built `dist/` against prod:

```json
{
"ts_sdk_version": "1.0.0",
"steps": [
{ "name": "listCategories", "ok": true, "category_count": 0, "shape_ok": true },
{
"name": "resolveIntent(invalid)",
"ok": true,
"threw": "ValidationSatRankError",
"code": "VALIDATION_ERROR",
"statusCode": 400,
"message": "Unknown category \"does/not/exist\". Call GET /api/intent/categories for the current list."
}
]
}
```

## Python SDK smoke

Ran `satrank@1.0.0` (installed from `python-sdk/` editable) against prod:

```json
{
"py_sdk_version": "1.0.0",
"steps": [
{ "name": "list_categories", "ok": true, "category_count": 0, "shape_ok": true },
{
"name": "resolve_intent(invalid)",
"ok": true,
"threw": "ValidationSatRankError",
"code": "INVALID_CATEGORY",
"message": "Unknown category \"does/not/exist\". Call GET /api/intent/categories for the current list."
}
]
}
```

## Observations

1. **Shape parity OK.** Both SDKs deserialize `/api/intent/categories` into the documented shape. Empty list is handled without exceptions.
2. **Error-class parity OK.** Both SDKs surface `ValidationSatRankError` for HTTP 400.
3. **Known pre-existing cross-SDK divergence on `error.code`:**
- TS SDK discards the server's `error.code` for known HTTP statuses and uses the class default (`VALIDATION_ERROR`).
- Python SDK preserves the server's `error.code` verbatim (`INVALID_CATEGORY`).
- This is not new drift from Phase 6.1 — it's how both SDKs shipped in Phase 6. Reconciling would be a BREAKING change in whichever we adjust. Flagged for a post-1.0 follow-up; not blocking for this release.
4. **Fulfill path not exercised.** No wallet configured (intentional — no LN ops without approval). The L402 flow is covered by the 116 Python tests + 125 TS tests, both green locally.

## Verdict

Integration smoke **GREEN** for both SDKs against prod. Ready to proceed to S5 (local build artifacts).
Loading
Loading