@affordance/core 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/dist/engine/compute.d.ts +2 -2
- package/dist/engine/compute.js.map +1 -1
- package/dist/engine/engine.d.ts +14 -22
- package/dist/engine/engine.js +35 -17
- package/dist/engine/engine.js.map +1 -1
- package/dist/errors.d.ts +3 -9
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -1
- package/dist/execution/delta.d.ts +1 -1
- package/dist/execution/delta.js +1 -1
- package/dist/execution/delta.js.map +1 -1
- package/dist/execution/execute.d.ts +19 -18
- package/dist/execution/execute.js +22 -25
- package/dist/execution/execute.js.map +1 -1
- package/dist/execution/index.d.ts +1 -3
- package/dist/execution/index.js +1 -3
- package/dist/execution/index.js.map +1 -1
- package/dist/execution/journal.d.ts +4 -12
- package/dist/execution/journal.js +20 -93
- package/dist/execution/journal.js.map +1 -1
- package/dist/execution/port.d.ts +15 -44
- package/dist/execution/port.js +1 -100
- package/dist/execution/port.js.map +1 -1
- package/dist/execution/replay.d.ts +1 -1
- package/dist/execution/replay.js.map +1 -1
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/ingestion/correlation.d.ts +0 -32
- package/dist/ingestion/correlation.js +1 -77
- package/dist/ingestion/correlation.js.map +1 -1
- package/dist/ingestion/index.d.ts +1 -2
- package/dist/ingestion/index.js +1 -2
- package/dist/ingestion/index.js.map +1 -1
- package/dist/ingestion/ingest.d.ts +10 -16
- package/dist/ingestion/ingest.js +12 -100
- package/dist/ingestion/ingest.js.map +1 -1
- package/dist/migration/migrate.d.ts +6 -7
- package/dist/migration/migrate.js +11 -40
- package/dist/migration/migrate.js.map +1 -1
- package/dist/model/casetype.d.ts +8 -8
- package/dist/model/casetype.js.map +1 -1
- package/dist/model/handler.d.ts +21 -15
- package/dist/model/handler.js.map +1 -1
- package/dist/model/index.d.ts +3 -3
- package/dist/model/index.js +1 -1
- package/dist/model/index.js.map +1 -1
- package/dist/model/step.d.ts +18 -13
- package/dist/model/step.js +2 -1
- package/dist/model/step.js.map +1 -1
- package/dist/model/target.d.ts +12 -15
- package/dist/model/target.js +2 -5
- package/dist/model/target.js.map +1 -1
- package/dist/storage.d.ts +93 -0
- package/dist/storage.js +4 -0
- package/dist/storage.js.map +1 -0
- package/dist/store/ids.d.ts +1 -2
- package/dist/store/ids.js +1 -2
- package/dist/store/ids.js.map +1 -1
- package/dist/store/index.d.ts +3 -8
- package/dist/store/index.js +2 -6
- package/dist/store/index.js.map +1 -1
- package/dist/store/resolve.d.ts +7 -14
- package/dist/store/resolve.js +4 -11
- package/dist/store/resolve.js.map +1 -1
- package/dist/store/store.d.ts +4 -41
- package/dist/store/store.js +1 -95
- package/dist/store/store.js.map +1 -1
- package/package.json +8 -8
- package/dist/execution/transaction.d.ts +0 -24
- package/dist/execution/transaction.js +0 -49
- package/dist/execution/transaction.js.map +0 -1
- package/dist/store/bootstrap.d.ts +0 -57
- package/dist/store/bootstrap.js +0 -268
- package/dist/store/bootstrap.js.map +0 -1
- package/dist/store/queryable.d.ts +0 -60
- package/dist/store/queryable.js +0 -7
- package/dist/store/queryable.js.map +0 -1
- package/dist/store/sql.d.ts +0 -26
- package/dist/store/sql.js +0 -21
- package/dist/store/sql.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journal.js","sourceRoot":"","sources":["../../src/execution/journal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The journal: the immutable, append-only record of a case's Executions\n * (CONTEXT.md).\n *\n * Historical questions are answered from what the system actually believed at\n * the time, never by re-deriving the past through present-day code — so a\n * `claimed` entry stores the guard evaluation *and* the Case State it was\n * evaluated against, and every entry repeats the Execution's identity (step,\n * scope key, actor). Per-track audit — \"everything that happened on buyer\n * #7\" — is therefore a filter, not a reconstruction.\n *\n * Only inserts exist in this module. There is no update or delete path for a\n * journal row anywhere in the library.\n */\n\nimport type { JournalEntryKind } from '@affordance/contract'\nimport type { GuardEvaluation } from '../guards/index.js'\nimport type { Queryable } from '../store/index.js'\nimport { FRAMEWORK_SCHEMA, mintId, sqlWhere } from '../store/index.js'\nimport type { StateDelta } from './delta.js'\n\nconst JOURNAL = `${FRAMEWORK_SCHEMA}.journal`\nconst JOURNAL_COLUMNS =\n 'ordinal, id, case_id, execution_id, entry, attempt, step, scope_key, actor, input, as_of, guard, state, delta, dormancy, error, recorded_at'\n\n/**\n * Which lifecycle moment an entry records.\n *\n * - `claimed` — the claim's transactional guard re-evaluation passed and the\n * Execution took the case; carries `guard`, `asOf` and `state`\n * - `attempt-failed` — one attempt threw and another will follow\n * - `completed` — the handler's Case State was committed; carries `delta`\n * - `failed` — retries exhausted (or a deterministic defect); case released\n * - `expired` — the claim lapsed without a terminal entry: the handler's\n * process died, and a later claimant recorded the abandonment\n */\nexport type JournalEntryType = JournalEntryKind\n\n/** A failure as journaled — the error's identity, not a live Error object. */\nexport interface JournalError {\n readonly name: string\n readonly message: string\n}\n\n/** One journal entry, JSON-serializable throughout (timestamps are ISO-8601 UTC). */\nexport interface JournalEntry {\n /** Total insertion order across all cases; per-case order is `(caseId, ordinal)`. */\n readonly ordinal: number\n readonly id: string\n readonly caseId: string\n /** The Execution this entry belongs to — several entries share one. */\n readonly executionId: string\n readonly entry: JournalEntryType\n /** 1-based attempt this entry is about. */\n readonly attempt: number\n readonly step: string\n /** The bound scope key, or `null` for an unscoped step. */\n readonly scopeKey: string | null\n /** The acting Actor, as supplied by the app. */\n readonly actor: unknown\n /** The step input, post-validation (schema output), or `null`. */\n readonly input: unknown\n /** The instant the claim's guard re-evaluation was made as of, on `claimed` entries. */\n readonly asOf: string | null\n /** The claim-time guard evaluation — the enforcement moment's full record. */\n readonly guard: GuardEvaluation | null\n /** The Case State the guard was evaluated against, on `claimed` entries. */\n readonly state: unknown\n /** The committed state delta, on `completed` entries. */\n readonly delta: StateDelta | null\n /** `end()` / `reopen()` called by the handler, on `completed` entries. */\n readonly dormancy: 'ended' | 'reopened' | null\n /** The failure, on `attempt-failed` / `failed` / `expired` entries. */\n readonly error: JournalError | null\n readonly recordedAt: string\n}\n\n/** The identity every journal entry carries, whatever its kind. */\ninterface JournalEntryIdentity {\n readonly caseId: string\n readonly executionId: string\n readonly attempt: number\n readonly step: string\n readonly scopeKey?: string | null\n readonly actor?: unknown\n readonly input?: unknown\n}\n\n/**\n * A `claimed` entry records the enforcement moment, so the evidence is\n * required: the instant, the guard evaluation, and the Case State it ran\n * against.\n */\nexport interface ClaimedEntryInput extends JournalEntryIdentity {\n readonly entry: 'claimed'\n readonly asOf: string\n readonly guard: GuardEvaluation\n readonly state: unknown\n}\n\n/** A `completed` entry records what the commit changed. */\nexport interface CompletedEntryInput extends JournalEntryIdentity {\n readonly entry: 'completed'\n readonly delta: StateDelta\n readonly dormancy?: 'ended' | 'reopened' | null\n}\n\n/** Every way an Execution stops without committing carries the failure that stopped it. */\nexport interface FailureEntryInput extends JournalEntryIdentity {\n readonly entry: 'attempt-failed' | 'failed' | 'expired'\n readonly error: JournalError\n}\n\n/**\n * What {@link appendEntry} needs — a discriminated union on `entry`, so\n * which fields accompany which lifecycle moment is stated by the type\n * itself rather than re-derived from prose by every reader.\n * `{ entry: 'failed', guard, delta }` is unrepresentable rather than\n * quietly journaled.\n */\nexport type JournalEntryInput =\n | ClaimedEntryInput\n | CompletedEntryInput\n | FailureEntryInput\n\n/**\n * A `claimed` entry as read back, with the enforcement-moment evidence\n * present — what {@link appendEntry}'s input union guarantees was written.\n */\nexport type ClaimedJournalEntry = JournalEntry & {\n readonly entry: 'claimed'\n readonly asOf: string\n readonly guard: GuardEvaluation\n}\n\n/**\n * Narrow a read entry to the claimed moment. The one predicate every reader\n * of claim-time evidence (`foldExecutions`, audit replay) shares, so what\n * counts as \"carries the evidence\" is decided once.\n */\nexport const isClaimedEntry = (\n entry: JournalEntry,\n): entry is ClaimedJournalEntry =>\n entry.entry === 'claimed' && entry.guard !== null && entry.asOf !== null\n\n/** Filters for {@link readJournal}; all optional, all AND-ed. */\nexport interface JournalFilter {\n /** Per-track audit: only entries bound to this scope key. */\n readonly scopeKey?: string\n /** Only entries for this step. */\n readonly step?: string\n /** Only entries belonging to this Execution. */\n readonly executionId?: string\n /** Only these entry types. */\n readonly entry?: JournalEntryType | readonly JournalEntryType[]\n /** Only entries after this ordinal (exclusive) — cursor paging. */\n readonly since?: number\n /** Cap the number of entries returned; the oldest matching entries win. */\n readonly limit?: number\n}\n\ntype JournalRow = {\n ordinal: string | number\n id: string\n case_id: string\n execution_id: string\n entry: string\n attempt: number\n step: string\n scope_key: string | null\n actor: unknown\n input: unknown\n as_of: Date | null\n guard: GuardEvaluation | null\n state: unknown\n delta: StateDelta | null\n dormancy: string | null\n error: JournalError | null\n recorded_at: Date\n}\n\nconst toEntry = (row: JournalRow): JournalEntry => ({\n ordinal: Number(row.ordinal),\n id: row.id,\n caseId: row.case_id,\n executionId: row.execution_id,\n entry: row.entry as JournalEntryType,\n attempt: row.attempt,\n step: row.step,\n scopeKey: row.scope_key,\n actor: row.actor,\n input: row.input,\n asOf: row.as_of === null ? null : row.as_of.toISOString(),\n guard: row.guard,\n state: row.state,\n delta: row.delta,\n dormancy: row.dormancy as 'ended' | 'reopened' | null,\n error: row.error,\n recordedAt: row.recorded_at.toISOString(),\n})\n\n/**\n * Serialize a value for a jsonb column. Actors and inputs are app-owned\n * shapes, and a journal append must never be the thing that fails an\n * otherwise-good Execution: a value that will not stringify (a cycle, a\n * BigInt) is journaled as a marker string rather than thrown over.\n */\nconst toJsonb = (value: unknown): string | null => {\n if (value === undefined || value === null) return null\n try {\n const json = JSON.stringify(value)\n return json === undefined ? null : json\n } catch {\n return JSON.stringify({ '~unserializable': String(value) })\n }\n}\n\n/** A stored entry minus what storage assigns: `ordinal`, `id`, `recordedAt`. */\nexport type JournalEntryColumns = Omit<\n JournalEntry,\n 'ordinal' | 'id' | 'recordedAt'\n>\n\n/**\n * Project an input onto a stored entry's fields — the one statement of the\n * defaulting and of which fields accompany which lifecycle moment. Every\n * adapter persists exactly this and assigns the rest; an adapter that could\n * disagree with another about what a `failed` entry looks like would be\n * a second, divergent copy of the journal's semantics.\n */\nexport const projectEntry = (input: JournalEntryInput): JournalEntryColumns => {\n const claimed = input.entry === 'claimed' ? input : null\n const completed = input.entry === 'completed' ? input : null\n const failure =\n input.entry !== 'claimed' && input.entry !== 'completed' ? input : null\n return {\n caseId: input.caseId,\n executionId: input.executionId,\n entry: input.entry,\n attempt: input.attempt,\n step: input.step,\n scopeKey: input.scopeKey ?? null,\n actor: input.actor ?? null,\n input: input.input ?? null,\n asOf: claimed?.asOf ?? null,\n guard: claimed?.guard ?? null,\n state: claimed?.state ?? null,\n delta: completed?.delta ?? null,\n dormancy: completed?.dormancy ?? null,\n error: failure?.error ?? null,\n }\n}\n\n/** Append one entry. Inserts only — journal rows are never updated or deleted. */\nexport const appendEntry = async (\n db: Queryable,\n input: JournalEntryInput,\n): Promise<JournalEntry> => {\n const entry = projectEntry(input)\n const { rows } = await db.query<JournalRow>(\n `insert into ${JOURNAL}\n (id, case_id, execution_id, entry, attempt, step, scope_key, actor, input, as_of, guard, state, delta, dormancy, error)\n values ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10::timestamptz, $11::jsonb, $12::jsonb, $13::jsonb, $14, $15::jsonb)\n returning ${JOURNAL_COLUMNS}`,\n [\n mintId('journal'),\n entry.caseId,\n entry.executionId,\n entry.entry,\n entry.attempt,\n entry.step,\n entry.scopeKey,\n toJsonb(entry.actor),\n toJsonb(entry.input),\n entry.asOf,\n toJsonb(entry.guard),\n toJsonb(entry.state),\n toJsonb(entry.delta),\n entry.dormancy,\n toJsonb(entry.error),\n ],\n )\n const row = rows[0]\n if (!row) throw new Error(`insert into ${JOURNAL} returned no row`)\n return toEntry(row)\n}\n\n/**\n * Read a case's journal in insertion order, oldest first. With no filter this\n * is the whole story of the case; with `scopeKey` it is one track's audit.\n */\nexport const readJournal = async (\n db: Queryable,\n caseId: string,\n filter: JournalFilter = {},\n): Promise<readonly JournalEntry[]> => {\n const { conditions, values, bind, where } = sqlWhere(\n ['case_id = $1'],\n [caseId],\n )\n\n if (filter.scopeKey !== undefined)\n conditions.push(`scope_key = ${bind(filter.scopeKey)}`)\n if (filter.step !== undefined) conditions.push(`step = ${bind(filter.step)}`)\n if (filter.executionId !== undefined)\n conditions.push(`execution_id = ${bind(filter.executionId)}`)\n if (filter.entry !== undefined) {\n const entries = Array.isArray(filter.entry) ? filter.entry : [filter.entry]\n conditions.push(`entry = any(${bind(entries)}::text[])`)\n }\n if (filter.since !== undefined)\n conditions.push(`ordinal > ${bind(filter.since)}`)\n\n const limit = filter.limit === undefined ? '' : ` limit ${bind(filter.limit)}`\n const { rows } = await db.query<JournalRow>(\n `select ${JOURNAL_COLUMNS} from ${JOURNAL}\n where ${where()}\n order by ordinal asc${limit}`,\n values,\n )\n return rows.map(toEntry)\n}\n\n/** How an Execution ended up, folded from its entries. */\nexport type ExecutionStatus = 'in-progress' | 'completed' | 'failed' | 'expired'\n\n/**\n * One Execution as a single record: its identity, the claim-time evidence,\n * and how it settled. This is a fold over entries of the *same* Execution —\n * assembling one record from the moments that constitute it, not deriving\n * state from a log (the design rejects the latter, not the former).\n */\nexport interface ExecutionRecord {\n readonly executionId: string\n readonly caseId: string\n readonly step: string\n readonly scopeKey: string | null\n readonly actor: unknown\n readonly input: unknown\n readonly status: ExecutionStatus\n /** Attempts observed — the highest attempt number any of its entries carries. */\n readonly attempts: number\n readonly asOf: string | null\n readonly guard: GuardEvaluation | null\n /** The Case State the claim's guard was evaluated against. */\n readonly state: unknown\n readonly delta: StateDelta | null\n readonly dormancy: 'ended' | 'reopened' | null\n readonly error: JournalError | null\n readonly claimedAt: string | null\n /** When the Execution reached a terminal entry; `null` while in progress. */\n readonly settledAt: string | null\n}\n\nconst TERMINAL: Record<string, ExecutionStatus | undefined> = {\n completed: 'completed',\n failed: 'failed',\n expired: 'expired',\n}\n\n/**\n * Fold journal entries into one record per Execution, in first-appearance\n * order. Feed it a filtered read (by scope key, say) to get that track's\n * Executions.\n */\nexport const foldExecutions = (\n entries: readonly JournalEntry[],\n): readonly ExecutionRecord[] => {\n const byExecution = new Map<string, ExecutionRecord>()\n for (const entry of entries) {\n const previous = byExecution.get(entry.executionId)\n const terminal = TERMINAL[entry.entry]\n const claimed = isClaimedEntry(entry) ? entry : null\n const base: ExecutionRecord = previous ?? {\n executionId: entry.executionId,\n caseId: entry.caseId,\n step: entry.step,\n scopeKey: entry.scopeKey,\n actor: entry.actor,\n input: entry.input,\n status: 'in-progress',\n attempts: entry.attempt,\n asOf: null,\n guard: null,\n state: undefined,\n delta: null,\n dormancy: null,\n error: null,\n claimedAt: null,\n settledAt: null,\n }\n byExecution.set(entry.executionId, {\n ...base,\n attempts: Math.max(base.attempts, entry.attempt),\n status: terminal ?? base.status,\n asOf: claimed !== null ? claimed.asOf : base.asOf,\n guard: claimed !== null ? claimed.guard : base.guard,\n state: claimed !== null ? claimed.state : base.state,\n delta: entry.delta ?? base.delta,\n dormancy: entry.dormancy ?? base.dormancy,\n // The terminal error is the one that matters; an attempt-failed error\n // only stands while nothing has superseded it.\n error: entry.error ?? base.error,\n claimedAt: claimed !== null ? claimed.recordedAt : base.claimedAt,\n settledAt: terminal === undefined ? base.settledAt : entry.recordedAt,\n })\n }\n return [...byExecution.values()]\n}\n"]}
|
|
1
|
+
{"version":3,"file":"journal.js","sourceRoot":"","sources":["../../src/execution/journal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The journal: the immutable, append-only record of a case's Executions\n * (CONTEXT.md).\n *\n * Historical questions are answered from what the system actually believed at\n * the time, never by re-deriving the past through present-day code — so a\n * `claimed` entry stores the guard evaluation *and* the Case State it was\n * evaluated against, and every entry repeats the Execution's identity (step,\n * scope key, actor). Per-track audit — \"everything that happened on buyer\n * #7\" — is therefore a filter, not a reconstruction.\n *\n * This module defines journal evidence and its projections. Storage adapters\n * append and read the records; the engine never rewrites historical entries.\n */\n\nimport type { GuardEvaluation } from '../guards/index.js'\nimport type { StateDelta } from './delta.js'\n\n/**\n * Which lifecycle moment an entry records.\n *\n * - `claimed` — the claim's transactional guard re-evaluation passed and the\n * Execution took the case; carries `guard`, `asOf` and `state`\n * - `attempt-failed` — one attempt threw and another will follow\n * - `completed` — the handler's Case State was committed; carries `delta`\n * - `failed` — retries exhausted (or a deterministic defect); case released\n * - `expired` — the claim lapsed without a terminal entry: the handler's\n * process died, and a later claimant recorded the abandonment\n */\nexport const JOURNAL_ENTRY_KINDS = [\n 'claimed',\n 'attempt-failed',\n 'completed',\n 'failed',\n 'expired',\n] as const\n\nexport type JournalEntryType = (typeof JOURNAL_ENTRY_KINDS)[number]\n\n/** A failure as journaled — the error's identity, not a live Error object. */\nexport interface JournalError {\n readonly name: string\n readonly message: string\n}\n\n/** One journal entry, JSON-serializable throughout (timestamps are ISO-8601 UTC). */\nexport interface JournalEntry {\n /** Total insertion order across all cases; per-case order is `(caseId, ordinal)`. */\n readonly ordinal: number\n readonly id: string\n readonly caseId: string\n /** The Execution this entry belongs to — several entries share one. */\n readonly executionId: string\n readonly entry: JournalEntryType\n /** 1-based attempt this entry is about. */\n readonly attempt: number\n readonly step: string\n /** The bound scope key, or `null` for an unscoped step. */\n readonly scopeKey: string | null\n /** The acting Actor, as supplied by the app. */\n readonly actor: unknown\n /** The step input, post-validation (schema output), or `null`. */\n readonly input: unknown\n /** The instant the claim's guard re-evaluation was made as of, on `claimed` entries. */\n readonly asOf: string | null\n /** The claim-time guard evaluation — the enforcement moment's full record. */\n readonly guard: GuardEvaluation | null\n /** The Case State the guard was evaluated against, on `claimed` entries. */\n readonly state: unknown\n /** The committed state delta, on `completed` entries. */\n readonly delta: StateDelta | null\n /** `end()` / `reopen()` called by the handler, on `completed` entries. */\n readonly dormancy: 'ended' | 'reopened' | null\n /** The failure, on `attempt-failed` / `failed` / `expired` entries. */\n readonly error: JournalError | null\n readonly recordedAt: string\n}\n\n/** The identity every journal entry carries, whatever its kind. */\ninterface JournalEntryIdentity {\n readonly caseId: string\n readonly executionId: string\n readonly attempt: number\n readonly step: string\n readonly scopeKey?: string | null\n readonly actor?: unknown\n readonly input?: unknown\n}\n\n/**\n * A `claimed` entry records the enforcement moment, so the evidence is\n * required: the instant, the guard evaluation, and the Case State it ran\n * against.\n */\nexport interface ClaimedEntryInput extends JournalEntryIdentity {\n readonly entry: 'claimed'\n readonly asOf: string\n readonly guard: GuardEvaluation\n readonly state: unknown\n}\n\n/** A `completed` entry records what the commit changed. */\nexport interface CompletedEntryInput extends JournalEntryIdentity {\n readonly entry: 'completed'\n readonly delta: StateDelta\n readonly dormancy?: 'ended' | 'reopened' | null\n}\n\n/** Every way an Execution stops without committing carries the failure that stopped it. */\nexport interface FailureEntryInput extends JournalEntryIdentity {\n readonly entry: 'attempt-failed' | 'failed' | 'expired'\n readonly error: JournalError\n}\n\n/**\n * What {@link appendEntry} needs — a discriminated union on `entry`, so\n * which fields accompany which lifecycle moment is stated by the type\n * itself rather than re-derived from prose by every reader.\n * `{ entry: 'failed', guard, delta }` is unrepresentable rather than\n * quietly journaled.\n */\nexport type JournalEntryInput =\n | ClaimedEntryInput\n | CompletedEntryInput\n | FailureEntryInput\n\n/**\n * A `claimed` entry as read back, with the enforcement-moment evidence\n * present — what {@link appendEntry}'s input union guarantees was written.\n */\nexport type ClaimedJournalEntry = JournalEntry & {\n readonly entry: 'claimed'\n readonly asOf: string\n readonly guard: GuardEvaluation\n}\n\n/**\n * Narrow a read entry to the claimed moment. The one predicate every reader\n * of claim-time evidence (`foldExecutions`, audit replay) shares, so what\n * counts as \"carries the evidence\" is decided once.\n */\nexport const isClaimedEntry = (\n entry: JournalEntry,\n): entry is ClaimedJournalEntry =>\n entry.entry === 'claimed' && entry.guard !== null && entry.asOf !== null\n\n/** Filters for {@link readJournal}; all optional, all AND-ed. */\nexport interface JournalFilter {\n /** Per-track audit: only entries bound to this scope key. */\n readonly scopeKey?: string\n /** Only entries for this step. */\n readonly step?: string\n /** Only entries belonging to this Execution. */\n readonly executionId?: string\n /** Only these entry types. */\n readonly entry?: JournalEntryType | readonly JournalEntryType[]\n /** Only entries after this ordinal (exclusive) — cursor paging. */\n readonly since?: number\n /** Cap the number of entries returned; the oldest matching entries win. */\n readonly limit?: number\n}\n\n/** A stored entry minus what storage assigns: `ordinal`, `id`, `recordedAt`. */\nexport type JournalEntryColumns = Omit<\n JournalEntry,\n 'ordinal' | 'id' | 'recordedAt'\n>\n\n/**\n * Project an input onto a stored entry's fields — the one statement of the\n * defaulting and of which fields accompany which lifecycle moment. Every\n * adapter persists exactly this and assigns the rest; an adapter that could\n * disagree with another about what a `failed` entry looks like would be\n * a second, divergent copy of the journal's semantics.\n */\nexport const projectEntry = (input: JournalEntryInput): JournalEntryColumns => {\n const claimed = input.entry === 'claimed' ? input : null\n const completed = input.entry === 'completed' ? input : null\n const failure =\n input.entry !== 'claimed' && input.entry !== 'completed' ? input : null\n return {\n caseId: input.caseId,\n executionId: input.executionId,\n entry: input.entry,\n attempt: input.attempt,\n step: input.step,\n scopeKey: input.scopeKey ?? null,\n actor: input.actor ?? null,\n input: input.input ?? null,\n asOf: claimed?.asOf ?? null,\n guard: claimed?.guard ?? null,\n state: claimed?.state ?? null,\n delta: completed?.delta ?? null,\n dormancy: completed?.dormancy ?? null,\n error: failure?.error ?? null,\n }\n}\n\n/** How an Execution ended up, folded from its entries. */\nexport type ExecutionStatus = 'in-progress' | 'completed' | 'failed' | 'expired'\n\n/**\n * One Execution as a single record: its identity, the claim-time evidence,\n * and how it settled. This is a fold over entries of the *same* Execution —\n * assembling one record from the moments that constitute it, not deriving\n * state from a log (the design rejects the latter, not the former).\n */\nexport interface ExecutionRecord {\n readonly executionId: string\n readonly caseId: string\n readonly step: string\n readonly scopeKey: string | null\n readonly actor: unknown\n readonly input: unknown\n readonly status: ExecutionStatus\n /** Attempts observed — the highest attempt number any of its entries carries. */\n readonly attempts: number\n readonly asOf: string | null\n readonly guard: GuardEvaluation | null\n /** The Case State the claim's guard was evaluated against. */\n readonly state: unknown\n readonly delta: StateDelta | null\n readonly dormancy: 'ended' | 'reopened' | null\n readonly error: JournalError | null\n readonly claimedAt: string | null\n /** When the Execution reached a terminal entry; `null` while in progress. */\n readonly settledAt: string | null\n}\n\nconst TERMINAL: Record<string, ExecutionStatus | undefined> = {\n completed: 'completed',\n failed: 'failed',\n expired: 'expired',\n}\n\n/**\n * Fold journal entries into one record per Execution, in first-appearance\n * order. Feed it a filtered read (by scope key, say) to get that track's\n * Executions.\n */\nexport const foldExecutions = (\n entries: readonly JournalEntry[],\n): readonly ExecutionRecord[] => {\n const byExecution = new Map<string, ExecutionRecord>()\n for (const entry of entries) {\n const previous = byExecution.get(entry.executionId)\n const terminal = TERMINAL[entry.entry]\n const claimed = isClaimedEntry(entry) ? entry : null\n const base: ExecutionRecord = previous ?? {\n executionId: entry.executionId,\n caseId: entry.caseId,\n step: entry.step,\n scopeKey: entry.scopeKey,\n actor: entry.actor,\n input: entry.input,\n status: 'in-progress',\n attempts: entry.attempt,\n asOf: null,\n guard: null,\n state: undefined,\n delta: null,\n dormancy: null,\n error: null,\n claimedAt: null,\n settledAt: null,\n }\n byExecution.set(entry.executionId, {\n ...base,\n attempts: Math.max(base.attempts, entry.attempt),\n status: terminal ?? base.status,\n asOf: claimed !== null ? claimed.asOf : base.asOf,\n guard: claimed !== null ? claimed.guard : base.guard,\n state: claimed !== null ? claimed.state : base.state,\n delta: entry.delta ?? base.delta,\n dormancy: entry.dormancy ?? base.dormancy,\n // The terminal error is the one that matters; an attempt-failed error\n // only stands while nothing has superseded it.\n error: entry.error ?? base.error,\n claimedAt: claimed !== null ? claimed.recordedAt : base.claimedAt,\n settledAt: terminal === undefined ? base.settledAt : entry.recordedAt,\n })\n }\n return [...byExecution.values()]\n}\n"]}
|
package/dist/execution/port.d.ts
CHANGED
|
@@ -1,26 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* An **internal** seam, deliberately. `docs/architecture.md` stands:
|
|
6
|
-
* Postgres is a hard dependency and there is no public storage-adapter
|
|
7
|
-
* abstraction — an app never sees this interface. It exists because the
|
|
8
|
-
* claim state machine (busy vs. takeover, the holder check, retry
|
|
9
|
-
* classification, per-attempt write discard) upholds more always-true
|
|
10
|
-
* rules in one place than anything else in the package, and testing those
|
|
11
|
-
* rules means staging precise situations — an expired lease, a takeover
|
|
12
|
-
* mid-run — that should not require a running database. Two adapters make
|
|
13
|
-
* the seam real: the pg one below for production, and the in-memory one in
|
|
14
|
-
* `test/execution/memory-port.ts` for the tests.
|
|
15
|
-
*
|
|
16
|
-
* The shape keeps the lifecycle's transactional structure explicit:
|
|
17
|
-
* {@link LifecyclePort.withCaseLock} is "one short transaction holding the
|
|
18
|
-
* case row's lock" — the claim and the commit are each exactly one of those
|
|
19
|
-
* — and everything else deliberately runs outside any transaction, because
|
|
20
|
-
* a handler is running and the lease, not a lock, carries exclusivity.
|
|
1
|
+
/** Storage mechanics for claim → run → commit, implemented by each adapter.
|
|
2
|
+
* Claims are exclusive per case. Expiry is judged by storage's authoritative clock.
|
|
3
|
+
* Commit checks ownership, so an expired but unreplaced claim may still commit.
|
|
4
|
+
* State, effects, completed evidence and claim removal share one atomic operation.
|
|
21
5
|
*/
|
|
22
|
-
import type {
|
|
23
|
-
import type {
|
|
6
|
+
import type { CommitEffect } from '../model/handler.js';
|
|
7
|
+
import type { Dormancy, StoredCase } from '../store/store.js';
|
|
24
8
|
import type { JournalEntry, JournalEntryInput } from './journal.js';
|
|
25
9
|
/** The claim sitting on a case, as the lifecycle needs to judge it. */
|
|
26
10
|
export interface HeldClaim {
|
|
@@ -33,22 +17,9 @@ export interface HeldClaim {
|
|
|
33
17
|
readonly expired: boolean;
|
|
34
18
|
}
|
|
35
19
|
/** What the lifecycle asks of storage inside one case-locked transaction. */
|
|
36
|
-
export interface LifecycleTx {
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
* up, stored state validated against its schema — the serialization
|
|
40
|
-
* point. Returning the resolved triple rather than a raw row is what
|
|
41
|
-
* keeps "load, resolve, validate, in that order" spelled once (in
|
|
42
|
-
* `store/resolve.ts`) instead of re-derived by each adapter's caller.
|
|
43
|
-
*/
|
|
44
|
-
readonly loadCase: () => Promise<ResolvedCase>;
|
|
45
|
-
/**
|
|
46
|
-
* Take the case row's lock without interpreting the row — the commit's
|
|
47
|
-
* serialization point. The commit already holds everything it computed at
|
|
48
|
-
* the claim; what it needs from the row is only the lock (and proof the
|
|
49
|
-
* row exists), never a second read-and-validate of the state document.
|
|
50
|
-
*/
|
|
51
|
-
readonly lockCase: () => Promise<void>;
|
|
20
|
+
export interface LifecycleTx<TCommit = unknown> {
|
|
21
|
+
/** Read unvalidated state inside the case's atomic operation. */
|
|
22
|
+
readonly loadCase: () => Promise<StoredCase>;
|
|
52
23
|
/** The claim on the case, `null` when nobody holds it. */
|
|
53
24
|
readonly currentClaim: () => Promise<HeldClaim | null>;
|
|
54
25
|
readonly insertClaim: (executionId: string, step: string, scopeKey: string | null, ttlMs: number) => Promise<{
|
|
@@ -62,12 +33,14 @@ export interface LifecycleTx {
|
|
|
62
33
|
readonly endedAt: string | null;
|
|
63
34
|
}>;
|
|
64
35
|
/** The app's own `ctx.onCommit` writes, riding the same transaction. */
|
|
65
|
-
readonly
|
|
36
|
+
readonly applyEffects: (writes: readonly CommitEffect<TCommit>[]) => Promise<void>;
|
|
66
37
|
}
|
|
67
38
|
/** The verbs the execution lifecycle needs from storage. */
|
|
68
|
-
export interface LifecyclePort {
|
|
69
|
-
/**
|
|
70
|
-
|
|
39
|
+
export interface LifecyclePort<TCommit = unknown> {
|
|
40
|
+
/** Serialize this case and commit all writes on return; roll everything back on throw.
|
|
41
|
+
* The callback runs once. Serialization conflicts must throw, not replay callbacks.
|
|
42
|
+
* No operation spans a handler. Missing cases throw CaseNotFoundError. */
|
|
43
|
+
readonly withCase: <T>(caseId: string, fn: (tx: LifecycleTx<TCommit>) => Promise<T>) => Promise<T>;
|
|
71
44
|
/** Journal outside any transaction — `attempt-failed` / `failed` entries. */
|
|
72
45
|
readonly appendEntry: (input: JournalEntryInput) => Promise<JournalEntry>;
|
|
73
46
|
/** Refresh the lease. Best-effort: a failed beat just lets the claim age. */
|
|
@@ -77,5 +50,3 @@ export interface LifecyclePort {
|
|
|
77
50
|
/** Delete this Execution's claim — scoped to `executionId`, so releasing a lease we no longer hold is a no-op. */
|
|
78
51
|
readonly releaseClaim: (caseId: string, executionId: string) => Promise<void>;
|
|
79
52
|
}
|
|
80
|
-
/** The production adapter: each port verb implemented as SQL over the claims, journal and cases tables. */
|
|
81
|
-
export declare const pgLifecyclePort: (db: DatabaseAccess, caseTypeFor: CaseTypeLookup) => LifecyclePort;
|
package/dist/execution/port.js
CHANGED
|
@@ -1,101 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* The lifecycle port: what claim → run → commit asks of storage, and nothing
|
|
3
|
-
* else.
|
|
4
|
-
*
|
|
5
|
-
* An **internal** seam, deliberately. `docs/architecture.md` stands:
|
|
6
|
-
* Postgres is a hard dependency and there is no public storage-adapter
|
|
7
|
-
* abstraction — an app never sees this interface. It exists because the
|
|
8
|
-
* claim state machine (busy vs. takeover, the holder check, retry
|
|
9
|
-
* classification, per-attempt write discard) upholds more always-true
|
|
10
|
-
* rules in one place than anything else in the package, and testing those
|
|
11
|
-
* rules means staging precise situations — an expired lease, a takeover
|
|
12
|
-
* mid-run — that should not require a running database. Two adapters make
|
|
13
|
-
* the seam real: the pg one below for production, and the in-memory one in
|
|
14
|
-
* `test/execution/memory-port.ts` for the tests.
|
|
15
|
-
*
|
|
16
|
-
* The shape keeps the lifecycle's transactional structure explicit:
|
|
17
|
-
* {@link LifecyclePort.withCaseLock} is "one short transaction holding the
|
|
18
|
-
* case row's lock" — the claim and the commit are each exactly one of those
|
|
19
|
-
* — and everything else deliberately runs outside any transaction, because
|
|
20
|
-
* a handler is running and the lease, not a lock, carries exclusivity.
|
|
21
|
-
*/
|
|
22
|
-
import { CaseNotFoundError, FRAMEWORK_SCHEMA, queryableOf, resolveCaseForUpdate, updateCaseState, } from '../store/index.js';
|
|
23
|
-
import { appendEntry } from './journal.js';
|
|
24
|
-
import { withTransaction } from './transaction.js';
|
|
25
|
-
const CASES = `${FRAMEWORK_SCHEMA}.cases`;
|
|
26
|
-
const CLAIMS = `${FRAMEWORK_SCHEMA}.claims`;
|
|
27
|
-
/** `now() + <ms>` as a SQL expression against a bound millisecond parameter. */
|
|
28
|
-
const expiryExpression = (parameter) => `now() + (${parameter}::double precision * interval '1 millisecond')`;
|
|
29
|
-
/** The production adapter: each port verb implemented as SQL over the claims, journal and cases tables. */
|
|
30
|
-
export const pgLifecyclePort = (db, caseTypeFor) => {
|
|
31
|
-
// The lease verbs are single self-contained statements; only the
|
|
32
|
-
// case-locked transactions care which arm of the access the caller brought.
|
|
33
|
-
const q = queryableOf(db);
|
|
34
|
-
return {
|
|
35
|
-
withCaseLock: (caseId, fn) => withTransaction(db, (tx) => fn({
|
|
36
|
-
loadCase: () => resolveCaseForUpdate(tx, caseTypeFor, caseId),
|
|
37
|
-
lockCase: async () => {
|
|
38
|
-
const { rows } = await tx.query(`select id from ${CASES} where id = $1 for update`, [caseId]);
|
|
39
|
-
if (rows.length === 0)
|
|
40
|
-
throw new CaseNotFoundError(caseId);
|
|
41
|
-
},
|
|
42
|
-
currentClaim: async () => {
|
|
43
|
-
const { rows } = await tx.query(`select execution_id, step, scope_key, attempt, expires_at, expires_at <= now() as expired
|
|
44
|
-
from ${CLAIMS} where case_id = $1`, [caseId]);
|
|
45
|
-
const row = rows[0];
|
|
46
|
-
if (!row)
|
|
47
|
-
return null;
|
|
48
|
-
return {
|
|
49
|
-
executionId: row.execution_id,
|
|
50
|
-
step: row.step,
|
|
51
|
-
scopeKey: row.scope_key,
|
|
52
|
-
attempt: row.attempt,
|
|
53
|
-
expiresAt: row.expires_at.toISOString(),
|
|
54
|
-
expired: row.expired,
|
|
55
|
-
};
|
|
56
|
-
},
|
|
57
|
-
insertClaim: async (executionId, step, scopeKey, ttlMs) => {
|
|
58
|
-
const { rows } = await tx.query(`insert into ${CLAIMS} (case_id, execution_id, step, scope_key, expires_at)
|
|
59
|
-
values ($1, $2, $3, $4, ${expiryExpression('$5')})
|
|
60
|
-
returning claimed_at`, [caseId, executionId, step, scopeKey, ttlMs]);
|
|
61
|
-
return {
|
|
62
|
-
claimedAt: rows[0]?.claimed_at.toISOString() ?? new Date().toISOString(),
|
|
63
|
-
};
|
|
64
|
-
},
|
|
65
|
-
deleteClaim: async (executionId) => {
|
|
66
|
-
await tx.query(`delete from ${CLAIMS} where case_id = $1 and execution_id = $2`, [caseId, executionId]);
|
|
67
|
-
},
|
|
68
|
-
appendEntry: (input) => appendEntry(tx, input),
|
|
69
|
-
updateCaseState: async (state, dormancy) => {
|
|
70
|
-
const updated = await updateCaseState(tx, caseId, state, dormancy);
|
|
71
|
-
return {
|
|
72
|
-
seq: updated.seq,
|
|
73
|
-
endedAt: updated.endedAt === null ? null : updated.endedAt.toISOString(),
|
|
74
|
-
};
|
|
75
|
-
},
|
|
76
|
-
appWrites: async (writes) => {
|
|
77
|
-
for (const write of writes)
|
|
78
|
-
await write(tx);
|
|
79
|
-
},
|
|
80
|
-
})),
|
|
81
|
-
appendEntry: (input) => appendEntry(q, input),
|
|
82
|
-
heartbeat: async (caseId, executionId, ttlMs) => {
|
|
83
|
-
await q
|
|
84
|
-
.query(`update ${CLAIMS}
|
|
85
|
-
set heartbeat_at = now(), expires_at = ${expiryExpression('$3')}
|
|
86
|
-
where case_id = $1 and execution_id = $2`, [caseId, executionId, ttlMs])
|
|
87
|
-
.catch(() => undefined);
|
|
88
|
-
},
|
|
89
|
-
bumpAttempt: async (caseId, executionId, attempt) => {
|
|
90
|
-
await q
|
|
91
|
-
.query(`update ${CLAIMS} set attempt = $3 where case_id = $1 and execution_id = $2`, [caseId, executionId, attempt])
|
|
92
|
-
.catch(() => undefined);
|
|
93
|
-
},
|
|
94
|
-
releaseClaim: async (caseId, executionId) => {
|
|
95
|
-
await q
|
|
96
|
-
.query(`delete from ${CLAIMS} where case_id = $1 and execution_id = $2`, [caseId, executionId])
|
|
97
|
-
.catch(() => undefined);
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
};
|
|
1
|
+
export {};
|
|
101
2
|
//# sourceMappingURL=port.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"port.js","sourceRoot":"","sources":["../../src/execution/port.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The lifecycle port: what claim → run → commit asks of storage, and nothing\n * else.\n *\n * An **internal** seam, deliberately. `docs/architecture.md` stands:\n * Postgres is a hard dependency and there is no public storage-adapter\n * abstraction — an app never sees this interface. It exists because the\n * claim state machine (busy vs. takeover, the holder check, retry\n * classification, per-attempt write discard) upholds more always-true\n * rules in one place than anything else in the package, and testing those\n * rules means staging precise situations — an expired lease, a takeover\n * mid-run — that should not require a running database. Two adapters make\n * the seam real: the pg one below for production, and the in-memory one in\n * `test/execution/memory-port.ts` for the tests.\n *\n * The shape keeps the lifecycle's transactional structure explicit:\n * {@link LifecyclePort.withCaseLock} is \"one short transaction holding the\n * case row's lock\" — the claim and the commit are each exactly one of those\n * — and everything else deliberately runs outside any transaction, because\n * a handler is running and the lease, not a lock, carries exclusivity.\n */\n\nimport type { CommitWrite } from '../model/index.js'\nimport type {\n CaseTypeLookup,\n DatabaseAccess,\n Dormancy,\n ResolvedCase,\n} from '../store/index.js'\nimport {\n CaseNotFoundError,\n FRAMEWORK_SCHEMA,\n queryableOf,\n resolveCaseForUpdate,\n updateCaseState,\n} from '../store/index.js'\nimport type { JournalEntry, JournalEntryInput } from './journal.js'\nimport { appendEntry } from './journal.js'\nimport { withTransaction } from './transaction.js'\n\nconst CASES = `${FRAMEWORK_SCHEMA}.cases`\nconst CLAIMS = `${FRAMEWORK_SCHEMA}.claims`\n\n/** `now() + <ms>` as a SQL expression against a bound millisecond parameter. */\nconst expiryExpression = (parameter: string): string =>\n `now() + (${parameter}::double precision * interval '1 millisecond')`\n\n/** The claim sitting on a case, as the lifecycle needs to judge it. */\nexport interface HeldClaim {\n readonly executionId: string\n readonly step: string\n readonly scopeKey: string | null\n readonly attempt: number\n readonly expiresAt: string\n /** True when the lease has lapsed — the next claimant may take the case over. */\n readonly expired: boolean\n}\n\n/** What the lifecycle asks of storage inside one case-locked transaction. */\nexport interface LifecycleTx {\n /**\n * The case, loaded under its lock and resolved whole — definition looked\n * up, stored state validated against its schema — the serialization\n * point. Returning the resolved triple rather than a raw row is what\n * keeps \"load, resolve, validate, in that order\" spelled once (in\n * `store/resolve.ts`) instead of re-derived by each adapter's caller.\n */\n readonly loadCase: () => Promise<ResolvedCase>\n /**\n * Take the case row's lock without interpreting the row — the commit's\n * serialization point. The commit already holds everything it computed at\n * the claim; what it needs from the row is only the lock (and proof the\n * row exists), never a second read-and-validate of the state document.\n */\n readonly lockCase: () => Promise<void>\n /** The claim on the case, `null` when nobody holds it. */\n readonly currentClaim: () => Promise<HeldClaim | null>\n readonly insertClaim: (\n executionId: string,\n step: string,\n scopeKey: string | null,\n ttlMs: number,\n ) => Promise<{ readonly claimedAt: string }>\n readonly deleteClaim: (executionId: string) => Promise<void>\n readonly appendEntry: (input: JournalEntryInput) => Promise<JournalEntry>\n /** Write the next Case State, bump `seq`, apply the dormancy transition. */\n readonly updateCaseState: (\n state: unknown,\n dormancy: Dormancy | null,\n ) => Promise<{ readonly seq: number; readonly endedAt: string | null }>\n /** The app's own `ctx.onCommit` writes, riding the same transaction. */\n readonly appWrites: (writes: readonly CommitWrite[]) => Promise<void>\n}\n\n/** The verbs the execution lifecycle needs from storage. */\nexport interface LifecyclePort {\n /** One short transaction holding the case row's lock — a claim or a commit. */\n readonly withCaseLock: <T>(\n caseId: string,\n fn: (tx: LifecycleTx) => Promise<T>,\n ) => Promise<T>\n /** Journal outside any transaction — `attempt-failed` / `failed` entries. */\n readonly appendEntry: (input: JournalEntryInput) => Promise<JournalEntry>\n /** Refresh the lease. Best-effort: a failed beat just lets the claim age. */\n readonly heartbeat: (\n caseId: string,\n executionId: string,\n ttlMs: number,\n ) => Promise<void>\n /** Keep the lease's attempt counter current across retries. Best-effort. */\n readonly bumpAttempt: (\n caseId: string,\n executionId: string,\n attempt: number,\n ) => Promise<void>\n /** Delete this Execution's claim — scoped to `executionId`, so releasing a lease we no longer hold is a no-op. */\n readonly releaseClaim: (caseId: string, executionId: string) => Promise<void>\n}\n\ntype ClaimRow = {\n execution_id: string\n step: string\n scope_key: string | null\n attempt: number\n expires_at: Date\n expired: boolean\n}\n\n/** The production adapter: each port verb implemented as SQL over the claims, journal and cases tables. */\nexport const pgLifecyclePort = (\n db: DatabaseAccess,\n caseTypeFor: CaseTypeLookup,\n): LifecyclePort => {\n // The lease verbs are single self-contained statements; only the\n // case-locked transactions care which arm of the access the caller brought.\n const q = queryableOf(db)\n return {\n withCaseLock: (caseId, fn) =>\n withTransaction(db, (tx) =>\n fn({\n loadCase: () => resolveCaseForUpdate(tx, caseTypeFor, caseId),\n lockCase: async () => {\n const { rows } = await tx.query<{ id: string }>(\n `select id from ${CASES} where id = $1 for update`,\n [caseId],\n )\n if (rows.length === 0) throw new CaseNotFoundError(caseId)\n },\n currentClaim: async () => {\n const { rows } = await tx.query<ClaimRow>(\n `select execution_id, step, scope_key, attempt, expires_at, expires_at <= now() as expired\n from ${CLAIMS} where case_id = $1`,\n [caseId],\n )\n const row = rows[0]\n if (!row) return null\n return {\n executionId: row.execution_id,\n step: row.step,\n scopeKey: row.scope_key,\n attempt: row.attempt,\n expiresAt: row.expires_at.toISOString(),\n expired: row.expired,\n }\n },\n insertClaim: async (executionId, step, scopeKey, ttlMs) => {\n const { rows } = await tx.query<{ claimed_at: Date }>(\n `insert into ${CLAIMS} (case_id, execution_id, step, scope_key, expires_at)\n values ($1, $2, $3, $4, ${expiryExpression('$5')})\n returning claimed_at`,\n [caseId, executionId, step, scopeKey, ttlMs],\n )\n return {\n claimedAt:\n rows[0]?.claimed_at.toISOString() ?? new Date().toISOString(),\n }\n },\n deleteClaim: async (executionId) => {\n await tx.query(\n `delete from ${CLAIMS} where case_id = $1 and execution_id = $2`,\n [caseId, executionId],\n )\n },\n appendEntry: (input) => appendEntry(tx, input),\n updateCaseState: async (state, dormancy) => {\n const updated = await updateCaseState(tx, caseId, state, dormancy)\n return {\n seq: updated.seq,\n endedAt:\n updated.endedAt === null ? null : updated.endedAt.toISOString(),\n }\n },\n appWrites: async (writes) => {\n for (const write of writes) await write(tx)\n },\n }),\n ),\n appendEntry: (input) => appendEntry(q, input),\n heartbeat: async (caseId, executionId, ttlMs) => {\n await q\n .query(\n `update ${CLAIMS}\n set heartbeat_at = now(), expires_at = ${expiryExpression('$3')}\n where case_id = $1 and execution_id = $2`,\n [caseId, executionId, ttlMs],\n )\n .catch(() => undefined)\n },\n bumpAttempt: async (caseId, executionId, attempt) => {\n await q\n .query(\n `update ${CLAIMS} set attempt = $3 where case_id = $1 and execution_id = $2`,\n [caseId, executionId, attempt],\n )\n .catch(() => undefined)\n },\n releaseClaim: async (caseId, executionId) => {\n await q\n .query(\n `delete from ${CLAIMS} where case_id = $1 and execution_id = $2`,\n [caseId, executionId],\n )\n .catch(() => undefined)\n },\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"port.js","sourceRoot":"","sources":["../../src/execution/port.ts"],"names":[],"mappings":"","sourcesContent":["/** Storage mechanics for claim → run → commit, implemented by each adapter.\n * Claims are exclusive per case. Expiry is judged by storage's authoritative clock.\n * Commit checks ownership, so an expired but unreplaced claim may still commit.\n * State, effects, completed evidence and claim removal share one atomic operation.\n */\nimport type { CommitEffect } from '../model/handler.js'\nimport type { Dormancy, StoredCase } from '../store/store.js'\nimport type { JournalEntry, JournalEntryInput } from './journal.js'\n\n/** The claim sitting on a case, as the lifecycle needs to judge it. */\nexport interface HeldClaim {\n readonly executionId: string\n readonly step: string\n readonly scopeKey: string | null\n readonly attempt: number\n readonly expiresAt: string\n /** True when the lease has lapsed — the next claimant may take the case over. */\n readonly expired: boolean\n}\n\n/** What the lifecycle asks of storage inside one case-locked transaction. */\nexport interface LifecycleTx<TCommit = unknown> {\n /** Read unvalidated state inside the case's atomic operation. */\n readonly loadCase: () => Promise<StoredCase>\n /** The claim on the case, `null` when nobody holds it. */\n readonly currentClaim: () => Promise<HeldClaim | null>\n readonly insertClaim: (\n executionId: string,\n step: string,\n scopeKey: string | null,\n ttlMs: number,\n ) => Promise<{ readonly claimedAt: string }>\n readonly deleteClaim: (executionId: string) => Promise<void>\n readonly appendEntry: (input: JournalEntryInput) => Promise<JournalEntry>\n /** Write the next Case State, bump `seq`, apply the dormancy transition. */\n readonly updateCaseState: (\n state: unknown,\n dormancy: Dormancy | null,\n ) => Promise<{ readonly seq: number; readonly endedAt: string | null }>\n /** The app's own `ctx.onCommit` writes, riding the same transaction. */\n readonly applyEffects: (\n writes: readonly CommitEffect<TCommit>[],\n ) => Promise<void>\n}\n\n/** The verbs the execution lifecycle needs from storage. */\nexport interface LifecyclePort<TCommit = unknown> {\n /** Serialize this case and commit all writes on return; roll everything back on throw.\n * The callback runs once. Serialization conflicts must throw, not replay callbacks.\n * No operation spans a handler. Missing cases throw CaseNotFoundError. */\n readonly withCase: <T>(\n caseId: string,\n fn: (tx: LifecycleTx<TCommit>) => Promise<T>,\n ) => Promise<T>\n /** Journal outside any transaction — `attempt-failed` / `failed` entries. */\n readonly appendEntry: (input: JournalEntryInput) => Promise<JournalEntry>\n /** Refresh the lease. Best-effort: a failed beat just lets the claim age. */\n readonly heartbeat: (\n caseId: string,\n executionId: string,\n ttlMs: number,\n ) => Promise<void>\n /** Keep the lease's attempt counter current across retries. Best-effort. */\n readonly bumpAttempt: (\n caseId: string,\n executionId: string,\n attempt: number,\n ) => Promise<void>\n /** Delete this Execution's claim — scoped to `executionId`, so releasing a lease we no longer hold is a no-op. */\n readonly releaseClaim: (caseId: string, executionId: string) => Promise<void>\n}\n"]}
|
|
@@ -55,4 +55,4 @@ export interface GuardReplay {
|
|
|
55
55
|
* reproduce, and the parameter type says so — narrow a read entry with
|
|
56
56
|
* `isClaimedEntry` first.
|
|
57
57
|
*/
|
|
58
|
-
export declare const replayGuard: (definition: AnyCaseType
|
|
58
|
+
export declare const replayGuard: <TCommit>(definition: AnyCaseType<TCommit>, entry: ClaimedJournalEntry) => GuardReplay;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../../src/execution/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AA4BtC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,
|
|
1
|
+
{"version":3,"file":"replay.js","sourceRoot":"","sources":["../../src/execution/replay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AA4BtC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,UAAgC,EAChC,KAA0B,EACb,EAAE;IACf,MAAM,QAAQ,GAAG;QACf,WAAW,EAAE,KAAK,CAAC,WAAW;QAC9B,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,IAAI,EAAE,KAAK,CAAC,IAAI;QAChB,QAAQ,EAAE,KAAK,CAAC,KAAK;KACtB,CAAA;IACD,MAAM,OAAO,GAAG,aAAa,CAC3B,UAAU,EACV,KAAK,CAAC,KAAK,EACX,KAAK,CAAC,IAAI,EACV,KAAK,CAAC,QAAQ,IAAI,SAAS,CAC5B,CAAA;IACD,IAAI,OAAO,CAAC,OAAO,KAAK,IAAI,EAAE,CAAC;QAC7B,OAAO;YACL,GAAG,QAAQ;YACX,UAAU,EAAE,IAAI;YAChB,OAAO,EAAE,KAAK;YACd,aAAa,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE;SACzD,CAAA;IACH,CAAC;IACD,MAAM,UAAU,GAAG,cAAc,CAAC,OAAO,CAAC,MAAM,EAAE;QAChD,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,IAAI,EAAE,KAAK,CAAC,IAAI;KACjB,CAAC,CAAA;IACF,OAAO;QACL,GAAG,QAAQ;QACX,UAAU;QACV,OAAO,EAAE,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,UAAU,CAAC;QAC3C,aAAa,EAAE,IAAI;KACpB,CAAA;AACH,CAAC,CAAA","sourcesContent":["/**\n * Audit reconstruction.\n *\n * \"Why was this affordance available last Tuesday?\" is answered from the\n * journaled record of what the system actually believed at the time, never by\n * re-deriving the past through present-day code. A `claimed` entry\n * carries everything that answer needs: the guard evaluation, the instant it\n * was made as of, the Actor, and the Case State it was evaluated against.\n *\n * `replayGuard` re-runs **today's** definition against **that** recorded\n * moment and reports both results side by side. A mismatch is not a bug in\n * the journal — it is the interesting signal: cases float to the latest\n * definitions, so it means the step's guard changed since the\n * Execution ran, and it says exactly how.\n *\n * Replay is a sweep over the Journal, so it is the lenient filter over\n * {@link addressTarget}: a journaled moment today's definitions can no\n * longer address — the step was removed, the element its scope key named no\n * longer selects, the selector throws over the historical document — is\n * itself definition drift, reported as `unaddressable` rather than thrown.\n * One drifted entry must never take down an audit of the rest.\n */\n\nimport type { GuardEvaluation } from '../guards/index.js'\nimport type { AnyCaseType } from '../model/index.js'\nimport { addressTarget, evaluateTarget } from '../model/index.js'\nimport { jsonEqual } from './delta.js'\nimport type { ClaimedJournalEntry } from './journal.js'\n\n/** What today's definitions make of a journaled moment. */\nexport interface GuardReplay {\n readonly executionId: string\n readonly step: string\n readonly scopeKey: string | null\n /** The instant the recorded evaluation was made as of. */\n readonly asOf: string\n /** The guard results as journaled at the enforcement moment. */\n readonly recorded: GuardEvaluation\n /**\n * The same guard, re-evaluated now against the journaled state, actor and\n * instant — or `null` when today's definitions cannot address the\n * journaled (step × scope key) at all.\n */\n readonly reproduced: GuardEvaluation | null\n /** True when the two agree exactly — the definitions have not drifted for this step. */\n readonly matches: boolean\n /**\n * Why today's definitions could not address the journaled moment, when\n * they could not — the strongest form of drift. `null` when `reproduced`\n * is present.\n */\n readonly unaddressable: { readonly reason: string } | null\n}\n\n/**\n * Re-evaluate a `claimed` entry's guard against the state, actor and instant\n * the entry recorded. Only `claimed` entries carry an evaluation to\n * reproduce, and the parameter type says so — narrow a read entry with\n * `isClaimedEntry` first.\n */\nexport const replayGuard = <TCommit>(\n definition: AnyCaseType<TCommit>,\n entry: ClaimedJournalEntry,\n): GuardReplay => {\n const identity = {\n executionId: entry.executionId,\n step: entry.step,\n scopeKey: entry.scopeKey,\n asOf: entry.asOf,\n recorded: entry.guard,\n }\n const address = addressTarget(\n definition,\n entry.state,\n entry.step,\n entry.scopeKey ?? undefined,\n )\n if (address.failure !== null) {\n return {\n ...identity,\n reproduced: null,\n matches: false,\n unaddressable: { reason: address.failure.error.message },\n }\n }\n const reproduced = evaluateTarget(address.target, {\n actor: entry.actor,\n asOf: entry.asOf,\n })\n return {\n ...identity,\n reproduced,\n matches: jsonEqual(entry.guard, reproduced),\n unaddressable: null,\n }\n}\n"]}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,16 +16,18 @@
|
|
|
16
16
|
export type { Affordance, AffordanceExplanation, BlockedStep, CaseAffordances, CaseSnapshot, Engine, EngineOptions, ExplainOptions, } from './engine/index.js';
|
|
17
17
|
export { createEngine, UnknownCaseTypeError } from './engine/index.js';
|
|
18
18
|
export type { AffordanceErrorCode } from './errors.js';
|
|
19
|
-
export { AffordanceError, isAffordanceError } from './errors.js';
|
|
19
|
+
export { AffordanceError, isAffordanceError, REFUSAL_CODES } from './errors.js';
|
|
20
20
|
export type { ClaimedEntryInput, ClaimedJournalEntry, CompletedEntryInput, ExecuteOptions, ExecutionRecord, ExecutionResult, ExecutionStatus, FailureEntryInput, GuardReplay, JournalEntry, JournalEntryInput, JournalEntryType, JournalError, JournalFilter, PatchOp, StateDelta, } from './execution/index.js';
|
|
21
21
|
export { CaseBusyError, ClaimLostError, diffState, foldExecutions, isClaimedEntry, jsonEqual, replayGuard, StepExecutionError, StepNotAvailableError, stepLabel, } from './execution/index.js';
|
|
22
|
+
export { JOURNAL_ENTRY_KINDS } from './execution/journal.js';
|
|
22
23
|
export type { AnyOfConditionResult, Condition, ConditionContext, ConditionMap, ConditionMapEntry, ConditionOutcome, ConditionResult, ConditionVerdict, Guard, GuardEvaluation, GuardEvaluationContext, GuardSection, Instant, SingleConditionResult, } from './guards/index.js';
|
|
23
24
|
export { anyOf, evaluateGuard, toEpochMs, toIso } from './guards/index.js';
|
|
24
25
|
export type { Correlation, CorrelationRegistration, DeadLetter, DeadLetterFilter, DeadLetterReason, ExternalActor, ExternalEvent, IngestionOptions, IngestionResult, IngestionStatus, } from './ingestion/index.js';
|
|
25
26
|
export { externalActor, routedStep } from './ingestion/index.js';
|
|
26
27
|
export type { MigrationFailure, MigrationOptions, MigrationProgress, MigrationReport, MigrationTransform, } from './migration/index.js';
|
|
27
28
|
export { hasMigrated, migrationStepName } from './migration/index.js';
|
|
28
|
-
export type { ActorMarker, AnyCaseType, BoundStep, CaseTypeDefinition, CaseTypeOptions, CommitWrite, CorrelationRequest, HandlerContext, RetryOptions, RetryPolicy, ScopeDeclaration, ScopedConditionContext, ScopedHandlerContext, ScopedStepHandler, ScopedStepOptions, StepDefinition, StepHandler, StepMetadata, StepOptions, } from './model/index.js';
|
|
29
|
-
export { actor, caseType, DEFAULT_RETRY, SCOPE_FAILURE_CONDITION, ScopeKeyError, StepInputValidationError, stepsOf, UnknownStepError, } from './model/index.js';
|
|
30
|
-
export type {
|
|
31
|
-
export {
|
|
29
|
+
export type { ActorMarker, AnyCaseType, BoundStep, CaseTypeDefinition, CaseTypeOptions, CommitContextMarker, CommitEffect, CommitWrite, CorrelationRequest, HandlerContext, RetryOptions, RetryPolicy, ScopeDeclaration, ScopedConditionContext, ScopedHandlerContext, ScopedStepHandler, ScopedStepOptions, StepDefinition, StepHandler, StepMetadata, StepOptions, } from './model/index.js';
|
|
30
|
+
export { actor, caseType, commitContext, DEFAULT_RETRY, SCOPE_FAILURE_CONDITION, ScopeKeyError, StepInputValidationError, stepsOf, UnknownStepError, } from './model/index.js';
|
|
31
|
+
export type { CaseListOptions, CasePage, EngineStorage } from './storage.js';
|
|
32
|
+
export type { CaseHandle, Dormancy } from './store/index.js';
|
|
33
|
+
export { CaseNotFoundError, CaseStateValidationError } from './store/index.js';
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// ── The engine ─────────────────────────────────────────────────────────────
|
|
3
3
|
export { createEngine, UnknownCaseTypeError } from './engine/index.js';
|
|
4
4
|
// ── The error taxonomy: every deliberate refusal, one closed code set ──────
|
|
5
|
-
export { AffordanceError, isAffordanceError } from './errors.js';
|
|
5
|
+
export { AffordanceError, isAffordanceError, REFUSAL_CODES } from './errors.js';
|
|
6
6
|
// ── Execution records: what came back, what is journaled ───────────────────
|
|
7
7
|
export { CaseBusyError, ClaimLostError, diffState, foldExecutions, isClaimedEntry, jsonEqual, replayGuard, StepExecutionError, StepNotAvailableError, stepLabel, } from './execution/index.js';
|
|
8
|
+
export { JOURNAL_ENTRY_KINDS } from './execution/journal.js';
|
|
8
9
|
// ── The authoring API: conditions and guards ───────────────────────────────
|
|
9
10
|
export { anyOf, evaluateGuard, toEpochMs, toIso } from './guards/index.js';
|
|
10
11
|
// ── Ingestion: events in, correlations, dead letters ───────────────────────
|
|
@@ -14,7 +15,6 @@ export { hasMigrated, migrationStepName } from './migration/index.js';
|
|
|
14
15
|
// ── The authoring API: case types and steps ────────────────────────────────
|
|
15
16
|
// `step` itself stays behind the barrel: `caseType` requires a state schema,
|
|
16
17
|
// so `stepsOf` covers every authoring case — one public way to write a step.
|
|
17
|
-
export { actor, caseType, DEFAULT_RETRY, SCOPE_FAILURE_CONDITION, ScopeKeyError, StepInputValidationError, stepsOf, UnknownStepError, } from './model/index.js';
|
|
18
|
-
|
|
19
|
-
export { bootstrap, CASE_TABLES, CaseNotFoundError, CaseStateValidationError, FRAMEWORK_SCHEMA, queryableOf, } from './store/index.js';
|
|
18
|
+
export { actor, caseType, commitContext, DEFAULT_RETRY, SCOPE_FAILURE_CONDITION, ScopeKeyError, StepInputValidationError, stepsOf, UnknownStepError, } from './model/index.js';
|
|
19
|
+
export { CaseNotFoundError, CaseStateValidationError } from './store/index.js';
|
|
20
20
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iDAAiD;AA4BjD,8EAA8E;AAC9E,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAEtE,8EAA8E;AAC9E,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iDAAiD;AA4BjD,8EAA8E;AAC9E,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAEtE,8EAA8E;AAC9E,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAmB/E,8EAA8E;AAC9E,OAAO,EACL,aAAa,EACb,cAAc,EACd,SAAS,EACT,cAAc,EACd,cAAc,EACd,SAAS,EACT,WAAW,EACX,kBAAkB,EAClB,qBAAqB,EACrB,SAAS,GACV,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAiB5D,8EAA8E;AAC9E,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAa1E,8EAA8E;AAC9E,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAQhE,8EAA8E;AAC9E,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAwBrE,8EAA8E;AAC9E,6EAA6E;AAC7E,6EAA6E;AAC7E,OAAO,EACL,KAAK,EACL,QAAQ,EACR,aAAa,EACb,aAAa,EACb,uBAAuB,EACvB,aAAa,EACb,wBAAwB,EACxB,OAAO,EACP,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AAGzB,OAAO,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,MAAM,kBAAkB,CAAA","sourcesContent":["// Copyright © 2026 Mochicode LLC — mochicode.com\n\n/**\n * @affordance/core — the case framework's engine.\n *\n * A **case** is state plus independently-defined guarded **steps**; the\n * engine computes the currently-available steps (**affordances**) from\n * guards over state. No declared flow, no program counter;\n * process changes deploy freely against in-flight cases.\n *\n * This barrel *is* the package's interface, and it is curated: the authoring\n * API an app writes definitions with, the engine it runs them on, the error\n * taxonomy an adapter translates, and the record types that cross the wire.\n * The store loaders, the execution lifecycle's internals\n * and the guard walk live behind the engine — submodules import them from\n * each other's `index.js`, apps should not need to.\n */\n\nexport type {\n Affordance,\n AffordanceExplanation,\n BlockedStep,\n CaseAffordances,\n CaseSnapshot,\n Engine,\n EngineOptions,\n ExplainOptions,\n} from './engine/index.js'\n// ── The engine ─────────────────────────────────────────────────────────────\nexport { createEngine, UnknownCaseTypeError } from './engine/index.js'\nexport type { AffordanceErrorCode } from './errors.js'\n// ── The error taxonomy: every deliberate refusal, one closed code set ──────\nexport { AffordanceError, isAffordanceError, REFUSAL_CODES } from './errors.js'\nexport type {\n ClaimedEntryInput,\n ClaimedJournalEntry,\n CompletedEntryInput,\n ExecuteOptions,\n ExecutionRecord,\n ExecutionResult,\n ExecutionStatus,\n FailureEntryInput,\n GuardReplay,\n JournalEntry,\n JournalEntryInput,\n JournalEntryType,\n JournalError,\n JournalFilter,\n PatchOp,\n StateDelta,\n} from './execution/index.js'\n// ── Execution records: what came back, what is journaled ───────────────────\nexport {\n CaseBusyError,\n ClaimLostError,\n diffState,\n foldExecutions,\n isClaimedEntry,\n jsonEqual,\n replayGuard,\n StepExecutionError,\n StepNotAvailableError,\n stepLabel,\n} from './execution/index.js'\nexport { JOURNAL_ENTRY_KINDS } from './execution/journal.js'\nexport type {\n AnyOfConditionResult,\n Condition,\n ConditionContext,\n ConditionMap,\n ConditionMapEntry,\n ConditionOutcome,\n ConditionResult,\n ConditionVerdict,\n Guard,\n GuardEvaluation,\n GuardEvaluationContext,\n GuardSection,\n Instant,\n SingleConditionResult,\n} from './guards/index.js'\n// ── The authoring API: conditions and guards ───────────────────────────────\nexport { anyOf, evaluateGuard, toEpochMs, toIso } from './guards/index.js'\nexport type {\n Correlation,\n CorrelationRegistration,\n DeadLetter,\n DeadLetterFilter,\n DeadLetterReason,\n ExternalActor,\n ExternalEvent,\n IngestionOptions,\n IngestionResult,\n IngestionStatus,\n} from './ingestion/index.js'\n// ── Ingestion: events in, correlations, dead letters ───────────────────────\nexport { externalActor, routedStep } from './ingestion/index.js'\nexport type {\n MigrationFailure,\n MigrationOptions,\n MigrationProgress,\n MigrationReport,\n MigrationTransform,\n} from './migration/index.js'\n// ── Migration: the journaled restructure ───────────────────────────────────\nexport { hasMigrated, migrationStepName } from './migration/index.js'\nexport type {\n ActorMarker,\n AnyCaseType,\n BoundStep,\n CaseTypeDefinition,\n CaseTypeOptions,\n CommitContextMarker,\n CommitEffect,\n CommitWrite,\n CorrelationRequest,\n HandlerContext,\n RetryOptions,\n RetryPolicy,\n ScopeDeclaration,\n ScopedConditionContext,\n ScopedHandlerContext,\n ScopedStepHandler,\n ScopedStepOptions,\n StepDefinition,\n StepHandler,\n StepMetadata,\n StepOptions,\n} from './model/index.js'\n// ── The authoring API: case types and steps ────────────────────────────────\n// `step` itself stays behind the barrel: `caseType` requires a state schema,\n// so `stepsOf` covers every authoring case — one public way to write a step.\nexport {\n actor,\n caseType,\n commitContext,\n DEFAULT_RETRY,\n SCOPE_FAILURE_CONDITION,\n ScopeKeyError,\n StepInputValidationError,\n stepsOf,\n UnknownStepError,\n} from './model/index.js'\nexport type { CaseListOptions, CasePage, EngineStorage } from './storage.js'\nexport type { CaseHandle, Dormancy } from './store/index.js'\nexport { CaseNotFoundError, CaseStateValidationError } from './store/index.js'\n"]}
|
|
@@ -1,22 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Correlation: the mapping from an external identifier to a case (and a
|
|
3
|
-
* scope element within it) — CONTEXT.md.
|
|
4
|
-
*
|
|
5
|
-
* An e-sign provider knows envelope `env_9f2`; it does not know case
|
|
6
|
-
* `4b0e…` or that the envelope is buyer #7's agreement. Something has
|
|
7
|
-
* to hold that mapping, and it has to be the framework, because it is the
|
|
8
|
-
* framework that has to route the resulting webhook to a step. This is that
|
|
9
|
-
* something — one of exactly two integration primitives in core (the other
|
|
10
|
-
* is {@link ingest}). There are no service-specific connectors here, and
|
|
11
|
-
* there never will be: DocuSign's payload shape is the app's business.
|
|
12
|
-
*
|
|
13
|
-
* The registration is written by the handler that *initiates* the external
|
|
14
|
-
* interaction, in the same commit that records having initiated it —
|
|
15
|
-
* `ctx.correlate(...)` rides the commit seam, so a case can never be
|
|
16
|
-
* left having sent an envelope it cannot route the answer for.
|
|
17
|
-
*/
|
|
18
1
|
import type { CorrelationRequest } from '../model/handler.js';
|
|
19
|
-
import type { Queryable } from '../store/index.js';
|
|
20
2
|
/**
|
|
21
3
|
* A handler's {@link CorrelationRequest} with the case made explicit — what
|
|
22
4
|
* the registry actually stores. `step` is the step an event on this
|
|
@@ -38,17 +20,3 @@ export interface Correlation {
|
|
|
38
20
|
readonly metadata: unknown;
|
|
39
21
|
readonly createdAt: string;
|
|
40
22
|
}
|
|
41
|
-
/**
|
|
42
|
-
* Register (or re-register) an external identifier against a case.
|
|
43
|
-
*
|
|
44
|
-
* Upserts on `(system, externalId)` — insert, or update the row already
|
|
45
|
-
* there: a retried handler attempt registering the same envelope again is
|
|
46
|
-
* not an error, it is the same fact. Pass any
|
|
47
|
-
* {@link Queryable} — from a handler this is the commit transaction, via
|
|
48
|
-
* `ctx.correlate` or `ctx.onCommit`.
|
|
49
|
-
*/
|
|
50
|
-
export declare const registerCorrelation: (db: Queryable, registration: CorrelationRegistration) => Promise<Correlation>;
|
|
51
|
-
/** Look up where an external identifier routes; `null` when nothing has claimed it. */
|
|
52
|
-
export declare const lookupCorrelation: (db: Queryable, system: string, externalId: string) => Promise<Correlation | null>;
|
|
53
|
-
/** Every identifier registered against a case — the "what is this case waiting on" view. */
|
|
54
|
-
export declare const correlationsFor: (db: Queryable, caseId: string, scopeKey?: string) => Promise<readonly Correlation[]>;
|
|
@@ -1,78 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* Correlation: the mapping from an external identifier to a case (and a
|
|
3
|
-
* scope element within it) — CONTEXT.md.
|
|
4
|
-
*
|
|
5
|
-
* An e-sign provider knows envelope `env_9f2`; it does not know case
|
|
6
|
-
* `4b0e…` or that the envelope is buyer #7's agreement. Something has
|
|
7
|
-
* to hold that mapping, and it has to be the framework, because it is the
|
|
8
|
-
* framework that has to route the resulting webhook to a step. This is that
|
|
9
|
-
* something — one of exactly two integration primitives in core (the other
|
|
10
|
-
* is {@link ingest}). There are no service-specific connectors here, and
|
|
11
|
-
* there never will be: DocuSign's payload shape is the app's business.
|
|
12
|
-
*
|
|
13
|
-
* The registration is written by the handler that *initiates* the external
|
|
14
|
-
* interaction, in the same commit that records having initiated it —
|
|
15
|
-
* `ctx.correlate(...)` rides the commit seam, so a case can never be
|
|
16
|
-
* left having sent an envelope it cannot route the answer for.
|
|
17
|
-
*/
|
|
18
|
-
import { FRAMEWORK_SCHEMA, mintId } from '../store/index.js';
|
|
19
|
-
const CORRELATIONS = `${FRAMEWORK_SCHEMA}.correlations`;
|
|
20
|
-
const toCorrelation = (row) => ({
|
|
21
|
-
id: row.id,
|
|
22
|
-
system: row.system,
|
|
23
|
-
externalId: row.external_id,
|
|
24
|
-
caseId: row.case_id,
|
|
25
|
-
scopeKey: row.scope_key,
|
|
26
|
-
step: row.step,
|
|
27
|
-
metadata: row.metadata,
|
|
28
|
-
createdAt: row.created_at.toISOString(),
|
|
29
|
-
});
|
|
30
|
-
/**
|
|
31
|
-
* Register (or re-register) an external identifier against a case.
|
|
32
|
-
*
|
|
33
|
-
* Upserts on `(system, externalId)` — insert, or update the row already
|
|
34
|
-
* there: a retried handler attempt registering the same envelope again is
|
|
35
|
-
* not an error, it is the same fact. Pass any
|
|
36
|
-
* {@link Queryable} — from a handler this is the commit transaction, via
|
|
37
|
-
* `ctx.correlate` or `ctx.onCommit`.
|
|
38
|
-
*/
|
|
39
|
-
export const registerCorrelation = async (db, registration) => {
|
|
40
|
-
const { rows } = await db.query(`insert into ${CORRELATIONS} (id, system, external_id, case_id, scope_key, step, metadata)
|
|
41
|
-
values ($1, $2, $3, $4, $5, $6, $7::jsonb)
|
|
42
|
-
on conflict (system, external_id) do update
|
|
43
|
-
set case_id = excluded.case_id,
|
|
44
|
-
scope_key = excluded.scope_key,
|
|
45
|
-
step = excluded.step,
|
|
46
|
-
metadata = excluded.metadata
|
|
47
|
-
returning id, system, external_id, case_id, scope_key, step, metadata, created_at`, [
|
|
48
|
-
mintId('correlation'),
|
|
49
|
-
registration.system,
|
|
50
|
-
registration.externalId,
|
|
51
|
-
registration.caseId,
|
|
52
|
-
registration.scopeKey ?? null,
|
|
53
|
-
registration.step ?? null,
|
|
54
|
-
registration.metadata === undefined
|
|
55
|
-
? null
|
|
56
|
-
: JSON.stringify(registration.metadata),
|
|
57
|
-
]);
|
|
58
|
-
const row = rows[0];
|
|
59
|
-
if (!row)
|
|
60
|
-
throw new Error(`insert into ${CORRELATIONS} returned no row`);
|
|
61
|
-
return toCorrelation(row);
|
|
62
|
-
};
|
|
63
|
-
/** Look up where an external identifier routes; `null` when nothing has claimed it. */
|
|
64
|
-
export const lookupCorrelation = async (db, system, externalId) => {
|
|
65
|
-
const { rows } = await db.query(`select id, system, external_id, case_id, scope_key, step, metadata, created_at
|
|
66
|
-
from ${CORRELATIONS} where system = $1 and external_id = $2`, [system, externalId]);
|
|
67
|
-
const row = rows[0];
|
|
68
|
-
return row === undefined ? null : toCorrelation(row);
|
|
69
|
-
};
|
|
70
|
-
/** Every identifier registered against a case — the "what is this case waiting on" view. */
|
|
71
|
-
export const correlationsFor = async (db, caseId, scopeKey) => {
|
|
72
|
-
const { rows } = await db.query(`select id, system, external_id, case_id, scope_key, step, metadata, created_at
|
|
73
|
-
from ${CORRELATIONS}
|
|
74
|
-
where case_id = $1 ${scopeKey === undefined ? '' : 'and scope_key = $2'}
|
|
75
|
-
order by created_at asc`, scopeKey === undefined ? [caseId] : [caseId, scopeKey]);
|
|
76
|
-
return rows.map(toCorrelation);
|
|
77
|
-
};
|
|
1
|
+
export {};
|
|
78
2
|
//# sourceMappingURL=correlation.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"correlation.js","sourceRoot":"","sources":["../../src/ingestion/correlation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"correlation.js","sourceRoot":"","sources":["../../src/ingestion/correlation.ts"],"names":[],"mappings":"","sourcesContent":["import type { CorrelationRequest } from '../model/handler.js'\n\n/**\n * A handler's {@link CorrelationRequest} with the case made explicit — what\n * the registry actually stores. `step` is the step an event on this\n * identifier should execute when the event does not name one itself:\n * typically the materializing step, \"record what the provider said\".\n */\nexport interface CorrelationRegistration extends CorrelationRequest {\n /** The case the answer belongs to. */\n readonly caseId: string\n}\n\n/** A registered correlation as stored. */\nexport interface Correlation {\n readonly id: string\n readonly system: string\n readonly externalId: string\n readonly caseId: string\n readonly scopeKey: string | null\n readonly step: string | null\n readonly metadata: unknown\n readonly createdAt: string\n}\n"]}
|
|
@@ -11,6 +11,5 @@
|
|
|
11
11
|
* owns payload shapes, the framework owns routing and exactly-once.
|
|
12
12
|
*/
|
|
13
13
|
export type { Correlation, CorrelationRegistration } from './correlation.js';
|
|
14
|
-
export { correlationsFor, lookupCorrelation, registerCorrelation, } from './correlation.js';
|
|
15
14
|
export type { DeadLetter, DeadLetterFilter, DeadLetterReason, ExternalActor, ExternalEvent, IngestionEnvironment, IngestionOptions, IngestionResult, IngestionSettings, IngestionStatus, } from './ingest.js';
|
|
16
|
-
export { classifyDeadLetter, externalActor, idempotencyKeyFor, ingest, normalizeIngestion, REOPENS_ON_REDELIVERY,
|
|
15
|
+
export { classifyDeadLetter, externalActor, idempotencyKeyFor, ingest, normalizeIngestion, REOPENS_ON_REDELIVERY, routedStep, } from './ingest.js';
|
package/dist/ingestion/index.js
CHANGED
|
@@ -10,6 +10,5 @@
|
|
|
10
10
|
* what a DocuSign webhook looks like, and nothing in it ever will — the app
|
|
11
11
|
* owns payload shapes, the framework owns routing and exactly-once.
|
|
12
12
|
*/
|
|
13
|
-
export {
|
|
14
|
-
export { classifyDeadLetter, externalActor, idempotencyKeyFor, ingest, normalizeIngestion, REOPENS_ON_REDELIVERY, readDeadLetters, routedStep, } from './ingest.js';
|
|
13
|
+
export { classifyDeadLetter, externalActor, idempotencyKeyFor, ingest, normalizeIngestion, REOPENS_ON_REDELIVERY, routedStep, } from './ingest.js';
|
|
15
14
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ingestion/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ingestion/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAeH,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,MAAM,EACN,kBAAkB,EAClB,qBAAqB,EACrB,UAAU,GACX,MAAM,aAAa,CAAA","sourcesContent":["/**\n * The two integration primitives that live in core.\n *\n * - **Correlation** — external identifier ↔ (case, scope element). Written\n * by the handler that initiates the external interaction.\n * - **Ingestion** — `ingest(event)`: dedup, correlate, then an ordinary\n * Execution with the external system as the actor (materialize-on-event).\n *\n * Deliberately absent, permanently: connectors. Nothing in this module knows\n * what a DocuSign webhook looks like, and nothing in it ever will — the app\n * owns payload shapes, the framework owns routing and exactly-once.\n */\n\nexport type { Correlation, CorrelationRegistration } from './correlation.js'\nexport type {\n DeadLetter,\n DeadLetterFilter,\n DeadLetterReason,\n ExternalActor,\n ExternalEvent,\n IngestionEnvironment,\n IngestionOptions,\n IngestionResult,\n IngestionSettings,\n IngestionStatus,\n} from './ingest.js'\nexport {\n classifyDeadLetter,\n externalActor,\n idempotencyKeyFor,\n ingest,\n normalizeIngestion,\n REOPENS_ON_REDELIVERY,\n routedStep,\n} from './ingest.js'\n"]}
|