From f8adce31b4c3b2612e4e1075afe62d4a59f3157a Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Sat, 16 May 2026 21:51:33 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20migration=20roadmap=20(phases=202b?= =?UTF-8?q?=E2=80=934)=20+=20canonical=20refresh?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docs/migrations/ index and per-phase plans for the Express → Hono Workers migration. Refreshes top-level CLAUDE.md with the current migration posture (Tier 4, ChittyAuth, Hyperdrive binding CHITTYASSETS_DB, GCS → R2 cutover, P/L/T/E/A entity types). Files: - docs/migrations/ROADMAP.md — phase index, dependency graph, PR status - docs/migrations/phase2b-simple-reads.md - docs/migrations/phase2c-external-reads.md - docs/migrations/phase3a-asset-writes.md - docs/migrations/phase3b-domain-writes.md - docs/migrations/phase3c-heavy-writes.md - docs/migrations/phase4-r2-routes.md - docs/migrations/test-strategy.md — real-Neon no-mocks harness - docs/migrations/registry-status.md — chittyassets NOT REGISTERED - CLAUDE.md — migration-status section added at top Read-only on code: no .ts/.tsx/.js/.json/.toml/.jsonc/.sql touched. Does not duplicate CHARTER.md/CHITTY.md (they land via PR #33). Route citations reference server/routes.ts line numbers. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 18 +++ docs/migrations/ROADMAP.md | 96 ++++++++++++++++ docs/migrations/phase2b-simple-reads.md | 105 +++++++++++++++++ docs/migrations/phase2c-external-reads.md | 118 +++++++++++++++++++ docs/migrations/phase3a-asset-writes.md | 108 +++++++++++++++++ docs/migrations/phase3b-domain-writes.md | 87 ++++++++++++++ docs/migrations/phase3c-heavy-writes.md | 106 +++++++++++++++++ docs/migrations/phase4-r2-routes.md | 134 ++++++++++++++++++++++ docs/migrations/registry-status.md | 76 ++++++++++++ docs/migrations/test-strategy.md | 121 +++++++++++++++++++ 10 files changed, 969 insertions(+) create mode 100644 docs/migrations/ROADMAP.md create mode 100644 docs/migrations/phase2b-simple-reads.md create mode 100644 docs/migrations/phase2c-external-reads.md create mode 100644 docs/migrations/phase3a-asset-writes.md create mode 100644 docs/migrations/phase3b-domain-writes.md create mode 100644 docs/migrations/phase3c-heavy-writes.md create mode 100644 docs/migrations/phase4-r2-routes.md create mode 100644 docs/migrations/registry-status.md create mode 100644 docs/migrations/test-strategy.md diff --git a/CLAUDE.md b/CLAUDE.md index 2a3e126..72edbf6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,7 +1,25 @@ # CLAUDE.md + + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +## Migration status (2026-05-16) + +ChittyAssets is in active Express → Cloudflare Workers (Hono) migration. See +[docs/migrations/ROADMAP.md](docs/migrations/ROADMAP.md) for phase index and +status of in-flight PRs. + +- **Tier**: 4 (Domain) — `chittycanon://core/services/chittyassets` +- **Entity types handled**: P, L, T, E, A (all five P/L/T/E/A — Authority included) +- **Auth**: ChittyAuth (JWKS) — Replit Auth listed below is the legacy stack + being replaced +- **DB binding (Worker)**: Hyperdrive `CHITTYASSETS_DB` +- **File storage**: GCS → R2 (cutover in Phase 4); ACL via `r2_object_acl` table + +While migration is in flight, both runtimes coexist: Express serves prod traffic +until Phase 4 lands and the Cloudflare route flips to the Worker. + ## Commands ### Development diff --git a/docs/migrations/ROADMAP.md b/docs/migrations/ROADMAP.md new file mode 100644 index 0000000..e10c653 --- /dev/null +++ b/docs/migrations/ROADMAP.md @@ -0,0 +1,96 @@ +--- +uri: chittycanon://docs/tech/spec/chittyassets-migration-roadmap +namespace: chittycanon://docs/tech +type: spec +version: 1.0.0 +status: DRAFT +registered_with: chittycanon://core/services/canon +title: ChittyAssets Express → Hono Migration Roadmap +author: chittycanon-code-cardinal +visibility: INTERNAL +tags: [migration, hono, cloudflare-workers, chittyassets] +category: migration +@canon: chittycanon://core/services/chittyassets +--- + +# ChittyAssets Migration Roadmap + +Source-of-truth route inventory: `server/routes.ts` (673 lines). This document is the +index for the Express → Cloudflare Workers (Hono) migration. Each phase has a +dedicated plan under `docs/migrations/`. + +## Canonical posture + +- **Service**: `chittycanon://core/services/chittyassets` +- **Tier**: 4 (Domain) +- **Entity types handled**: P (Person), L (Location), T (Thing), E (Event), A (Authority) — all five P/L/T/E/A +- **Primary Thing (T)**: `asset` (the ChittyAsset itself) +- **Primary Event (E)**: `timeline_event` (asset lifecycle: created, frozen, minted, evidence_added) +- **Primary Authority (A)**: evidence trust score + ChittyChain mint receipt +- **Person (P)**: asset owner (`userId` from ChittyAuth, `chitty_${auth.userId}`) +- **Location (L)**: jurisdiction on legal_case, asset physical location metadata + +## Dependency graph + +``` +chittyassets (Tier 4) + ├─ chittyauth (Tier 1) — JWKS verify, Person principal + ├─ chittyid (Tier 0) — chittyId mint for new assets + ├─ chittytrust (Tier 0) — trust score on create + ├─ chittyledger (Tier 4) — evidence ledger submit/verify + ├─ chittymint (Tier 4) — ChittyChain freeze/mint + ├─ openai (external) — document analysis, legal-doc generation + ├─ Neon Postgres — via Hyperdrive binding CHITTYASSETS_DB + └─ R2 bucket — evidence files (replaces GCS) +``` + +## Phase status + +| Phase | Branch | PR | Status | +|------|--------|----|--------| +| 0 — canonical artifacts (CHARTER, CHITTY, AGENTS, SECURITY, register.json) | `feat/hono-migration-phase-1` | #33 | OPEN | +| 1 — Hono Worker skeleton (`/health`, `/api/v1/status`, `/api/auth/user`, JWKS) | `feat/hono-migration-phase-1` | #33 | OPEN | +| Schema remediation — `chitty_id`, P/L/T/E/A enum, `entities` registry, `r2_object_acl` (Drizzle migration `0003`) | `chore/schema-canonical-remediation` | #34 | OPEN | +| 2a — asset reads (`GET /api/assets`, `/:id`, `/stats`, `/:id/evidence`, `/:id/timeline`) | `feat/hono-phase-2a-asset-reads` | in flight | IN PROGRESS | +| 2b — simple reads (warranties, insurance, legal-cases, tools/resources) | `feat/hono-phase-2b-simple-reads` (planned) | — | PLANNED → see [phase2b-simple-reads.md](./phase2b-simple-reads.md) | +| 2c — external reads (evidence-ledger get, ecosystem/status) | `feat/hono-phase-2c-external-reads` (planned) | — | PLANNED → see [phase2c-external-reads.md](./phase2c-external-reads.md) | +| 3a — asset writes (POST/PUT/DELETE `/api/assets`, evidence create) | `feat/hono-phase-3a-asset-writes` (planned) | — | PLANNED → see [phase3a-asset-writes.md](./phase3a-asset-writes.md) | +| 3b — domain writes (warranty, insurance, legal-case creates) | `feat/hono-phase-3b-domain-writes` (planned) | — | PLANNED → see [phase3b-domain-writes.md](./phase3b-domain-writes.md) | +| 3c — heavy writes (freeze, mint, AI analyze, trust-score, legal-doc, ledger submit/verify, seed-demo) | `feat/hono-phase-3c-heavy-writes` (planned) | — | PLANNED → see [phase3c-heavy-writes.md](./phase3c-heavy-writes.md) | +| 4 — R2 object routes (`/objects/:path`, upload, evidence-files ACL) | `feat/hono-phase-4-r2-routes` (planned) | — | PLANNED → see [phase4-r2-routes.md](./phase4-r2-routes.md) | + +Expected merge order: #33 → #34 → 2a → 2b → 2c → 3a → 3b → 3c → 4 → Express server retirement. + +## Cross-cutting docs + +- [test-strategy.md](./test-strategy.md) — real-Neon integration test harness (no mocks) +- [registry-status.md](./registry-status.md) — ChittyRegistry registration state + +## ChittyRegistry status + +Query against `https://registry.chitty.cc/api/v1/search?q=chittyassets` on +2026-05-16 returned `count: 0`. Service is **NOT REGISTERED**. See +[registry-status.md](./registry-status.md) for recommended registration payload. +Registration is a sensitive-intent operation and requires explicit operator +authorization via `ch1tty -> ChittyConnect`; this doc does not execute it. + +## Compliance triad status + +`CHARTER.md`, `CHITTY.md`, `AGENTS.md`, `SECURITY.md`, and `register.json` +land via PR #33 (`feat/hono-migration-phase-1`, commit `806c8a5`). They are +**not present on `main`** at the time this roadmap was written, but will be once +PR #33 merges. This doc PR does not recreate them to avoid merge conflicts +with PR #33. After PR #33 merges, re-validate the triad against this roadmap's +canonical posture section (especially Tier 4 classification, the binding name +`CHITTYASSETS_DB`, and the full P/L/T/E/A entity-type list). + +## Routes not covered by phase plans + +All routes in `server/routes.ts` are accounted for across phases 1, 2a–c, 3a–c, 4 +except: + +- `GET /api/auth/user` (`server/routes.ts:27`) — implemented in Phase 1 Worker + (`worker/src/index.ts`). No further migration work. + +When the last phase merges, `server/index.ts`, `server/routes.ts`, and the +Express middleware chain can be deleted; the Worker becomes the sole runtime. diff --git a/docs/migrations/phase2b-simple-reads.md b/docs/migrations/phase2b-simple-reads.md new file mode 100644 index 0000000..2196aac --- /dev/null +++ b/docs/migrations/phase2b-simple-reads.md @@ -0,0 +1,105 @@ +--- +uri: chittycanon://docs/tech/spec/chittyassets-phase2b +namespace: chittycanon://docs/tech +type: spec +version: 1.0.0 +status: DRAFT +title: ChittyAssets Phase 2b — Simple Reads +visibility: INTERNAL +tags: [migration, hono, phase-2b] +@canon: chittycanon://core/services/chittyassets +--- + +# Phase 2b — Simple Reads (Neon-only, no external services) + +## Scope + +Pure-read endpoints that hit Neon via Hyperdrive (`CHITTYASSETS_DB`) and return +JSON. No external service calls, no AI, no R2. + +| Method | Path | Source (Express) | Storage call | +|--------|------|------------------|--------------| +| GET | `/api/assets/:assetId/warranties` | `server/routes.ts:453` | `storage.getAssetWarranties(assetId, userId)` | +| GET | `/api/warranties/expiring?days=N` | `server/routes.ts:464` | `storage.getExpiringWarranties(userId, daysAhead)` | +| GET | `/api/assets/:assetId/insurance` | `server/routes.ts:497` | `storage.getAssetInsurance(assetId, userId)` | +| GET | `/api/legal-cases` | `server/routes.ts:529` | `storage.getUserLegalCases(userId)` | +| GET | `/api/tools/resources` | `server/routes.ts:117` | `listToolResources()` (static) | + +## File layout + +``` +worker/src/routes/ + warranties.ts # GET /api/assets/:assetId/warranties, GET /api/warranties/expiring + insurance.ts # GET /api/assets/:assetId/insurance + legal-cases.ts # GET /api/legal-cases + tools.ts # GET /api/tools/resources +worker/src/db/ + queries/warranties.ts + queries/insurance.ts + queries/legal-cases.ts +``` + +Each route module exports a `Hono` sub-app mounted in `worker/src/index.ts` +under the existing auth-guarded group established in Phase 2a. + +## Query shapes (Drizzle pseudocode) + +```ts +// queries/warranties.ts +import { eq, and, lte, gte } from 'drizzle-orm'; +import { warranties, assets } from '../../../shared/schema'; + +export async function getAssetWarranties(db: DrizzleDB, assetId: string, userId: string) { + // join enforces ownership at the SQL layer (defense in depth alongside auth) + return db + .select() + .from(warranties) + .innerJoin(assets, eq(warranties.assetId, assets.id)) + .where(and(eq(warranties.assetId, assetId), eq(assets.userId, userId))) + .orderBy(warranties.expirationDate); +} + +export async function getExpiringWarranties(db: DrizzleDB, userId: string, daysAhead: number) { + const cutoff = new Date(Date.now() + daysAhead * 86_400_000); + return db + .select() + .from(warranties) + .innerJoin(assets, eq(warranties.assetId, assets.id)) + .where(and( + eq(assets.userId, userId), + gte(warranties.expirationDate, new Date()), + lte(warranties.expirationDate, cutoff), + )) + .orderBy(warranties.expirationDate); +} +``` + +`getAssetInsurance` and `getUserLegalCases` follow the same ownership-via-join +pattern. `tools/resources` returns a static manifest derived from +`server/toolResources.ts` — port the module to `worker/src/lib/tool-resources.ts` +verbatim (no DB). + +## External deps + +None. This is the cleanest phase to land — pure Neon reads via the Hyperdrive +binding established in Phase 1. + +## Validation gates + +1. `npm run check` clean (Worker tsconfig). +2. `wrangler deploy --dry-run --env production` ≤ 260 KiB. +3. Real-Neon integration test (see [test-strategy.md](./test-strategy.md)): + - Seed one user (Person, P) `did:chitty:user:phase2b-test` + - Seed one asset (Thing, T) with `chitty_id` matching the canonical pattern + `VV-G-LLL-SSSS-T-YM-C-X` + - Seed two warranties (one expiring in 15 days, one in 90 days) + - Hit each route via `app.request(...)` and assert shape + ownership filter +4. `curl` against deployed Worker post-merge: + - `curl https://assets.chitty.cc/api/legal-cases -H 'Authorization: Bearer …'` + - Assert `200` + array shape + +## Estimated PR size + +~400 LOC added (4 route modules + 4 query modules + tests). 0 LOC removed +(Express stays running in parallel — the Worker takes traffic only after the +Cloudflare route is flipped, post-Phase 4). diff --git a/docs/migrations/phase2c-external-reads.md b/docs/migrations/phase2c-external-reads.md new file mode 100644 index 0000000..d8f210a --- /dev/null +++ b/docs/migrations/phase2c-external-reads.md @@ -0,0 +1,118 @@ +--- +uri: chittycanon://docs/tech/spec/chittyassets-phase2c +namespace: chittycanon://docs/tech +type: spec +version: 1.0.0 +status: DRAFT +title: ChittyAssets Phase 2c — External Reads +visibility: INTERNAL +tags: [migration, hono, phase-2c, external-services] +@canon: chittycanon://core/services/chittyassets +--- + +# Phase 2c — External Reads (ChittyLedger, ecosystem status) + +## Scope + +| Method | Path | Source | External dep | +|--------|------|--------|--------------| +| GET | `/api/evidence-ledger/:chittyId` | `server/routes.ts:80` | ChittyLedger (`chittycanon://core/services/chittyledger`) | +| GET | `/api/ecosystem/status` | `server/routes.ts:105` | All Tier 0–4 service `/health` endpoints | + +These wrap remote HTTP calls. The Express implementation uses +`server/chittyCloudMcp.ts` helpers (`getEvidenceLedger()`, +`getChittyServices()`); under Hono we replace MCP-style indirection with thin +typed HTTP clients. + +## File layout + +``` +worker/src/ + clients/ + chittyledger.ts # HTTP client for ledger.chitty.cc + ecosystem.ts # parallel /health probes + routes/ + evidence-ledger.ts # GET /api/evidence-ledger/:chittyId + ecosystem.ts # GET /api/ecosystem/status +``` + +## Client contracts + +### `clients/chittyledger.ts` + +```ts +import { z } from 'zod'; + +const EvidenceRecordSchema = z.object({ + chittyId: z.string().regex(/^[0-9A-Z]{2}-[0-9]-[A-Z]{3}-[0-9]{4}-[PLTEA]-[0-9A-Z]{2}-[0-9]-[0-9A-Z]$/), + status: z.enum(['draft', 'verified', 'sealed', 'disputed']), + trustScore: z.number().min(0).max(1), + retentionUntil: z.string().datetime(), + chainResult: z.object({ ipfsHash: z.string(), txHash: z.string().optional() }).nullable(), +}); + +export class ChittyLedgerClient { + constructor(private baseUrl: string, private bearerToken: string) {} + + async getEvidence(chittyId: string, signal?: AbortSignal) { + const res = await fetch(`${this.baseUrl}/api/v1/evidence/${encodeURIComponent(chittyId)}`, { + headers: { authorization: `Bearer ${this.bearerToken}` }, + signal, + }); + if (res.status === 404) return null; + if (!res.ok) throw new LedgerError(res.status, await res.text()); + return EvidenceRecordSchema.parse(await res.json()); + } +} +``` + +### `clients/ecosystem.ts` + +Probes all configured service base URLs in parallel with a 2s per-probe timeout +via `AbortSignal.timeout(2000)`. Returns +`{ services: Array<{ name, url, status: 'ok'|'degraded'|'down', latencyMs }> }`. + +Service URL list is read from environment, not hardcoded. Suggested env vars +(Worker `[vars]`): + +```jsonc +"CHITTY_LEDGER_URL": "https://ledger.chitty.cc", +"CHITTY_ID_URL": "https://id.chitty.cc", +"CHITTY_TRUST_URL": "https://trust.chitty.cc", +"CHITTY_AUTH_URL": "https://auth.chitty.cc", +"CHITTY_MINT_URL": "https://mint.chitty.cc", +``` + +The bearer token used for upstream ledger calls is the same ChittyAuth JWT +presented by the caller (token forwarding), not a service-to-service secret. +This preserves the authorization subject end-to-end. + +## Retry / timeout policy + +- **Per-call timeout**: 5s for `getEvidence`, 2s for each `/health` probe +- **Retries**: 2 retries on 5xx or network error, exponential backoff + 100ms → 300ms; no retry on 4xx +- **Circuit breaker**: out of scope for Phase 2c; tracked in + `chittyobservability` integration (deferred) +- **Failure mode**: ledger 5xx after retries → return 503 with + `{ error: 'upstream_unavailable', service: 'chittyledger', correlationId }`; + ecosystem probe failure does NOT fail the whole status response — per-service + `status: 'down'` with `error` field + +## Validation gates + +1. Type-check clean. +2. Real upstream integration test against staging ledger + (`https://ledger-staging.chitty.cc`): + - Submit a known ChittyID, then `GET /api/evidence-ledger/:chittyId` through + the Worker, assert round-trip +3. Failure-injection test: point `CHITTY_LEDGER_URL` at a `127.0.0.1:1` URL, + assert 503 + correlation ID, not a 500 with stack trace. +4. `curl https://assets.chitty.cc/api/ecosystem/status` returns 200 with + per-service health. + +## Estimated PR size + +~500 LOC (clients, routes, tests, env wiring). External-service integration +tests are the long pole; budget time for staging-env coordination with the +ChittyLedger team. diff --git a/docs/migrations/phase3a-asset-writes.md b/docs/migrations/phase3a-asset-writes.md new file mode 100644 index 0000000..c14bfae --- /dev/null +++ b/docs/migrations/phase3a-asset-writes.md @@ -0,0 +1,108 @@ +--- +uri: chittycanon://docs/tech/spec/chittyassets-phase3a +namespace: chittycanon://docs/tech +type: spec +version: 1.0.0 +status: DRAFT +title: ChittyAssets Phase 3a — Asset Writes +visibility: INTERNAL +tags: [migration, hono, phase-3a, writes] +@canon: chittycanon://core/services/chittyassets +--- + +# Phase 3a — Asset Writes + +## Scope + +| Method | Path | Source | Side effects | +|--------|------|--------|--------------| +| POST | `/api/assets` | `server/routes.ts:259` | mint ChittyID (T), trust score, create timeline event (E) | +| PUT | `/api/assets/:id` | `server/routes.ts:306` | update + timeline event (E) on status change | +| DELETE | `/api/assets/:id` | `server/routes.ts:323` | soft delete (recommended) + timeline event (E) | +| POST | `/api/assets/:assetId/evidence` | `server/routes.ts:346` | create evidence (T), timeline event (E) | + +## Canonical entity model + +Per `chittycanon://gov/governance`, an asset is **Thing (T)**, the owner is +**Person (P)**, creation is **Event (E)**, and the resulting trust score is +**Authority (A) — Earned**. All five P/L/T/E/A appear in the surrounding domain +model when location metadata (L) is captured on the asset payload. + +## File layout + +``` +worker/src/ + routes/ + assets-write.ts # POST, PUT, DELETE /api/assets[/:id] + evidence-write.ts # POST /api/assets/:assetId/evidence + db/ + queries/assets.ts # insertAsset, updateAsset, softDeleteAsset + queries/evidence.ts # insertEvidence + queries/timeline-events.ts # insertTimelineEvent + schemas/ + asset-input.ts # zod CreateAssetSchema, UpdateAssetSchema + evidence-input.ts # zod CreateEvidenceSchema +``` + +## Validation (zod) + +```ts +// schemas/asset-input.ts +import { z } from 'zod'; +import { ENTITY_TYPES, CHITTY_ID_PATTERN } from '../env'; // ['P','L','T','E','A'] + +export const CreateAssetSchema = z.object({ + name: z.string().min(1).max(200), + assetType: z.enum(['vehicle','property','jewelry','electronics','art','document','other']), + purchasePrice: z.string().regex(/^\d+(\.\d{1,2})?$/).optional(), + purchaseDate: z.coerce.date().optional(), + description: z.string().max(2000).optional(), + location: z.object({ // L — Location + jurisdiction: z.string().optional(), + address: z.string().optional(), + }).optional(), + metadata: z.record(z.string(), z.unknown()).optional(), +}); + +export const UpdateAssetSchema = CreateAssetSchema.partial().strict(); +``` + +## Side effects + +`POST /api/assets` flow (mirrors `server/routes.ts:259`): + +1. `services.id.generate('asset')` → ChittyID matching `CHITTY_ID_PATTERN` +2. `services.trust.calculate(chittyId, payload)` → trustScore (Authority, A) +3. `db.transaction(tx => { insertAsset(tx, …); insertTimelineEvent(tx, { eventType: 'created' }); })` +4. Return 201 with full asset row + +`POST /api/assets/:assetId/evidence` flow (mirrors `server/routes.ts:346`): + +1. Verify asset ownership (`db.assets.where(id, userId)`); 404 if not found +2. Insert evidence (T) + timeline event (E, `eventType: 'evidence_added'`) in a + single transaction +3. Return 201 + +## Ownership checks + +Every write enforces `WHERE asset.user_id = :authUserId` at the SQL layer in +addition to ChittyAuth-derived `Person` principal check. This is intentional +defense-in-depth — a JWT compromise alone should not be enough to pivot to +another tenant's data. + +## Validation gates + +1. Type-check clean. +2. Real-Neon integration test (ephemeral branch): + - Create asset → assert ChittyID matches `CHITTY_ID_PATTERN` + - Update asset → assert `updated_at` advanced + timeline event row exists + - Delete asset → assert soft-delete flag set, list endpoint excludes it + - Cross-tenant probe: create asset as user A, attempt update as user B → 404 +3. `curl` post-deploy with a real ChittyAuth token (operator-scoped) against + `assets.chitty.cc`. + +## Estimated PR size + +~700 LOC (routes + zod + queries + tx wrapper + tests). Largest write PR until +3c; ChittyID and ChittyTrust dependencies make this the first phase where the +Worker takes a hard dep on Tier 0 services. diff --git a/docs/migrations/phase3b-domain-writes.md b/docs/migrations/phase3b-domain-writes.md new file mode 100644 index 0000000..91fc061 --- /dev/null +++ b/docs/migrations/phase3b-domain-writes.md @@ -0,0 +1,87 @@ +--- +uri: chittycanon://docs/tech/spec/chittyassets-phase3b +namespace: chittycanon://docs/tech +type: spec +version: 1.0.0 +status: DRAFT +title: ChittyAssets Phase 3b — Domain Writes +visibility: INTERNAL +tags: [migration, hono, phase-3b, writes] +@canon: chittycanon://core/services/chittyassets +--- + +# Phase 3b — Domain Writes (warranties, insurance, legal-cases) + +## Scope + +| Method | Path | Source | Insert schema | +|--------|------|--------|---------------| +| POST | `/api/assets/:assetId/warranties` | `server/routes.ts:476` | `insertWarrantySchema` | +| POST | `/api/assets/:assetId/insurance` | `server/routes.ts:508` | `insertInsurancePolicySchema` | +| POST | `/api/legal-cases` | `server/routes.ts:540` | `insertLegalCaseSchema` | + +These are straight INSERTs after zod validation and ownership check; no +external service calls, no transactions beyond a single row write. + +## Canonical entity model + +- Warranty: **Authority (A) — Granted** (manufacturer authority over coverage) +- Insurance policy: **Authority (A) — Granted** (insurer authority over claim) +- Legal case: **Event (E)** anchored to **Location (L) — jurisdiction** + +All P/L/T/E/A appear: P (filer), L (jurisdiction), T (asset under claim), +E (case filing event), A (granted coverage). Code referencing entity-type +classification must declare all five — never omit Authority. + +## File layout + +``` +worker/src/routes/ + warranties-write.ts + insurance-write.ts + legal-cases-write.ts +worker/src/db/queries/ + (extend warranties.ts, insurance.ts, legal-cases.ts from Phase 2b) +``` + +Re-use the zod schemas already exported from `shared/schema.ts` +(`insertWarrantySchema`, `insertInsurancePolicySchema`, +`insertLegalCaseSchema`) — do NOT redefine them in the Worker. Phase 2a +established that pattern. + +## Validation + +```ts +// example: warranties-write.ts +const body = await c.req.json(); +const parsed = insertWarrantySchema.parse({ + ...body, + assetId: c.req.param('assetId'), + userId: c.get('userId'), +}); + +// ownership: insertWarrantySchema does not check assetId↔userId — do it explicitly +const asset = await db.query.assets.findFirst({ + where: and(eq(assets.id, parsed.assetId), eq(assets.userId, parsed.userId)), +}); +if (!asset) return c.json({ error: 'asset_not_found' }, 404); + +const [row] = await db.insert(warranties).values(parsed).returning(); +return c.json(row, 201); +``` + +## External deps + +None. + +## Validation gates + +1. Type-check clean. +2. Real-Neon integration tests covering happy path + cross-tenant probe per + route (3 routes × 2 tests = 6 tests minimum). +3. zod failure path: post malformed body → 400 with `error.errors` echoed + (matches Express behavior at `server/routes.ts:489`). + +## Estimated PR size + +~350 LOC. Smallest write phase — good candidate to land back-to-back with 3a. diff --git a/docs/migrations/phase3c-heavy-writes.md b/docs/migrations/phase3c-heavy-writes.md new file mode 100644 index 0000000..4fb8dee --- /dev/null +++ b/docs/migrations/phase3c-heavy-writes.md @@ -0,0 +1,106 @@ +--- +uri: chittycanon://docs/tech/spec/chittyassets-phase3c +namespace: chittycanon://docs/tech +type: spec +version: 1.0.0 +status: DRAFT +title: ChittyAssets Phase 3c — Heavy Writes (chain, AI, ledger) +visibility: INTERNAL +tags: [migration, hono, phase-3c, writes, ai, chittychain] +@canon: chittycanon://core/services/chittyassets +--- + +# Phase 3c — Heavy Writes + +## Scope + +| Method | Path | Source | External dep | +|--------|------|--------|--------------| +| POST | `/api/assets/:id/freeze` | `server/routes.ts:125` | ChittyMint (`mint.chitty.cc`) | +| POST | `/api/assets/:id/mint` | `server/routes.ts:166` | ChittyMint | +| POST | `/api/evidence/:evidenceId/analyze` | `server/routes.ts:379` | OpenAI GPT-4o | +| POST | `/api/legal/generate-document` | `server/routes.ts:557` | OpenAI GPT-4o | +| POST | `/api/assets/:assetId/calculate-trust-score` | `server/routes.ts:596` | ChittyTrust + OpenAI | +| POST | `/api/evidence-ledger/submit` | `server/routes.ts:51` | ChittyLedger | +| POST | `/api/evidence-ledger/:chittyId/verify` | `server/routes.ts:92` | ChittyLedger | +| POST | `/api/seed-demo` | `server/routes.ts:39` | none (Neon writes) | + +## Canonical entity model + +- **Freeze** event (E) initiates 7-day immutability window — produces Authority + (A, Earned) attestation when mint completes +- **Mint** event (E) anchors asset Thing (T) to ChittyChain — `transactionHash` + is the Authority (A) artifact +- **AI analysis** produces evidence trust scoring — Authority (A, Earned) +- All entity-type listings in this code path must enumerate P/L/T/E/A in full + +## File layout + +``` +worker/src/ + clients/ + chittymint.ts # freeze, mint + chittyledger.ts # extend Phase 2c client with submit, verify + openai.ts # GPT-4o wrapper (analyze, legal-doc, trust-score) + routes/ + chain.ts # freeze, mint + ai.ts # analyze, legal-doc, trust-score + evidence-ledger-write.ts + seed-demo.ts + jobs/ + queue-bindings.ts # if AI calls move to Cloudflare Queues +``` + +## External service bindings + +Wrangler config (Phase 1 already established `wrangler.jsonc` — modifications +land in PR #33 or a follow-up; do NOT touch here): + +```jsonc +{ + "vars": { + "CHITTY_MINT_URL": "https://mint.chitty.cc", + "CHITTY_LEDGER_URL":"https://ledger.chitty.cc", + "OPENAI_MODEL": "gpt-4o-2024-08-06" + }, + "secrets": ["OPENAI_API_KEY", "CHITTY_MINT_TOKEN"] +} +``` + +`OPENAI_API_KEY` and `CHITTY_MINT_TOKEN` are delivered via `wrangler secret +put` in CI from 1Password (`op run` pattern from operator manifest); never +inlined. + +## Long-running concerns + +Cloudflare Worker request CPU limit is ~30s on paid plans. GPT-4o calls for +legal-doc generation can exceed this. Recommended approach: + +1. **Short path (analyze, trust-score)**: synchronous fetch with 25s timeout, + return 504 on timeout. +2. **Long path (legal-doc, batch analyze)**: enqueue to Cloudflare Queues, return + 202 with a job ID; client polls `GET /api/jobs/:id`. Queue worker writes + result into `ai_analysis_results` table (schema already exists per top-level + `CLAUDE.md`). + +Phase 3c lands the sync path first; queue migration tracked as a follow-up. + +## Validation gates + +1. Type-check clean. +2. Real-staging integration tests against `mint-staging.chitty.cc` and + `ledger-staging.chitty.cc`. OpenAI tests use a recorded-replay fixture + (`vcr`-style) against the real API in CI nightly, mocked-recording in PR CI + (this is the one place we permit cassette-style replay — real upstream calls + are exercised nightly). +3. Side-effect verification: after `POST /freeze`, assert + `assets.chittyChainStatus = 'frozen'` AND a `timeline_events` row with + `eventType = 'other'` and the freeze title exists. +4. Idempotency: double-POST `/freeze` on the same asset returns 409 (Express + currently does not — see follow-up TODO). + +## Estimated PR size + +~1100 LOC + cassette fixtures. Largest phase. Strongly recommended to split +into 3c.1 (chain: freeze/mint), 3c.2 (AI: analyze/legal-doc/trust-score), +3c.3 (ledger writes + seed-demo) if review velocity matters. diff --git a/docs/migrations/phase4-r2-routes.md b/docs/migrations/phase4-r2-routes.md new file mode 100644 index 0000000..1dc8958 --- /dev/null +++ b/docs/migrations/phase4-r2-routes.md @@ -0,0 +1,134 @@ +--- +uri: chittycanon://docs/tech/spec/chittyassets-phase4 +namespace: chittycanon://docs/tech +type: spec +version: 1.0.0 +status: DRAFT +title: ChittyAssets Phase 4 — R2 Object Routes +visibility: INTERNAL +tags: [migration, hono, phase-4, r2, storage] +@canon: chittycanon://core/services/chittyassets +--- + +# Phase 4 — R2 Object Routes + +## Scope + +| Method | Path | Source | Backend (Express) | Backend (Worker) | +|--------|------|--------|-------------------|------------------| +| GET | `/objects/:objectPath(*)` | `server/routes.ts:618` | GCS via `ObjectStorageService` | R2 bucket via Worker binding | +| POST | `/api/objects/upload` | `server/routes.ts:641` | GCS signed URL | R2 presigned PUT | +| PUT | `/api/evidence-files` | `server/routes.ts:647` | GCS ACL | `r2_object_acl` table + R2 key write | + +## Storage migration + +Express implementation uses Google Cloud Storage (`server/objectStorage.ts`, +ACL stored in object metadata). Phase 4 cuts over to Cloudflare R2: + +- **Bucket binding**: `EVIDENCE_BUCKET` (wrangler `[[r2_buckets]]`) +- **ACL store**: `r2_object_acl` table — landed by PR #34 (Drizzle migration + `0003`, schema canonical remediation). Columns: `object_path`, `owner_user_id`, + `visibility`, `acl_rules` (jsonb), `created_at`, `updated_at`. +- **Historical GCS objects**: keep GCS read-through for 90 days; new uploads + go to R2. A separate one-shot job migrates GCS → R2 in batches (not part of + this PR; tracked as `chore/migrate-gcs-to-r2`). + +## File layout + +``` +worker/src/ + routes/ + objects.ts # GET /objects/:path(*), POST /api/objects/upload, PUT /api/evidence-files + storage/ + r2-acl.ts # canAccessObject(objectPath, userId, permission) + r2-upload.ts # createPresignedPut, finalizeUpload (writes acl row) +``` + +## ACL model + +```ts +// storage/r2-acl.ts +import { eq } from 'drizzle-orm'; +import { r2ObjectAcl } from '../../../shared/schema'; // landed by PR #34 + +export type Permission = 'READ' | 'WRITE' | 'DELETE'; + +export async function canAccessObject( + db: DrizzleDB, + objectPath: string, + userId: string | undefined, + permission: Permission, +): Promise { + const acl = await db.query.r2ObjectAcl.findFirst({ where: eq(r2ObjectAcl.objectPath, objectPath) }); + if (!acl) return false; + if (acl.ownerUserId === userId) return true; + if (acl.visibility === 'public' && permission === 'READ') return true; + // acl.aclRules is jsonb: { allowedUsers: string[], allowedRoles: string[] } + const rules = acl.aclRules ?? {}; + return userId !== undefined && Array.isArray(rules.allowedUsers) && rules.allowedUsers.includes(userId); +} +``` + +## GET `/objects/:path(*)` + +```ts +app.get('/objects/:path{.+}', requireChittyAuth(), async (c) => { + const objectPath = c.req.param('path'); + const userId = c.get('userId'); + const allowed = await canAccessObject(c.var.db, objectPath, userId, 'READ'); + if (!allowed) return c.body(null, 401); + const obj = await c.env.EVIDENCE_BUCKET.get(objectPath); + if (!obj) return c.body(null, 404); + return new Response(obj.body, { + headers: { 'content-type': obj.httpMetadata?.contentType ?? 'application/octet-stream' }, + }); +}); +``` + +## POST `/api/objects/upload` + +Returns a presigned PUT URL. Cloudflare R2 supports S3-compatible presigned +URLs; alternatively the Worker proxies the upload itself (simpler — no +external presigning needed): + +- Generate object key `evidence/${userId}/${uuid()}` +- Insert pending `r2_object_acl` row with `owner_user_id = userId`, + `visibility = 'private'` +- Return `{ uploadURL: "/api/objects/upload-stream/${objectKey}", objectPath: objectKey }` + +The client streams PUT to that URL; the Worker pipes to `EVIDENCE_BUCKET.put(...)`. +This avoids R2's need for AWS Sig-V4 client-side signing. + +## PUT `/api/evidence-files` + +Finalize: update `r2_object_acl` row from `pending` → `active`, attach to an +evidence row via FK. Mirrors `server/routes.ts:647`. + +## Bindings (Wrangler — modifications NOT in this PR) + +```jsonc +"r2_buckets": [ + { "binding": "EVIDENCE_BUCKET", "bucket_name": "chittyassets-evidence-prod", + "preview_bucket_name": "chittyassets-evidence-staging" } +] +``` + +These additions land in the Phase 4 implementation PR, not in this doc PR. + +## Validation gates + +1. Type-check clean. +2. Real-R2 integration (preview bucket): + - Upload via Worker → assert R2 object exists, ACL row inserted with + `visibility = 'private'` + - GET as owner → 200 with bytes + - GET as different authed user → 401 + - GET unauthenticated → 401 +3. GCS read-through fallback: assert legacy `gs://...` paths still resolve + during the 90-day cutover. + +## Estimated PR size + +~600 LOC + bucket bindings. Final phase before Express retirement. Post-merge, +delete `server/objectStorage.ts`, the Express boot in `server/index.ts`, and +flip the `assets.chitty.cc` Cloudflare route to the Worker exclusively. diff --git a/docs/migrations/registry-status.md b/docs/migrations/registry-status.md new file mode 100644 index 0000000..dffc4e5 --- /dev/null +++ b/docs/migrations/registry-status.md @@ -0,0 +1,76 @@ +--- +uri: chittycanon://docs/ops/registry/chittyassets-registration-status +namespace: chittycanon://docs/ops +type: registry +version: 1.0.0 +status: DRAFT +title: ChittyAssets Registry Status +visibility: INTERNAL +tags: [registry, chittyregistry, compliance] +@canon: chittycanon://core/services/chittyassets +--- + +# ChittyRegistry Status — chittyassets + +## Current state (2026-05-16) + +Query: `GET https://registry.chitty.cc/api/v1/search?q=chittyassets` + +```json +{ "success": true, "query": { "query": "chittyassets" }, "results": [], "count": 0 } +``` + +**Verdict: NOT REGISTERED.** + +## Recommended registration payload + +`register.json` lands on `main` via PR #33 (`feat/hono-migration-phase-1`, +commit `806c8a5`). Once that PR merges, the registration source of truth lives +at the repo root. Until then, the expected payload shape is: + +```jsonc +{ + "name": "chittyassets", + "canonical_uri": "chittycanon://core/services/chittyassets", + "tier": 4, + "domain": "domain", + "url": "https://assets.chitty.cc", + "health_url": "https://assets.chitty.cc/health", + "status": "MIGRATING", + "repo": "https://github.com/CHITTYOS/chittyassets", + "compliance_triad": ["CHARTER.md", "CHITTY.md", "AGENTS.md"], + "entity_types_handled": ["P", "L", "T", "E", "A"], + "dependencies": ["chittyauth", "chittyid", "chittytrust", "chittyledger", "chittymint"], + "bindings": { + "hyperdrive": "CHITTYASSETS_DB", + "r2_bucket": "EVIDENCE_BUCKET (Phase 4)" + } +} +``` + +Note all five P/L/T/E/A entity types — Authority must not be omitted. + +## Registration command (DO NOT RUN WITHOUT OPERATOR AUTHORIZATION) + +Registry mutations are sensitive-intent operations per +`/home/ubuntu/.ch1tty/canon/system-wide-sensitive-intent-contract-v1.md`. +Route via `ch1tty -> ChittyConnect`: + +```bash +# Suggested — requires operator approval; do not execute from agent context +ch1tty registry register \ + --canonical-uri chittycanon://core/services/chittyassets \ + --tier 4 \ + --from ./register.json +``` + +If the broker path is unavailable, fail closed with +`POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE` — do not paste payloads inline. + +## Recommended sequencing + +1. Merge PR #33 (lands `register.json`) +2. Merge PR #34 (schema canonical remediation) +3. Operator runs `ch1tty registry register` once Phase 2a is in production +4. Subsequent phases update the registry record's `status` field + (`MIGRATING` → `ACTIVE` after Phase 4 cuts Express over) diff --git a/docs/migrations/test-strategy.md b/docs/migrations/test-strategy.md new file mode 100644 index 0000000..33e5a3e --- /dev/null +++ b/docs/migrations/test-strategy.md @@ -0,0 +1,121 @@ +--- +uri: chittycanon://docs/tech/spec/chittyassets-test-strategy +namespace: chittycanon://docs/tech +type: spec +version: 1.0.0 +status: DRAFT +title: ChittyAssets Migration Test Strategy +visibility: INTERNAL +tags: [testing, neon, integration, no-mocks] +@canon: chittycanon://core/services/chittyassets +--- + +# Test Strategy — Migration Phases 2a → 4 + +## Binding rule: no mocks for DB or upstream services + +Per the operator's no-mocks policy (global `CLAUDE.md` — "No Mocks, Fake Data, +or Placeholder Endpoints"): every test in the migration must exercise real +behavior against a real datastore or a real deployed service. Specifically: + +- **DB tests**: hit a real Neon branch via Hyperdrive +- **Upstream service tests**: hit staging deployments + (`*-staging.chitty.cc`) — never `vi.mock('../clients/chittyledger')` +- **OpenAI tests**: recorded-replay cassettes refreshed against the real API + on a nightly schedule (the single permitted exception, gated by cost) + +## Neon branching strategy + +Three modes, picked per phase: + +### A. Ephemeral branch per test suite (recommended for write phases) + +- CI creates a fresh Neon branch from `main-prod-snapshot` at suite start via + the Neon MCP `create_branch` tool +- Connection string injected as `CHITTYASSETS_DB_TEST_URL` +- Suite tears branch down on completion (`delete_branch`) +- **Pros**: full isolation, parallelism-safe, real schema, real constraints +- **Cons**: ~5s branch creation overhead per suite, branch-quota pressure + +Use for: Phase 3a, 3b, 3c, 4 (write paths). Each PR's CI gets a clean slate. + +### B. Shared preview branch (recommended for read phases) + +- Single long-lived `preview-chittyassets-tests` branch seeded with a known + fixture set +- All read tests query against this branch, never mutate +- **Pros**: zero per-suite overhead, faster CI +- **Cons**: requires discipline — any test that mutates breaks the next test + +Use for: Phase 2a, 2b, 2c (pure reads). + +### C. Local Postgres (dev loop only, never CI) + +- `docker compose up -d postgres` for fast iteration +- Schema applied via `npm run db:push` +- **Never** the validation gate for a PR — CI must use A or B + +## Fixture seeding + +Single source of truth: `worker/test/fixtures/seed.ts` (lands with Phase 2a). +Seeds canonical entities covering all P/L/T/E/A types: + +```ts +// canonical fixture seeds — realistic values, no Lorem ipsum +export const SEED_USER_ID = 'did:chitty:user:00-1-USR-0001-P-A1-2-X'; // P +export const SEED_LOCATION_ID = '00-1-LOC-0001-L-A1-2-X'; // L +export const SEED_ASSET_ID = '00-1-AST-0001-T-A1-2-X'; // T +export const SEED_EVENT_ID = '00-1-EVT-0001-E-A1-2-X'; // E +export const SEED_AUTHORITY_ID = '00-1-AUT-0001-A-A1-2-X'; // A (Authority — never omit) +``` + +ChittyID format `VV-G-LLL-SSSS-T-YM-C-X` per `chittycanon://gov/governance`. +Type segment `T` is one of `P` (Person), `L` (Location), `T` (Thing), +`E` (Event), `A` (Authority). All five must appear in any entity-type list. + +## Test harness skeleton + +```ts +// worker/test/setup.ts +import { beforeAll, afterAll } from 'vitest'; +import { app } from '../src/index'; +import { seedCanonical } from './fixtures/seed'; + +let testDbUrl: string; + +beforeAll(async () => { + testDbUrl = process.env.CHITTYASSETS_DB_TEST_URL!; + if (!testDbUrl) throw new Error('CHITTYASSETS_DB_TEST_URL required — real Neon branch only, no mocks'); + await seedCanonical(testDbUrl); +}); + +// In tests: +const res = await app.request('/api/assets/abc/warranties', { + headers: { authorization: `Bearer ${TEST_JWT}` }, +}, { CHITTYASSETS_DB: testDbUrl, CHITTY_AUTH_JWKS_URL: 'https://auth-staging.chitty.cc/jwks' }); +expect(res.status).toBe(200); +const body = await res.json(); +expect(body).toEqual(expect.arrayContaining([expect.objectContaining({ assetId: 'abc' })])); +``` + +`TEST_JWT` is a real ChittyAuth-issued token for a synthetic test Person, +minted at CI startup against `auth-staging.chitty.cc`. + +## Pre-merge validation checklist + +Every migration PR must demonstrate in the PR body: + +1. `npm run check` clean (link to CI run) +2. `wrangler deploy --dry-run --env production` succeeded (paste size in KiB) +3. Integration suite run against real Neon (link to CI run) +4. For Phase 2c+: at least one `curl` against the deployed staging Worker + showing the new route returns expected shape +5. For Phase 3a+: ownership probe (cross-tenant) test passing + +## What we deliberately do NOT test + +- The Express implementation is not under test during migration. It continues + to receive production traffic until the Cloudflare route flips post-Phase 4. + Migration tests target only the Worker. +- Vendor-side failures (OpenAI throttling, ChittyLedger maintenance) — these + are validated by the runtime circuit-breaker behavior, not unit tests.