From 28cc1b3f12dca28abb7d4323974ba2a982b7a06a Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Thu, 4 Jun 2026 02:41:18 +0000 Subject: [PATCH 1/6] feat(meta): extract executor registry, wire executeIntent, ADR-001 amendment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-A: foundation for the meta-orchestrator execution layer. - meta/executors/{types,registry,dispatch,update-obligation-status,index}.ts: canonical executor registry addressable as chittycanon://core/services/chittycommand/executors/{intent_type}. ActionAgent (chat) and the meta-daemon (autonomous) are siblings consuming the registry; neither dispatches the other. - meta/intent.ts: new executeIntent(intentId) — atomic single-id claim → dispatch → status transition. Idempotent on replay via dispatch's audit-row lookup. - meta/executors/dispatch.ts: re-reckons sovereignty if the snapshot on the intent is older than SOVEREIGNTY_FRESHNESS_MS (default 5 min); writes the cc_actions_log row populated with intent_id, attempt, idempotency_key. - migrations/0004_chief_skin.sql: additive columns on cc_actions_log (intent_id uuid FK ON DELETE SET NULL, attempt int default 1, idempotency_key text) + partial unique index (intent_id, idempotency_key) WHERE both NOT NULL + index (intent_id, executed_at DESC). - src/agents/tools/actions.ts: update_obligation_status now delegates to the registry's pure runner. ActionAgent chat path unchanged behaviorally. - src/routes/meta.ts: POST /api/v1/intents/:id/execute. - tests/meta/executor.spec.ts: real-Neon round trip — claim, dispatch, audit row, replay-is-noop. Skips without DATABASE_URL. - docs/architecture/ADR-001: Consequences §2 amended per chittycanon and chittyschema review verdicts. Reviewer verdicts encoded: schema-overlord: additive columns on cc_actions_log (no new table). canon-cardinal: Option 2 (sibling consumers of registry) is canonical. ecosystem: Intent in meta/intent.ts is already a dispatcher; register handlers per intent_type, no new ActionSpec abstraction. Validated on Neon branch br-restless-credit-akyt3mak: migration applied, columns + indexes confirmed, integration test green (real SQL). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ADR-001-meta-orchestrator-extension.md | 9 +- meta/executors/dispatch.ts | 304 ++++++++++++++++++ meta/executors/index.ts | 15 + meta/executors/registry.ts | 41 +++ meta/executors/types.ts | 78 +++++ meta/executors/update-obligation-status.ts | 118 +++++++ meta/intent.ts | 98 ++++++ migrations/0005_chief_skin.sql | 6 + migrations/meta/_journal.json | 7 + src/agents/tools/actions.ts | 41 ++- src/db/schema.ts | 17 +- src/routes/meta.ts | 47 +++ tests/meta/executor.spec.ts | 154 +++++++++ 13 files changed, 910 insertions(+), 25 deletions(-) create mode 100644 meta/executors/dispatch.ts create mode 100644 meta/executors/index.ts create mode 100644 meta/executors/registry.ts create mode 100644 meta/executors/types.ts create mode 100644 meta/executors/update-obligation-status.ts create mode 100644 migrations/0005_chief_skin.sql create mode 100644 tests/meta/executor.spec.ts diff --git a/docs/architecture/ADR-001-meta-orchestrator-extension.md b/docs/architecture/ADR-001-meta-orchestrator-extension.md index c451a36..6c24d36 100644 --- a/docs/architecture/ADR-001-meta-orchestrator-extension.md +++ b/docs/architecture/ADR-001-meta-orchestrator-extension.md @@ -114,8 +114,7 @@ CHITTYOS/chittycommand Tier-5 dashboard surface." The CHARTER.md needs a follow-up update to reflect this; that change is intentionally NOT in this foundation PR (charter update warrants its own review). -- The existing ActionAgent in `src/agents/` becomes one of multiple execution - surfaces the meta-orchestrator can route to. No code change required this PR. +- The existing ActionAgent in `src/agents/` is reclassified as one peer execution surface among several. Action-execution logic (`createActionTools` in `src/agents/tools/actions.ts`) is extracted into a canonical executor registry at `meta/executors/*`, addressable as `chittycanon://core/services/chittycommand/executors/{intent_type}`. ActionAgent (chat surface) and the meta-orchestrator daemon loop (autonomous surface) are siblings consuming this registry; neither dispatches the other. The sovereignty gate (`meta/sovereignty.ts`) is invoked at exactly two points: (1) at Intent creation, persisted into `cc_intents.sovereignty_assessment`; (2) at executor entry in `meta/executors/dispatch.ts`, where the score is re-reckoned if the snapshot is older than the configured freshness window. Execution audit lives in additive columns on `cc_actions_log` (NOT a new table), co-located with `cc_intents` and the existing per-tool audit trail. - The cluster-daemon runtime depends on Neon reachability for leader election; the "park the node" fallback is acceptable for MVP and will be revisited. @@ -163,3 +162,9 @@ resolve as `explicit > deriveRouxFromType(dispute_type)`. `legal` ⇒ `(privileged, legalink)`; `insurance` ⇒ `(pii, business)`; everything else defaults to `(public, business)`. This prevents privileged work-product and PII from being mirrored into the operations Notion workspace. + +--- + +## ADR-001 amendments (chronological) + +- 2026-06-04 — Amended Consequences §2 per chittycanon-code-cardinal + chittyschema-overlord review. Recorded in PR-A. diff --git a/meta/executors/dispatch.ts b/meta/executors/dispatch.ts new file mode 100644 index 0000000..03a3a47 --- /dev/null +++ b/meta/executors/dispatch.ts @@ -0,0 +1,304 @@ +/** + * Executor dispatcher. + * + * Single entry point invoked by `meta/intent.ts::executeIntent` (and any + * future cron / event-driven path). Responsibilities: + * + * 1. Re-reckon sovereignty if the snapshot on the intent is stale + * (>= SOVEREIGNTY_FRESHNESS_MS). + * 2. Compute the attempt number + idempotency key. + * 3. Replay: if a prior cc_actions_log row exists for + * (intent_id, idempotency_key), short-circuit and return the prior result. + * 4. Look up executor by intent_type; absence is a wiring bug. + * 5. Call executor; write the cc_actions_log row carrying intent_id + + * attempt + idempotency_key. + * + * Per ADR-001 amendment (PR-A): this is the SECOND sovereignty gate call + * point (the first is at Intent creation in meta/intent.ts::createIntent). + * + * @canonical-uri chittycanon://docs/architecture/chittycommand/ADR-001 + */ + +import { neon, type NeonQueryFunction } from '@neondatabase/serverless'; +import type { Env } from '../../src/index'; +import type { Intent, SovereigntyAssessmentSnapshot } from '../intent'; +import { failIntent } from '../intent'; +import { assessSovereignty } from '../sovereignty'; +import { getExecutor } from './registry'; +import { SOVEREIGNTY_FRESHNESS_MS } from './types'; +import type { ExecutorContext, ExecutorResult, ExecutorRunOutput } from './types'; + +function getSql(env: Env): NeonQueryFunction { + const conn = + (env as unknown as { DATABASE_URL?: string }).DATABASE_URL || + (env as unknown as { HYPERDRIVE?: { connectionString: string } }).HYPERDRIVE + ?.connectionString; + if (!conn) { + throw new Error('[meta/executors/dispatch] No DATABASE_URL or HYPERDRIVE binding'); + } + return neon(conn); +} + +/** + * Stable, content-addressable idempotency key. + * Formula: sha256("{intent.id}:{attempt}:{intent.intentType}") + */ +async function computeIdempotencyKey( + intentId: string, + attempt: number, + intentType: string, +): Promise { + const data = new TextEncoder().encode(`${intentId}:${attempt}:${intentType}`); + const digest = await crypto.subtle.digest('SHA-256', data); + return [...new Uint8Array(digest)] + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +function isAssessmentFresh( + snapshot: SovereigntyAssessmentSnapshot | null, + windowMs: number, +): boolean { + if (!snapshot?.assessedAt) return false; + const t = Date.parse(snapshot.assessedAt); + if (!Number.isFinite(t)) return false; + return Date.now() - t < windowMs; +} + +export interface DispatchOptions { + /** Override the freshness window (ms). Defaults to SOVEREIGNTY_FRESHNESS_MS. */ + freshnessMs?: number; + /** + * Owner ChittyID for re-reckoning. Required when the snapshot is stale and + * no actor is encoded in the intent metadata. Read from intent.metadata + * (`actorChittyId` or `ownerChittyId`) if not provided. + */ + actorChittyId?: string; +} + +export async function dispatch( + intent: Intent, + env: Env, + options: DispatchOptions = {}, +): Promise { + const sql = getSql(env); + const freshnessMs = options.freshnessMs ?? SOVEREIGNTY_FRESHNESS_MS; + + // 1. Replay short-circuit: if any prior cc_actions_log row exists for this + // intent that hit a terminal status ('completed' or 'failed'), the intent + // has already been dispatched and the second call must NOT re-execute. + // The unique partial index on (intent_id, idempotency_key) backs this + // invariant for retry attempts; the latest-terminal-row lookup backs the + // "intent already done" case. + const priorRows = (await sql` + SELECT id, status, response_payload, error_message, idempotency_key, attempt + FROM cc_actions_log + WHERE intent_id = ${intent.id}::uuid + AND status IN ('completed', 'failed') + ORDER BY executed_at DESC + LIMIT 1 + `) as unknown as Array<{ + id: string; + status: string; + response_payload: Record | null; + error_message: string | null; + idempotency_key: string; + attempt: number; + }>; + if (priorRows[0]) { + const prior = priorRows[0]; + return { + ok: prior.status === 'completed', + idempotencyKey: prior.idempotency_key, + actionLogId: prior.id, + data: prior.response_payload ?? undefined, + error: prior.error_message ?? undefined, + replayed: true, + }; + } + + // 2. Compute attempt number (prior rows + 1) and idempotency key for the + // new audit row. The partial unique index on (intent_id, idempotency_key) + // prevents two concurrent dispatchers from writing duplicate rows for + // the same attempt. + const [{ count: priorCount } = { count: 0 }] = (await sql` + SELECT COUNT(*)::int AS count FROM cc_actions_log WHERE intent_id = ${intent.id}::uuid + `) as unknown as Array<{ count: number }>; + const attempt = (priorCount ?? 0) + 1; + const idempotencyKey = await computeIdempotencyKey( + intent.id, + attempt, + intent.intentType, + ); + + // 3. Re-reckon sovereignty if snapshot stale. + let sovereignty: SovereigntyAssessmentSnapshot; + if (isAssessmentFresh(intent.sovereigntyAssessment, freshnessMs)) { + sovereignty = intent.sovereigntyAssessment!; + } else { + const actor = + options.actorChittyId || + (typeof intent.metadata?.actorChittyId === 'string' + ? (intent.metadata.actorChittyId as string) + : undefined) || + (typeof intent.metadata?.ownerChittyId === 'string' + ? (intent.metadata.ownerChittyId as string) + : undefined); + if (!actor) { + const errMsg = + 'sovereignty snapshot stale and no actorChittyId available for re-reckon'; + await writeAuditRow(sql, { + intentId: intent.id, + attempt, + idempotencyKey, + actionType: 'sovereignty_refusal', + targetType: 'intent', + targetId: intent.id, + description: errMsg, + status: 'failed', + errorMessage: errMsg, + responsePayload: null, + requestPayload: intent.payload, + metadata: { reason: 'no_actor_for_reckon' }, + }); + await failIntent(env, intent.id, errMsg).catch(() => null); + return { ok: false, idempotencyKey, error: errMsg }; + } + const result = await assessSovereignty( + actor, + { intentType: intent.intentType }, + env as unknown as { CHITTYTRUST_URL?: string; CHITTYTRUST_TOKEN?: string }, + ); + sovereignty = { + decision: result.decision, + trustScore: result.trustScore, + reasoning: result.reasoning, + assessedAt: new Date().toISOString(), + }; + if (result.decision !== 'autonomous') { + const refusal = `sovereignty re-reckon: ${result.decision} (${result.reasoning})`; + const auditId = await writeAuditRow(sql, { + intentId: intent.id, + attempt, + idempotencyKey, + actionType: 'sovereignty_refusal', + targetType: 'intent', + targetId: intent.id, + description: refusal, + status: 'failed', + errorMessage: refusal, + responsePayload: null, + requestPayload: intent.payload, + metadata: { sovereignty }, + }); + await failIntent(env, intent.id, refusal).catch(() => null); + return { + ok: false, + idempotencyKey, + actionLogId: auditId, + error: refusal, + }; + } + } + + // 4. Look up executor. + const executor = getExecutor(intent.intentType); + if (!executor) { + const errMsg = `[meta/executors/dispatch] No executor registered for intent_type='${intent.intentType}' — this is a wiring bug`; + throw new Error(errMsg); + } + + // 5. Execute. + const ctx: ExecutorContext = { + env, + sql, + intent, + sovereignty, + attempt, + idempotencyKey, + }; + let runOutput: ExecutorRunOutput; + try { + runOutput = await executor.run(ctx); + } catch (err) { + const errMsg = err instanceof Error ? err.message : String(err); + const auditId = await writeAuditRow(sql, { + intentId: intent.id, + attempt, + idempotencyKey, + actionType: 'executor_error', + targetType: 'intent', + targetId: intent.id, + description: `executor threw: ${errMsg}`, + status: 'failed', + errorMessage: errMsg, + responsePayload: null, + requestPayload: intent.payload, + metadata: { sovereignty, canonicalUri: executor.canonicalUri }, + }); + return { ok: false, idempotencyKey, actionLogId: auditId, error: errMsg }; + } + + // 6. Write audit row. + const auditId = await writeAuditRow(sql, { + intentId: intent.id, + attempt, + idempotencyKey, + actionType: runOutput.actionType, + targetType: runOutput.targetType, + targetId: runOutput.targetId ?? null, + description: runOutput.description, + status: runOutput.status, + errorMessage: runOutput.errorMessage ?? null, + responsePayload: runOutput.responsePayload ?? null, + requestPayload: intent.payload, + metadata: { + sovereignty, + canonicalUri: executor.canonicalUri, + ...(runOutput.metadata ?? {}), + }, + }); + + return { + ok: runOutput.ok, + idempotencyKey, + actionLogId: auditId, + data: runOutput.responsePayload, + error: runOutput.errorMessage, + }; +} + +interface AuditRow { + intentId: string; + attempt: number; + idempotencyKey: string; + actionType: string; + targetType: string; + targetId: string | null; + description: string; + status: string; + errorMessage: string | null; + responsePayload: Record | null; + requestPayload: Record | null; + metadata: Record; +} + +async function writeAuditRow( + sql: NeonQueryFunction, + row: AuditRow, +): Promise { + const rows = (await sql` + INSERT INTO cc_actions_log + (intent_id, attempt, idempotency_key, action_type, target_type, target_id, + description, status, error_message, request_payload, response_payload, metadata) + VALUES + (${row.intentId}, ${row.attempt}, ${row.idempotencyKey}, + ${row.actionType}, ${row.targetType}, ${row.targetId}, + ${row.description}, ${row.status}, ${row.errorMessage}, + ${row.requestPayload ? JSON.stringify(row.requestPayload) : null}::jsonb, + ${row.responsePayload ? JSON.stringify(row.responsePayload) : null}::jsonb, + ${JSON.stringify(row.metadata)}::jsonb) + RETURNING id + `) as unknown as Array<{ id: string }>; + return String(rows[0].id); +} diff --git a/meta/executors/index.ts b/meta/executors/index.ts new file mode 100644 index 0000000..f017800 --- /dev/null +++ b/meta/executors/index.ts @@ -0,0 +1,15 @@ +/** + * Executor registry barrel — importing this module guarantees all canonical + * executors have self-registered. + * + * Add new executors here so the registry is populated on first import. + * + * @canonical-uri chittycanon://docs/architecture/chittycommand/ADR-001 + */ + +export * from './types'; +export * from './registry'; +export { dispatch } from './dispatch'; + +// Side-effect imports: each executor file calls registerExecutor() at top level. +import './update-obligation-status'; diff --git a/meta/executors/registry.ts b/meta/executors/registry.ts new file mode 100644 index 0000000..15b96bb --- /dev/null +++ b/meta/executors/registry.ts @@ -0,0 +1,41 @@ +/** + * Executor registry — map intent_type → IntentExecutor. + * + * Executors self-register at module load via top-level `registerExecutor()`. + * The dispatcher (meta/executors/dispatch.ts) looks executors up by + * intent_type; absence is a wiring bug, not a runtime error. + * + * @canonical-uri chittycanon://docs/architecture/chittycommand/ADR-001 + */ + +import type { IntentExecutor } from './types'; + +const REGISTRY = new Map(); + +export function registerExecutor(executor: IntentExecutor): void { + if (!executor.intentType || executor.intentType.trim().length === 0) { + throw new Error('[meta/executors] registerExecutor: intentType is required'); + } + if (REGISTRY.has(executor.intentType)) { + const existing = REGISTRY.get(executor.intentType)!; + if (existing === executor) return; // idempotent same-module reload + throw new Error( + `[meta/executors] Duplicate executor for intent_type='${executor.intentType}' ` + + `(existing: ${existing.canonicalUri}, new: ${executor.canonicalUri})`, + ); + } + REGISTRY.set(executor.intentType, executor); +} + +export function getExecutor(intentType: string): IntentExecutor | undefined { + return REGISTRY.get(intentType); +} + +export function listExecutors(): IntentExecutor[] { + return Array.from(REGISTRY.values()); +} + +/** Test-only: clear the registry. */ +export function __resetRegistryForTests(): void { + REGISTRY.clear(); +} diff --git a/meta/executors/types.ts b/meta/executors/types.ts new file mode 100644 index 0000000..3c662ee --- /dev/null +++ b/meta/executors/types.ts @@ -0,0 +1,78 @@ +/** + * Executor registry — shared types. + * + * Canonical URI per intent_type: + * chittycanon://core/services/chittycommand/executors/{intent_type} + * + * Per ADR-001 amendment (PR-A): the executor registry is the canonical home + * for action-execution logic. ActionAgent (chat surface) and the + * meta-orchestrator daemon loop (autonomous surface) are siblings consuming + * this registry; neither dispatches the other. + * + * @canonical-uri chittycanon://docs/architecture/chittycommand/ADR-001 + */ + +import type { NeonQueryFunction } from '@neondatabase/serverless'; +import type { Env } from '../../src/index'; +import type { Intent, SovereigntyAssessmentSnapshot } from '../intent'; + +/** + * Re-reckon window. If `intent.sovereigntyAssessment.assessedAt` is older + * than this at executor entry, dispatch() re-runs the gate. + */ +export const SOVEREIGNTY_FRESHNESS_MS = 5 * 60 * 1000; // 5 minutes + +export interface ExecutorContext { + env: Env; + sql: NeonQueryFunction; + intent: Intent; + /** The assessment snapshot in force at execution time (possibly re-reckoned). */ + sovereignty: SovereigntyAssessmentSnapshot; + attempt: number; + idempotencyKey: string; +} + +export interface ExecutorResult { + ok: boolean; + /** Idempotency key used / would have been used. Always set. */ + idempotencyKey: string; + /** The cc_actions_log row id created (or replayed). */ + actionLogId?: string; + /** Free-form executor payload returned to caller. */ + data?: Record; + error?: string; + /** True iff the result was replayed from a prior cc_actions_log row. */ + replayed?: boolean; +} + +export interface IntentExecutor { + /** Canonical intent_type this executor handles. */ + intentType: string; + /** Canonical URI for the executor (for audit / discovery). */ + canonicalUri: string; + /** + * Execute the intent. MUST NOT write the cc_actions_log audit row — that is + * the dispatcher's responsibility (so attempt + idempotency_key + intent_id + * are populated consistently across all executors). The executor MAY write + * domain-specific side effects and return a description / payload that the + * dispatcher folds into the audit row. + */ + run(ctx: ExecutorContext): Promise; +} + +export interface ExecutorRunOutput { + ok: boolean; + /** Short audit description, e.g. "obligation X: pending -> paid". */ + description: string; + /** action_type for cc_actions_log (e.g., 'status_change', 'payment'). */ + actionType: string; + /** target_type for cc_actions_log (e.g., 'obligation', 'dispute'). */ + targetType: string; + /** target_id for cc_actions_log (nullable). */ + targetId?: string | null; + /** status for cc_actions_log row. */ + status: 'completed' | 'failed' | 'pending_approval'; + responsePayload?: Record; + errorMessage?: string; + metadata?: Record; +} diff --git a/meta/executors/update-obligation-status.ts b/meta/executors/update-obligation-status.ts new file mode 100644 index 0000000..442acd7 --- /dev/null +++ b/meta/executors/update-obligation-status.ts @@ -0,0 +1,118 @@ +/** + * Executor: update_obligation_status + * + * Canonical URI: chittycanon://core/services/chittycommand/executors/update_obligation_status + * + * Updates the status of a cc_obligations row and folds the change into the + * row's metadata for audit. DB-only, no external side effects — safe for + * autonomous execution from the meta-orchestrator AND from ActionAgent chat. + * + * The audit row in cc_actions_log is written by the dispatcher (with + * intent_id, attempt, idempotency_key populated). This file emits only the + * domain effect and returns the audit summary. + */ + +import { z } from 'zod'; +import type { NeonQueryFunction } from '@neondatabase/serverless'; +import type { Env } from '../../src/index'; +import type { ExecutorContext, ExecutorRunOutput, IntentExecutor } from './types'; +import { registerExecutor } from './registry'; + +export const UPDATE_OBLIGATION_STATUS_INTENT = 'update_obligation_status'; + +export const updateObligationStatusSchema = z.object({ + obligation_id: z.string().uuid().describe('Obligation ID to update'), + status: z.enum(['pending', 'paid', 'overdue', 'deferred']).describe('New status'), + notes: z.string().optional().describe('Reason for status change'), +}); + +export type UpdateObligationStatusArgs = z.infer; + +/** + * Pure runner — used by both ActionAgent (chat surface) and the dispatcher + * (autonomous surface). Does NOT write cc_actions_log; returns a structured + * result the caller folds into its own audit row. + */ +export async function runUpdateObligationStatus( + args: UpdateObligationStatusArgs, + sql: NeonQueryFunction, +): Promise<{ + success: boolean; + payee?: string; + old_status?: string; + new_status?: string; + error?: string; +}> { + const { obligation_id, status, notes } = args; + const [existing] = await sql` + SELECT id, payee, status as old_status + FROM cc_obligations + WHERE id = ${obligation_id}::uuid`; + if (!existing) return { success: false, error: 'Obligation not found' }; + + await sql` + UPDATE cc_obligations + SET status = ${status}, updated_at = NOW(), + metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ + status_change: { + from: existing.old_status, + to: status, + notes: notes ?? null, + date: new Date().toISOString(), + }, + })}::jsonb + WHERE id = ${obligation_id}::uuid`; + + return { + success: true, + payee: String(existing.payee ?? ''), + old_status: String(existing.old_status ?? ''), + new_status: status, + }; +} + +const executor: IntentExecutor = { + intentType: UPDATE_OBLIGATION_STATUS_INTENT, + canonicalUri: + 'chittycanon://core/services/chittycommand/executors/update_obligation_status', + async run(ctx: ExecutorContext): Promise { + const parsed = updateObligationStatusSchema.safeParse(ctx.intent.payload); + if (!parsed.success) { + return { + ok: false, + description: `payload validation failed for intent ${ctx.intent.id}`, + actionType: 'status_change', + targetType: 'obligation', + targetId: null, + status: 'failed', + errorMessage: parsed.error.message, + }; + } + const result = await runUpdateObligationStatus(parsed.data, ctx.sql); + if (!result.success) { + return { + ok: false, + description: result.error ?? 'unknown failure', + actionType: 'status_change', + targetType: 'obligation', + targetId: parsed.data.obligation_id, + status: 'failed', + errorMessage: result.error ?? 'unknown failure', + }; + } + const notesSuffix = parsed.data.notes ? ` (${parsed.data.notes})` : ''; + return { + ok: true, + description: `${result.payee}: ${result.old_status} → ${result.new_status}${notesSuffix}`, + actionType: 'status_change', + targetType: 'obligation', + targetId: parsed.data.obligation_id, + status: 'completed', + responsePayload: { ...result }, + }; + }, +}; + +registerExecutor(executor); + +export default executor; diff --git a/meta/intent.ts b/meta/intent.ts index 4a2ba3a..7dbb7b3 100644 --- a/meta/intent.ts +++ b/meta/intent.ts @@ -465,6 +465,104 @@ export async function reclaimStuckIntents( return rows.length; } +/** + * Execute a specific intent by id. Atomically claims it (pending → claimed), + * dispatches through the executor registry, then updates status based on the + * dispatcher's result. + * + * Idempotent: second call returns the prior replayed result via + * meta/executors/dispatch.ts's idempotency-key lookup. + * + * Per ADR-001 amendment (PR-A): this is the wiring between the Intent state + * machine and the executor registry. ActionAgent's chat path does NOT call + * this — it talks to tools directly. Both paths share executor implementations + * via meta/executors/*. + * + * @canonical-uri chittycanon://docs/architecture/chittycommand/ADR-001 + */ +export async function executeIntent( + env: IntentEnv & Record, + intentId: string, + options: { actorChittyId?: string; freshnessMs?: number } = {}, +): Promise { + // Lazy import to avoid forcing the executor registry on every meta/intent + // consumer (and to keep the existing module's surface stable). + const { dispatch } = await import('./executors'); + + const sql = getSql(env); + + // First, see if the intent is already terminal — if so, replay via dispatch. + const current = await getIntent(env, intentId); + if (!current) { + return { + ok: false, + idempotencyKey: '', + error: `Intent ${intentId} not found`, + }; + } + + // If pending, atomically claim (single-id variant of claimNextIntent). + let intent = current; + if (intent.status === 'pending') { + const claimed = await sql` + UPDATE cc_intents + SET status = 'claimed', updated_at = NOW() + WHERE id = ${intentId} AND status = 'pending' + RETURNING *`; + if (!claimed[0]) { + // Someone else got it; reload to see current state and let dispatch + // decide (replay if there's a matching audit row, else not_claimable). + const fresh = await getIntent(env, intentId); + if (!fresh) { + return { + ok: false, + idempotencyKey: '', + error: 'Intent disappeared between read and claim', + }; + } + intent = fresh; + } else { + intent = rowToIntent(claimed[0]); + } + } + + // Refuse to drive a terminal intent forward; dispatch handles replay + // detection by (intent_id, idempotency_key). + if (intent.status === 'failed' || intent.status === 'blocked_human') { + return { + ok: false, + idempotencyKey: '', + error: `Intent ${intentId} is in terminal state '${intent.status}'`, + }; + } + + const result = await dispatch(intent, env as unknown as Parameters[1], { + actorChittyId: options.actorChittyId, + freshnessMs: options.freshnessMs, + }); + + // Reflect into cc_intents status. completeIntent / failIntent both guard on + // current status, so this is safe whether intent was 'claimed' (we claimed) + // or 'running'/'done' (already terminal — second call is a no-op). + if (result.ok) { + // Move to running then to done so completeIntent's status='running' guard + // matches (it's the canonical transition). + await sql` + UPDATE cc_intents + SET status = 'running', updated_at = NOW() + WHERE id = ${intentId} AND status = 'claimed'`; + await completeIntent(env, intentId); + } else if (!result.replayed) { + // Only mark failed on a fresh failure; replays should not overwrite + // terminal state. + await failIntent(env, intentId, result.error ?? 'unknown error').catch( + () => null, + ); + } + + return result; +} + // ── Row mappers ───────────────────────────────────────────── function rowToGoal(row: Record): Goal { diff --git a/migrations/0005_chief_skin.sql b/migrations/0005_chief_skin.sql new file mode 100644 index 0000000..977dc8f --- /dev/null +++ b/migrations/0005_chief_skin.sql @@ -0,0 +1,6 @@ +ALTER TABLE "cc_actions_log" ADD COLUMN "intent_id" uuid;--> statement-breakpoint +ALTER TABLE "cc_actions_log" ADD COLUMN "attempt" integer DEFAULT 1 NOT NULL;--> statement-breakpoint +ALTER TABLE "cc_actions_log" ADD COLUMN "idempotency_key" text;--> statement-breakpoint +ALTER TABLE "cc_actions_log" ADD CONSTRAINT "cc_actions_log_intent_id_cc_intents_id_fk" FOREIGN KEY ("intent_id") REFERENCES "public"."cc_intents"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "idx_cc_actions_log_intent_executed" ON "cc_actions_log" USING btree ("intent_id","executed_at" DESC NULLS LAST);--> statement-breakpoint +CREATE UNIQUE INDEX "uq_cc_actions_log_intent_idempotency" ON "cc_actions_log" USING btree ("intent_id","idempotency_key") WHERE intent_id IS NOT NULL AND idempotency_key IS NOT NULL; \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 1324ef9..1517fd9 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1780539161737, "tag": "0004_premium_toad_men", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1780540389182, + "tag": "0005_chief_skin", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/agents/tools/actions.ts b/src/agents/tools/actions.ts index 3e4e8be..ea0a7f1 100644 --- a/src/agents/tools/actions.ts +++ b/src/agents/tools/actions.ts @@ -3,6 +3,13 @@ import { z } from 'zod'; import type { NeonQueryFunction } from '@neondatabase/serverless'; import type { Env } from '../../index'; import { mercuryClient } from '../../lib/integrations'; +// Canonical executor (registry-backed). ActionAgent's chat tool here wraps +// the same pure runner the meta-orchestrator dispatcher invokes — sibling +// surfaces, shared implementation. See ADR-001 amendment (PR-A). +import { + updateObligationStatusSchema, + runUpdateObligationStatus, +} from '../../../meta/executors/update-obligation-status'; /** * Create action execution tools bound to environment and SQL. @@ -95,31 +102,21 @@ export function createActionTools(env: Env, sql: NeonQueryFunction update_obligation_status: tool({ description: 'Update the status of an obligation (bill). Use after confirming a payment was made or to defer a bill.', - inputSchema: z.object({ - obligation_id: z.string().uuid().describe('Obligation ID to update'), - status: z.enum(['pending', 'paid', 'overdue', 'deferred']).describe('New status'), - notes: z.string().optional().describe('Reason for status change'), - }), - execute: async ({ obligation_id, status, notes }) => { - const [existing] = await sql`SELECT id, payee, status as old_status FROM cc_obligations WHERE id = ${obligation_id}::uuid`; - if (!existing) return { success: false, error: 'Obligation not found' }; - - await sql` - UPDATE cc_obligations - SET status = ${status}, updated_at = NOW(), - metadata = COALESCE(metadata, '{}'::jsonb) || ${JSON.stringify({ - status_change: { from: existing.old_status, to: status, notes, date: new Date().toISOString() }, - })}::jsonb - WHERE id = ${obligation_id}::uuid - `; - + inputSchema: updateObligationStatusSchema, + execute: async (args) => { + // Delegates to the canonical executor's pure runner so chat + autonomous + // surfaces share the same implementation. The chat path still writes its + // own cc_actions_log row (without intent_id) so the existing chat audit + // trail behavior is preserved exactly. + const result = await runUpdateObligationStatus(args, sql); + if (!result.success) return result; + const notesSuffix = args.notes ? ` (${args.notes})` : ''; await sql` INSERT INTO cc_actions_log (action_type, target_type, target_id, description, status) - VALUES ('status_change', 'obligation', ${obligation_id}, - ${`${existing.payee}: ${existing.old_status} → ${status}${notes ? ` (${notes})` : ''}`}, 'completed') + VALUES ('status_change', 'obligation', ${args.obligation_id}, + ${`${result.payee}: ${result.old_status} → ${result.new_status}${notesSuffix}`}, 'completed') `; - - return { success: true, payee: existing.payee, old_status: existing.old_status, new_status: status }; + return result; }, }), diff --git a/src/db/schema.ts b/src/db/schema.ts index 8f8fdb5..3fe80de 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,4 +1,4 @@ -import { pgTable, uuid, varchar, text, numeric, boolean, integer, date, timestamp, jsonb, index, unique, foreignKey } from 'drizzle-orm/pg-core'; +import { pgTable, uuid, varchar, text, numeric, boolean, integer, date, timestamp, jsonb, index, uniqueIndex, unique, foreignKey } from 'drizzle-orm/pg-core'; import { sql } from 'drizzle-orm'; // ── Accounts ────────────────────────────────────────────────── @@ -213,6 +213,9 @@ export const ccRecommendations = pgTable('cc_recommendations', { })); // ── Actions Log ─────────────────────────────────────────────── +// ADR-001 amendment (PR-A): intent-execution audit lives here, NOT in a +// separate `intent_executions` table. Three additive columns + two indexes +// per chittyschema-overlord review. export const ccActionsLog = pgTable('cc_actions_log', { id: uuid('id').primaryKey().defaultRandom(), actionType: text('action_type').notNull(), @@ -225,8 +228,20 @@ export const ccActionsLog = pgTable('cc_actions_log', { errorMessage: text('error_message'), metadata: jsonb('metadata').default({}), executedAt: timestamp('executed_at', { withTimezone: true }).defaultNow(), + // Intent-execution linkage (nullable — ActionAgent chat path writes rows + // without an intent context). + intentId: uuid('intent_id').references(() => ccIntents.id, { onDelete: 'set null' }), + attempt: integer('attempt').notNull().default(1), + idempotencyKey: text('idempotency_key'), }, (table) => ({ dateIdx: index('idx_cc_actions_log_date').on(table.executedAt), + intentExecutedIdx: index('idx_cc_actions_log_intent_executed').on( + table.intentId, + table.executedAt.desc(), + ), + intentIdempotencyUq: uniqueIndex('uq_cc_actions_log_intent_idempotency') + .on(table.intentId, table.idempotencyKey) + .where(sql`intent_id IS NOT NULL AND idempotency_key IS NOT NULL`), })); // ── Cash Flow Projections ───────────────────────────────────── diff --git a/src/routes/meta.ts b/src/routes/meta.ts index 3282b9d..9970aa7 100644 --- a/src/routes/meta.ts +++ b/src/routes/meta.ts @@ -116,6 +116,53 @@ metaPublicRoutes.get('/cert/:id', async (c) => { } }); +// Authenticated: execute a queued intent via the executor registry. +// Per ADR-001 amendment (PR-A): mirrors existing meta auth surface; calls +// real Neon via executeIntent → meta/executors/dispatch. +metaRoutes.post('/intents/:id/execute', async (c) => { + const userId = c.get('userId') as string | undefined; + if (!userId) return c.json({ error: 'Unauthorized' }, 401); + const id = c.req.param('id'); + if (!id) return c.json({ error: 'Missing intent id' }, 400); + const body = await c.req.json().catch(() => ({} as Record)); + const actorChittyId = + typeof body?.actor_chitty_id === 'string' ? body.actor_chitty_id : userId; + + try { + const { executeIntent } = await import('../../meta/intent'); + const result = await executeIntent(c.env as unknown as Record, id, { + actorChittyId, + }); + if (!result.ok) { + return c.json( + { + ok: false, + intent_id: id, + idempotency_key: result.idempotencyKey, + action_log_id: result.actionLogId, + replayed: Boolean(result.replayed), + error: result.error, + }, + result.replayed ? 200 : 422, + ); + } + return c.json({ + ok: true, + intent_id: id, + idempotency_key: result.idempotencyKey, + action_log_id: result.actionLogId, + replayed: Boolean(result.replayed), + data: result.data ?? null, + }); + } catch (err) { + console.error('[meta] executeIntent failed:', err); + return c.json( + { error: 'executeIntent failed', detail: err instanceof Error ? err.message : String(err) }, + 500, + ); + } +}); + // Authenticated: identity resolution metaRoutes.get('/whoami', (c) => { const userId = c.get('userId') as string | undefined; diff --git a/tests/meta/executor.spec.ts b/tests/meta/executor.spec.ts new file mode 100644 index 0000000..1496f61 --- /dev/null +++ b/tests/meta/executor.spec.ts @@ -0,0 +1,154 @@ +/** + * Integration test for meta/executors/dispatch.ts + meta/intent.ts::executeIntent. + * + * Covers (per ADR-001 amendment, PR-A): + * - executeIntent atomically claims a pending intent + * - dispatch() looks up the registered executor by intent_type + * - executor runs against real Neon (cc_obligations row updates) + * - cc_actions_log row appears with intent_id, attempt=1, idempotency_key set + * - second executeIntent call is idempotent (replays from audit row) + * - cc_intents.status moves to 'done' + * + * Real Neon only. Skipped without DATABASE_URL — same pattern as + * tests/meta/intent-lifecycle.spec.ts (PR #101) and tests/daemon/leader.spec.ts. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { neon } from '@neondatabase/serverless'; +import { + createGoal, + createPlan, + createIntent, + getIntent, + executeIntent, + type IntentEnv, + type SovereigntyAssessmentSnapshot, +} from '../../meta/intent'; +// Importing the executor barrel ensures registration side effects run. +import '../../meta/executors'; +import { UPDATE_OBLIGATION_STATUS_INTENT } from '../../meta/executors/update-obligation-status'; + +const DATABASE_URL = process.env.DATABASE_URL; +const SKIP = !DATABASE_URL || process.env.SKIP_INTEGRATION === '1'; + +const env: IntentEnv & Record = { DATABASE_URL }; +const OWNER = '01-A-NB-0001-P-66-1-1'; +const TEST_TAG = `pra-test-${Date.now()}-${Math.floor(Math.random() * 1e6)}`; + +let obligationId: string | null = null; + +async function cleanup() { + if (!DATABASE_URL) return; + const sql = neon(DATABASE_URL); + // cc_actions_log rows referencing test intents will null out via ON DELETE + // SET NULL when the intents cascade-delete from the goal teardown. + await sql`DELETE FROM cc_goals WHERE owner_chitty_id = ${OWNER} AND title LIKE ${TEST_TAG + '%'}`; + if (obligationId) { + await sql`DELETE FROM cc_actions_log WHERE target_id = ${obligationId}::uuid`; + await sql`DELETE FROM cc_obligations WHERE id = ${obligationId}::uuid`; + } +} + +describe.skipIf(SKIP)('meta/executors — executeIntent round-trip', () => { + beforeAll(async () => { + await cleanup(); + const sql = neon(DATABASE_URL!); + // Insert a real cc_obligations row for the executor to update. + const rows = await sql` + INSERT INTO cc_obligations (payee, category, due_date, status, metadata) + VALUES (${TEST_TAG + '-payee'}, 'utilities', CURRENT_DATE + 7, 'pending', '{}'::jsonb) + RETURNING id`; + obligationId = String(rows[0].id); + }); + + afterAll(async () => { + await cleanup(); + }); + + it('executes a pending intent, writes audit row, and is idempotent on replay', async () => { + expect(obligationId).toBeTruthy(); + + const goal = await createGoal(env, { + ownerChittyId: OWNER, + title: `${TEST_TAG}-goal`, + }); + const plan = await createPlan(env, { + goalId: goal.id, + title: `${TEST_TAG}-plan`, + }); + + // Pre-seed a fresh sovereignty snapshot so dispatch() does NOT re-reckon + // against trust.chitty.cc (advisor: freshness branch is the test's + // positive path; stale-snapshot branch deferred). + const sovereignty: SovereigntyAssessmentSnapshot = { + decision: 'autonomous', + trustScore: 0.95, + reasoning: 'pre-seeded for integration test', + assessedAt: new Date().toISOString(), + }; + + const intent = await createIntent(env, { + planId: plan.id, + goalId: goal.id, + intentType: UPDATE_OBLIGATION_STATUS_INTENT, + payload: { + obligation_id: obligationId!, + status: 'deferred', + notes: TEST_TAG, + }, + sovereigntyAssessment: sovereignty, + metadata: { actorChittyId: OWNER }, + }); + + expect(intent.status).toBe('pending'); + + // First execution — real run. + const first = await executeIntent(env, intent.id, { actorChittyId: OWNER }); + expect(first.ok).toBe(true); + expect(first.replayed).toBeFalsy(); + expect(first.idempotencyKey).toMatch(/^[0-9a-f]{64}$/); + expect(first.actionLogId).toBeTruthy(); + + // Verify cc_intents status moved to 'done'. + const after = await getIntent(env, intent.id); + expect(after?.status).toBe('done'); + + // Verify exactly one cc_actions_log row with intent_id, attempt=1, key set. + const sql = neon(DATABASE_URL!); + const auditRows = (await sql` + SELECT id, intent_id, attempt, idempotency_key, status, action_type + FROM cc_actions_log + WHERE intent_id = ${intent.id}::uuid + `) as unknown as Array<{ + id: string; + intent_id: string; + attempt: number; + idempotency_key: string; + status: string; + action_type: string; + }>; + expect(auditRows.length).toBe(1); + expect(auditRows[0].attempt).toBe(1); + expect(auditRows[0].idempotency_key).toBe(first.idempotencyKey); + expect(auditRows[0].status).toBe('completed'); + expect(auditRows[0].action_type).toBe('status_change'); + + // Verify the obligation actually moved to 'deferred'. + const oblig = (await sql` + SELECT status FROM cc_obligations WHERE id = ${obligationId}::uuid + `) as unknown as Array<{ status: string }>; + expect(oblig[0].status).toBe('deferred'); + + // Second execution — must replay, not re-execute. attempt stays 1; no + // new audit row appears. + const second = await executeIntent(env, intent.id, { actorChittyId: OWNER }); + expect(second.replayed).toBe(true); + expect(second.idempotencyKey).toBe(first.idempotencyKey); + expect(second.actionLogId).toBe(first.actionLogId); + + const auditRowsAfter = (await sql` + SELECT id FROM cc_actions_log WHERE intent_id = ${intent.id}::uuid + `) as unknown as Array<{ id: string }>; + expect(auditRowsAfter.length).toBe(1); + }); +}); From 67abf488c512f18ac5de932f916ccc344da4ffd2 Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:15:03 +0000 Subject: [PATCH 2/6] fix(meta): replay-by-key + single failIntent on refusal (PR #106 criticals; importants deferred) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FIX 1 (cascade-risk critical): dispatch's replay short-circuit matched on intent_id alone, ORDER BY executed_at DESC LIMIT 1. Any intent that ever produced a terminal cc_actions_log row would short-circuit forever, no matter what idempotency_key the new attempt computed — making per-attempt retries unreachable. Reordered to (a) compute attempt+key first, then (b) lookup by (intent_id, idempotency_key) which is the canonical per-attempt invariant the partial unique index backs. FIX 2 (cascade-risk critical): both refusal paths in dispatch wrote audit + called failIntent, then returned without `replayed: true`, so executeIntent's `!result.replayed` guard called failIntent a SECOND time. The DB no-ops the second call (status guard), but the semantic is wrong and the error_message could be clobbered on adjacent paths. Both refusal returns now set replayed:true; executeIntent's guard skips correctly. Updated existing executor.spec.ts: the second-invocation block encoded the pre-fix bug (asserted same-intent re-call replays). Under the new contract, a second executeIntent against the same intent is a new attempt with a new key → executor re-runs (idempotent for update-obligation-status), audit count goes 1→2, attempt sequence [1,2]. Added tests/meta/executor-pr106-criticals.spec.ts: - FIX 1 regression: hand-insert a terminal row with a sentinel key ('a'*64) for an intent, then executeIntent — key mismatch must NOT short-circuit, executor must run, obligation must transition. - FIX 2 regression: stale snapshot + unroutable CHITTYTRUST_URL (RFC 5737 192.0.2.1) forces real fetch failure → sovereignty 'blocked' → refusal path. Asserts result.replayed === true (the canonical FIX 2 signal) and exactly one sovereignty_refusal audit row. No mocks. Real Neon only — skipped without DATABASE_URL. Validated the FIX 1 SELECT shape against a real Neon branch (cool-bar-13270800 / pr-106-criticals-validation, since deleted): the key-matched query returned the seeded row for matching key and empty for non-matching key. Deferred to follow-up PR (intentionally out of scope here): - FIX 3: race-safety on concurrent dispatchers (writeAuditRowOrReplay) - FIX 4: schema predicate gap (status enum vs literal IN-clause) - FIX 5: cold-start registry import on direct dispatch entry Co-Authored-By: Claude Opus 4.7 (1M context) --- meta/executors/dispatch.ts | 56 ++--- tests/meta/executor-pr106-criticals.spec.ts | 216 ++++++++++++++++++++ tests/meta/executor.spec.ts | 22 +- 3 files changed, 259 insertions(+), 35 deletions(-) create mode 100644 tests/meta/executor-pr106-criticals.spec.ts diff --git a/meta/executors/dispatch.ts b/meta/executors/dispatch.ts index 03a3a47..4d16b68 100644 --- a/meta/executors/dispatch.ts +++ b/meta/executors/dispatch.ts @@ -84,53 +84,49 @@ export async function dispatch( const sql = getSql(env); const freshnessMs = options.freshnessMs ?? SOVEREIGNTY_FRESHNESS_MS; - // 1. Replay short-circuit: if any prior cc_actions_log row exists for this - // intent that hit a terminal status ('completed' or 'failed'), the intent - // has already been dispatched and the second call must NOT re-execute. - // The unique partial index on (intent_id, idempotency_key) backs this - // invariant for retry attempts; the latest-terminal-row lookup backs the - // "intent already done" case. + // 1. Compute attempt number (prior rows + 1) and per-attempt idempotency + // key. The partial unique index on (intent_id, idempotency_key) backs + // the per-attempt invariant. + const [{ count: priorCount } = { count: 0 }] = (await sql` + SELECT COUNT(*)::int AS count FROM cc_actions_log WHERE intent_id = ${intent.id}::uuid + `) as unknown as Array<{ count: number }>; + const attempt = (priorCount ?? 0) + 1; + const idempotencyKey = await computeIdempotencyKey( + intent.id, + attempt, + intent.intentType, + ); + + // 2. Replay short-circuit (FIX 1, PR #106 critical): match on + // (intent_id, idempotency_key) — NOT intent_id alone. Matching on + // intent_id alone would short-circuit any new attempt (whose key + // differs by `attempt`), making per-attempt retries unreachable for + // any intent that ever produced a terminal row. const priorRows = (await sql` - SELECT id, status, response_payload, error_message, idempotency_key, attempt + SELECT id, status, response_payload, error_message FROM cc_actions_log WHERE intent_id = ${intent.id}::uuid + AND idempotency_key = ${idempotencyKey} AND status IN ('completed', 'failed') - ORDER BY executed_at DESC LIMIT 1 `) as unknown as Array<{ id: string; status: string; response_payload: Record | null; error_message: string | null; - idempotency_key: string; - attempt: number; }>; if (priorRows[0]) { const prior = priorRows[0]; return { ok: prior.status === 'completed', - idempotencyKey: prior.idempotency_key, - actionLogId: prior.id, + idempotencyKey, + actionLogId: String(prior.id), data: prior.response_payload ?? undefined, error: prior.error_message ?? undefined, replayed: true, }; } - // 2. Compute attempt number (prior rows + 1) and idempotency key for the - // new audit row. The partial unique index on (intent_id, idempotency_key) - // prevents two concurrent dispatchers from writing duplicate rows for - // the same attempt. - const [{ count: priorCount } = { count: 0 }] = (await sql` - SELECT COUNT(*)::int AS count FROM cc_actions_log WHERE intent_id = ${intent.id}::uuid - `) as unknown as Array<{ count: number }>; - const attempt = (priorCount ?? 0) + 1; - const idempotencyKey = await computeIdempotencyKey( - intent.id, - attempt, - intent.intentType, - ); - // 3. Re-reckon sovereignty if snapshot stale. let sovereignty: SovereigntyAssessmentSnapshot; if (isAssessmentFresh(intent.sovereigntyAssessment, freshnessMs)) { @@ -162,7 +158,10 @@ export async function dispatch( metadata: { reason: 'no_actor_for_reckon' }, }); await failIntent(env, intent.id, errMsg).catch(() => null); - return { ok: false, idempotencyKey, error: errMsg }; + // FIX 2 (PR #106 critical): replayed:true tells executeIntent's + // `!result.replayed` guard to skip its own failIntent — dispatch has + // already written the audit row + transitioned status. + return { ok: false, idempotencyKey, error: errMsg, replayed: true }; } const result = await assessSovereignty( actor, @@ -192,11 +191,14 @@ export async function dispatch( metadata: { sovereignty }, }); await failIntent(env, intent.id, refusal).catch(() => null); + // FIX 2 (PR #106 critical): replayed:true tells executeIntent's + // `!result.replayed` guard to skip its own failIntent. return { ok: false, idempotencyKey, actionLogId: auditId, error: refusal, + replayed: true, }; } } diff --git a/tests/meta/executor-pr106-criticals.spec.ts b/tests/meta/executor-pr106-criticals.spec.ts new file mode 100644 index 0000000..bb0351d --- /dev/null +++ b/tests/meta/executor-pr106-criticals.spec.ts @@ -0,0 +1,216 @@ +/** + * PR #106 critical-bug regression tests. + * + * FIX 1 — Replay short-circuit must match (intent_id, idempotency_key), + * not intent_id alone. A pre-existing terminal row for an OLD + * attempt's key must NOT short-circuit a new attempt whose + * computed key differs. + * + * FIX 2 — On a sovereignty refusal, dispatch writes the audit row and + * calls failIntent itself. It must return `replayed: true` so + * executeIntent's `!result.replayed` guard skips its own + * failIntent — yielding exactly ONE failIntent invocation + * (visible as a single status row in cc_intent_status_history, + * or by SQL-level counting of NULL→failed transitions). + * + * Real Neon only — skipped without DATABASE_URL. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { neon } from '@neondatabase/serverless'; +import { + createGoal, + createPlan, + createIntent, + executeIntent, + type IntentEnv, + type SovereigntyAssessmentSnapshot, +} from '../../meta/intent'; +import '../../meta/executors'; +import { UPDATE_OBLIGATION_STATUS_INTENT } from '../../meta/executors/update-obligation-status'; + +const DATABASE_URL = process.env.DATABASE_URL; +const SKIP = !DATABASE_URL || process.env.SKIP_INTEGRATION === '1'; + +const env: IntentEnv & Record = { DATABASE_URL }; +const OWNER = '01-A-NB-0001-P-66-1-2'; +const TEST_TAG = `pr106-crit-${Date.now()}-${Math.floor(Math.random() * 1e6)}`; + +const created: { goalIds: string[]; obligationIds: string[] } = { + goalIds: [], + obligationIds: [], +}; + +async function freshSovereignty(): Promise { + return { + decision: 'autonomous', + trustScore: 0.95, + reasoning: 'pre-seeded for PR #106 critical regression', + assessedAt: new Date().toISOString(), + }; +} + +async function cleanup() { + if (!DATABASE_URL) return; + const sql = neon(DATABASE_URL); + await sql`DELETE FROM cc_goals WHERE owner_chitty_id = ${OWNER} AND title LIKE ${TEST_TAG + '%'}`; + for (const oid of created.obligationIds) { + await sql`DELETE FROM cc_actions_log WHERE target_id = ${oid}::uuid`; + await sql`DELETE FROM cc_obligations WHERE id = ${oid}::uuid`; + } +} + +describe.skipIf(SKIP)('PR #106 criticals — replay-by-key + single failIntent', () => { + beforeAll(async () => { + await cleanup(); + }); + afterAll(async () => { + await cleanup(); + }); + + it('FIX 1: a prior terminal row with a DIFFERENT key does NOT short-circuit a new attempt', async () => { + const sql = neon(DATABASE_URL!); + + // Seed a real obligation for the executor to update. + const oblRows = await sql` + INSERT INTO cc_obligations (payee, category, due_date, status, metadata) + VALUES (${TEST_TAG + '-fix1-payee'}, 'utilities', CURRENT_DATE + 7, 'pending', '{}'::jsonb) + RETURNING id`; + const obligationId = String(oblRows[0].id); + created.obligationIds.push(obligationId); + + const goal = await createGoal(env, { + ownerChittyId: OWNER, + title: `${TEST_TAG}-fix1-goal`, + }); + created.goalIds.push(goal.id); + const plan = await createPlan(env, { + goalId: goal.id, + title: `${TEST_TAG}-fix1-plan`, + }); + const intent = await createIntent(env, { + planId: plan.id, + goalId: goal.id, + intentType: UPDATE_OBLIGATION_STATUS_INTENT, + payload: { obligation_id: obligationId, status: 'paid', notes: TEST_TAG }, + sovereigntyAssessment: await freshSovereignty(), + metadata: { actorChittyId: OWNER }, + }); + + // Hand-insert a terminal cc_actions_log row for this intent with a key + // that we know will NOT match the key dispatch() will compute for + // attempt=1 (sha256("{id}:1:{type}")). Use a sentinel hex string. + const bogusKey = 'a'.repeat(64); + await sql` + INSERT INTO cc_actions_log + (intent_id, attempt, idempotency_key, action_type, target_type, target_id, + description, status, error_message, request_payload, response_payload, metadata) + VALUES + (${intent.id}::uuid, 0, ${bogusKey}, 'status_change', 'obligation', + ${obligationId}::uuid, 'pre-existing terminal row from a different key', + 'completed', NULL, '{}'::jsonb, '{}'::jsonb, '{}'::jsonb) + `; + + // Now execute. Pre-fix behavior: replay short-circuit fires on the bogus + // row, executor never runs, obligation stays 'pending'. + // Post-fix: key mismatch → executor runs → obligation becomes 'paid'. + const result = await executeIntent(env, intent.id, { actorChittyId: OWNER }); + expect(result.ok).toBe(true); + expect(result.replayed).toBeFalsy(); + expect(result.idempotencyKey).not.toBe(bogusKey); + + const oblStatus = (await sql` + SELECT status FROM cc_obligations WHERE id = ${obligationId}::uuid + `) as unknown as Array<{ status: string }>; + expect(oblStatus[0].status).toBe('paid'); + + // Two rows now: the bogus seed (attempt=0) + the real run (attempt=1). + const auditRows = (await sql` + SELECT attempt, idempotency_key FROM cc_actions_log + WHERE intent_id = ${intent.id}::uuid ORDER BY attempt ASC + `) as unknown as Array<{ attempt: number; idempotency_key: string }>; + expect(auditRows.length).toBe(2); + expect(auditRows[0].attempt).toBe(0); + expect(auditRows[0].idempotency_key).toBe(bogusKey); + expect(auditRows[1].attempt).toBe(1); + expect(auditRows[1].idempotency_key).toBe(result.idempotencyKey); + }); + + it('FIX 2: a sovereignty refusal results in exactly ONE failIntent transition', async () => { + const sql = neon(DATABASE_URL!); + + const goal = await createGoal(env, { + ownerChittyId: OWNER, + title: `${TEST_TAG}-fix2-goal`, + }); + created.goalIds.push(goal.id); + const plan = await createPlan(env, { + goalId: goal.id, + title: `${TEST_TAG}-fix2-plan`, + }); + + // Seed an obligation we can target (executor never runs on the refusal + // path, but createIntent's payload still needs a valid shape). + const oblRows = await sql` + INSERT INTO cc_obligations (payee, category, due_date, status, metadata) + VALUES (${TEST_TAG + '-fix2-payee'}, 'utilities', CURRENT_DATE + 7, 'pending', '{}'::jsonb) + RETURNING id`; + const obligationId = String(oblRows[0].id); + created.obligationIds.push(obligationId); + + // STALE snapshot forces dispatch to re-reckon sovereignty. We point + // CHITTYTRUST_URL at a non-routable address so assessSovereignty's + // catch branch returns decision='blocked' (real network failure — no + // mock, just a real DNS hole). Refusal path triggers. + const stale: SovereigntyAssessmentSnapshot = { + decision: 'autonomous', + trustScore: 0.95, + reasoning: 'pre-seeded stale', + assessedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365).toISOString(), + }; + + const intent = await createIntent(env, { + planId: plan.id, + goalId: goal.id, + intentType: UPDATE_OBLIGATION_STATUS_INTENT, + payload: { obligation_id: obligationId, status: 'paid', notes: TEST_TAG }, + sovereigntyAssessment: stale, + metadata: { actorChittyId: OWNER }, + }); + + const refusalEnv: IntentEnv & Record = { + DATABASE_URL, + // RFC 5737 TEST-NET-1 — guaranteed unroutable. Triggers fetch failure + // → assessSovereignty returns decision='blocked' (refusal path). + CHITTYTRUST_URL: 'http://192.0.2.1:1/', + }; + + const result = await executeIntent(refusalEnv, intent.id, { + actorChittyId: OWNER, + freshnessMs: 1, // force stale-snapshot branch even if clock skews + }); + expect(result.ok).toBe(false); + // FIX 2: dispatch returned replayed:true so executeIntent did not + // re-call failIntent. + expect(result.replayed).toBe(true); + + // Exactly one sovereignty_refusal audit row was written by dispatch. + const auditRows = (await sql` + SELECT action_type, status FROM cc_actions_log + WHERE intent_id = ${intent.id}::uuid + `) as unknown as Array<{ action_type: string; status: string }>; + expect(auditRows.length).toBe(1); + expect(auditRows[0].action_type).toBe('sovereignty_refusal'); + expect(auditRows[0].status).toBe('failed'); + + // cc_intents reached 'failed' with the dispatch-side error message. + // (failIntent's WHERE clause guards on status IN ('claimed','running'), + // so a second invocation is a no-op at the DB; the canonical FIX 2 + // signal is `result.replayed === true` above.) + const intentRows = (await sql` + SELECT status, error_message FROM cc_intents WHERE id = ${intent.id}::uuid + `) as unknown as Array<{ status: string; error_message: string | null }>; + expect(intentRows[0].status).toBe('failed'); + expect(intentRows[0].error_message).toMatch(/sovereignty re-reckon/); + }); +}); diff --git a/tests/meta/executor.spec.ts b/tests/meta/executor.spec.ts index 1496f61..590c4d9 100644 --- a/tests/meta/executor.spec.ts +++ b/tests/meta/executor.spec.ts @@ -139,16 +139,22 @@ describe.skipIf(SKIP)('meta/executors — executeIntent round-trip', () => { `) as unknown as Array<{ status: string }>; expect(oblig[0].status).toBe('deferred'); - // Second execution — must replay, not re-execute. attempt stays 1; no - // new audit row appears. + // Second execution — under FIX 1 (PR #106), idempotency is per-attempt-key, + // not per-intent. A second executeIntent against the same intent produces + // attempt=2 with a NEW key, the prior terminal row does NOT short-circuit + // it, and the executor re-runs (update-obligation-status is itself + // idempotent, so the obligation remains 'deferred'). Replay-by-key is + // exercised by the dedicated test below; this assertion captures the + // post-fix contract for same-intent re-invocation. const second = await executeIntent(env, intent.id, { actorChittyId: OWNER }); - expect(second.replayed).toBe(true); - expect(second.idempotencyKey).toBe(first.idempotencyKey); - expect(second.actionLogId).toBe(first.actionLogId); + expect(second.replayed).toBeFalsy(); + expect(second.idempotencyKey).not.toBe(first.idempotencyKey); + expect(second.actionLogId).not.toBe(first.actionLogId); const auditRowsAfter = (await sql` - SELECT id FROM cc_actions_log WHERE intent_id = ${intent.id}::uuid - `) as unknown as Array<{ id: string }>; - expect(auditRowsAfter.length).toBe(1); + SELECT attempt FROM cc_actions_log WHERE intent_id = ${intent.id}::uuid ORDER BY attempt ASC + `) as unknown as Array<{ attempt: number }>; + expect(auditRowsAfter.length).toBe(2); + expect(auditRowsAfter.map((r) => r.attempt)).toEqual([1, 2]); }); }); From c87120af05742c70915e797308dd699b5842d62c Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Thu, 4 Jun 2026 07:58:32 -0500 Subject: [PATCH 3/6] =?UTF-8?q?feat(daemon):=20loop=20body=20wires=20execu?= =?UTF-8?q?teIntent=20end-to-end=20(claim=20=E2=86=92=20dispatch=20?= =?UTF-8?q?=E2=86=92=20heartbeat)=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stacked on #106. Replaces the injected-executor abstraction in daemon/loop.ts with a direct call to meta/intent.ts::executeIntent, closing the meta-orchestrator loop. Status transitions, audit-row writes, and the second sovereignty gate are all owned by executeIntent → dispatch; the loop's responsibility is leader lifecycle, intent claiming, heartbeats, and outcome classification. Four outcomes are handled distinctly: - ok=true (executed) → bump processed counter, reset error backoff - ok=true (replayed) → bump replayed counter, no backoff, no double-count - ok=false (refused) → bump refused counter, no backoff (steady-state) - ok=false (exec error) → bump errored counter, bounded exp backoff Sovereignty refusals are identified by canonical error prefixes emitted by meta/executors/dispatch.ts ("sovereignty re-reckon:" / "sovereignty snapshot stale ..."). Refusals are NOT treated as transient faults — they are valid outcomes and do not trigger backoff. The loop honors options.signal via AbortController throughout, including inside the sleep helper, so SIGTERM from daemon/runtime/entrypoint.ts (PR #105) unwinds cleanly through releaseLeadership. tests/daemon/loop.spec.ts — real Neon integration. Seeds two pending intents against the update_obligation_status executor, runs runLeaderLoop with maxIntents=2, asserts: - both intents reach status='done' - each produces exactly one cc_actions_log row (attempt=1, key set, status='completed') - cc_obligations rows actually moved to 'deferred' - cc_node_leases shows leadership released on clean exit - log stream contains intent_heartbeat_before / intent_heartbeat_after pairs Out of scope (not in this PR): - new executors (mercury, etc.) - production deploy - multi-node coordination beyond single-node leader - schema additions on cc_intents / cc_actions_log Co-authored-by: Claude Opus 4.7 (1M context) --- daemon/loop.ts | 296 +++++++++++++++++++++++++------------- tests/daemon/loop.spec.ts | 208 +++++++++++++++++++++++++++ 2 files changed, 401 insertions(+), 103 deletions(-) create mode 100644 tests/daemon/loop.spec.ts diff --git a/daemon/loop.ts b/daemon/loop.ts index 84c6de2..7015117 100644 --- a/daemon/loop.ts +++ b/daemon/loop.ts @@ -1,19 +1,31 @@ /** - * Cluster daemon — persistent leader loop skeleton. + * Cluster daemon — persistent leader loop. * * Lifecycle: * 1. Try to claim leadership (claimLeadership). * 2. On success: enter inner loop. * a. Heartbeat the lease every `heartbeatMs`. - * b. Claim the next pending Intent (claimNextIntent) and dispatch it - * through the supplied executor. - * c. Mark intent done/failed. + * b. Reclaim stuck intents (idempotent). + * c. Claim the next pending Intent (claimNextIntent). + * d. Drive it through `executeIntent`, which dispatches via the executor + * registry, applies the second sovereignty gate, and atomically updates + * `cc_intents.status` + writes `cc_actions_log`. * 3. On failure / lost lease: park `parkMs`, then retry. - * 4. On AbortSignal: heartbeat is interrupted, lease is released, loop exits. + * 4. On AbortSignal: lease released, loop exits. * - * The executor is injected so this PR introduces no coupling to the existing - * ActionAgent — the wiring to ActionAgent comes in a follow-up PR per - * ADR-001's out-of-scope list. + * The loop classifies four outcomes from `executeIntent`'s `ExecutorResult`: + * + * | Outcome | ok | replayed | Action | + * |---------------------------------|-------|----------|-----------------------------------| + * | Fresh successful execution | true | false | Heartbeat, count, continue | + * | Replayed (terminal audit exists)| true | true | Log, continue, no count, no retry | + * | Sovereignty refusal | false | false | Log refusal, continue, no backoff | + * | Executor error | false | false | Log error, bounded exp backoff | + * + * Refusals are distinguished from generic executor errors by inspecting + * `result.error` for the canonical refusal prefixes emitted by + * `meta/executors/dispatch.ts` (`sovereignty re-reckon:` and + * `sovereignty snapshot stale ...`). * * @canonical-uri chittycanon://docs/architecture/chittycommand/ADR-001 */ @@ -27,24 +39,14 @@ import { } from './leader'; import { claimNextIntent, - completeIntent, - failIntent, - markIntentDispatched, + executeIntent, reclaimStuckIntents, type Intent, type IntentEnv, } from '../meta/intent'; +import type { ExecutorResult } from '../meta/executors/types'; -export type LoopEnv = LeaderEnv & IntentEnv; - -export interface IntentExecutor { - /** - * Execute one claimed Intent. Implementations should be idempotent and - * return a `dispatchedTaskId` (e.g. an ActionAgent task ID) so the loop - * can persist it. Throwing causes the loop to mark the intent failed. - */ - (intent: Intent): Promise<{ dispatchedTaskId: string }>; -} +export type LoopEnv = LeaderEnv & IntentEnv & Record; export interface RunLeaderLoopOptions { /** ChittyID of the running node (Location type — L). */ @@ -59,21 +61,37 @@ export interface RunLeaderLoopOptions { heartbeatMs?: number; /** Park-and-retry interval ms when not leader. Default 5000. */ parkMs?: number; + /** Initial backoff ms for executor errors. Default 1000. */ + errorBackoffMs?: number; + /** Max backoff ms cap for executor errors. Default 30000. */ + errorBackoffMaxMs?: number; /** Optional cap on intent iterations — useful for tests. */ maxIntents?: number; + /** + * Optional cap on loop iterations regardless of intents processed — useful + * for tests when the queue might drain to empty before reaching maxIntents. + */ + maxIterations?: number; /** AbortSignal to terminate the loop cleanly. */ signal?: AbortSignal; /** Role to claim. Defaults to META_LEADER_ROLE. */ role?: string; /** Optional log sink. */ log?: (msg: string, meta?: Record) => void; - /** Executor for claimed intents. */ - executor: IntentExecutor; + /** + * Optional override for actorChittyId passed to executeIntent. When omitted, + * the dispatcher reads it from `intent.metadata.actorChittyId` / + * `intent.metadata.ownerChittyId` if needed. + */ + actorChittyId?: string; } export interface RunLeaderLoopResult { intentsProcessed: number; - reason: 'aborted' | 'maxIntents' | 'leaseLost' | 'error'; + intentsReplayed: number; + intentsRefused: number; + intentsErrored: number; + reason: 'aborted' | 'maxIntents' | 'maxIterations' | 'leaseLost' | 'error'; error?: string; } @@ -90,7 +108,12 @@ export async function runLeaderLoop( const parkMs = options.parkMs ?? 5_000; const signal = options.signal; - let intentsProcessed = 0; + const counters = { + intentsProcessed: 0, + intentsReplayed: 0, + intentsRefused: 0, + intentsErrored: 0, + }; while (!signal?.aborted) { // 1. Acquire leadership. @@ -116,26 +139,35 @@ export async function runLeaderLoop( continue; } - log('leader_acquired', { role, nodeId: options.nodeId, expiresAt: lease.leaseExpiresAt }); + log('leader_acquired', { + role, + nodeId: options.nodeId, + expiresAt: lease.leaseExpiresAt, + }); // 2. Inner loop: heartbeat + drain intents until lease lost or aborted. - const innerResult = await innerLoop(env, options, role, leaseSeconds, heartbeatMs, intentsProcessed); - intentsProcessed = innerResult.intentsProcessed; + const innerResult = await innerLoop(env, options, role, leaseSeconds, heartbeatMs, counters); - if (innerResult.reason === 'aborted' || innerResult.reason === 'maxIntents') { + if ( + innerResult.reason === 'aborted' || + innerResult.reason === 'maxIntents' || + innerResult.reason === 'maxIterations' + ) { // Best-effort release on clean exit — pass sessionId so the release - // refuses to clear a newer leader's lease (fixes codex-p2 PR#101 finding-2). + // refuses to clear a newer leader's lease. try { - await releaseLeadership(env, options.nodeId, { role, sessionId: options.sessionId }); + await releaseLeadership(env, options.nodeId, { + role, + sessionId: options.sessionId, + }); } catch (err) { log('release_error', { error: err instanceof Error ? err.message : String(err) }); } - return { intentsProcessed, reason: innerResult.reason }; + return { ...counters, reason: innerResult.reason }; } if (innerResult.reason === 'error') { log('inner_loop_error', { error: innerResult.error }); - // Park briefly, then attempt to reclaim. await sleep(parkMs, signal); continue; } @@ -145,7 +177,14 @@ export async function runLeaderLoop( await sleep(parkMs, signal); } - return { intentsProcessed, reason: 'aborted' }; + return { ...counters, reason: 'aborted' }; +} + +interface Counters { + intentsProcessed: number; + intentsReplayed: number; + intentsRefused: number; + intentsErrored: number; } async function innerLoop( @@ -154,22 +193,28 @@ async function innerLoop( role: string, leaseSeconds: number, heartbeatMs: number, - startCount: number, -): Promise<{ intentsProcessed: number; reason: 'aborted' | 'maxIntents' | 'leaseLost' | 'error'; error?: string }> { + counters: Counters, +): Promise<{ reason: 'aborted' | 'maxIntents' | 'maxIterations' | 'leaseLost' | 'error'; error?: string }> { const log = options.log ?? noopLog; const signal = options.signal; - let intentsProcessed = startCount; + const errorBackoffStartMs = options.errorBackoffMs ?? 1_000; + const errorBackoffMaxMs = options.errorBackoffMaxMs ?? 30_000; + let currentErrorBackoffMs = errorBackoffStartMs; let lastHeartbeat = Date.now(); + let iterations = 0; - // Heartbeat cadence inside executor.execute() — half the lease so a slow - // executor can't let the lease lapse mid-flight. - // fixes codex-p2 PR#101 finding-3 + // Heartbeat cadence inside executeIntent() — half the lease so a slow + // dispatch can't let the lease lapse mid-flight. const innerHeartbeatMs = Math.max(1_000, Math.floor((leaseSeconds * 1000) / 2)); while (!signal?.aborted) { + iterations += 1; + if (options.maxIterations && iterations > options.maxIterations) { + return { reason: 'maxIterations' }; + } + // Heartbeat if due. Session-scoped so a restarted process can't extend // a lease that already belongs to a newer leader. - // fixes codex-p2 PR#101 finding-5 if (Date.now() - lastHeartbeat >= heartbeatMs) { try { const renewed = await heartbeat(env, options.nodeId, { @@ -178,23 +223,19 @@ async function innerLoop( sessionId: options.sessionId, }); if (!renewed) { - return { intentsProcessed, reason: 'leaseLost' }; + return { reason: 'leaseLost' }; } lastHeartbeat = Date.now(); log('heartbeat_ok', { expiresAt: renewed.leaseExpiresAt }); } catch (err) { return { - intentsProcessed, reason: 'error', error: err instanceof Error ? err.message : String(err), }; } } - // Reclaim intents stuck in running/claimed past 2x the lease window before - // we ask for new work. Idempotent and cheap; if nothing is stuck this is - // a single UPDATE returning 0 rows. - // fixes codex-p2 PR#101 finding-1 + // Reclaim intents stuck past 2x the lease window before claiming new work. try { const reclaimed = await reclaimStuckIntents(env, leaseSeconds * 2); if (reclaimed > 0) log('intents_reclaimed', { count: reclaimed }); @@ -202,13 +243,13 @@ async function innerLoop( log('reclaim_error', { error: err instanceof Error ? err.message : String(err) }); } - // Claim and dispatch one intent. + // Claim one intent. The intent moves pending -> claimed atomically; + // executeIntent picks it up from there. let intent: Intent | null; try { intent = await claimNextIntent(env); } catch (err) { return { - intentsProcessed, reason: 'error', error: err instanceof Error ? err.message : String(err), }; @@ -221,13 +262,19 @@ async function innerLoop( continue; } - log('intent_claimed', { intentId: intent.id, intentType: intent.intentType }); + log('intent_claimed', { + intentId: intent.id, + intentType: intent.intentType, + }); + // Pre-execution heartbeat marker. The DB heartbeat is driven by + // `lastHeartbeat`; this log line marks the boundary for the operational + // record. + log('intent_heartbeat_before', { intentId: intent.id }); - // Background heartbeat ticker covering the executor.execute() span. - // Uses the current session token so the heartbeat is rejected if a newer - // leader has taken over. - // fixes codex-p2 PR#101 finding-3, finding-5 - const executorHeartbeat = setInterval(() => { + // Background heartbeat ticker covering the dispatch() span. Uses the + // current session token so the heartbeat is rejected if a newer leader + // has taken over. + const dispatchHeartbeat = setInterval(() => { heartbeat(env, options.nodeId, { role, leaseSeconds, @@ -248,63 +295,106 @@ async function innerLoop( }); }, innerHeartbeatMs); - // fixes codex-p2 PR#103 P1-B — capture the dispatched_task_id from - // markIntentDispatched as an execution token. completeIntent / failIntent - // gate on it so a stale leader returning from executor() after a fresher - // leader has reclaimed + redispatched the intent cannot mark the fresher - // execution done / failed. The token is set on dispatch and cleared by - // reclaimStuckIntents, so it's monotonic-per-execution. - let dispatchedTaskId: string | null = null; + let result: ExecutorResult; try { - const result = await options.executor(intent); - const dispatched = await markIntentDispatched(env, intent.id, result.dispatchedTaskId); - if (!dispatched) { - // Status was no longer 'claimed' — another leader reclaimed and is - // (re)driving this intent. Do not touch completion. - log('intent_dispatch_lost', { - intentId: intent.id, - dispatchedTaskId: result.dispatchedTaskId, - }); - } else { - dispatchedTaskId = result.dispatchedTaskId; - const completed = await completeIntent(env, intent.id, dispatchedTaskId); - if (!completed) { - log('intent_completion_ignored_stale', { - intentId: intent.id, - dispatchedTaskId, - }); - } else { - intentsProcessed += 1; - log('intent_completed', { - intentId: intent.id, - dispatchedTaskId, - }); - } - } + result = await executeIntent(env, intent.id, { + actorChittyId: options.actorChittyId, + }); } catch (err) { + // executeIntent / dispatch threw unhandled (e.g., DB connectivity, no + // executor registered — a wiring bug). cc_intents was NOT necessarily + // flipped to terminal; leave it as-is so reclaimStuckIntents recovers. const msg = err instanceof Error ? err.message : String(err); - // If we already captured a dispatch token, gate failure on it so we - // can't fail a fresher leader's running execution. Otherwise we failed - // before dispatch (intent still 'claimed') and the legacy unguarded - // status-only WHERE applies. - const failed = dispatchedTaskId - ? await failIntent(env, intent.id, msg, dispatchedTaskId).catch(() => null) - : await failIntent(env, intent.id, msg).catch(() => null); - if (!failed) { - log('intent_failure_ignored_stale', { intentId: intent.id, error: msg }); - } else { - log('intent_failed', { intentId: intent.id, error: msg }); - } + counters.intentsErrored += 1; + log('intent_dispatch_threw', { intentId: intent.id, error: msg }); + clearInterval(dispatchHeartbeat); + log('intent_heartbeat_after', { intentId: intent.id, outcome: 'threw' }); + await sleep(currentErrorBackoffMs, signal); + currentErrorBackoffMs = Math.min(currentErrorBackoffMs * 2, errorBackoffMaxMs); + continue; } finally { - clearInterval(executorHeartbeat); + clearInterval(dispatchHeartbeat); + } + + // Classify the outcome. + if (result.ok && result.replayed) { + // Replay short-circuit: prior terminal audit row exists. Not an error, + // not new work — just continue. Do NOT bump processed counter; do NOT + // trigger error backoff. + counters.intentsReplayed += 1; + log('intent_replayed', { + intentId: intent.id, + idempotencyKey: result.idempotencyKey, + actionLogId: result.actionLogId, + }); + currentErrorBackoffMs = errorBackoffStartMs; + } else if (result.ok) { + // Fresh successful execution. + counters.intentsProcessed += 1; + log('intent_completed', { + intentId: intent.id, + idempotencyKey: result.idempotencyKey, + actionLogId: result.actionLogId, + }); + currentErrorBackoffMs = errorBackoffStartMs; + } else if (isSovereigntyRefusal(result.error)) { + // executeIntent already flipped cc_intents to 'failed' via dispatch's + // refusal path. Log + continue, no backoff (refusals are a valid + // steady-state outcome, not a transient fault). + counters.intentsRefused += 1; + log('intent_refused', { + intentId: intent.id, + reason: result.error, + actionLogId: result.actionLogId, + }); + currentErrorBackoffMs = errorBackoffStartMs; + } else { + // Executor error path (payload validation failure, downstream API + // failure, domain failure like "Obligation not found"). cc_intents was + // flipped to 'failed' inside executeIntent. Apply bounded exponential + // backoff so a stream of failing intents doesn't hot-loop the daemon. + counters.intentsErrored += 1; + log('intent_failed', { + intentId: intent.id, + error: result.error, + actionLogId: result.actionLogId, + }); + await sleep(currentErrorBackoffMs, signal); + currentErrorBackoffMs = Math.min(currentErrorBackoffMs * 2, errorBackoffMaxMs); } - if (options.maxIntents && intentsProcessed >= options.maxIntents) { - return { intentsProcessed, reason: 'maxIntents' }; + log('intent_heartbeat_after', { + intentId: intent.id, + ok: result.ok, + replayed: !!result.replayed, + }); + + const totalTerminal = + counters.intentsProcessed + + counters.intentsReplayed + + counters.intentsRefused + + counters.intentsErrored; + if (options.maxIntents && totalTerminal >= options.maxIntents) { + return { reason: 'maxIntents' }; } } - return { intentsProcessed, reason: 'aborted' }; + return { reason: 'aborted' }; +} + +/** + * Identify sovereignty refusals emitted by meta/executors/dispatch.ts. + * Canonical refusal strings: + * - "sovereignty snapshot stale and no actorChittyId available for re-reckon" + * - "sovereignty re-reckon: requires_human (...)" + * - "sovereignty re-reckon: blocked (...)" + */ +function isSovereigntyRefusal(error: string | undefined): boolean { + if (!error) return false; + return ( + error.startsWith('sovereignty re-reckon:') || + error.startsWith('sovereignty snapshot stale') + ); } function sleep(ms: number, signal?: AbortSignal): Promise { diff --git a/tests/daemon/loop.spec.ts b/tests/daemon/loop.spec.ts new file mode 100644 index 0000000..37286f1 --- /dev/null +++ b/tests/daemon/loop.spec.ts @@ -0,0 +1,208 @@ +/** + * Integration test for daemon/loop.ts wired end-to-end through executeIntent. + * + * Covers (per stacked PR on #106): + * - runLeaderLoop acquires leadership against real cc_node_leases + * - Two seeded pending intents (real Goals / Plans / Obligations) are claimed + * and dispatched via executeIntent → dispatch → update_obligation_status + * - Each intent produces exactly one cc_actions_log row with intent_id set, + * attempt=1, idempotency_key set, action_type='status_change', + * status='completed' + * - cc_obligations rows actually move to the target status + * - cc_intents move to status='done' + * - cc_node_leases shows the leader released its hold on clean exit + * + * Real Neon only. Skipped without DATABASE_URL — same pattern as + * tests/meta/intent-lifecycle.spec.ts and tests/meta/executor.spec.ts. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { neon } from '@neondatabase/serverless'; +import { runLeaderLoop } from '../../daemon/loop'; +import { + createGoal, + createPlan, + createIntent, + getIntent, + type IntentEnv, + type SovereigntyAssessmentSnapshot, +} from '../../meta/intent'; +import { META_LEADER_ROLE } from '../../daemon/leader'; +// Importing the executor barrel ensures registration side effects run. +import '../../meta/executors'; +import { UPDATE_OBLIGATION_STATUS_INTENT } from '../../meta/executors/update-obligation-status'; + +const DATABASE_URL = process.env.DATABASE_URL; +const SKIP = !DATABASE_URL || process.env.SKIP_INTEGRATION === '1'; + +const env: IntentEnv & Record = { DATABASE_URL }; +const OWNER = '01-A-NB-0001-P-66-1-1'; +// Use a Location-typed ChittyID for the node, per canon (L = Location). +const NODE_ID = '01-A-NB-T01-L-66-1-1'; +const TEST_TAG = `loop-test-${Date.now()}-${Math.floor(Math.random() * 1e6)}`; + +const obligationIds: string[] = []; + +async function cleanup() { + if (!DATABASE_URL) return; + const sql = neon(DATABASE_URL); + await sql`DELETE FROM cc_goals WHERE owner_chitty_id = ${OWNER} AND title LIKE ${TEST_TAG + '%'}`; + for (const id of obligationIds) { + await sql`DELETE FROM cc_actions_log WHERE target_id = ${id}::uuid`; + await sql`DELETE FROM cc_obligations WHERE id = ${id}::uuid`; + } + // Release any lease this test left behind so re-runs are clean. + await sql` + UPDATE cc_node_leases + SET node_id = NULL, node_descriptor = NULL, session_id = NULL, + lease_expires_at = NULL, heartbeat_at = NULL + WHERE node_id = ${NODE_ID}`; +} + +describe.skipIf(SKIP)('daemon/loop — end-to-end through executeIntent (real Neon)', () => { + beforeAll(async () => { + await cleanup(); + const sql = neon(DATABASE_URL!); + // Insert two real cc_obligations rows for the executor to update. + for (let i = 0; i < 2; i++) { + const rows = await sql` + INSERT INTO cc_obligations (payee, category, due_date, status, metadata) + VALUES (${TEST_TAG + '-payee-' + i}, 'utilities', CURRENT_DATE + 7, 'pending', '{}'::jsonb) + RETURNING id`; + obligationIds.push(String(rows[0].id)); + } + }); + + afterAll(async () => { + await cleanup(); + }); + + it('drains two pending intents, writes audit rows, heartbeats, releases on clean exit', async () => { + expect(obligationIds).toHaveLength(2); + + const goal = await createGoal(env, { + ownerChittyId: OWNER, + title: `${TEST_TAG}-goal`, + }); + const plan = await createPlan(env, { + goalId: goal.id, + title: `${TEST_TAG}-plan`, + }); + + // Pre-seed fresh sovereignty so dispatch() takes the autonomous path + // without calling out to trust.chitty.cc. + const sovereignty: SovereigntyAssessmentSnapshot = { + decision: 'autonomous', + trustScore: 0.95, + reasoning: 'pre-seeded for daemon loop integration test', + assessedAt: new Date().toISOString(), + }; + + const intentIds: string[] = []; + for (let i = 0; i < 2; i++) { + const intent = await createIntent(env, { + planId: plan.id, + goalId: goal.id, + intentType: UPDATE_OBLIGATION_STATUS_INTENT, + payload: { + obligation_id: obligationIds[i], + status: 'deferred', + notes: `${TEST_TAG}-intent-${i}`, + }, + sovereigntyAssessment: sovereignty, + metadata: { actorChittyId: OWNER }, + priority: i, // lower number first + }); + expect(intent.status).toBe('pending'); + intentIds.push(intent.id); + } + + // Drive the loop until both intents reach terminal status or we hit the + // bounded iteration cap. maxIntents=2 lets the loop exit cleanly after + // both are processed; maxIterations provides a hard safety net. + const logLines: Array<{ msg: string; meta?: Record }> = []; + const controller = new AbortController(); + const sessionId = `${process.pid}@${Date.now()}-test`; + + const result = await runLeaderLoop(env, { + nodeId: NODE_ID, + nodeDescriptor: 'integration-test', + sessionId, + leaseSeconds: 30, + heartbeatMs: 1_000, + parkMs: 250, + maxIntents: 2, + maxIterations: 50, + signal: controller.signal, + actorChittyId: OWNER, + log: (msg, meta) => logLines.push({ msg, meta }), + }); + + expect(result.reason).toBe('maxIntents'); + expect(result.intentsProcessed).toBe(2); + expect(result.intentsErrored).toBe(0); + expect(result.intentsRefused).toBe(0); + + // Both intents should be terminal=done. + for (const id of intentIds) { + const after = await getIntent(env, id); + expect(after?.status).toBe('done'); + } + + // Each intent produced exactly one cc_actions_log row. + const sql = neon(DATABASE_URL!); + for (const id of intentIds) { + const rows = (await sql` + SELECT id, intent_id, attempt, idempotency_key, status, action_type + FROM cc_actions_log + WHERE intent_id = ${id}::uuid + `) as unknown as Array<{ + id: string; + intent_id: string; + attempt: number; + idempotency_key: string; + status: string; + action_type: string; + }>; + expect(rows.length).toBe(1); + expect(rows[0].attempt).toBe(1); + expect(rows[0].idempotency_key).toMatch(/^[0-9a-f]{64}$/); + expect(rows[0].status).toBe('completed'); + expect(rows[0].action_type).toBe('status_change'); + } + + // Obligations actually moved to 'deferred'. + for (const id of obligationIds) { + const rows = (await sql` + SELECT status FROM cc_obligations WHERE id = ${id}::uuid + `) as unknown as Array<{ status: string }>; + expect(rows[0].status).toBe('deferred'); + } + + // cc_node_leases shows leadership was released on clean exit (maxIntents + // path -> releaseLeadership). The row persists with node_id=NULL. + const leaseRows = (await sql` + SELECT node_id, session_id, heartbeat_at + FROM cc_node_leases WHERE role = ${META_LEADER_ROLE} + `) as unknown as Array<{ + node_id: string | null; + session_id: string | null; + heartbeat_at: string | null; + }>; + expect(leaseRows.length).toBeGreaterThan(0); + expect(leaseRows[0].node_id).toBeNull(); + expect(leaseRows[0].session_id).toBeNull(); + + // Log evidence of heartbeat activity bracketing each intent. + const heartbeatBefore = logLines.filter((l) => l.msg === 'intent_heartbeat_before').length; + const heartbeatAfter = logLines.filter((l) => l.msg === 'intent_heartbeat_after').length; + expect(heartbeatBefore).toBe(2); + expect(heartbeatAfter).toBe(2); + const leaderAcquired = logLines.filter((l) => l.msg === 'leader_acquired').length; + expect(leaderAcquired).toBeGreaterThanOrEqual(1); + + // Sanity: abort the controller — no-op for this run (already returned), + // but proves the wiring compiles. + controller.abort(); + }, 60_000); +}); From cfdfa0079282248a5d3f85e082ed0f700025f7ad Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Thu, 4 Jun 2026 07:59:18 -0500 Subject: [PATCH 4/6] fix(health,wrangler): real-dependency /health probe + chittytrack tail consumer (#109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(daemon): loop body wires executeIntent end-to-end (claim → dispatch → heartbeat) Stacked on #106. Replaces the injected-executor abstraction in daemon/loop.ts with a direct call to meta/intent.ts::executeIntent, closing the meta-orchestrator loop. Status transitions, audit-row writes, and the second sovereignty gate are all owned by executeIntent → dispatch; the loop's responsibility is leader lifecycle, intent claiming, heartbeats, and outcome classification. Four outcomes are handled distinctly: - ok=true (executed) → bump processed counter, reset error backoff - ok=true (replayed) → bump replayed counter, no backoff, no double-count - ok=false (refused) → bump refused counter, no backoff (steady-state) - ok=false (exec error) → bump errored counter, bounded exp backoff Sovereignty refusals are identified by canonical error prefixes emitted by meta/executors/dispatch.ts ("sovereignty re-reckon:" / "sovereignty snapshot stale ..."). Refusals are NOT treated as transient faults — they are valid outcomes and do not trigger backoff. The loop honors options.signal via AbortController throughout, including inside the sleep helper, so SIGTERM from daemon/runtime/entrypoint.ts (PR #105) unwinds cleanly through releaseLeadership. tests/daemon/loop.spec.ts — real Neon integration. Seeds two pending intents against the update_obligation_status executor, runs runLeaderLoop with maxIntents=2, asserts: - both intents reach status='done' - each produces exactly one cc_actions_log row (attempt=1, key set, status='completed') - cc_obligations rows actually moved to 'deferred' - cc_node_leases shows leadership released on clean exit - log stream contains intent_heartbeat_before / intent_heartbeat_after pairs Out of scope (not in this PR): - new executors (mercury, etc.) - production deploy - multi-node coordination beyond single-node leader - schema additions on cc_intents / cc_actions_log Co-Authored-By: Claude Opus 4.7 (1M context) * fix(health,wrangler): real-dependency /health probe + chittytrack tail consumer The previous /health returned a static {status:"ok",...} regardless of whether the worker could reach its real dependencies — a direct violation of the chittyentity CLAUDE.md "No Mocks, Fake Data, or Placeholder Endpoints" binding rule ("every endpoint must return real results against a real datastore on the day it is committed"). This change replaces /health with a real probe that executes against the worker's actual dependencies: * db — SELECT 1 via Neon HTTP driver. Critical: failure -> 503. * chittyconnect — GET ${CHITTYCONNECT_URL}/health. Degraded if unreachable. * daemon — newest cc_node_leases.heartbeat_at, stale if older than 2x daemon/loop.ts default heartbeatMs (10000ms -> 20000ms cutoff). Missing table -> not_provisioned (degraded, NOT down) so deploys against bases without #101 don't 503. Per-dep timeout 2000ms; total probe bounded ≤ 5000ms via Promise.all. Runs unauthenticated (no auth middleware on /health). The probe handler is extracted to src/routes/health.ts so it can be integration-tested in pure Node — importing src/index.ts directly drags in `cloudflare:` modules (Agents SDK / DOs) that only resolve under workerd. Integration test exercises the handler against a real Neon branch (no mocks), validates DB probe ok and shape of all three probes, and asserts 503 + status=down when DB is unreachable. wrangler.jsonc already declares `tail_consumers: [{service: chittytrack}]` (present on the base branch) — no change required there. Verified against Neon branch br-spring-queen-akggkmso of project cool-bar-13270800 (ChittyCommand). Sample real response: { "status": "degraded", "service": "chittycommand", "version": "0.1.0", "timestamp": "2026-06-04T05:27:16.111Z", "probes": { "db": { "status": "ok", "latency_ms": 226 }, "chittyconnect": { "status": "degraded", "latency_ms": 0, "error": "CHITTYCONNECT_URL not configured" }, "daemon": { "status": "not_provisioned", "newest_heartbeat_age_ms": null, "error": "relation \"cc_node_leases\" does not exist" } } } Co-Authored-By: Claude Opus 4.7 (1M context) * fix(health): query active leases by heartbeat_at, not released_at Addresses Codex P2 on PR #109. The cc_node_leases schema in src/db/schema.ts has no released_at column — release is represented by NULLing heartbeat_at/lease_expires_at in daemon/leader.ts. The previous WHERE released_at IS NULL clause raised 'column released_at does not exist', which the catch block then mis-classified as 'not_provisioned', so /health would never report ok/stale based on real heartbeats. Co-Authored-By: Claude Opus 4.7 (1M context) --------- Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: chitcommit --- src/index.ts | 15 +-- src/routes/health.ts | 173 ++++++++++++++++++++++++++++++ tests/health/health-probe.spec.ts | 74 +++++++++++++ 3 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 src/routes/health.ts create mode 100644 tests/health/health-probe.spec.ts diff --git a/src/index.ts b/src/index.ts index cf4b77d..6a264dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ import { transactionRoutes } from './routes/transactions'; import { timelineRoutes } from './routes/timeline'; import { triageRoutes } from './routes/triage'; import { workspaceStudioRoutes } from './routes/workspace-studio'; +import { runHealthProbes } from './routes/health'; // Re-export ActionAgent DO class so the runtime can find it export { ActionAgent } from './agents/action-agent'; @@ -101,13 +102,13 @@ app.notFound((c) => { return c.json({ error: 'Not Found' }, 404); }); -// Health endpoint (unauthenticated) -app.get('/health', (c) => c.json({ - status: 'ok', - service: 'chittycommand', - version: '0.1.0', - timestamp: new Date().toISOString(), -})); +// Health endpoint (unauthenticated) — real dependency probes (db, chittyconnect, +// daemon heartbeat). See src/routes/health.ts. Returns 503 only if the DB is +// down; chittyconnect/daemon problems surface as `degraded` with 200. +app.get('/health', async (c) => { + const { body, httpStatus } = await runHealthProbes(c.env); + return c.json(body, httpStatus); +}); // Service status (unauthenticated) — ChittyOS standard app.get('/api/v1/status', (c) => c.json({ diff --git a/src/routes/health.ts b/src/routes/health.ts new file mode 100644 index 0000000..aaa9301 --- /dev/null +++ b/src/routes/health.ts @@ -0,0 +1,173 @@ +/** + * /health — real-dependency probe handler. + * + * Extracted from src/index.ts so it can be unit/integration-tested in pure + * Node without dragging in `cloudflare:`-namespaced imports (Agents SDK, + * Durable Objects, etc.) that only resolve under wrangler/workerd. + * + * Probes (each per-dep timeout 2000ms; total ≤ ~5000ms via Promise.all): + * - db SELECT 1 via Neon HTTP driver. Critical: failure → 503. + * - chittyconnect GET ${CHITTYCONNECT_URL}/health. Degraded if unreachable. + * - daemon max(heartbeat_at) FROM cc_node_leases. Stale if older than + * 2× daemon/loop.ts default heartbeatMs (10000ms → 20000ms). + * Missing table → not_provisioned (degraded, not down) so + * deploys against bases without #101 don't 503. + * + * Runs unauthenticated; does not touch auth middleware. + * + * @canonical-uri chittycanon://docs/architecture/chittycommand/health + */ + +import { getDb } from '../lib/db'; + +export const SERVICE_VERSION = '0.1.0'; +// daemon/loop.ts default heartbeatMs is 10000; flag stale at 2× = 20000ms. +const DAEMON_STALE_MS = 20_000; + +export type HealthEnv = { + DATABASE_URL?: string; + HYPERDRIVE?: { connectionString: string }; + CHITTYCONNECT_URL?: string; +}; + +export interface DbProbe { + status: 'ok' | 'down'; + latency_ms: number; + error?: string; +} +export interface ChittyConnectProbe { + status: 'ok' | 'degraded' | 'down'; + latency_ms: number; + error?: string; +} +export interface DaemonProbe { + status: 'ok' | 'stale' | 'not_provisioned'; + newest_heartbeat_age_ms: number | null; + error?: string; +} + +export interface HealthBody { + status: 'ok' | 'degraded' | 'down'; + service: 'chittycommand'; + version: string; + timestamp: string; + probes: { db: DbProbe; chittyconnect: ChittyConnectProbe; daemon: DaemonProbe }; +} + +async function withTimeout(p: Promise, ms: number, label: string): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + p, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`${label} timeout after ${ms}ms`)), ms); + }), + ]); + } finally { + if (timer) clearTimeout(timer); + } +} + +async function probeDb(env: HealthEnv): Promise { + const t0 = Date.now(); + try { + // Cast: HealthEnv is a narrow subset of the worker Env type and getDb + // only reads DATABASE_URL / HYPERDRIVE.connectionString. + const sql = getDb(env as unknown as Parameters[0]); + await withTimeout(sql`SELECT 1 AS ok`, 2000, 'db'); + return { status: 'ok', latency_ms: Date.now() - t0 }; + } catch (err) { + return { + status: 'down', + latency_ms: Date.now() - t0, + error: err instanceof Error ? err.message : String(err), + }; + } +} + +async function probeChittyConnect(env: HealthEnv): Promise { + const url = env.CHITTYCONNECT_URL; + const t0 = Date.now(); + if (!url) { + return { status: 'degraded', latency_ms: 0, error: 'CHITTYCONNECT_URL not configured' }; + } + try { + const r = await withTimeout( + fetch(`${url.replace(/\/$/, '')}/health`, { headers: { accept: 'application/json' } }), + 2000, + 'chittyconnect', + ); + return r.ok + ? { status: 'ok', latency_ms: Date.now() - t0 } + : { status: 'degraded', latency_ms: Date.now() - t0, error: `HTTP ${r.status}` }; + } catch (err) { + return { + status: 'degraded', + latency_ms: Date.now() - t0, + error: err instanceof Error ? err.message : String(err), + }; + } +} + +async function probeDaemon(env: HealthEnv): Promise { + try { + const sql = getDb(env as unknown as Parameters[0]); + const rows = (await withTimeout( + // NOTE: cc_node_leases has no `released_at` column. Release is + // represented by NULLing `heartbeat_at`/`lease_expires_at` in + // daemon/leader.ts::releaseLeadership. We treat any row whose + // heartbeat_at is non-null as a currently-held lease and take the + // newest heartbeat across them. + sql`SELECT EXTRACT(EPOCH FROM (NOW() - max(heartbeat_at))) * 1000 AS age_ms + FROM cc_node_leases + WHERE heartbeat_at IS NOT NULL`, + 2000, + 'daemon', + )) as Array<{ age_ms: number | string | null }>; + const raw = rows[0]?.age_ms; + if (raw === null || raw === undefined) { + return { status: 'stale', newest_heartbeat_age_ms: null }; + } + const ageMs = typeof raw === 'string' ? parseFloat(raw) : Number(raw); + return { + status: ageMs > DAEMON_STALE_MS ? 'stale' : 'ok', + newest_heartbeat_age_ms: Math.round(ageMs), + }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + if (/cc_node_leases|relation .* does not exist|does not exist/i.test(msg)) { + return { status: 'not_provisioned', newest_heartbeat_age_ms: null, error: msg }; + } + return { status: 'stale', newest_heartbeat_age_ms: null, error: msg }; + } +} + +export async function runHealthProbes(env: HealthEnv): Promise<{ body: HealthBody; httpStatus: 200 | 503 }> { + const [db, chittyconnect, daemon] = await Promise.all([ + probeDb(env), + probeChittyConnect(env), + probeDaemon(env), + ]); + + let status: HealthBody['status'] = 'ok'; + if (db.status === 'down') { + status = 'down'; + } else if ( + chittyconnect.status === 'degraded' || + chittyconnect.status === 'down' || + daemon.status === 'stale' || + daemon.status === 'not_provisioned' + ) { + status = 'degraded'; + } + + const body: HealthBody = { + status, + service: 'chittycommand', + version: SERVICE_VERSION, + timestamp: new Date().toISOString(), + probes: { db, chittyconnect, daemon }, + }; + + return { body, httpStatus: status === 'down' ? 503 : 200 }; +} diff --git a/tests/health/health-probe.spec.ts b/tests/health/health-probe.spec.ts new file mode 100644 index 0000000..48eb147 --- /dev/null +++ b/tests/health/health-probe.spec.ts @@ -0,0 +1,74 @@ +/** + * Integration test for the real-dependency /health probe handler. + * + * Runs against a REAL Neon branch (no mocks — chittyentity CLAUDE.md + * "No Mocks, Fake Data, or Placeholder Endpoints" rule). + * + * Usage: + * DATABASE_URL='postgres://...neon...' npx vitest run tests/health/health-probe.spec.ts + * + * Skipped automatically when DATABASE_URL is absent or SKIP_INTEGRATION=1 — + * same pattern as tests/daemon/leader.spec.ts and tests/meta/intent-lifecycle.spec.ts. + * + * We import the probe handler directly from src/routes/health.ts (pure Node + * compatible). Importing src/index.ts would drag in `cloudflare:` namespaced + * modules (Agents SDK / Durable Objects) which only resolve under workerd. + * + * Verifies: + * 1. probes.db.status === 'ok' against the real Neon branch with latency_ms > 0. + * 2. probes.daemon.status ∈ { ok, stale, not_provisioned } — not over-asserted + * because the branch may or may not have cc_node_leases provisioned or + * an active node holding a lease. + * 3. probes.chittyconnect.status is one of { ok, degraded, down }. + * 4. httpStatus is 200 when db.status === 'ok'; 503 only when db is down. + * 5. Response shape matches the documented spec. + */ + +import { describe, it, expect } from 'vitest'; +import { runHealthProbes } from '../../src/routes/health'; + +const DATABASE_URL = process.env.DATABASE_URL; +const SKIP = !DATABASE_URL || process.env.SKIP_INTEGRATION === '1'; + +describe.skipIf(SKIP)('/health real-dependency probe (real Neon)', () => { + it('runs all probes and reports DB ok against the real Neon branch', async () => { + const { body, httpStatus } = await runHealthProbes({ + DATABASE_URL, + // CHITTYCONNECT_URL intentionally omitted: probe will report 'degraded' + // with "not configured", which is the documented behavior. + }); + + // Spec: 200 unless db is down. DB is reachable on this branch. + expect(httpStatus).toBe(200); + + expect(body.service).toBe('chittycommand'); + expect(typeof body.version).toBe('string'); + expect(typeof body.timestamp).toBe('string'); + expect(['ok', 'degraded']).toContain(body.status); + + // DB probe must succeed. + expect(body.probes.db.status).toBe('ok'); + expect(body.probes.db.latency_ms).toBeGreaterThan(0); + + // ChittyConnect probe shape — value depends on env, just assert shape. + expect(['ok', 'degraded', 'down']).toContain(body.probes.chittyconnect.status); + expect(typeof body.probes.chittyconnect.latency_ms).toBe('number'); + + // Daemon probe — could be ok (active node), stale (no recent heartbeat), + // or not_provisioned (table missing on this branch). Don't over-assert. + expect(['ok', 'stale', 'not_provisioned']).toContain(body.probes.daemon.status); + }); + + it('marks status `down` and returns 503 when DB is unreachable', async () => { + // Real connection-refused — no mock, just an invalid host that fails fast. + const { body, httpStatus } = await runHealthProbes({ + DATABASE_URL: + 'postgresql://nobody:nobody@127.0.0.1:1/neondb?sslmode=disable', + }); + + expect(httpStatus).toBe(503); + expect(body.status).toBe('down'); + expect(body.probes.db.status).toBe('down'); + expect(typeof body.probes.db.error).toBe('string'); + }); +}); From ec22dd481380936e31d534653398a0fd29518093 Mon Sep 17 00:00:00 2001 From: "@chitcommit" <208086304+chitcommit@users.noreply.github.com> Date: Thu, 4 Jun 2026 07:59:46 -0500 Subject: [PATCH 5/6] docs: reclassify Tier 5 -> Tier 2 (Platform) with Tier-5 dashboard surface (#110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(charter,chitty,claude): reclassify Tier 5 → Tier 2 platform with Tier-5 surface; add meta endpoints and dependencies Stacked follow-on to PR #106 (ADR-001 + executor registry). Documentation-only PR that brings the compliance triad and `/api/v1/status` in line with the ADR-001 amendment that landed on `feat/meta-executors-registry`. CHARTER.md / CHITTY.md / CLAUDE.md - Canonical phrasing: "Tier 2 (Platform) with Tier-5 dashboard surface" everywhere a tier is declared. - RY (Authority) row rewritten to reflect sovereignty enforcement + intent execution + multi-source ingest. No longer a pure consumer. - API contract gains the one real new endpoint introduced by #106: POST /api/v1/intents/:id/execute. Per the global no-fake-endpoints rule, the planned intent CRUD (POST /api/v1/intents, GET .../:id, GET ..., GET /api/v1/executors) is NOT documented here because it isn't implemented; it will be added when the routes are. - New Executor Registry table with canonical URI chittycanon://core/services/chittycommand/executors/{intent_type}. Lists the one executor that actually self-registers today (update_obligation_status). mercury_payment is flagged as a tracked future executor with REAL-MONEY constraints (fresh autonomous sovereignty, USD 500 per-intent cap) and explicitly NOT documented as registered. - New Cluster Runtime section: daemon is launchd/systemd, NOT a Worker; per-node L-type ChittyIDs register as sub-channels via agent.chitty.cc/api/v1/channels, NOT in the main ChittyRegister payload. - Dependencies expanded: ChittyTrust, ChittyID, chittyagent-orchestrator, chittyagent-tasks, chittyagent-ch1tty added. ChittyConnect entry expanded to call out ContextConsciousness + MemoryCloude (forever context) and sensitive-intent secret brokerage. - Compliance section flags the ChittyID re-mint as required operator action (T → P-Synthetic) and explicitly defers /health real-probes and tail_consumers wiring to separate PRs. - MCP tool count reconciled to 50 (CLAUDE.md was already correct; CHARTER/ CHITTY updated from stale "48"). src/index.ts - /api/v1/status returns tier: 2 plus tierSurface phrasing and a meta.endpoints array listing the registered intent-execute route. No other route, middleware, or handler touched. src/routes/meta.ts - /api/v1/canon returns tier: 2 + tierSurface so the public canon view matches /status and the docs. Same handler signature; no behavior change. Not in this PR (deferred): - Real-dependency /health probes (separate PR). - tail_consumers wiring to chittytrack (separate observability PR). - ChittyRegister payload submission (operator action; blocked on re-mint). - Re-mint of service ChittyID as P-Synthetic (operator action). - Intent CRUD endpoints and /api/v1/executors enumeration (future PRs). Compliance coverage: - Addresses: tier reclassification, RY language, meta endpoint surface, executor URI registry, dependency expansion, cluster sub-channel declaration (items 1, 4, 5, 7, 8 of the registration-readiness audit). - Remaining (out of scope): registration submission (item 2, operational), real /health (item 3, separate PR), tail_consumers (item 6, separate PR), P-type ChittyID re-mint (item 9, operator action). Co-Authored-By: Claude Opus 4.7 (1M context) * docs(charter): clarify Worker vs daemon runtime split in Classification Addresses Codex P2 on PR #110: the previous wording said the Tier-2 platform and Tier-5 surface 'both run from the same worker', which contradicts the Cluster Runtime section stating daemon/ runs as a supervised launchd/systemd process, not as a Worker. Co-Authored-By: Claude Opus 4.7 (1M context) * docs(registration): draft chittycommand Tier-2 registration payload + submission runbook (#111) --------- Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: chitcommit --- CHARTER.md | 55 ++++++++++-- CHITTY.md | 12 ++- CLAUDE.md | 13 ++- docs/registration/SUBMISSION_RUNBOOK.md | 88 +++++++++++++++++++ .../chittycommand-registration-payload.json | 73 +++++++++++++++ src/index.ts | 8 +- src/routes/meta.ts | 3 +- 7 files changed, 238 insertions(+), 14 deletions(-) create mode 100644 docs/registration/SUBMISSION_RUNBOOK.md create mode 100644 docs/registration/chittycommand-registration-payload.json diff --git a/CHARTER.md b/CHARTER.md index 432106c..d69c4f9 100644 --- a/CHARTER.md +++ b/CHARTER.md @@ -32,10 +32,18 @@ This section is a persistent discovery hint for humans and agents. It is not an ## Classification - **Canonical URI**: `chittycanon://core/services/chittycommand` -- **Tier**: 5 (Application) +- **Tier**: 2 (Platform) with Tier-5 dashboard surface - **Organization**: CHITTYOS - **Domain**: command.chitty.cc +Per [ADR-001](docs/architecture/ADR-001-meta-orchestrator-extension.md), ChittyCommand is reclassified from Tier 5 Application to **Tier 2 (Platform) with Tier-5 dashboard surface**. The platform tier covers the meta-orchestrator (`meta/`), executor registry, sovereignty gate, and cluster daemon (`daemon/`). The Tier-5 surface is the existing user-facing dashboard + ActionAgent + MCP at `command.chitty.cc`. + +Deployment artifacts split across two runtimes (see Cluster Runtime section below): +- **Cloudflare Worker** (`src/`, `meta/` HTTP routes, dashboard, ActionAgent, MCP): the Tier-5 surface and the meta-orchestrator's request-handling plane run from the same Worker at `command.chitty.cc`. +- **Persistent daemon** (`daemon/`): the cluster leader / intent dispatcher does **not** run as a Worker. It is supervised as a launchd (macOS) / systemd (Linux) process on ChittyServ nodes and connects to the same Neon DB as the Worker. + +The dashboard is one consumer of the platform among many. + ## Mission Provide a unified life management and action dashboard that ingests data from 15+ financial, legal, and administrative sources, scores urgency with AI, recommends actions, and executes them via APIs, email, or browser automation. @@ -80,12 +88,17 @@ Provide a unified life management and action dashboard that ingests data from 15 | Upstream | ChittyScrape | Browser-based scraping for portals without APIs | | Upstream | ChittyLedger | Evidence and document ledger sync | | Upstream | ChittyEvidence | Evidence facts, documents, entities for case timelines | -| Upstream | ChittyConnect | Inter-service connectivity and discovery | +| Upstream | ChittyConnect | Inter-service connectivity and discovery; **forever-context** read/write via ContextConsciousness + MemoryCloude; **sensitive-intent secret brokerage** path for all credential access (no inline secrets) | | Upstream | ChittyRouter | Unified ingestion gateway (scrape, email routing) | | Upstream | ChittySchema | Canonical schema validation and drift detection | | Upstream | ChittyCert | Certificate verification | | Upstream | ChittyRegister | Service registration, beacon, compliance | | Upstream | ChittyChat | Project/task data API | +| Upstream | ChittyTrust | Sovereignty gate — trust score / autonomy assessment for actor + intent (`meta/sovereignty.ts`) | +| Upstream | ChittyID | Identity minting — service ChittyID (re-mint pending: P-Synthetic) and per-node L-type IDs for cluster nodes | +| Upstream | chittyagent-orchestrator | Intent routing + cross-channel fanout via `POST agent.chitty.cc/agent/message` | +| Upstream | chittyagent-tasks | Durable distributed task queue pattern reused for intent dispatch + `node_leases` (mirrors `task_leases`) | +| Upstream | chittyagent-ch1tty | MCP portal / OAuth gateway — `command` listed as upstream in `AGENT_MCP_UPSTREAMS` | | Platform | Cloudflare Workers | Compute runtime | | Platform | Cloudflare R2 | Document storage | | Platform | Cloudflare KV | Sync state, auth tokens, service tokens | @@ -130,7 +143,32 @@ Provide a unified life management and action dashboard that ingests data from 15 | `/api/v1/jobs/:id/retry` | POST | Bearer | Retry failed scrape job | | `/api/v1/jobs/dead-letters` | GET | Bearer | Dead letter queue | | `/api/bridge/*` | Various | Service/Bearer | Inter-service bridge routes | -| `/mcp/*` | Various | Service | MCP server (48 tools across 12 domains) | +| `/api/v1/intents/:id/execute` | POST | Bearer | Dispatch a queued Intent through the executor registry (sovereignty re-checked at executor entry) | +| `/mcp/*` | Various | Service | MCP server (50 tools across 12 domains) | + +### Executor Registry + +Canonical URI namespace: `chittycanon://core/services/chittycommand/executors/{intent_type}` + +Executors self-register at module load (side-effect imports from `meta/executors/index.ts`). The dispatcher (`meta/executors/dispatch.ts`) looks executors up by `intent_type`; absence is a wiring bug, not a runtime error. Per ADR-001, the sovereignty gate (`meta/sovereignty.ts`) is invoked at executor entry and re-reckoned if the persisted snapshot is older than the configured freshness window. + +| `intent_type` | Canonical URI | Notes | +|---------------|---------------|-------| +| `update_obligation_status` | `chittycanon://core/services/chittycommand/executors/update_obligation_status` | Real Neon write to `cc_obligations`. Source: `meta/executors/update-obligation-status.ts` | + +> **Future executors** (e.g. `mercury_payment` — 🔒 REAL-MONEY, will require fresh `autonomous` sovereignty assessment and an enforced USD 500 per-intent cap) are tracked in ADR-001 but are NOT yet registered in `meta/executors/index.ts`. They will be added in follow-up PRs and listed here at the same time the executor file is committed. This table is the authoritative list of currently-registered executors — do not document executors that do not exist. + +### Cluster Runtime + +Per [ADR-001](docs/architecture/ADR-001-meta-orchestrator-extension.md), the persistent cluster daemon (`daemon/`) is **not** a Cloudflare Worker. It runs as a supervised long-lived process on each ChittyServ cluster node: + +- **Hosts**: `chittymini-01..06` (Mac Mini 2012 homelab) + `chittyserv-vm` +- **Supervision**: launchd on macOS Minis, systemd on Ubuntu Minis + VM +- **Leader election**: Float-free across cluster via Neon `node_leases` (atomic `UPDATE ... RETURNING`, mirrors `task_leases` shape) +- **Loop**: claim → execute (via the same `meta/executors/*` registry the Worker uses) → heartbeat → release +- **Neon-loss fallback**: Park the node (MVP) — avoids split-brain; LAN gossip is a follow-up + +**Channel registration** for cluster nodes is explicitly **out of scope for the main ChittyRegister service payload**. Each node mints its own L-type ChittyID and registers as a **sub-channel** via `POST agent.chitty.cc/api/v1/channels` (per the ADR-001 preferred path). A future ChittyRegister submission for `chittycommand` must NOT attempt to model the cluster daemon as Worker compute, additional routes, or service bindings — the daemon is a peer execution surface to the Worker, not part of its deploy artifact. ### Cron Schedule | Schedule | Purpose | @@ -156,7 +194,7 @@ Source: `chittycanon://gov/governance#three-aspects` |--------|--------|----------|--------------------| | **Identity** | TY | What IS it? | Unified life management dashboard — ingests financial, legal, and administrative data from 15+ sources, scores urgency, recommends and executes actions | | **Connectivity** | VY | How does it ACT? | Cron-scheduled syncs (Plaid, Mercury, court dockets, utilities); bridge API to ChittyScrape, ChittyLedger, ChittyFinance; MCP server for Claude-driven queries; action execution via API, email, or browser automation | -| **Authority** | RY | Where does it SIT? | Tier 5 Application — consumer of upstream data, not source of truth; delegates scraping to ChittyScrape, identity to ChittyID, financials to ChittyFinance | +| **Authority** | RY | Where does it SIT? | Tier 2 (Platform) with Tier-5 dashboard surface — sovereign meta-orchestrator that enforces trust gates on intent execution, dispatches actions across registered executors and channels, and ingests from 15+ upstreams. Source of truth for: intent ladder (`cc_intents`), executor registry, sovereignty assessments, cluster node leases. Still delegates: identity to ChittyID, browser scraping to ChittyScrape, financial aggregation to ChittyFinance, forever-context storage to ChittyConnect (ContextConsciousness + MemoryCloude). | ## Document Triad @@ -175,12 +213,15 @@ This charter is part of a synchronized documentation triad. Changes to shared fi ## Compliance -- [x] Service registered in ChittyRegister (03-1-USA-3846-T-2602-0-57, pending_cert) +- [x] Service registered in ChittyRegister (03-1-USA-3846-T-2602-0-57, pending_cert) — ⚠️ **DEPRECATED PENDING RE-MINT** (see below) - [x] Health endpoint operational at /health -- [x] Status endpoint operational at /api/v1/status +- [x] Status endpoint operational at /api/v1/status (reflects Tier 2 + meta endpoints) - [x] CLAUDE.md development guide present - [x] CHARTER.md present - [x] CHITTY.md present +- [ ] **ChittyID re-mint required (operator action, blocks Tier 2 ChittyCertify).** The currently registered ChittyID `03-1-USA-3846-T-2602-0-57` encodes type `T` (Thing). Per `chittycanon://gov/governance#core-types` and the global "actors with agency are always Person" rule, a sovereign meta-orchestrator that takes autonomous action (intent execution, sovereignty enforcement, channel fanout) is a **Person — Synthetic** (P-Synthetic), not a Thing. A new ChittyID must be minted as `VV-G-USA-NNNN-P-YM-S-X` (T-slot = `P`, subtype Synthetic) and the registry record updated. The existing T-type ID is retained for historical lookup only and must NOT be cited as the service identity in new code, telemetry, or downstream contracts after the re-mint. **Blocks**: formal ChittyCertify at Tier 2; sovereign-intent signing; ChittyTrust score binding for the service-as-actor. +- [ ] Real-dependency `/health` probes (db / chittyconnect / daemon-heartbeat) — tracked in a separate PR; not in this docs PR. +- [ ] Service-level `tail_consumers` wiring (`chittytrack`) — tracked in a separate observability PR; not in this docs PR. --- -*Charter Version: 1.2.0 | Last Updated: 2026-03-24* +*Charter Version: 1.3.0 | Last Updated: 2026-06-04* diff --git a/CHITTY.md b/CHITTY.md index 4579ac0..3cfd50a 100644 --- a/CHITTY.md +++ b/CHITTY.md @@ -17,7 +17,7 @@ discovery_refs: # ChittyCommand -> `chittycanon://core/services/chittycommand` | Tier 5 (Application) | command.chitty.cc +> `chittycanon://core/services/chittycommand` | Tier 2 (Platform) with Tier-5 dashboard surface | command.chitty.cc ## Persistent Context @@ -71,7 +71,7 @@ Source: `chittycanon://gov/governance#three-aspects` |--------|--------|--------| | **Identity** | TY | Unified life management dashboard — ingests financial, legal, and administrative data from 15+ sources, scores urgency, recommends and executes actions | | **Connectivity** | VY | Cron-scheduled syncs (Plaid, Mercury, court dockets, utilities); bridge API to ChittyScrape, ChittyLedger, ChittyFinance; MCP server for Claude-driven queries; action execution via API, email, or browser automation | -| **Authority** | RY | Tier 5 Application — consumer of upstream data, not source of truth; delegates scraping to ChittyScrape, identity to ChittyID, financials to ChittyFinance | +| **Authority** | RY | Tier 2 (Platform) with Tier-5 dashboard surface — sovereign meta-orchestrator that enforces trust gates on intent execution, dispatches actions across registered executors and channels, and ingests from 15+ upstreams. Source of truth for: intent ladder (`cc_intents`), executor registry, sovereignty assessments, cluster node leases. Still delegates: identity to ChittyID, browser scraping to ChittyScrape, financial aggregation to ChittyFinance, forever-context storage to ChittyConnect (ContextConsciousness + MemoryCloude). _Canonical source: [CHARTER.md](CHARTER.md#three-aspects-ty-vy-ry)._ | ## ChittyOS Ecosystem @@ -85,6 +85,11 @@ Source: `chittycanon://gov/governance#three-aspects` - **DNA Hash**: -- - **Lineage**: root (life management) +### Ecosystem Position + +- **Upstream**: ChittyAuth, ChittyTrust, ChittyID, ChittyConnect (ContextConsciousness + MemoryCloude + sensitive-intent secret brokerage), chittyagent-orchestrator (routing + channel fanout), chittyagent-tasks (queue + lease pattern), chittyagent-ch1tty (MCP portal/OAuth), ChittyFinance, ChittyBooks, ChittyAssets, ChittyCharge, ChittyScrape, ChittyLedger, ChittyEvidence, ChittyRouter, ChittySchema, ChittyCert, ChittyRegister, ChittyChat. +- **Downstream / sub-channels**: ChittyServ cluster nodes (`chittymini-01..06`, `chittyserv-vm`) — each runs a supervised `daemon/` process and registers as an L-type sub-channel via `agent.chitty.cc/api/v1/channels`. ActionAgent (chat surface) and dashboard SPA at `app.command.chitty.cc` are sibling consumers of the same `meta/executors/*` registry. + ### Dependencies See [CHARTER.md](CHARTER.md) (Dependencies section) — canonical source for the full dependency graph. @@ -130,7 +135,8 @@ See [CHARTER.md](CHARTER.md) (Dependencies section) — canonical source for the | `/api/v1/jobs/:id` | GET | Bearer | Scrape job details | | `/api/v1/jobs/:id/retry` | POST | Bearer | Retry failed scrape job | | `/api/v1/jobs/dead-letters` | GET | Bearer | Dead letter queue | -| `/mcp/*` | Various | Service | MCP server (48 tools across 12 domains) | +| `/api/v1/intents/:id/execute` | POST | Bearer | Dispatch a queued Intent through the executor registry (sovereignty re-checked at executor entry) | +| `/mcp/*` | Various | Service | MCP server (50 tools across 12 domains) | ## Document Triad diff --git a/CLAUDE.md b/CLAUDE.md index 539c7e5..8aa5fe0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ ChittyCommand is a unified life management and action dashboard for the ChittyOS **Repo:** `CHITTYOS/chittycommand` **Deploy:** Cloudflare Workers at `command.chitty.cc` (alias: `disputes.chitty.cc`) **Stack:** Hono TypeScript, React + Tailwind, Neon PostgreSQL (via Hyperdrive), Cloudflare R2/KV -**Canonical URI:** `chittycanon://core/services/chittycommand` | Tier 5 +**Canonical URI:** `chittycanon://core/services/chittycommand` | Tier 2 (Platform) with Tier-5 dashboard surface ## Common Commands @@ -41,7 +41,16 @@ wrangler secret put DATABASE_URL ## Architecture -Single Cloudflare Worker (`chittycommand`) serving API + cron. Frontend is a separate React SPA at `app.command.chitty.cc` (Cloudflare Pages). +Per [ADR-001](docs/architecture/ADR-001-meta-orchestrator-extension.md), ChittyCommand is a Tier-2 platform with a Tier-5 dashboard surface. The Cloudflare Worker (`chittycommand`) and a separate supervised cluster daemon both consume the same canonical executor registry. + +### Tier-2 Platform Layers + +- **`meta/`** — Goal → Plan → Intent ladder (`meta/intent.ts`), sovereignty gate (`meta/sovereignty.ts`, trust-score → `autonomous | requires_human | blocked`), channel fanout (`meta/channels.ts`), forever-context wrapper (`meta/context.ts`), and the **executor registry** (`meta/executors/*`) with self-registration at module load. Canonical executor URIs: `chittycanon://core/services/chittycommand/executors/{intent_type}`. The sovereignty gate is invoked at (1) Intent creation (persisted into `cc_intents.sovereignty_assessment`) and (2) executor entry in `meta/executors/dispatch.ts` (re-reckoned if snapshot is older than the configured freshness window). Execution audit is additive columns on `cc_actions_log` — NOT a new table. +- **`daemon/`** — Persistent supervised cluster process (launchd / systemd) running on each ChittyServ node. Leader election via Neon `cc_node_leases` (mirrors `task_leases` shape, atomic `UPDATE ... RETURNING`). Loop: claim → execute (same executor registry) → heartbeat → release. Float-free leadership; Neon-loss fallback is "park the node" (MVP). + +### Tier-5 Dashboard Surface + +Single Cloudflare Worker (`chittycommand`) serving API + cron + ActionAgent + MCP. Frontend is a separate React SPA at `app.command.chitty.cc` (Cloudflare Pages). ActionAgent (chat surface) and the cluster daemon (autonomous surface) are **siblings** that consume the same `meta/executors/*` registry; neither dispatches the other. ### Data Sources diff --git a/docs/registration/SUBMISSION_RUNBOOK.md b/docs/registration/SUBMISSION_RUNBOOK.md new file mode 100644 index 0000000..cefce07 --- /dev/null +++ b/docs/registration/SUBMISSION_RUNBOOK.md @@ -0,0 +1,88 @@ +# ChittyCommand Registration Submission Runbook + +Operator-facing runbook for submitting `chittycommand` to `register.chitty.cc` as a Tier-2 platform service. The payload draft lives alongside this file at `chittycommand-registration-payload.json`. + +This runbook does NOT submit. Submission is a separate, gated operator action routed through ChittyConnect (the Chico concierge). + +## Pre-requisites + +1. **Stacked PRs merged to `main`, in order:** + - PR #106 + - PR #107 + - PR #109 + - PR #110 (Tier-2 reclassification — CHARTER/CHITTY/CLAUDE updates) + + This PR (registration payload draft) is stacked on #110 and should merge AFTER #110 lands on main. + +2. **Live pre-flight health probe:** + + ```bash + curl -sS https://command.chitty.cc/health | jq . + ``` + + Must return the real-dependency probe JSON shape — fields for `db`, `chittyconnect`, and `daemon` heartbeat must reflect actual probed state. A static `{"status":"ok"}` response is a regression and blocks submission per the global "no fake/non-working endpoints" rule. + +3. **New P-Synthetic ChittyID minted** via the canonical Chico path: + + - Route: `ch1tty → ChittyConnect → chittyid` + - The previous ID `03-1-USA-3846-T-2602-0-57` is deprecated because the 5th field encoded `T` (Thing). `chittycommand` is a sovereign actor and must be `P` (Person, Synthetic characterization). + - Verify the minted ID's 5th `-`-separated field is `P` before substituting into the payload. + +## Substitutions Before Submission + +The committed payload contains two placeholder strings. Both must be substituted at submission time. NEITHER value is ever pasted into chat, committed to git, or stored in shell history in plaintext. + +| Placeholder | Substitution Source | Routing | +|---|---|---| +| `<>` | 1Password (cold source) → Cloudflare Secrets (runtime) | ChittyConnect via Chico — operator never handles the bearer directly | +| `<>` | Newly minted via ChittyID service | Operator confirms `P` in 5th field, then injects | + +Per `/home/ubuntu/.ch1tty/canon/system-wide-sensitive-intent-contract-v1.md`, the operator does not paste secrets — the request must route through ChittyConnect. If the broker path is unavailable, fail closed with `POLICY_BLOCKED_CHITTYCONNECT_UNAVAILABLE`. + +## Submission Command (shape only) + +The actual injection uses `op run` per the operator manifest. The template below shows the request shape — do NOT run it verbatim with raw env vars. + +```bash +jq '.registrationToken="$CHITTY_REGISTER_TOKEN" | .service.chittyId="$NEW_CHITTYID"' \ + docs/registration/chittycommand-registration-payload.json | \ + curl -sS -X POST https://register.chitty.cc/api/v1/register \ + -H "Authorization: Bearer $CHITTY_REGISTER_TOKEN" \ + -H "content-type: application/json" \ + --data @- +``` + +Production invocation wraps the above under `op run --env-file=... --` with the token resolved by ChittyConnect at request time. + +## Verification + +After a 2xx response from `register.chitty.cc`: + +```bash +curl -sS 'https://registry.chitty.cc/api/v1/search?q=chittycommand' | jq . +``` + +Expected: the new entry is returned with `tier: 2`, `category: "core-infrastructure"`, and the new P-Synthetic ChittyID. + +Record the verification response (with the token field redacted) in a follow-up commit to `CHARTER.md` under a "Registration Evidence" section. + +## ChittyCertify Next Step + +Once registered, `chittycommand` is eligible for Tier-2 ChittyCertify audit. Open the audit request via the canonical ChittyCertify intake — do not self-assert the certification level in the payload (the payload's `certificationLevel` is `null` by design; ChittyCertify writes it). + +## Rollback / Failure Handling + +If `register.chitty.cc` rejects the submission: + +1. Capture the full response body (headers + JSON) — redact token-shaped fields before storing. +2. Do NOT retry blindly. +3. File an issue against `chittyos/chittyregistry` referencing this runbook, the response body, and the payload shape (NOT the resolved token). +4. Diagnose the schema or auth mismatch before any second attempt. The `ServiceRegistrationSchema` in `chittyregistry/src/types/index.ts` is the authoritative shape — payload must match. + +## What This Runbook Does NOT Do + +- Does not submit the registration. +- Does not handle the bearer token directly — Chico/ChittyConnect owns that. +- Does not modify CHARTER/CHITTY/CLAUDE (PR #110's lane). +- Does not deploy any worker. +- Does not enable auto-merge on this PR. diff --git a/docs/registration/chittycommand-registration-payload.json b/docs/registration/chittycommand-registration-payload.json new file mode 100644 index 0000000..7077f17 --- /dev/null +++ b/docs/registration/chittycommand-registration-payload.json @@ -0,0 +1,73 @@ +{ + "registrationToken": "<>", + "environment": "production", + "service": { + "chittyId": "<>", + "serviceName": "chittycommand", + "displayName": "ChittyCommand", + "description": "Sovereign meta-orchestrator and life management platform — ingests data from 15+ financial, legal, and administrative sources; scores urgency; enforces trust gates on intent execution; dispatches actions via registered executor registry, cluster daemon, and MCP server.", + "version": "0.1.0", + "baseUrl": "https://command.chitty.cc", + "endpoints": [ + { "path": "/health", "method": "GET", "description": "Real-dependency health probe (db + chittyconnect + daemon heartbeat)", "authenticated": false }, + { "path": "/api/v1/status", "method": "GET", "description": "Service metadata, tier, meta endpoint surface", "authenticated": false }, + { "path": "/api/v1/canon", "method": "GET", "description": "Canon info and registry status", "authenticated": false }, + { "path": "/api/v1/schema", "method": "GET", "description": "Lightweight schema references", "authenticated": false }, + { "path": "/api/v1/beacon", "method": "GET", "description": "Last beacon timestamp and status", "authenticated": false }, + { "path": "/api/v1/cert/verify", "method": "POST", "description": "Verify a ChittyCert certificate", "authenticated": false }, + { "path": "/api/v1/cert/:id", "method": "GET", "description": "Certificate details by ID", "authenticated": false }, + { "path": "/api/v1/whoami", "method": "GET", "description": "Identity: subject and scopes", "authenticated": true }, + { "path": "/api/v1/intents/:id/execute", "method": "POST", "description": "Atomic intent execution — claim, dispatch via executor registry, transition status", "authenticated": true }, + { "path": "/api/v1/context", "method": "GET", "description": "Get active persona, label, and tags", "authenticated": true }, + { "path": "/api/v1/context", "method": "POST", "description": "Set active persona, label, and tags", "authenticated": true }, + { "path": "/api/v1/connect/status", "method": "GET", "description": "ChittyConnect health", "authenticated": true }, + { "path": "/api/v1/connect/discover", "method": "POST", "description": "Resolve service URL via ChittyConnect", "authenticated": true }, + { "path": "/api/v1/ledger/evidence", "method": "GET", "description": "List evidence for a case via ChittyLedger", "authenticated": true }, + { "path": "/api/v1/ledger/record-custody", "method": "POST", "description": "Record custody entry for an evidence_id", "authenticated": true }, + { "path": "/api/v1/timeline/:caseId", "method": "GET", "description": "Unified case timeline (facts, deadlines, disputes, docs)", "authenticated": true }, + { "path": "/api/v1/litigation/synthesize", "method": "POST", "description": "AI fact synthesis from raw notes", "authenticated": true }, + { "path": "/api/v1/litigation/draft", "method": "POST", "description": "AI email drafting from synthesized facts", "authenticated": true }, + { "path": "/api/v1/litigation/qc", "method": "POST", "description": "AI risk scan of draft vs source notes", "authenticated": true }, + { "path": "/api/v1/jobs", "method": "GET", "description": "Scrape job queue", "authenticated": true }, + { "path": "/api/v1/jobs", "method": "POST", "description": "Enqueue scrape job", "authenticated": true }, + { "path": "/api/v1/jobs/:id", "method": "GET", "description": "Scrape job details", "authenticated": true }, + { "path": "/api/v1/jobs/:id/retry", "method": "POST", "description": "Retry failed scrape job", "authenticated": true }, + { "path": "/api/v1/jobs/dead-letters", "method": "GET", "description": "Dead letter queue", "authenticated": true }, + { "path": "/api/dashboard/summary", "method": "GET", "description": "Dashboard summary with urgency scores", "authenticated": true }, + { "path": "/api/accounts", "method": "GET", "description": "Financial account list", "authenticated": true }, + { "path": "/api/accounts", "method": "POST", "description": "Create financial account", "authenticated": true }, + { "path": "/api/obligations", "method": "GET", "description": "Bills, debts, recurring obligations", "authenticated": true }, + { "path": "/api/obligations", "method": "POST", "description": "Create obligation", "authenticated": true }, + { "path": "/api/disputes", "method": "GET", "description": "Active dispute management", "authenticated": true }, + { "path": "/api/disputes", "method": "POST", "description": "Create dispute", "authenticated": true }, + { "path": "/api/recommendations", "method": "GET", "description": "AI action recommendations", "authenticated": true }, + { "path": "/api/cashflow", "method": "GET", "description": "Cash flow analysis and projections", "authenticated": true }, + { "path": "/api/sync", "method": "POST", "description": "Manual data sync trigger", "authenticated": true }, + { "path": "/api/bridge/*", "method": "GET", "description": "Inter-service bridge passthrough (read)", "authenticated": true }, + { "path": "/api/bridge/*", "method": "POST", "description": "Inter-service bridge passthrough (write)", "authenticated": true }, + { "path": "/mcp/*", "method": "POST", "description": "MCP server — 50 tools across 12 domains (service-auth)", "authenticated": true } + ], + "healthCheck": { "path": "/health", "method": "GET", "expectedStatus": 200, "timeout": 5000 }, + "category": "core-infrastructure", + "dependencies": [ + "chittyauth","chittyid","chittycert","chittyconnect","chittyregistry","chittyschema","chittyrouter","chittyfinance","chittyscrape","chittyledger","chittyevidence","chittyassets","chittycharge","chittybooks","chittytrust","chittyagent-orchestrator","chittyagent-tasks","chittyagent-ch1tty","chittychat" + ], + "capabilities": [ + "intent-execution","executor-registry","sovereignty-gate","meta-orchestration","cluster-daemon","financial-ingest","legal-ingest","urgency-scoring","litigation-support","mcp-server","scrape-job-dispatch","document-storage","action-recommendations","bridge-api" + ], + "certificationLevel": null, + "metadata": { + "canonicalUri": "chittycanon://core/services/chittycommand", + "tier": 2, + "tierSurface": "Tier 2 (Platform) with Tier-5 dashboard surface", + "domain": "command.chitty.cc", + "adrs": ["ADR-001"], + "executors": ["update_obligation_status"], + "clusterDaemonRegistration": "sub-channel via agent.chitty.cc/api/v1/channels — NOT in this payload", + "previousChittyId": "03-1-USA-3846-T-2602-0-57", + "previousChittyIdDeprecationReason": "T-type (Thing) encoding — invalid for a P-Synthetic sovereign actor", + "mcpToolCount": 50, + "mcpDomains": 12 + } + } +} diff --git a/src/index.ts b/src/index.ts index 6a264dd..01093bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -116,7 +116,13 @@ app.get('/api/v1/status', (c) => c.json({ version: '0.1.0', environment: c.env.ENVIRONMENT || 'production', canonicalUri: 'chittycanon://core/services/chittycommand', - tier: 5, + tier: 2, + tierSurface: 'Tier 2 (Platform) with Tier-5 dashboard surface', + meta: { + endpoints: [ + '/api/v1/intents/:id/execute', + ], + }, })); // Canon/schema (unauthenticated) diff --git a/src/routes/meta.ts b/src/routes/meta.ts index 9970aa7..7216cf3 100644 --- a/src/routes/meta.ts +++ b/src/routes/meta.ts @@ -19,7 +19,8 @@ metaPublicRoutes.get('/canon', async (c) => { environment: env.ENVIRONMENT || 'production', canonicalUri, namespace: 'chittycanon://core/services', - tier: 5, + tier: 2, + tierSurface: 'Tier 2 (Platform) with Tier-5 dashboard surface', registered_with: env.CHITTYREGISTER_URL || null, registration: { service_id: serviceId || null, last_beacon_at: lastBeaconAt || null, last_status: lastBeaconStatus || null }, }); From fc60bfc302b05a2b95ce615b2f1b969e6dd91551 Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:39:05 +0000 Subject: [PATCH 6/6] fix(migrations): renumber executor migration 0004_chief_skin -> 0005 after rebase Resolves the 0004 collision with main's 0004_premium_toad_men (Roux privilege/space). drizzle db:generate regenerated as 0005_sour_dreadnoughts (statement-identical to chief_skin) with a cumulative snapshot, so prod drizzle-kit migrate (no already-exists tolerance) stays clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...ef_skin.sql => 0005_sour_dreadnoughts.sql} | 0 migrations/meta/0005_snapshot.json | 3526 +++++++++++++++++ migrations/meta/_journal.json | 4 +- 3 files changed, 3528 insertions(+), 2 deletions(-) rename migrations/{0005_chief_skin.sql => 0005_sour_dreadnoughts.sql} (100%) create mode 100644 migrations/meta/0005_snapshot.json diff --git a/migrations/0005_chief_skin.sql b/migrations/0005_sour_dreadnoughts.sql similarity index 100% rename from migrations/0005_chief_skin.sql rename to migrations/0005_sour_dreadnoughts.sql diff --git a/migrations/meta/0005_snapshot.json b/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..c8c0d7a --- /dev/null +++ b/migrations/meta/0005_snapshot.json @@ -0,0 +1,3526 @@ +{ + "id": "32cf64a8-71bc-4a69-96d0-9aa3e4304c68", + "prevId": "6e6fff66-21a5-4a75-b92d-be92b3b7035b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.cc_accounts": { + "name": "cc_accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_name": { + "name": "account_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "institution": { + "name": "institution", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_balance": { + "name": "current_balance", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "credit_limit": { + "name": "credit_limit", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "interest_rate": { + "name": "interest_rate", + "type": "numeric(5, 3)", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_actions_log": { + "name": "cc_actions_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_payload": { + "name": "request_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_payload": { + "name": "response_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "executed_at": { + "name": "executed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "intent_id": { + "name": "intent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_cc_actions_log_date": { + "name": "idx_cc_actions_log_date", + "columns": [ + { + "expression": "executed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_actions_log_intent_executed": { + "name": "idx_cc_actions_log_intent_executed", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "executed_at", + "isExpression": false, + "asc": false, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "uq_cc_actions_log_intent_idempotency": { + "name": "uq_cc_actions_log_intent_idempotency", + "columns": [ + { + "expression": "intent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "intent_id IS NOT NULL AND idempotency_key IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cc_actions_log_intent_id_cc_intents_id_fk": { + "name": "cc_actions_log_intent_id_cc_intents_id_fk", + "tableFrom": "cc_actions_log", + "tableTo": "cc_intents", + "columnsFrom": [ + "intent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_cashflow_projections": { + "name": "cc_cashflow_projections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "projection_date": { + "name": "projection_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "projected_inflow": { + "name": "projected_inflow", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "projected_outflow": { + "name": "projected_outflow", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "projected_balance": { + "name": "projected_balance", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "obligations": { + "name": "obligations", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "generated_at": { + "name": "generated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_cashflow_date": { + "name": "idx_cc_cashflow_date", + "columns": [ + { + "expression": "projection_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_decision_feedback": { + "name": "cc_decision_feedback", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "recommendation_id": { + "name": "recommendation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "obligation_id": { + "name": "obligation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_action": { + "name": "original_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "modified_action": { + "name": "modified_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence_at_decision": { + "name": "confidence_at_decision", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "outcome_status": { + "name": "outcome_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome_recorded_at": { + "name": "outcome_recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_decision_feedback_rec": { + "name": "idx_cc_decision_feedback_rec", + "columns": [ + { + "expression": "recommendation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_decision_feedback_ob": { + "name": "idx_cc_decision_feedback_ob", + "columns": [ + { + "expression": "obligation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_decision_feedback_created": { + "name": "idx_cc_decision_feedback_created", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cc_decision_feedback_recommendation_id_cc_recommendations_id_fk": { + "name": "cc_decision_feedback_recommendation_id_cc_recommendations_id_fk", + "tableFrom": "cc_decision_feedback", + "tableTo": "cc_recommendations", + "columnsFrom": [ + "recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cc_decision_feedback_obligation_id_cc_obligations_id_fk": { + "name": "cc_decision_feedback_obligation_id_cc_obligations_id_fk", + "tableFrom": "cc_decision_feedback", + "tableTo": "cc_obligations", + "columnsFrom": [ + "obligation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_dispute_correspondence": { + "name": "cc_dispute_correspondence", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "dispute_id": { + "name": "dispute_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "channel": { + "name": "channel", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subject": { + "name": "subject", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attachments": { + "name": "attachments", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "sent_at": { + "name": "sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + } + }, + "indexes": { + "idx_cc_dispute_corr_dispute": { + "name": "idx_cc_dispute_corr_dispute", + "columns": [ + { + "expression": "dispute_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cc_dispute_correspondence_dispute_id_cc_disputes_id_fk": { + "name": "cc_dispute_correspondence_dispute_id_cc_disputes_id_fk", + "tableFrom": "cc_dispute_correspondence", + "tableTo": "cc_disputes", + "columnsFrom": [ + "dispute_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_disputes": { + "name": "cc_disputes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counterparty": { + "name": "counterparty", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dispute_type": { + "name": "dispute_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_claimed": { + "name": "amount_claimed", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "amount_at_stake": { + "name": "amount_at_stake", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "stage": { + "name": "stage", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'filed'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_action_date": { + "name": "next_action_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "resolution_target": { + "name": "resolution_target", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "privilege": { + "name": "privilege", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "space": { + "name": "space", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'business'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_disputes_privilege": { + "name": "idx_cc_disputes_privilege", + "columns": [ + { + "expression": "privilege", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_disputes_space": { + "name": "idx_cc_disputes_space", + "columns": [ + { + "expression": "space", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_documents": { + "name": "cc_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chitty_id": { + "name": "chitty_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "doc_type": { + "name": "doc_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "r2_key": { + "name": "r2_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_text": { + "name": "content_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parsed_data": { + "name": "parsed_data", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "linked_obligation_id": { + "name": "linked_obligation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_account_id": { + "name": "linked_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_dispute_id": { + "name": "linked_dispute_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cc_documents_linked_obligation_id_cc_obligations_id_fk": { + "name": "cc_documents_linked_obligation_id_cc_obligations_id_fk", + "tableFrom": "cc_documents", + "tableTo": "cc_obligations", + "columnsFrom": [ + "linked_obligation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cc_documents_linked_account_id_cc_accounts_id_fk": { + "name": "cc_documents_linked_account_id_cc_accounts_id_fk", + "tableFrom": "cc_documents", + "tableTo": "cc_accounts", + "columnsFrom": [ + "linked_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cc_documents_linked_dispute_id_cc_disputes_id_fk": { + "name": "cc_documents_linked_dispute_id_cc_disputes_id_fk", + "tableFrom": "cc_documents", + "tableTo": "cc_disputes", + "columnsFrom": [ + "linked_dispute_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_email_connections": { + "name": "cc_email_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_address": { + "name": "email_address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "connect_ref": { + "name": "connect_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "last_synced_at": { + "name": "last_synced_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_email_conn_email_user": { + "name": "idx_cc_email_conn_email_user", + "columns": [ + { + "expression": "email_address", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_email_conn_user": { + "name": "idx_cc_email_conn_user", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_email_conn_namespace": { + "name": "idx_cc_email_conn_namespace", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_goals": { + "name": "cc_goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "owner_chitty_id": { + "name": "owner_chitty_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "target_date": { + "name": "target_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "achieved_at": { + "name": "achieved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_goals_owner": { + "name": "idx_cc_goals_owner", + "columns": [ + { + "expression": "owner_chitty_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_goals_status": { + "name": "idx_cc_goals_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_goals_priority": { + "name": "idx_cc_goals_priority", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_intents": { + "name": "cc_intents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_id": { + "name": "plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "intent_type": { + "name": "intent_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_channel": { + "name": "target_channel", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 5 + }, + "sovereignty_assessment": { + "name": "sovereignty_assessment", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "human_gate_reason": { + "name": "human_gate_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "dispatched_task_id": { + "name": "dispatched_task_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scheduled_for": { + "name": "scheduled_for", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reclaim_count": { + "name": "reclaim_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "privilege": { + "name": "privilege", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "space": { + "name": "space", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'business'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_intents_plan": { + "name": "idx_cc_intents_plan", + "columns": [ + { + "expression": "plan_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_intents_goal": { + "name": "idx_cc_intents_goal", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_intents_status": { + "name": "idx_cc_intents_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_intents_priority": { + "name": "idx_cc_intents_priority", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_intents_scheduled": { + "name": "idx_cc_intents_scheduled", + "columns": [ + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_intents_privilege": { + "name": "idx_cc_intents_privilege", + "columns": [ + { + "expression": "privilege", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_intents_space": { + "name": "idx_cc_intents_space", + "columns": [ + { + "expression": "space", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cc_intents_goal_id_cc_goals_id_fk": { + "name": "cc_intents_goal_id_cc_goals_id_fk", + "tableFrom": "cc_intents", + "tableTo": "cc_goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "cc_intents_plan_goal_cc_plans_fk": { + "name": "cc_intents_plan_goal_cc_plans_fk", + "tableFrom": "cc_intents", + "tableTo": "cc_plans", + "columnsFrom": [ + "plan_id", + "goal_id" + ], + "columnsTo": [ + "id", + "goal_id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_legal_deadlines": { + "name": "cc_legal_deadlines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chitty_id": { + "name": "chitty_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "case_ref": { + "name": "case_ref", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "case_system": { + "name": "case_system", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deadline_type": { + "name": "deadline_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "deadline_date": { + "name": "deadline_date", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "reminder_days": { + "name": "reminder_days", + "type": "integer[]", + "primaryKey": false, + "notNull": false, + "default": "'{7,3,1}'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'upcoming'" + }, + "urgency_score": { + "name": "urgency_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "evidence_db_ref": { + "name": "evidence_db_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_legal_deadlines_date": { + "name": "idx_cc_legal_deadlines_date", + "columns": [ + { + "expression": "deadline_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_node_leases": { + "name": "cc_node_leases", + "schema": "", + "columns": { + "role": { + "name": "role", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "node_id": { + "name": "node_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "node_descriptor": { + "name": "node_descriptor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "heartbeat_at": { + "name": "heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_node_leases_node": { + "name": "idx_cc_node_leases_node", + "columns": [ + { + "expression": "node_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_node_leases_expires": { + "name": "idx_cc_node_leases_expires", + "columns": [ + { + "expression": "lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_obligations": { + "name": "cc_obligations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chitty_id": { + "name": "chitty_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subcategory": { + "name": "subcategory", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payee": { + "name": "payee", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_due": { + "name": "amount_due", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "amount_minimum": { + "name": "amount_minimum", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "recurrence": { + "name": "recurrence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recurrence_day": { + "name": "recurrence_day", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'pending'" + }, + "auto_pay": { + "name": "auto_pay", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "negotiable": { + "name": "negotiable", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "late_fee": { + "name": "late_fee", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "grace_period_days": { + "name": "grace_period_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "urgency_score": { + "name": "urgency_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_payload": { + "name": "action_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source_doc_id": { + "name": "source_doc_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "escalation_type": { + "name": "escalation_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "escalation_threshold_days": { + "name": "escalation_threshold_days", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "escalation_amount": { + "name": "escalation_amount", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "credit_impact_score": { + "name": "credit_impact_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "preferred_account_id": { + "name": "preferred_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_obligations_due": { + "name": "idx_cc_obligations_due", + "columns": [ + { + "expression": "due_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_obligations_status": { + "name": "idx_cc_obligations_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_obligations_urgency": { + "name": "idx_cc_obligations_urgency", + "columns": [ + { + "expression": "urgency_score", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cc_obligations_account_id_cc_accounts_id_fk": { + "name": "cc_obligations_account_id_cc_accounts_id_fk", + "tableFrom": "cc_obligations", + "tableTo": "cc_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cc_obligations_preferred_account_id_cc_accounts_id_fk": { + "name": "cc_obligations_preferred_account_id_cc_accounts_id_fk", + "tableFrom": "cc_obligations", + "tableTo": "cc_accounts", + "columnsFrom": [ + "preferred_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_payment_plans": { + "name": "cc_payment_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plan_type": { + "name": "plan_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "horizon_days": { + "name": "horizon_days", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 90 + }, + "starting_balance": { + "name": "starting_balance", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "ending_balance": { + "name": "ending_balance", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "lowest_balance": { + "name": "lowest_balance", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "lowest_balance_date": { + "name": "lowest_balance_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "total_inflows": { + "name": "total_inflows", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "total_outflows": { + "name": "total_outflows", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "total_late_fees_avoided": { + "name": "total_late_fees_avoided", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_late_fees_risked": { + "name": "total_late_fees_risked", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "schedule": { + "name": "schedule", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "warnings": { + "name": "warnings", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'[]'::jsonb" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'draft'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_payment_plans_status": { + "name": "idx_cc_payment_plans_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_plans": { + "name": "cc_plans", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'draft'" + }, + "supersedes_plan_id": { + "name": "supersedes_plan_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "authored_by": { + "name": "authored_by", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "sovereignty_assessment": { + "name": "sovereignty_assessment", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_plans_goal": { + "name": "idx_cc_plans_goal", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_plans_status": { + "name": "idx_cc_plans_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cc_plans_goal_id_cc_goals_id_fk": { + "name": "cc_plans_goal_id_cc_goals_id_fk", + "tableFrom": "cc_plans", + "tableTo": "cc_goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cc_plans_id_goal_id_unique": { + "name": "cc_plans_id_goal_id_unique", + "nullsNotDistinct": false, + "columns": [ + "id", + "goal_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_properties": { + "name": "cc_properties", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chitty_id": { + "name": "chitty_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "property_name": { + "name": "property_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "address": { + "name": "address", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "doorloop_id": { + "name": "doorloop_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "property_type": { + "name": "property_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "monthly_hoa": { + "name": "monthly_hoa", + "type": "numeric(8, 2)", + "primaryKey": false, + "notNull": false + }, + "hoa_payee": { + "name": "hoa_payee", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "annual_tax": { + "name": "annual_tax", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "tax_pin": { + "name": "tax_pin", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mortgage_account_id": { + "name": "mortgage_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mortgage_servicer": { + "name": "mortgage_servicer", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mortgage_account": { + "name": "mortgage_account", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "cc_properties_mortgage_account_id_cc_accounts_id_fk": { + "name": "cc_properties_mortgage_account_id_cc_accounts_id_fk", + "tableFrom": "cc_properties", + "tableTo": "cc_accounts", + "columnsFrom": [ + "mortgage_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cc_properties_tax_pin_unique": { + "name": "cc_properties_tax_pin_unique", + "nullsNotDistinct": false, + "columns": [ + "tax_pin" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_recommendations": { + "name": "cc_recommendations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "obligation_id": { + "name": "obligation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dispute_id": { + "name": "dispute_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rec_type": { + "name": "rec_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "estimated_savings": { + "name": "estimated_savings", + "type": "numeric(10, 2)", + "primaryKey": false, + "notNull": false + }, + "action_type": { + "name": "action_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_payload": { + "name": "action_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "action_url": { + "name": "action_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'active'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "model_version": { + "name": "model_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false + }, + "suggested_account_id": { + "name": "suggested_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "suggested_amount": { + "name": "suggested_amount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": false + }, + "payment_sequence": { + "name": "payment_sequence", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "escalation_risk": { + "name": "escalation_risk", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scenario_impact": { + "name": "scenario_impact", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "acted_on_at": { + "name": "acted_on_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "idx_cc_recommendations_priority": { + "name": "idx_cc_recommendations_priority", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_recommendations_status": { + "name": "idx_cc_recommendations_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cc_recommendations_obligation_id_cc_obligations_id_fk": { + "name": "cc_recommendations_obligation_id_cc_obligations_id_fk", + "tableFrom": "cc_recommendations", + "tableTo": "cc_obligations", + "columnsFrom": [ + "obligation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cc_recommendations_dispute_id_cc_disputes_id_fk": { + "name": "cc_recommendations_dispute_id_cc_disputes_id_fk", + "tableFrom": "cc_recommendations", + "tableTo": "cc_disputes", + "columnsFrom": [ + "dispute_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cc_recommendations_suggested_account_id_cc_accounts_id_fk": { + "name": "cc_recommendations_suggested_account_id_cc_accounts_id_fk", + "tableFrom": "cc_recommendations", + "tableTo": "cc_accounts", + "columnsFrom": [ + "suggested_account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_revenue_sources": { + "name": "cc_revenue_sources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "recurrence": { + "name": "recurrence", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recurrence_day": { + "name": "recurrence_day", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "next_expected_date": { + "name": "next_expected_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "confidence": { + "name": "confidence", + "type": "numeric(3, 2)", + "primaryKey": false, + "notNull": false, + "default": "'0.50'" + }, + "verified_by": { + "name": "verified_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "contract_ref": { + "name": "contract_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'active'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_revenue_sources_next": { + "name": "idx_cc_revenue_sources_next", + "columns": [ + { + "expression": "next_expected_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_revenue_sources_status": { + "name": "idx_cc_revenue_sources_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cc_revenue_sources_account_id_cc_accounts_id_fk": { + "name": "cc_revenue_sources_account_id_cc_accounts_id_fk", + "tableFrom": "cc_revenue_sources", + "tableTo": "cc_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_scrape_jobs": { + "name": "cc_scrape_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chitty_id": { + "name": "chitty_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "job_type": { + "name": "job_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "target": { + "name": "target", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "scheduled_at": { + "name": "scheduled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parent_job_id": { + "name": "parent_job_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cron_source": { + "name": "cron_source", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_scrape_jobs_status": { + "name": "idx_cc_scrape_jobs_status", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scheduled_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_scrape_jobs_type": { + "name": "idx_cc_scrape_jobs_type", + "columns": [ + { + "expression": "job_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_scrape_jobs_chitty": { + "name": "idx_cc_scrape_jobs_chitty", + "columns": [ + { + "expression": "chitty_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_sync_log": { + "name": "cc_sync_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chitty_id": { + "name": "chitty_id", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "sync_type": { + "name": "sync_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "records_synced": { + "name": "records_synced", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_tasks": { + "name": "cc_tasks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notion_page_id": { + "name": "notion_page_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "task_type": { + "name": "task_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'notion'" + }, + "priority": { + "name": "priority", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 5 + }, + "backend_status": { + "name": "backend_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "assigned_to": { + "name": "assigned_to", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "due_date": { + "name": "due_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "verification_type": { + "name": "verification_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'soft'" + }, + "verification_artifact": { + "name": "verification_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verification_notes": { + "name": "verification_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "verified_at": { + "name": "verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "spawned_recommendation_id": { + "name": "spawned_recommendation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "ledger_record_id": { + "name": "ledger_record_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_tasks_status": { + "name": "idx_cc_tasks_status", + "columns": [ + { + "expression": "backend_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_tasks_external_id": { + "name": "idx_cc_tasks_external_id", + "columns": [ + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_tasks_notion_page_id": { + "name": "idx_cc_tasks_notion_page_id", + "columns": [ + { + "expression": "notion_page_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_tasks_due_date": { + "name": "idx_cc_tasks_due_date", + "columns": [ + { + "expression": "due_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_tasks_priority": { + "name": "idx_cc_tasks_priority", + "columns": [ + { + "expression": "priority", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_tasks_type": { + "name": "idx_cc_tasks_type", + "columns": [ + { + "expression": "task_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cc_tasks_spawned_recommendation_id_cc_recommendations_id_fk": { + "name": "cc_tasks_spawned_recommendation_id_cc_recommendations_id_fk", + "tableFrom": "cc_tasks", + "tableTo": "cc_recommendations", + "columnsFrom": [ + "spawned_recommendation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cc_tasks_external_id_unique": { + "name": "cc_tasks_external_id_unique", + "nullsNotDistinct": false, + "columns": [ + "external_id" + ] + }, + "cc_tasks_notion_page_id_unique": { + "name": "cc_tasks_notion_page_id_unique", + "nullsNotDistinct": false, + "columns": [ + "notion_page_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_transactions": { + "name": "cc_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "obligation_id": { + "name": "obligation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "counterparty": { + "name": "counterparty", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount": { + "name": "amount", + "type": "numeric(12, 2)", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tx_date": { + "name": "tx_date", + "type": "date", + "primaryKey": false, + "notNull": true + }, + "posted_at": { + "name": "posted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": { + "idx_cc_transactions_date": { + "name": "idx_cc_transactions_date", + "columns": [ + { + "expression": "tx_date", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_transactions_account": { + "name": "idx_cc_transactions_account", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_cc_transactions_source": { + "name": "idx_cc_transactions_source", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cc_transactions_account_id_cc_accounts_id_fk": { + "name": "cc_transactions_account_id_cc_accounts_id_fk", + "tableFrom": "cc_transactions", + "tableTo": "cc_accounts", + "columnsFrom": [ + "account_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cc_transactions_obligation_id_cc_obligations_id_fk": { + "name": "cc_transactions_obligation_id_cc_obligations_id_fk", + "tableFrom": "cc_transactions", + "tableTo": "cc_obligations", + "columnsFrom": [ + "obligation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cc_user_namespaces": { + "name": "cc_user_namespaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "cc_user_namespaces_user_id_unique": { + "name": "cc_user_namespaces_user_id_unique", + "nullsNotDistinct": false, + "columns": [ + "user_id" + ] + }, + "cc_user_namespaces_namespace_unique": { + "name": "cc_user_namespaces_namespace_unique", + "nullsNotDistinct": false, + "columns": [ + "namespace" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index 1517fd9..320fad2 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -40,8 +40,8 @@ { "idx": 5, "version": "7", - "when": 1780540389182, - "tag": "0005_chief_skin", + "when": 1781091511650, + "tag": "0005_sour_dreadnoughts", "breakpoints": true } ]